Compare commits

...

128 Commits
2.8.1 ... 5.5.0

Author SHA1 Message Date
c6dff0b0e5 Merge pull request #260 from Mastermindzh/5.5.0
5.5.0
2023-07-31 21:13:39 +02:00
644beea2a6 fixed listenbrainz link 2023-07-31 21:13:24 +02:00
df1c45982b 5.5.0 docs, versions, etc 2023-07-31 15:49:29 +02:00
ec82aa8401 Merge pull request #258 from Mar0xy/master2
Add ListenBrainz implementation
2023-07-31 15:06:39 +02:00
586f7b595b various code improvements and some boyscout rule fixes :) 2023-07-31 13:43:32 +02:00
Mar0xy
de8a5a1b07 Fix bug where it does not run if condition 2023-07-31 12:14:06 +02:00
Mar0xy
38c1f05c35 Allow listenbrainz to be triggered on every play 2023-07-31 12:06:31 +02:00
Mar0xy
ed6f04b6d4 Fix bug where it does not unhide 2023-07-30 21:25:35 +02:00
Mar0xy
ffe8278c8c Fix complainy by sonarcloud 2023-07-30 21:22:38 +02:00
Mar0xy
e9434cc5ea Hide/Show ListenBrainz settings 2023-07-30 21:18:25 +02:00
Marie
d81912db0c Fix music_service domain 2023-07-30 11:46:27 +02:00
Mar0xy
c0110632e6 Seperate old ListenBrainz data from config 2023-07-30 10:42:32 +02:00
Mar0xy
3571289d28 Add ListenBrainz implementation 2023-07-30 02:38:38 +02:00
11cc209025 Merge pull request #255 from Mastermindzh/feature/5.4.0
Feature/5.4.0
2023-07-24 21:59:57 +02:00
f5ccbda7d9 set versions to 5.4.0 2023-07-24 12:04:08 +02:00
e8cf1783e8 chore(docs): updated README spelling 2023-07-23 23:59:45 +02:00
8037a73e57 Merge branch 'feature/5.4.0' of github.com:Mastermindzh/tidal-hifi into feature/5.4.0 2023-07-23 23:51:24 +02:00
45e191dae0 updated dependencies 2023-07-23 23:51:20 +02:00
f147536b12 updated dependencies 2023-07-23 23:43:28 +02:00
d03bb58afa removed windows builds from publishes 2023-07-23 23:20:01 +02:00
a39fef8d49 fix(hotkeys): Fixed bug with several hotkeys not working due to Tidal's HTML/css changes. fixes #250 2023-07-23 23:13:37 +02:00
41ca1d5a43 added songwhip 2023-07-23 23:12:18 +02:00
6969de8270 added several dev improvements 2023-07-23 23:07:19 +02:00
ad05b767d8 Merge pull request #245 from Mastermindzh/dependabot/npm_and_yarn/stylelint-15.10.1
chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
2023-07-09 00:38:31 +02:00
dependabot[bot]
6d873ce287 chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 15.6.0 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/15.6.0...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 22:30:49 +00:00
63d123f96a Merge pull request #241 from Mastermindzh/release/5.3.0
Release/5.3.0
2023-06-24 13:05:52 +02:00
f038412c50 release 5.3.0 2023-06-24 12:41:41 +02:00
ff02287df7 Merge pull request #240 from SPKChaosPhoenix/patch-1
Update Tokyo Night.scss
2023-06-23 15:19:31 +02:00
Marces
f221ded108 Update Tokyo Night.scss
Updatet Tokyo Night to work with the newest version of Tidal.
2023-06-22 16:12:41 +02:00
1440f70100 Merge pull request #237 from Mastermindzh/release/5.2.0
Release/5.2.0
2023-06-18 21:39:30 +02:00
439333e15a Merge pull request #236 from Mastermindzh/feature/drone
added drone builds
2023-06-18 20:25:55 +02:00
b9854e0595 Merge pull request #200 from drom98/fix-album-not-updating
Fix album not updating on playlists
2023-06-18 19:45:45 +02:00
8b56c28d75 Merge branch 'release/5.2.0' of github.com:Mastermindzh/tidal-hifi into fix-album-not-updating 2023-06-18 15:45:01 +02:00
700a14fe88 Merge pull request #227 from Mastermindzh/feature/theming
Feature/theming
2023-06-18 15:42:59 +02:00
3c835077d5 added drone builds 2023-06-18 15:39:14 +02:00
194de286c8 fix: customCSS default value was still a string, causing new users to have settings issues 2023-05-18 17:56:45 +02:00
a7dee5c2c9 fix: settings window was unresponsive on first start because of fs.mkdir that wasn't awaited 2023-05-16 23:41:00 +02:00
8036cbb919 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-14 23:49:25 +02:00
90cf231c76 fix: user uploaded themes are now stored in the config directory. Missing directories will be created and added docs for theming 2023-05-14 23:48:13 +02:00
42a70534f2 --amend 2023-05-14 14:42:05 +02:00
b07865d98b removed sass-cache 2023-05-14 14:41:30 +02:00
cc26bfa080 Merge pull request #225 from Mastermindzh/feature/typescript
Feature/typescript
2023-05-14 14:40:20 +02:00
822bdf401e Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-13 22:55:10 +02:00
a169c57a52 don't run double builds on PR, just release builds (for pre-releases + testing) 2023-05-13 22:47:36 +02:00
60eb1bbef9 chore: removed last 'any' types + added declaration for mpris-service's Player class 2023-05-13 22:45:15 +02:00
1761c8dd40 feat: theme files are now loaded & applied on startup 2023-05-10 22:07:11 +02:00
62244f432a ci: release now also runs on feature branches (for test builds) 2023-05-10 08:48:13 +02:00
a408a6a8cc theme: added Tokyo Night by https://github.com/wojciech-zurek 2023-05-10 00:02:19 +02:00
6e5a2c626c feat: theme selection is now stored in the config file 2023-05-09 23:57:16 +02:00
4350ab9bd9 added styling on theme selector 2023-05-09 23:28:45 +02:00
77a853e980 feat: add .css theme file upload and a unstyled theme selector 2023-05-08 22:31:22 +02:00
757f8511c0 ci: added theme files to resources 2023-05-08 00:03:05 +02:00
2ef457be2c Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-07 23:47:21 +02:00
2c5d2b9530 ci: cross-platform copy-files 2023-05-07 23:46:18 +02:00
757bd0da80 Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-07 23:28:02 +02:00
d823f07ed8 last files transformed from js -> ts 2023-05-07 23:27:46 +02:00
32ade76ae3 chore: compile sass themes 2023-05-07 23:25:35 +02:00
a1c02dfed3 last files transformed from js -> ts 2023-05-07 16:13:30 +02:00
21d6e57cb9 set up css folder 2023-05-07 15:48:00 +02:00
53e4711c39 chore: more typescript 2023-05-07 15:45:45 +02:00
e8509d42e7 organize imports 2023-05-01 23:31:37 +02:00
46d030cf8e transitioning to ts 2023-05-01 23:23:56 +02:00
412f1ae3e3 feat: added first typescript support
Didn't add many types yet. Just used to test out typescript compiler, copying files and building.
Now that all that seems to go well I can start converting all files to .ts and then adding proper typing everywhere
2023-05-01 13:44:02 +02:00
68f0c89ec2 replaced sass-lint with style-lint 2023-05-01 13:43:07 +02:00
8d44ff8afb Merge pull request #223 from Mastermindzh/release/5.1.0
Release/5.1.0
2023-04-27 15:30:09 +02:00
bccc979f43 fixed pacman icon 2023-04-27 15:07:40 +02:00
6849952c41 feat: added proper updates through the mediasession api. fixes #198 2023-04-27 14:29:28 +02:00
07be74af9f feat: added custom CSS settings. fixes #213 2023-04-27 14:13:32 +02:00
fc6adc25ca release: docs 2023-04-27 11:35:10 +02:00
4498e8a73e feat: you can now set updateFrequency in the settings window 2023-04-27 11:35:00 +02:00
3d2a9c3992 Merge pull request #219 from thanasistrisp/artists
[Bug]: get multiple artists instead of a single one
2023-04-27 10:51:35 +02:00
af6bfaf55e Merge pull request #222 from mdh34/mdh34-desktop-icon-fix
Fix Linux Desktop Icons
2023-04-27 10:46:44 +02:00
Matt Harris
8bac90e0f1 Fix Linux Icons 2023-04-26 21:34:17 +01:00
Matt Harris
887c75f61a change icon name in desktop file 2023-04-25 21:46:20 +01:00
Thanasis Trispiotis
cde7408cc4 fix: get multiple artists
- in mrpis multiple names are showing
- also at title
- skipping from settings any artist that is present in current artists at Tidal
2023-04-23 22:24:04 +03:00
05b422e045 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi 2023-04-22 21:19:42 +02:00
35289d8216 ci: updated workflow node versions 2023-04-22 21:19:36 +02:00
ea42b79cd8 ci: updated workflow node versions 2023-04-22 21:14:56 +02:00
6d859cf780 chore: updated deps. fixes #203 (kinda.. as much as we can) 2023-04-22 20:57:38 +02:00
af20092053 fix: fixed tray click bug. fixes #196 2023-04-22 20:51:20 +02:00
166ca353cf chore: updating deps 2023-04-22 16:56:23 +02:00
b807aa2f76 Merge pull request #215 from Mastermindzh/dependabot/npm_and_yarn/minimatch-and-electron-builder-3.1.2
Bump minimatch and electron-builder
2023-04-21 11:47:10 +02:00
dependabot[bot]
ef8ffe47f5 Bump minimatch and electron-builder
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependency [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder). These dependencies need to be updated together.


Updates `minimatch` from 3.0.4 to 3.1.2
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

Updates `electron-builder` from 23.5.1 to 24.2.1
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.2.1/packages/electron-builder)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
- dependency-name: electron-builder
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-21 09:32:10 +00:00
ba7b2a5717 Merge pull request #218 from thanasistrisp/adblocking
Add real Ad blocking using custom ad filters
2023-04-21 11:31:24 +02:00
0d93bedb4d prep v5.0.0, refactored getTrackUrl to use the duplicated getTrackId, and applied some formatting 2023-04-21 11:26:10 +02:00
Thanasis Trispiotis
1de71aa82b update docs 2023-04-21 01:38:20 +03:00
Thanasis Trispiotis
b2e68f5a8f add trackid
- mpris important property (maybe needed sometime)
2023-04-20 20:11:22 +03:00
Thanasis Trispiotis
a2a2023853 exit when restart
- if tray minimize is enabled quit hides the app to tray
2023-04-20 20:09:40 +03:00
Thanasis Trispiotis
26c8a38350 adblocking thanks to custom ad filters
derived from uBlock Origin: https://github.com/uBlockOrigin/uAssets/issues/17495
2023-04-20 20:07:44 +03:00
eb93fbc35d Merge pull request #216 from Mastermindzh/snyk-upgrade-5b1ac3f20e6c8576da25c1dd4616f859
[Snyk] Upgrade sass from 1.58.3 to 1.60.0
2023-04-19 16:49:07 +02:00
snyk-bot
d3c56fa445 fix: upgrade sass from 1.58.3 to 1.60.0
Snyk has created this PR to upgrade sass from 1.58.3 to 1.60.0.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-19 08:58:36 +00:00
6998992011 Merge pull request #210 from Mastermindzh/snyk-upgrade-24c736c74c97f0ad9986633990f52b6f
[Snyk] Upgrade @electron/remote from 2.0.8 to 2.0.9
2023-04-18 10:03:56 +02:00
108e1d65d4 Merge pull request #209 from Mastermindzh/snyk-upgrade-5f032e18d6ad67fa2ee5145171e2630c
[Snyk] Upgrade hotkeys-js from 3.9.4 to 3.10.1
2023-04-18 10:03:38 +02:00
1097f83911 Merge pull request #208 from Mastermindzh/snyk-upgrade-d65f22e3a110c0ac5f17545a20ca2873
[Snyk] Upgrade sass from 1.54.9 to 1.58.3
2023-04-18 10:03:20 +02:00
8c734777cc Merge pull request #204 from Mastermindzh/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-04-18 10:02:54 +02:00
snyk-bot
de17ac6113 fix: upgrade @electron/remote from 2.0.8 to 2.0.9
Snyk has created this PR to upgrade @electron/remote from 2.0.8 to 2.0.9.

See this package in npm:
https://www.npmjs.com/package/@electron/remote

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-27 22:16:23 +00:00
snyk-bot
ced41c00d7 fix: upgrade hotkeys-js from 3.9.4 to 3.10.1
Snyk has created this PR to upgrade hotkeys-js from 3.9.4 to 3.10.1.

See this package in npm:
https://www.npmjs.com/package/hotkeys-js

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-27 22:16:17 +00:00
snyk-bot
744016f307 fix: upgrade sass from 1.54.9 to 1.58.3
Snyk has created this PR to upgrade sass from 1.54.9 to 1.58.3.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-27 22:16:13 +00:00
dependabot[bot]
ad8ef71c6b Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-03 05:12:03 +00:00
Diogo Oliveira
0120391418 Fix album not updating on playlists 2023-01-30 17:49:04 +00:00
d0f9a34f9c Merge pull request #195 from Mastermindzh/feature/4.4.0
Feature/4.4.0
2023-01-22 22:30:43 +01:00
3b316f2301 chore: upping the version numbers 2023-01-22 22:19:07 +01:00
c0d9cd2834 feat: added the ability to skip artists automatically. Takes precedence over muting. fixes #175 2023-01-22 21:33:00 +01:00
0620d87d8b feature: added click handler to tray icon to focus/show tidal-hifi. fixes #193 #143 2023-01-22 21:32:48 +01:00
57b7f9148f feat: Move the quit command from the system sub-menu to the main menu fixes #185 2023-01-20 22:18:25 +01:00
63ccff97ea feature: Add support to autoHide the menubar and showing it with the key. fixes #188 2023-01-20 22:15:36 +01:00
3a4d23738f fix: Reverted icon path to instead of the hardcoded linux path fixes #170 2023-01-20 22:00:19 +01:00
c96bdb0d28 fix: Updated shortcut hint on the menubar to reflect the new shortcut. fixes #174 2023-01-20 21:57:41 +01:00
dependabot[bot]
115d8c6c5c Bump json5 from 2.2.1 to 2.2.3 (#191)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-10 21:26:12 +01:00
Marie
cd2a068470 Fix CHANGELOG (#181) 2022-10-25 14:35:07 +02:00
bf260b14e0 https://github.com/Mastermindzh/tidal-hifi/pull/178 (#180)
* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

* Redesign of the settings window (#168)

* Pr dest (#166)

* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>

* Redesign of the settings window

* changed sass to scss, fixed color of switches and disabled rounded corners

Co-authored-by: Rick van Lieshout <info@rickvanlieshout.com>

* - icon is set to new static path based on Arch/Debian
  - Name has changed to Tidal-Hifi

* Check if app is default protocol client before setting (#178)

* Release of settings window and desktop file fixes (#169)

* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

* Redesign of the settings window (#168)

* Pr dest (#166)

* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>

* Redesign of the settings window

* changed sass to scss, fixed color of switches and disabled rounded corners

Co-authored-by: Rick van Lieshout <info@rickvanlieshout.com>

* - icon is set to new static path based on Arch/Debian
  - Name has changed to Tidal-Hifi

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>

* Disable background throttling (#171)

* disable background throttling for consistent setInterval

* add disable throttle as config option

* 4.3.0

* Check if app is default protocol client before setting

Co-authored-by: Rick van Lieshout <info@rickvanlieshout.com>
Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>
Co-authored-by: Cukmekerb <cukmekerb@gmail.com>
Co-authored-by: Brecht Yperman <brecht.yperman@trustbuilder.com>

* docs

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>
Co-authored-by: Brecht Yperman <brecht@yperman.eu>
Co-authored-by: Cukmekerb <cukmekerb@gmail.com>
Co-authored-by: Brecht Yperman <brecht.yperman@trustbuilder.com>
2022-10-24 11:38:00 +02:00
d161a68c95 4.3.0 2022-10-05 19:44:04 +02:00
Cukmekerb
9de8cea50e Disable background throttling (#171)
* disable background throttling for consistent setInterval

* add disable throttle as config option
2022-10-05 19:38:01 +02:00
5f330a7c48 Release of settings window and desktop file fixes (#169)
* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

* Redesign of the settings window (#168)

* Pr dest (#166)

* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>

* Redesign of the settings window

* changed sass to scss, fixed color of switches and disabled rounded corners

Co-authored-by: Rick van Lieshout <info@rickvanlieshout.com>

* - icon is set to new static path based on Arch/Debian
  - Name has changed to Tidal-Hifi

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>
2022-09-25 12:50:41 +02:00
732710c3ef Pr dest (#166)
* Update configuration of the desktop file (#165)

* - Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"

Co-authored-by: Ivo Šmerek <ivo97@centrum.cz>
2022-09-11 22:54:08 +02:00
4941aae950 Bugfix/4.1.1 (#161)
* - Fixed `cannot read property of undefined` error because of not passing mainWindow around.
- vincens2005, fixed inconsistent auto muting

* Fix inconsistent auto-muting (#159)

* fix muting sometimes not working

* fix inconsistent unmuting

* fix bad code in inconsistent muting fig

Co-authored-by: Cukmekerb <cukmekerb@gmail.com>
2022-08-23 21:20:46 +02:00
1439a11969 Feature/4.1.0 (#156)
* added protocol handler

* Switched icon strategies to fix bugs with icons

* fixed tray icon issues

* fixed about :)

* Fixed playback, mpris and API issues
2022-08-07 16:05:48 +02:00
Tomasz Hołubowicz
3a3e0e1a2d Add instruction for installation via Nix (#153) 2022-07-05 16:41:12 +02:00
fa9ab22867 - Updated build config to make use of a base file that doesn't build anything.
- This fixes the issue of unwanted extra build targets that were introduced with the electron-builder update
2022-06-25 21:45:30 +02:00
207a61d199 4.0.0 with electron 19.0.5 release 2022-06-23 17:06:17 +02:00
Tomasz Hołubowicz
7b18322e17 Bump electron version (#152) 2022-06-23 17:00:35 +02:00
8f47756244 fixed hardware media flag upadting gpu rasterization options 2022-06-18 10:40:31 +02:00
Marie
cdcf9431bf Fix bugs related to media info (#150)
* Fix duration time element

* change interval time from 200 to 500
2022-06-18 10:26:21 +02:00
374f3da740 Added a separate advanced options settings panel with flags
- Added gpu-rasterization flag

config setting `disableHardwareMediaKeys` moved to `flags.disableHardwareMediaKeys`, it will be migrated automatically
2022-05-07 18:13:36 +02:00
3965ada0a2 Add link to flathub 2022-04-23 23:18:08 +02:00
79ff02d06c Electron 15, single-instance-lock and setting to disable hardwareMediaKeyHandling (#134)
* Update to Electron 15 and add Flatpak to README (#131)

* Update functional

* Change to 15

* Replace window.hide, window.restart with counterparts

* Fix openexternal by doing default JS

* Change mouse to pointer for <a> elements

* turn mouse to pointer for exit button

* Fix deprecation of Audio for AudioVideo

* Made a small mistake

* Add Flatpak to readme

* 3.0.0 prep

* Added setting to disable multiple tidal-hifi windows (defaults to true)

fixes #121

* Added setting to disable HardwareMediaKeyHandling (defaults to false)

fixes #133

Co-authored-by: Marie <marie@kaifa.ch>
2022-04-23 22:59:32 +02:00
7b2afd2290 added separate build-unpacked config 2022-04-22 00:21:05 +02:00
5fde20ace1 hotfix, downgrade packaged version to 8.5.2 2022-04-21 23:58:48 +02:00
96 changed files with 9419 additions and 7277 deletions

16
.drone.yml Normal file
View File

@@ -0,0 +1,16 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:19.4.0
commands:
- npm install
- name: build_with_linux
image: node:19.4.0
commands:
- apt-get update && apt-get upgrade -y
- apt-get install -y libarchive-tools rpm
- npm run build-unpacked

View File

@@ -10,6 +10,10 @@ insert_final_newline = true
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[**.ts]
indent_style = space
indent_size = 2
[**.json] [**.json]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

16
.eslintrc Normal file
View File

@@ -0,0 +1,16 @@
{
"root": true,
"env": {
"node": true,
"browser": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}

View File

@@ -5,6 +5,10 @@ on:
branches-ignore: branches-ignore:
- master - master
- develop - develop
pull_request:
branches-ignore:
- master
- develop
jobs: jobs:
build_on_linux: build_on_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,17 +20,17 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
build_on_mac: build_on_mac:
runs-on: macOS-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
@@ -36,6 +40,6 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build

View File

@@ -5,6 +5,10 @@ on:
branches: branches:
- master - master
- develop - develop
pull_request:
branches:
- master
jobs: jobs:
build_on_linux: build_on_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,7 +20,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
@@ -25,12 +29,12 @@ jobs:
path: dist/ path: dist/
build_on_mac: build_on_mac:
runs-on: macOS-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
@@ -44,7 +48,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master

10
.gitignore vendored
View File

@@ -7,3 +7,13 @@ build/linux/arch/*
!build/linux/arch/.SRCINFO !build/linux/arch/.SRCINFO
!build/linux/arch/tidal-hifi.desktop !build/linux/arch/tidal-hifi.desktop
!build/linux/arch/install.sh !build/linux/arch/install.sh
*.css
*.css.map
# JetBrains IDE configuration
.idea
ts-dist/**
ts-dist
themes
!src/themes
.sass-cache

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
19.8.1

13
.stylelintrc.json Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": [
"stylelint-prettier"
],
"extends": [
"stylelint-config-standard-scss"
],
"rules": {
"prettier/prettier": true,
"scss/at-extend-no-missing-placeholder": null,
"no-descending-specificity": null
}
}

19
.vscode/settings.json vendored
View File

@@ -1,3 +1,20 @@
{ {
"cSpell.words": ["hifi", "rescrobbler", "widevine"] "cSpell.words": [
"Brainz",
"Castlabs",
"flac",
"Flatpak",
"geqnfr",
"hifi",
"listenbrainz",
"playpause",
"rescrobbler",
"scrobble",
"scrobbling",
"Songwhip",
"trackid",
"tracklist",
"widevine",
"xesam"
]
} }

View File

@@ -4,6 +4,138 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 5.5.0
- ListenBrainz integration added (thanks @Mar0xy)
## 5.4.0
- Removed Windows builds (from publishes) as they don't work anymore.
- Added [Songwhip](https://songwhip.com/) integration
- Fixed bug with several hotkeys not working due to Tidal's HTML/css changes
- [DEV]:
- added a logger to log into STDout
- added "watchStart" which will automatically restart electron when it detects a source code change
- added "listen.tidal.com-parsing-scripts" folder with a script to verify whether all elements (in the main preload.ts) are present on the page
## 5.3.0
- SPKChaosPhoenix updated the beautiful Tokyo Night theme:
![](./docs/images/tokyo-night.png)
## 5.2.0
- moved from Javascript to Typescript for all files
- use `npm run watch` to watch for changes & recompile typescript and sass files
- Added support for theming the application
- Added drone build file use `drone exec` or drone.ci to build it
## 5.1.0
### New features
- Added proper updates through the MediaSession API
- You can now add custom CSS in the "advanced" settings tab
- You can now configure the updateFrequency in the settings window
- Default value is set to 500 and will overwrite the hardcoded value of 100
### Fixes
- Any songs **including** an artist listed in the `skipped artists` setting will now be skipped even if the song is a collaboration.
- Linux desktop icons have been fixed. See [#222](https://github.com/Mastermindzh/tidal-hifi/pull/222) for details.
## 5.0.0
- Replaced "muting artists" with a full implementation of an Adblock mechanism
> Disabled audio & visual ads, unlocked lyrics, suggested track, track info, unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- @thanasistrisp updated Electron to 24.1.2 and fixed the tray bug :)
## 4.4.0
- Updated shortcut hint on the menubar to reflect the new `ctrl+=` shortcut.
- Reverted icon path to `icon.png` instead of the hardcoded linux path.
- Add support to autoHide the menubar and showing it with the `alt` key.
- Move the quit command from the system sub-menu to the main menu
- Added single click focus/show on the tray icon
- Doesn't work on all platforms. Nothing I can do about that unfortunately!
- Added a list of artists to automatically skip.
- I don't like the vast majority of dutch music so I added one of them to my list to test: [./docs/no-dutch-music.mp4](./docs/no-dutch-music.mp4)
## 4.3.1
- fix: App always requests a default-url-handler-scheme change on start
## 4.3.0
- Added a setting to disable background throttling ([docs](https://www.electronjs.org/docs/latest/api/browser-window))
## 4.2.0
- New settings window by BlueManCZ
- Fixed the desktop files in electron-builder
- icon is set to new static path based on Arch/Debian
- Name has changed to Tidal-Hifi
## 4.1.2
- Changed the category of the desktop file to AudioVideo
- Changed desktop file name to "TIDAL Hi-Fi"
## 4.1.1
- Fixed `cannot read property of undefined` error because of not passing mainWindow around.
- vincens2005, fixed inconsistent auto muting
## 4.1.0
- Added `tidal://` protocol support
- Switched icon strategies to fix bugs with icons
- Fixed tray icon bugs
- Menu now shows in KDE as well
- Toggle window is supported from tray icon
- regular click is still ignored, see [this issue](https://github.com/electron/electron/issues/6773)
- Fixed about tab not showing
- Fixed playback, mpris and API issues
## 4.0.1
- Updated build config to make use of a base file that doesn't build anything.
- This fixes the issue of unwanted extra build targets that were introduced with the electron-builder update
## 4.0.0
- Updated to Electron 19.0.5
## 3.1.1
- Media update timeout set to 500 instead of 200
- Updated property name for duration because of a tidal update
- flag for "disable hardware media keys" now working again
## 3.1.0
- Added a separate advanced options settings panel with flags
- Added gpu-rasterization flag
- config setting `disableHardwareMediaKeys` moved to `flags.disableHardwareMediaKeys`, it will be migrated automatically
## 3.0.0
- Updated to Electron 15
- Fixed the develop "build-unpacked" command
- Added setting to disable multiple tidal-hifi windows (defaults to true)
- Added setting to disable HardwareMediaKeyHandling (defaults to false)
## 2.8.2
- Updated dependencies
- Downgraded packaged version of electron to 8.5.2, doesn't seem to like a newer build
- Fixed the annoying (and useless) terminal warning about `allowRendererProcessReuse`
## 2.8.1 ## 2.8.1
- Mar0xy fixed some build issues (thanks!) - Mar0xy fixed some build issues (thanks!)

172
README.md
View File

@@ -1,57 +1,103 @@
<h1> # Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
Tidal-hifi
<img src = "./build/icon.png" height="40" align="right" /> ![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg) [![github builds](https://github.com/mastermindzh/tidal-hifi/actions/workflows/build.yml/badge.svg)](https://github.com/Mastermindzh/tidal-hifi/actions) [![Build Status](https://ci.mastermindzh.tech/api/badges/Mastermindzh/tidal-hifi/status.svg)](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [![Discord logo](./docs/images/discord.png)](https://discord.gg/yhNwf4v4He)
</h1>
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine. The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
![tidal-hifi preview](./docs/preview.png) ![tidal-hifi preview](./docs/images/preview.png)
## Table of contents ## Table of Contents
<!-- toc --> <!-- toc -->
- [Installation](#installation) - [Tidal-hifi](#tidal-hifi)
- [Using releases](#using-releases) - [Table of Contents](#table-of-contents)
- [Snap install](#snap-install) - [Features](#features)
- [Arch Linux](#arch-linux) - [Contributions](#contributions)
- [Using source](#using-source) - [Why did I create tidal-hifi?](#why-did-i-create-tidal-hifi)
- [features](#features) - [Why not extend existing projects?](#why-not-extend-existing-projects)
- [Integrations](#integrations) - [Installation](#installation)
- [not included](#not-included) - [Dependencies](#dependencies)
- [Known bugs](#known-bugs) - [Using releases](#using-releases)
- [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround) - [Snap](#snap)
- [Why](#why) - [Arch Linux](#arch-linux)
- [Why not extend existing projects?](#why-not-extend-existing-projects) - [Flatpak](#flatpak)
- [Special thanks to...](#special-thanks-to) - [Nix](#nix)
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont) - [Using source](#using-source)
- [Images](#images) - [Integrations](#integrations)
- [settings window](#settings-window) - [Known bugs](#known-bugs)
- [user setups](#user-setups) - [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround)
- [DRM not working on Windows](#drm-not-working-on-windows)
- [Special thanks to](#special-thanks-to)
- [Donations](#donations)
- [Images](#images)
- [Settings window](#settings-window)
- [User setups](#user-setups)
<!-- tocstop --> <!-- tocstop -->
## Features
- HiFi playback
- Notifications
- Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- Songwhip.com integration (hotkey `ctrl + w`)
- API for status and playback
- Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- Custom [integrations](#integrations)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
## Contributions
To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals.
- ![Discord logo](./docs/images/discord.png) [Join the Discord server](https://discord.gg/yhNwf4v4He)
## Why did I create tidal-hifi?
I moved from Spotify over to Tidal and found Linux support to be lacking.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
### Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
- Lack of maintainers/developers. (no hotfixes, no issues being handled etc)
- Most are simple web wrappers, not my cup of tea.
- Some are DE-oriented. I want this to work on WM's too.
- None have Widevine working at the moment
Sometimes it's just easier to start over, cover my own needs and after that making it available to the public :)
## Installation ## Installation
### Dependencies
Note that you **need** a notification library such as [libnotify](https://github.com/GNOME/libnotify) or [dunst](https://github.com/dunst-project/dunst) for the software to work properly.
### Using releases ### Using releases
Various packaged versions of the software are available on the [releases](https://github.com/Mastermindzh/tidal-hifi/releases) tab. Various packaged versions of the software are available on the [releases](https://github.com/Mastermindzh/tidal-hifi/releases) tab.
#### Snap install ### Snap
To install with `snap` you need to download the pre-packaged snap-package from this repository, found under releases: To install with `snap` you need to download the pre-packaged snap-package from this repository, found under releases:
1. Download 1. Download
```sh ```sh
wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap
``` ```
2. Install 2. Install
```sh ```sh
snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap
``` ```
### Arch Linux ### Arch Linux
@@ -61,6 +107,22 @@ Arch Linux users can use the AUR to install tidal-hifi:
trizen tidal-hifi-bin trizen tidal-hifi-bin
``` ```
### Flatpak
To install via [Flatpak](https://flathub.org/apps/details/com.mastermindzh.tidal-hifi) run the following command:
```sh
flatpak install flathub com.mastermindzh.tidal-hifi
```
### Nix
To install with Nix run the following command:
```sh
nix-env -iA nixpkgs.tidal-hifi
```
### Using source ### Using source
To install and work with the code on this project follow these steps: To install and work with the code on this project follow these steps:
@@ -70,30 +132,19 @@ To install and work with the code on this project follow these steps:
- npm install - npm install
- npm start - npm start
## features
- HiFi playback
- Notifications
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- API for status and playback
- [Mute artists automatically (defaults to "Tidal")]("./docs/muting-artists.md")
- Custom [integrations](#integrations)
- [Settings feature](./docs/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
## Integrations ## Integrations
Tidal-hifi comes with several integrations out of the box. tidal-hifi comes with several integrations out of the box.
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab. You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
![integrations menu, showing a list of integrations](./docs/integrations.png) ![integrations menu, showing a list of integrations](./docs/images/integrations.png)
It currently includes: It currently includes:
- MPRIS - MPRIS media player controls/status - MPRIS - MPRIS media player controls/status
- Discord - Shows what you're listening to on Discord. - Discord - Shows what you're listening to on Discord.
### not included Not included:
- [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit) - [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit)
@@ -103,42 +154,29 @@ It currently includes:
The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4). The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4).
However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled). However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled).
For now that will be the default workaround. For now, that will be the default workaround.
## Why #### DRM not working on Windows
I moved from Spotify over to Tidal and found Linux support to be lacking. Most Windows users run into DRM issues when trying to use tidal-hifi.
Nothing I can do about that I'm afraid... Tidal is working on removing/changing DRM so when they finish with that we can give it another shot.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it. ## Special thanks to
## Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
- Lack of a maintainers/developers. (no hotfixes, no issues being handled etc)
- Most are simple web wrappers, not my cup of tea.
- Some are DE oriented. I want this to work on WM's too.
- None have widevine working at the moment
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)
## Special thanks to...
- [Castlabs](https://castlabs.com/) - [Castlabs](https://castlabs.com/)
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID) For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)
## Buy me a coffee? Please don't ## Donations
Instead spend some money on a charity I care for: [kwf.nl](https://www.kwf.nl/donatie/donation). You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh)
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
## Images ## Images
### settings window ### Settings window
![settings window](./docs/settings-preview.png) ![settings window](./docs/images/settings-preview.png)
### user setups ### User setups
Some of our users are kind enough to share their usage pictures. Some of our users are kind enough to share their usage pictures.
If you want to see them or possibly even add one please do so in the following issue: [#3 - image thread](https://github.com/Mastermindzh/tidal-hifi/issues/3). If you want to see them or possibly even add one please do so in the following issue: [#3 - image thread](https://github.com/Mastermindzh/tidal-hifi/issues/3).

BIN
assets/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/icons/22x22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
assets/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/icons/384x384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
assets/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,42 @@
appId: com.rickvanlieshout.tidal-hifi
electronVersion: 24.1.2
electronDownload:
version: 24.1.2+wvcus
mirror: https://github.com/castlabs/electron-releases/releases/download/v
snap:
plugs:
- default
- screen-inhibit-control
extraResources:
- "themes/**"
linux:
category: AudioVideo
icon: assets/icons
target:
- dir
executableName: tidal-hifi
desktop:
Encoding: UTF-8
Name: TIDAL Hi-Fi
GenericName: TIDAL Hi-Fi
Comment: The web version of listen.tidal.com running in electron with hifi support thanks to widevine.
Icon: tidal-hifi
StartupNotify: true
Terminal: false
Type: Application
Categories: Network;Application;AudioVideo;Audio;Video
StartupWMClass: tidal-hifi
X-PulseAudio-Properties: media.role=music
MimeType: x-scheme-handler/tidal;
mac:
category: public.app-category.entertainment
win:
icon: icon.png
artifactName: "tidalhifi"
appId: com.rickvanlieshout.tidalhifi
executableName: tidalhifi
protocols:
name: "tidal"
role: "Viewer"
schemes: ["tidal"]

View File

@@ -1,6 +1,4 @@
extends: ./build/electron-builder.yml extends: ./build/electron-builder.base.yml
linux: linux:
category: Audio
icon: ./assets/icon.png
target: target:
- deb - deb

View File

@@ -1,6 +1,4 @@
extends: ./build/electron-builder.yml extends: ./build/electron-builder.base.yml
linux: linux:
category: Audio
icon: ./assets/icon.png
target: target:
- pacman - pacman

View File

@@ -1,6 +1,4 @@
extends: ./build/electron-builder.yml extends: ./build/electron-builder.base.yml
linux: linux:
category: Audio target:
icon: ./assets/TIDAL.icns - rpm
target:
- rpm

View File

@@ -1,6 +1,4 @@
extends: ./build/electron-builder.yml extends: ./build/electron-builder.base.yml
linux: linux:
category: Audio
icon: ./assets/icon.png
target: target:
- snap - snap

View File

@@ -0,0 +1,4 @@
extends: ./build/electron-builder.base.yml
linux:
target:
- dir

View File

@@ -1,14 +1,5 @@
appId: com.rickvanlieshout.tidal-hifi extends: ./build/electron-builder.base.yml
electronVersion: 16.0.4
electronDownload:
version: 16.0.4+wvcus.2
mirror: https://github.com/castlabs/electron-releases/releases/download/v
snap:
plugs:
- default
- screen-inhibit-control
linux: linux:
category: Audio
target: target:
- pacman - pacman
- tar.gz - tar.gz
@@ -17,24 +8,9 @@ linux:
- AppImage - AppImage
- snap - snap
- freebsd - freebsd
executableName: tidal-hifi
desktop:
Encoding: UTF-8
Name: tidal-hifi
GenericName: tidal-hifi
Comment: The web version of listen.tidal.com running in electron with hifi support thanks to widevine.
Icon: assets/icon.png
StartupNotify: true
Terminal: false
Type: Application
Categories: Network;Application;Audio;Video
StartupWMClass: tidal-hifi
X-PulseAudio-Properties: media.role=music
mac:
category: public.app-category.entertainment
win: win:
target: msi target: msi
icon: build/icon.png icon: icon.png
artifactName: "tidalhifi" artifactName: "tidalhifi"
appId: com.rickvanlieshout.tidalhifi appId: com.rickvanlieshout.tidalhifi
executableName: tidalhifi executableName: tidalhifi

BIN
build/icon-inverted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
build/icon.icns Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/images/customcss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 800 KiB

After

Width:  |  Height:  |  Size: 800 KiB

View File

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 726 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

BIN
docs/images/theming.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/images/tokyo-night.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -1,11 +0,0 @@
# Muting artists
If you feel that some of your music is embarrassing for others you can mute specific artists in the settings window.
This functionality is inspired by the [adblock ticket](https://github.com/Mastermindzh/tidal-hifi/issues/112), and whilst I personally feel you should simply buy Tidal, I also believe in muting sound that you don't want to hear.
Anyway, to block an artist, open the settings window (see image below) and enter a list of artists in the textarea as seen below.
Don't forget to turn the feature on and Tidal-hifi will automatically mute the player whenever that artist is playing.
This will allow you to skip the song without anyone noticing. (you can always say "no idea, it seems to have no audio").
![muted artists settings window](./settings-muted-artists.png)

BIN
docs/no-dutch-music.mp4 Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

38
docs/theming.md Normal file
View File

@@ -0,0 +1,38 @@
# Theming tidal-hifi
## Table of contents
<!-- toc -->
- [Theming tidal-hifi](#theming-tidal-hifi)
- [Table of contents](#table-of-contents)
- [Custom CSS](#custom-css)
- [config](#config)
- [Warning! Themes might break](#warning-themes-might-break)
<!-- tocstop -->
By default tidal-hifi comes with a few themes.
You can select these in the settings window under the theming tab as shown below.
![Settings window with the theming tab opened](./images/theming.png)
## Custom CSS
The custom CSS will be added to the HTML document last.
This means that it will overwrite any existing CSS, even that of themes, unless the original has an access modifier such as `$important`.
![settings window on the theming tab with a custom CSS override](./images/customcss.png)
## config
The theme selector and customCSS are stored in the config file.
The custom CSS is stored as a list of lines.
![settings window on the theming tab next to the config file](./images/customcss-config.png)
## Warning! Themes might break
Themes might break at any point. Tidal changes their webpage structure a ton (they probably generate classNames and don't provide roles/ids/attributes.)
If one breaks you can create an Issue on GitHub or ask for assistance in the [Discord channel](https://discord.gg/yhNwf4v4He).

View File

@@ -0,0 +1,43 @@
// for some dumb reason `listen.tidal.com` has disabled console.log
// we can simply return an array with values though...
// run this on a playlist or mix page and observe the result
// NOTE: play & pause can't live together so one or the other will throw an error
(() => {
let elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
};
let results = [];
Object.entries(elements).forEach(([key, value]) => {
const returnValue = document.querySelector(`${value}`);
if (!returnValue) {
results.push(`element ${key} not found`);
}
});
return results;
})();

11440
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,75 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "2.8.1", "version": "5.5.0",
"description": "Tidal on Electron with widevine(hifi) support", "description": "Tidal on Electron with widevine(hifi) support",
"main": "src/main.js", "main": "ts-dist/main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron --inspect=0.0.0.0:5858 .",
"build": "electron-builder --publish=never -c ./build/electron-builder.yml", "watchStart": "nodemon dist -x \"npm run start\"",
"build-deb": "electron-builder --publish=never -c ./build/electron-builder.deb.yml", "compile": "tsc && npm run sass-and-copy",
"build-rpm": "electron-builder --publish=never -c ./build/electron-builder.rpm.yml", "watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"build-snap": "electron-builder --publish=never -c ./build/electron-builder.snap.yml", "copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
"build-arch": "electron-builder --publish=never -c ./build/electron-builder.pacman.yml", "copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources",
"build-wl": "electron-builder --publish=never -c ./build/electron-builder.yml -wl", "sass-and-copy": "npm run sass && npm run copy-files && npm run copy-themes-dev",
"build-mac": "electron-builder --publish=never -c ./build/electron-builder.yml -m" "build": "npm run builder -- -c ./build/electron-builder.yml",
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
"build-unpacked": "npm run builder -- -c ./build/electron-builder.unpacked.yml",
"build-rpm": "npm run builder -- -c ./build/electron-builder.rpm.yml",
"build-snap": "npm run builder -- -c ./build/electron-builder.snap.yml",
"build-arch": "npm run builder -- -c ./build/electron-builder.pacman.yml",
"build-wl": "npm run builder -- -c ./build/electron-builder.yml -wl",
"build-mac": "npm run builder -- -c ./build/electron-builder.yml -m",
"build-base": "npm run builder -- -c ./build/electron-builder.base.yml",
"prebuilder": "npm run compile",
"builder": "electron-builder --publish=never",
"sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css && sass --no-source-map src/themes:themes",
"style-lint": "npx stylelint **/*.scss",
"style-lint-fix": "npx stylelint --fix **/*.scss"
}, },
"keywords": [ "keywords": [
"electron", "electron",
"hifi", "hifi",
"widevine", "widevine",
"linux" "linux",
"drm",
"castlabs"
], ],
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)", "author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
"homepage": "https://github.com/Mastermindzh/tidal-hifi", "homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.10",
"axios": "^1.4.0",
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"electron-store": "^8.0.1", "electron-store": "^8.1.0",
"express": "^4.17.1", "express": "^4.18.2",
"hotkeys-js": "^3.8.7", "hotkeys-js": "^3.11.2",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"request": "^2.88.2" "request": "^2.88.2",
"sass": "^1.64.1"
}, },
"devDependencies": { "devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0", "@mastermindzh/prettier-config": "^1.0.0",
"electron": "git+https://github.com/castlabs/electron-releases.git#v16.0.4+wvcus.2", "@types/discord-rpc": "^4.0.5",
"electron-builder": "^22.14.5", "@types/express": "^4.17.17",
"electron-reload": "^1.5.0", "@types/node": "^20.4.4",
"prettier": "^2.5.0" "@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"copyfiles": "^2.4.1",
"electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus",
"electron-builder": "^24.4.0",
"eslint": "^8.45.0",
"js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0",
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
"stylelint": "^15.10.2",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-prettier": "^4.0.0",
"tsc-watch": "^6.0.4",
"typescript": "^5.1.6"
}, },
"prettier": "@mastermindzh/prettier-config" "prettier": "@mastermindzh/prettier-config"
} }

4
src/constants/flags.ts Normal file
View File

@@ -0,0 +1,4 @@
export const flags: { [key: string]: { flag: string; value?: string }[] } = {
gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }],
disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }],
};

View File

@@ -1,4 +1,4 @@
const globalEvents = { export const globalEvents = {
play: "play", play: "play",
pause: "pause", pause: "pause",
playPause: "playPause", playPause: "playPause",
@@ -10,6 +10,6 @@ const globalEvents = {
showSettings: "showSettings", showSettings: "showSettings",
storeChanged: "storeChanged", storeChanged: "storeChanged",
error: "error", error: "error",
whip: "whip",
log: "log",
}; };
module.exports = globalEvents;

View File

@@ -1,9 +1,7 @@
const globalEvents = require("./globalEvents"); import { globalEvents } from "./globalEvents";
const mediaKeys = { export const mediaKeys = {
MediaPlayPause: globalEvents.playPause, MediaPlayPause: globalEvents.playPause,
MediaNextTrack: globalEvents.next, MediaNextTrack: globalEvents.next,
MediaPreviousTrack: globalEvents.previous, MediaPreviousTrack: globalEvents.previous,
}; };
module.exports = mediaKeys;

View File

@@ -8,27 +8,43 @@
* }, * },
* windowBounds: { width: 800, height: 600 }, * windowBounds: { width: 800, height: 600 },
*/ */
const settings = { export const settings = {
notifications: "notifications", adBlock: "adBlock",
api: "api", api: "api",
menuBar: "menuBar",
playBackControl: "playBackControl",
muteArtists: "muteArtists",
mutedArtists: "mutedArtists",
apiSettings: { apiSettings: {
root: "apiSettings", root: "apiSettings",
port: "apiSettings.port", port: "apiSettings.port",
}, },
mpris: "mpris", customCSS: "customCSS",
disableBackgroundThrottle: "disableBackgroundThrottle",
disableHardwareMediaKeys: "disableHardwareMediaKeys",
enableCustomHotkeys: "enableCustomHotkeys", enableCustomHotkeys: "enableCustomHotkeys",
trayIcon: "trayIcon",
enableDiscord: "enableDiscord", enableDiscord: "enableDiscord",
ListenBrainz: {
root: "ListenBrainz",
enabled: "ListenBrainz.enabled",
api: "ListenBrainz.api",
token: "ListenBrainz.token",
},
flags: {
root: "flags",
disableHardwareMediaKeys: "flags.disableHardwareMediaKeys",
gpuRasterization: "flags.gpuRasterization",
},
menuBar: "menuBar",
minimizeOnClose: "minimizeOnClose",
mpris: "mpris",
notifications: "notifications",
playBackControl: "playBackControl",
singleInstance: "singleInstance",
skipArtists: "skipArtists",
skippedArtists: "skippedArtists",
theme: "theme",
trayIcon: "trayIcon",
updateFrequency: "updateFrequency",
windowBounds: { windowBounds: {
root: "windowBounds", root: "windowBounds",
width: "windowBounds.width", width: "windowBounds.width",
height: "windowBounds.height", height: "windowBounds.height",
}, },
minimizeOnClose: "minimizeOnClose",
}; };
module.exports = settings;

View File

@@ -1,4 +0,0 @@
module.exports = {
playing: "playing",
paused: "paused",
};

3
src/constants/values.ts Normal file
View File

@@ -0,0 +1,3 @@
export default {
name: "tidal-hifi",
};

1
src/declarations.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "mpris-service";

View File

@@ -0,0 +1,134 @@
import axios from "axios";
import { ipcRenderer } from "electron";
import Store from "electron-store";
import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
import { StoreData } from "./models/storeData";
const ListenBrainzStore = new Store({ name: "listenbrainz" });
export const ListenBrainzConstants = {
oldData: "oldData",
};
export class ListenBrainz {
/**
* Create the object to store old information in the Store :)
* @param title
* @param artists
* @param duration
* @returns data passed along in an object + a "listenedAt" key with the current time
*/
private static constructStoreData(title: string, artists: string, duration: number): StoreData {
return {
listenedAt: Math.floor(new Date().getTime() / 1000),
title,
artists,
duration,
};
}
/**
* Call the ListenBrainz API and create playing now payload and scrobble old song
* @param title
* @param artists
* @param status
* @param duration
*/
public static async scrobble(
title: string,
artists: string,
status: string,
duration: number
): Promise<void> {
try {
if (status === MediaStatus.paused) {
return;
} else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
const playing_data = {
listen_type: "playing_now",
payload: [
{
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "tidal.com",
duration: duration,
},
artist_name: artists,
track_name: title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
playing_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
if (!oldData) {
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
} else {
if (oldData.title !== title) {
// This constructs the data required to scrobble the data after the song finishes
const scrobble_data = {
listen_type: "single",
payload: [
{
listened_at: oldData.listenedAt,
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "listen.tidal.com",
duration: oldData.duration,
},
artist_name: oldData.artists,
track_name: oldData.title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
scrobble_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
}
}
}
} catch (error) {
const logger = new Logger(ipcRenderer);
logger.log(JSON.stringify(error));
}
}
}
export { ListenBrainzStore };

View File

@@ -0,0 +1,9 @@
/**
* Data saved for ListenBrainz
*/
export interface StoreData {
listenedAt: number;
title: string;
artists: string;
duration: number;
}

32
src/features/logger.ts Normal file
View File

@@ -0,0 +1,32 @@
import { IpcMain, IpcRenderer } from "electron";
import { globalEvents } from "../constants/globalEvents";
export class Logger {
/**
*
* @param ipcRenderer renderer IPC client so we can send messages to the main thread
*/
constructor(private ipcRenderer: IpcRenderer) {}
/**
* Subscribe to watch for logs from the IPC client
* @param ipcMain main thread IPC client so we can subscribe to events
*/
public static watch(ipcMain: IpcMain) {
ipcMain.on(globalEvents.log, (event, content, object) => {
console.log(content, JSON.stringify(object, null, 2));
});
}
/**
* Log content to STDOut
* @param content
* @param object js(on) object that will be prettyPrinted
*/
public log(content: string, object: object = {}) {
if (this.ipcRenderer) {
this.ipcRenderer.send(globalEvents.log, { content, object });
}
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
}
}

View File

@@ -0,0 +1,21 @@
import { ServiceLinks } from "./ServiceLinks";
export interface Artist {
type: string;
id: number;
path: string;
name: string;
sourceUrl: string;
sourceCountry: string;
url: string;
image: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
serviceIds: { [key: string]: string };
orchardId: string;
spotifyId: string;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
description: string;
}

View File

@@ -0,0 +1,4 @@
export interface ServiceLinks {
link: string;
countries: string[];
}

View File

@@ -0,0 +1,27 @@
import { Artist } from "./Artist";
import { ServiceLinks } from "./ServiceLinks";
export interface WhippedResult {
status: string;
data: {
item: {
type: string;
id: number;
path: string;
name: string;
url: string;
sourceUrl: string;
sourceCountry: string;
releaseDate: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
image: string;
isrc: string;
isExplicit: boolean;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
artists: Artist[];
};
};
}

View File

@@ -0,0 +1,32 @@
import { WhippedResult } from "./models/whip";
import axios from "axios";
export class Songwhip {
/**
* Call the songwhip API and create a shareable songwhip page
* @param currentUrl
* @returns
*/
public static async whip(currentUrl: string): Promise<WhippedResult> {
try {
const response = await axios.post("https://songwhip.com/api/songwhip/create", {
url: currentUrl,
// doesn't actually matter.. returns everything the same way anyway
country: "NL",
});
return response.data;
} catch (error) {
console.log(JSON.stringify(error));
}
}
/**
* Transform a songwhip response into a shareable url
* @param response
* @returns
*/
public static getWhipUrl(response: WhippedResult) {
return `https://songwhip.com${response.data.item.url}`;
}
}

View File

@@ -1,150 +0,0 @@
const { app, BrowserWindow, globalShortcut, ipcMain } = require("electron");
const {
settings,
store,
createSettingsWindow,
showSettingsWindow,
closeSettingsWindow,
hideSettingsWindow,
} = require("./scripts/settings");
const { addTray, refreshTray } = require("./scripts/tray");
const { addMenu } = require("./scripts/menu");
const path = require("path");
const tidalUrl = "https://listen.tidal.com";
const expressModule = require("./scripts/express");
const mediaKeys = require("./constants/mediaKeys");
const mediaInfoModule = require("./scripts/mediaInfo");
const discordModule = require("./scripts/discord");
const globalEvents = require("./constants/globalEvents");
let mainWindow;
let icon = path.join(__dirname, "../assets/icon.png");
/**
* Enable live reload in development builds
*/
if (!app.isPackaged) {
require("electron-reload")(`${__dirname}`, {
electron: require(`${__dirname}/../node_modules/electron`),
});
}
/**
* Fix Display Compositor issue.
*/
app.commandLine.appendSwitch('disable-seccomp-filter-sandbox');
/**
* Update the menuBarVisbility according to the store value
*
*/
function syncMenuBarWithStore() {
mainWindow.setMenuBarVisibility(store.get(settings.menuBar));
}
function createWindow(options = {}) {
// Create the browser window.
mainWindow = new BrowserWindow({
x: options.x,
y: options.y,
width: store && store.get(settings.windowBounds.width),
height: store && store.get(settings.windowBounds.height),
icon,
tray: true,
backgroundColor: options.backgroundColor,
webPreferences: {
affinity: "window",
preload: path.join(__dirname, "preload.js"),
plugins: true,
devTools: true, // I like tinkering, others might too
enableRemoteModule: true,
},
});
syncMenuBarWithStore();
// load the Tidal website
mainWindow.loadURL(tidalUrl);
// run stuff after first load
mainWindow.webContents.once("did-finish-load", () => {});
mainWindow.on("close", function (event) {
if (!app.isQuiting && store.get(settings.minimizeOnClose)) {
event.preventDefault();
mainWindow.hide();
refreshTray(mainWindow);
}
return false;
});
// Emitted when the window is closed.
mainWindow.on("closed", function () {
closeSettingsWindow();
app.quit();
});
mainWindow.on("resize", () => {
let { width, height } = mainWindow.getBounds();
store.set(settings.windowBounds.root, { width, height });
});
}
function addGlobalShortcuts() {
Object.keys(mediaKeys).forEach((key) => {
globalShortcut.register(`${key}`, () => {
mainWindow.webContents.send("globalEvent", `${mediaKeys[key]}`);
});
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", () => {
createWindow();
addMenu();
createSettingsWindow();
addGlobalShortcuts();
store.get(settings.trayIcon) && addTray({ icon }) && refreshTray();
store.get(settings.api) && expressModule.run(mainWindow);
store.get(settings.enableDiscord) && discordModule.initRPC();
// mainWindow.webContents.openDevTools();
});
app.on("activate", function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
// IPC
ipcMain.on(globalEvents.updateInfo, (event, arg) => {
mediaInfoModule.update(arg);
});
ipcMain.on(globalEvents.hideSettings, (event, arg) => {
hideSettingsWindow();
});
ipcMain.on(globalEvents.showSettings, (event, arg) => {
showSettingsWindow();
});
ipcMain.on(globalEvents.refreshMenuBar, (event, arg) => {
syncMenuBarWithStore();
});
ipcMain.on(globalEvents.storeChanged, (event, arg) => {
syncMenuBarWithStore();
if (store.get(settings.enableDiscord) && !discordModule.rpc) {
discordModule.initRPC();
} else if (!store.get(settings.enableDiscord) && discordModule.rpc) {
discordModule.unRPC();
}
});
ipcMain.on(globalEvents.error, (event, arg) => {
console.log(event);
});

230
src/main.ts Normal file
View File

@@ -0,0 +1,230 @@
import { enable, initialize } from "@electron/remote/main";
import {
BrowserWindow,
app,
components,
globalShortcut,
ipcMain,
protocol,
session,
} from "electron";
import path from "path";
import { flags } from "./constants/flags";
import { globalEvents } from "./constants/globalEvents";
import { mediaKeys } from "./constants/mediaKeys";
import { initRPC, rpc, unRPC } from "./scripts/discord";
import { startExpress } from "./scripts/express";
import { updateMediaInfo } from "./scripts/mediaInfo";
import { addMenu } from "./scripts/menu";
import {
closeSettingsWindow,
createSettingsWindow,
hideSettingsWindow,
showSettingsWindow,
settingsStore,
} from "./scripts/settings";
import { settings } from "./constants/settings";
import { addTray, refreshTray } from "./scripts/tray";
import { MediaInfo } from "./models/mediaInfo";
import { Songwhip } from "./features/songwhip/songwhip";
import { Logger } from "./features/logger";
const tidalUrl = "https://listen.tidal.com";
initialize();
let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal";
setFlags();
function setFlags() {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flags)) {
if (value) {
flags[key].forEach((flag) => {
console.log(`enabling command line switch ${flag.flag} with value ${flag.value}`);
app.commandLine.appendSwitch(flag.flag, flag.value);
});
}
}
}
/**
* Fix Display Compositor issue.
*/
app.commandLine.appendSwitch("disable-seccomp-filter-sandbox");
}
/**
* Update the menuBarVisibility according to the store value
*
*/
function syncMenuBarWithStore() {
const fixedMenuBar = !!settingsStore.get(settings.menuBar);
mainWindow.autoHideMenuBar = !fixedMenuBar;
mainWindow.setMenuBarVisibility(fixedMenuBar);
}
/**
* Determine whether the current window is the main window
* if singleInstance is requested.
* If singleInstance isn't requested simply return true
* @returns true if singInstance is not requested, otherwise true/false based on whether the current window is the main window
*/
function isMainInstanceOrMultipleInstancesAllowed() {
if (settingsStore.get(settings.singleInstance)) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
return false;
}
}
return true;
}
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
// Create the browser window.
mainWindow = new BrowserWindow({
x: options.x,
y: options.y,
width: settingsStore && settingsStore.get(settings.windowBounds.width),
height: settingsStore && settingsStore.get(settings.windowBounds.height),
icon,
backgroundColor: options.backgroundColor,
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
preload: path.join(__dirname, "preload.js"),
plugins: true,
devTools: true, // I like tinkering, others might too
},
});
enable(mainWindow.webContents);
registerHttpProtocols();
syncMenuBarWithStore();
// load the Tidal website
mainWindow.loadURL(tidalUrl);
if (settingsStore.get(settings.disableBackgroundThrottle)) {
// prevent setInterval lag
mainWindow.webContents.setBackgroundThrottling(false);
}
mainWindow.on("close", function (event: CloseEvent) {
if (settingsStore.get(settings.minimizeOnClose)) {
event.preventDefault();
mainWindow.hide();
refreshTray(mainWindow);
}
return false;
});
// Emitted when the window is closed.
mainWindow.on("closed", function () {
closeSettingsWindow();
app.quit();
});
mainWindow.on("resize", () => {
const { width, height } = mainWindow.getBounds();
settingsStore.set(settings.windowBounds.root, { width, height });
});
}
function registerHttpProtocols() {
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request) => {
mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`);
});
if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) {
app.setAsDefaultProtocolClient(PROTOCOL_PREFIX);
}
}
function addGlobalShortcuts() {
Object.keys(mediaKeys).forEach((key) => {
globalShortcut.register(`${key}`, () => {
mainWindow.webContents.send("globalEvent", `${(mediaKeys as any)[key]}`);
});
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", async () => {
if (isMainInstanceOrMultipleInstancesAllowed()) {
await components.whenReady();
// Adblock
if (settingsStore.get(settings.adBlock)) {
const filter = { urls: ["https://listen.tidal.com/*"] };
session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => {
if (details.url.match(/\/users\/.*\d\?country/)) callback({ cancel: true });
else callback({ cancel: false });
});
}
createWindow();
addMenu(mainWindow);
createSettingsWindow();
addGlobalShortcuts();
if (settingsStore.get(settings.trayIcon)) {
addTray(mainWindow, { icon });
refreshTray(mainWindow);
}
settingsStore.get(settings.api) && startExpress(mainWindow);
settingsStore.get(settings.enableDiscord) && initRPC();
} else {
app.quit();
}
});
app.on("activate", function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
app.on("browser-window-created", (_, window) => {
enable(window.webContents);
});
// IPC
ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => {
updateMediaInfo(arg);
});
ipcMain.on(globalEvents.hideSettings, () => {
hideSettingsWindow();
});
ipcMain.on(globalEvents.showSettings, () => {
showSettingsWindow();
});
ipcMain.on(globalEvents.refreshMenuBar, () => {
syncMenuBarWithStore();
});
ipcMain.on(globalEvents.storeChanged, () => {
syncMenuBarWithStore();
if (settingsStore.get(settings.enableDiscord) && !rpc) {
initRPC();
} else if (!settingsStore.get(settings.enableDiscord) && rpc) {
unRPC();
}
});
ipcMain.on(globalEvents.error, (event) => {
console.log(event);
});
ipcMain.handle(globalEvents.whip, async (event, url) => {
return await Songwhip.whip(url);
});
Logger.watch(ipcMain);

13
src/models/mediaInfo.ts Normal file
View File

@@ -0,0 +1,13 @@
import { MediaStatus } from "./mediaStatus";
export interface MediaInfo {
title: string;
artists: string;
album: string;
icon: string;
status: MediaStatus;
url: string;
current: string;
duration: string;
image: string;
}

View File

@@ -0,0 +1,4 @@
export enum MediaStatus {
playing = "playing",
paused = "paused",
}

12
src/models/options.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface Options {
title: string;
artists: string;
album: string;
status: string;
url: string;
current: string;
duration: string;
"app-name": string;
image: string;
icon: string;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,121 +0,0 @@
let trayIcon,
minimizeOnClose,
mpris,
enableCustomHotkeys,
enableDiscord,
muteArtists,
notifications,
playBackControl,
api,
port,
menuBar,
mutedArtists;
const { store, settings } = require("./../../scripts/settings");
const { ipcRenderer } = require("electron");
const globalEvents = require("./../../constants/globalEvents");
/**
* Sync the UI forms with the current settings
*/
function refreshSettings() {
notifications.checked = store.get(settings.notifications);
playBackControl.checked = store.get(settings.playBackControl);
api.checked = store.get(settings.api);
port.value = store.get(settings.apiSettings.port);
menuBar.checked = store.get(settings.menuBar);
trayIcon.checked = store.get(settings.trayIcon);
mpris.checked = store.get(settings.mpris);
enableCustomHotkeys.checked = store.get(settings.enableCustomHotkeys);
enableDiscord.checked = store.get(settings.enableDiscord);
minimizeOnClose.checked = store.get(settings.minimizeOnClose);
muteArtists.checked = store.get(settings.muteArtists);
mutedArtists.value = store.get(settings.mutedArtists).join("\n");
}
/**
* Open an url in the default browsers
*/
window.openExternal = function (url) {
const { shell } = require("electron");
shell.openExternal(url);
};
/**
* hide the settings window
*/
window.hide = function () {
ipcRenderer.send(globalEvents.hideSettings);
};
/**
* Restart tidal-hifi after changes
*/
window.restart = function () {
const remote = require("electron").remote;
remote.app.relaunch();
remote.app.exit(0);
};
/**
* Bind UI components to functions after DOMContentLoaded
*/
window.addEventListener("DOMContentLoaded", () => {
function get(id) {
return document.getElementById(id);
}
function addInputListener(source, key) {
source.addEventListener("input", function (event, data) {
if (this.value === "on") {
store.set(key, source.checked);
} else {
store.set(key, this.value);
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
function addTextAreaListener(source, key) {
source.addEventListener("input", function (event, data) {
store.set(key, source.value.split("\n"));
ipcRenderer.send(globalEvents.storeChanged);
});
}
ipcRenderer.on("refreshData", () => {
refreshSettings();
});
ipcRenderer.on("goToTab", (_, tab) => {
document.getElementById(tab).click();
});
notifications = get("notifications");
playBackControl = get("playBackControl");
api = get("apiCheckbox");
port = get("port");
menuBar = get("menuBar");
trayIcon = get("trayIcon");
minimizeOnClose = get("minimizeOnClose");
mpris = get("mprisCheckbox");
enableCustomHotkeys = get("enableCustomHotkeys");
enableDiscord = get("enableDiscord");
muteArtists = get("muteArtists");
mutedArtists = get("mutedArtists");
refreshSettings();
addInputListener(notifications, settings.notifications);
addInputListener(playBackControl, settings.playBackControl);
addInputListener(api, settings.api);
addInputListener(port, settings.apiSettings.port);
addInputListener(menuBar, settings.menuBar);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(mpris, settings.mpris);
addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys);
addInputListener(enableDiscord, settings.enableDiscord);
addInputListener(minimizeOnClose, settings.minimizeOnClose);
addInputListener(muteArtists, settings.muteArtists);
addTextAreaListener(mutedArtists, settings.mutedArtists);
});

View File

@@ -0,0 +1,227 @@
import { app } from "@electron/remote";
import { ipcRenderer, shell } from "electron";
import fs from "fs";
import { globalEvents } from "../../constants/globalEvents";
import { settings } from "../../constants/settings";
import { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
let adBlock: HTMLInputElement,
api: HTMLInputElement,
customCSS: HTMLInputElement,
disableBackgroundThrottle: HTMLInputElement,
disableHardwareMediaKeys: HTMLInputElement,
enableCustomHotkeys: HTMLInputElement,
enableDiscord: HTMLInputElement,
gpuRasterization: HTMLInputElement,
menuBar: HTMLInputElement,
minimizeOnClose: HTMLInputElement,
mpris: HTMLInputElement,
notifications: HTMLInputElement,
playBackControl: HTMLInputElement,
port: HTMLInputElement,
singleInstance: HTMLInputElement,
skipArtists: HTMLInputElement,
skippedArtists: HTMLInputElement,
theme: HTMLSelectElement,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement,
enableListenBrainz: HTMLInputElement,
ListenBrainzAPI: HTMLInputElement,
ListenBrainzToken: HTMLInputElement;
function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
const userThemes = getThemeListFromDirectory(`${app.getPath("userData")}/themes`);
let allThemes = [
getOptionsHeader("Built-in Themes"),
new Option("Tidal - Default", "none"),
].concat(getOptions(builtInThemes));
if (userThemes.length >= 1) {
allThemes = allThemes.concat([getOptionsHeader("User Themes")]).concat(getOptions(userThemes));
}
// empty old options
const oldOptions = document.querySelectorAll("#themesList option");
oldOptions.forEach((o) => o.remove());
allThemes.forEach((option) => {
selectElement.add(option, null);
});
}
function handleFileUploads() {
const fileMessage = document.getElementById("file-message");
fileMessage.innerText = "or drag and drop files here";
document.getElementById("theme-files").addEventListener("change", function (e: any) {
Array.from(e.target.files).forEach((file: File) => {
const destination = `${app.getPath("userData")}/themes/${file.name}`;
fs.copyFileSync(file.path, destination, null);
});
fileMessage.innerText = `${e.target.files.length} files successfully uploaded`;
getThemeFiles();
});
}
/**
* Sync the UI forms with the current settings
*/
function refreshSettings() {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
notifications.checked = settingsStore.get(settings.notifications);
playBackControl.checked = settingsStore.get(settings.playBackControl);
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api);
ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token);
}
/**
* Open an url in the default browsers
*/
function openExternal(url: string) {
shell.openExternal(url);
}
/**
* hide the settings window
*/
function hide() {
ipcRenderer.send(globalEvents.hideSettings);
}
/**
* Restart tidal-hifi after changes
*/
function restart() {
app.relaunch();
app.exit();
}
/**
* Bind UI components to functions after DOMContentLoaded
*/
window.addEventListener("DOMContentLoaded", () => {
function get<T = HTMLInputElement>(id: string): T {
return document.getElementById(id) as T;
}
getThemeFiles();
handleFileUploads();
document.getElementById("close").addEventListener("click", hide);
document.getElementById("restart").addEventListener("click", restart);
document.querySelectorAll(".external-link").forEach((elem) =>
elem.addEventListener("click", function (event) {
openExternal((event.target as HTMLElement).getAttribute("data-url"));
})
);
function addInputListener(source: HTMLInputElement, key: string) {
source.addEventListener("input", () => {
if (source.value === "on") {
settingsStore.set(key, source.checked);
} else {
settingsStore.set(key, source.value);
}
// Live update the view for ListenBrainz input, hide if disabled/show if enabled
if (source.value === "on" && source.id === "enableListenBrainz") {
source.checked ? document.getElementById("listenbrainz__options").removeAttribute("hidden") : document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
function addTextAreaListener(source: HTMLInputElement, key: string) {
source.addEventListener("input", () => {
settingsStore.set(key, source.value.split("\n"));
ipcRenderer.send(globalEvents.storeChanged);
});
}
function addSelectListener(source: HTMLSelectElement, key: string) {
source.addEventListener("change", () => {
settingsStore.set(key, source.value);
ipcRenderer.send(globalEvents.storeChanged);
});
}
ipcRenderer.on("refreshData", () => {
refreshSettings();
});
ipcRenderer.on("goToTab", (_, tab) => {
document.getElementById(tab).click();
});
adBlock = get("adBlock");
api = get("apiCheckbox");
customCSS = get("customCSS");
disableBackgroundThrottle = get("disableBackgroundThrottle");
disableHardwareMediaKeys = get("disableHardwareMediaKeys");
enableCustomHotkeys = get("enableCustomHotkeys");
enableDiscord = get("enableDiscord");
gpuRasterization = get("gpuRasterization");
menuBar = get("menuBar");
minimizeOnClose = get("minimizeOnClose");
mpris = get("mprisCheckbox");
notifications = get("notifications");
playBackControl = get("playBackControl");
port = get("port");
theme = get<HTMLSelectElement>("themesList");
trayIcon = get("trayIcon");
skipArtists = get("skipArtists");
skippedArtists = get("skippedArtists");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz");
ListenBrainzAPI = get("ListenBrainzAPI");
ListenBrainzToken = get("ListenBrainzToken");
refreshSettings();
enableListenBrainz.checked ? document.getElementById("listenbrainz__options").removeAttribute("hidden") : document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api);
addTextAreaListener(customCSS, settings.customCSS);
addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle);
addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys);
addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys);
addInputListener(enableDiscord, settings.enableDiscord);
addInputListener(gpuRasterization, settings.flags.gpuRasterization);
addInputListener(menuBar, settings.menuBar);
addInputListener(minimizeOnClose, settings.minimizeOnClose);
addInputListener(mpris, settings.mpris);
addInputListener(notifications, settings.notifications);
addInputListener(playBackControl, settings.playBackControl);
addInputListener(port, settings.apiSettings.port);
addInputListener(skipArtists, settings.skipArtists);
addTextAreaListener(skippedArtists, settings.skippedArtists);
addInputListener(singleInstance, settings.singleInstance);
addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(updateFrequency, settings.updateFrequency);
addInputListener(enableListenBrainz, settings.ListenBrainz.enabled);
addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api);
addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token);
});

View File

@@ -2,498 +2,370 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Tidal-hifi settings</title> <title>Tidal Hi-Fi settings</title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="./settings.css" />
</head> </head>
<body> <body class="settings-window">
<div class="header"> <div class="settings-window__wrapper">
<h1 class="title">Settings</h1> <div class="settings-window__drag-area"></div>
<a href="javascript:hide();" class="exitWindow"> <a id="close" class="settings-window__close-button" title="Close settings">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 348.333 348.334" class="settings-window__svg-icon">
<path fill="white" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85
c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563
c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85
l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554
L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" />
</svg>
</a>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 348.333 348.334"> <main class="settings">
<g> <input type="radio" name="tab" id="general" checked />
<path fill="white" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85 <label for="general">General</label>
c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563
c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85
l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554
L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" />
</svg>
</a> <input type="radio" name="tab" id="api" />
</div> <label for="api">API</label>
<div class="body">
<div class="tabset">
<!-- Tab 1 -->
<input type="radio" name="tabset" id="general" checked />
<label for="general">General</label>
<!-- Tab 2 -->
<input type="radio" name="tabset" id="api" />
<label for="api">Api</label>
<!-- Integrations tab --> <input type="radio" name="tab" id="integrations" />
<input type="radio" name="tabset" id="integrations" /> <label for="integrations">Integrations</label>
<label for="integrations">Integrations</label>
<!-- about tab --> <input type="radio" name="tab" id="advanced" />
<input type="radio" name="tabset" id="about" /> <label for="advanced">Advanced</label>
<label for="about">About</label>
<div class="tab-panels"> <input type="radio" name="tab" id="theming" />
<section id="general" class="tab-panel"> <label for="theming">Theming</label>
<div class="section">
<h3>Playback</h3>
<div class="option">
<h4>Notifications</h4>
<p>
Whether to show a notification when a new song starts.
</p>
<label class="switch">
<input id="notifications" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Mute Artists automatically</h4>
<p>
The following list of artists (1 per line) will be muted automatically.
</p>
<label class="switch" style="margin-bottom:10px">
<input id="muteArtists" type="checkbox">
<span class="slider round" ></span>
</label>
<textarea id="mutedArtists" cols="40" rows="5"></textarea>
</div>
</div>
<div class="section">
<h3>UI</h3>
<div class="option">
<h4>Menubar</h4>
<p>
Show Tidal-hifi's menu bar
</p>
<label class="switch">
<input id="menuBar" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="section">
<h3>System</h3>
<div class="option">
<h4>Tray icon</h4>
<p>
Show Tidal-hifi's tray icon<br />
</p>
<label class="switch">
<input id="trayIcon" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Minimize on Close</h4>
<p>
Minimize window on close instead <br />
</p>
<label class="switch">
<input id="minimizeOnClose" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Hotkeys</h4>
<p>
Enables extra hotkeys to achieve feature parity with the <a href = "javascript:openExternal('https://defkey.com/tidal-desktop-shortcuts')">desktop apps</a><br />
</p>
<label class="switch">
<input id="enableCustomHotkeys" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="api" class="tab-panel">
<div class="section">
<h3>Api</h3>
<p style="margin-bottom: 15px;">
Tidal-hifi has a web api built in to allow users to get current song information. You can optionally enable playback control as well.
</p>
<div class="option"> <input type="radio" name="tab" id="about" />
<h4>Web API</h4> <label for="about">About</label>
<p>
Whether to enable the Tidal-hifi web api
</p>
<label class="switch">
<input id="apiCheckbox" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4 style="margin-bottom: 5px;">API port</h4>
<input id="port" type="text" class="freeTextInput" name="port">
</div>
<div class="option">
<h4>Playback control</h4>
<p>
Whether to enable playback control from the api
</p>
<label class="switch">
<input id="playBackControl" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="integrations" class="tab-panel">
<div class="section">
<h3>integrations</h3>
<p style="margin-bottom: 15px;">
Tidal-hifi is extensible through the use of integrations. You can enable or disable integrations here
</p>
<div class="option">
<h4>MPRIS-player</h4>
<p>
Whether to enable the MPRIS media player controls for Linux systems
</p>
<label class="switch">
<input id="mprisCheckbox" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Discord RPC</h4>
<p>
Show what you're listening to on Discord
</p>
<label class="switch">
<input id="enableDiscord" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="about" class="tab-panel">
<div class="section">
<img alt="tidal icon" style="width: 100px; height: auto; display: block; margin: 0 auto; margin-bottom: 20px; margin-top: 20px;" src = "./icon.png">
<p style="max-width: 350px; display:block; margin: 0 auto; text-align: center;">
<a href ="javascript:openExternal('https://github.com/Mastermindzh/tidal-hifi');">Tidal-hifi</a> is made by <a href ="javascript:openExternal('https://www.rickvanlieshout.com')">Rick van Lieshout</a>.<br />
It uses <a href="javascript:openExternal('https://castlabs.com/');">Castlabs'</a> versions of Electron for widevine support.
</p>
</div> <div class="tabs">
</section> <section id="general-section" class="tabs__section">
<div class="group">
<small>Some settings require a restart of Tidal-hifi. To do so, click the button below:</small> <p class="group__title">Playback</p>
<button onClick="restart()" style ="width: 100%">Restart Tidal-hifi</button> <div class="group__option">
<div class="group__description">
<h4>Notifications</h4>
<p>Show a notification when a new song starts.</p>
</div>
<label class="switch">
<input id="notifications" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div> </div>
</div> <div class="group__option">
</div> <div class="group__description">
<h4>Skip Artists automatically</h4>
<p>The following list of artists (1 per line) will be skipped automatically.</p>
</div>
<label class="switch">
<input id="skipArtists" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<textarea id="skippedArtists" class="textarea" cols="40" rows="5" spellcheck="false"></textarea>
<div class="group__option">
<div class="group__description">
<h4>Block ads</h4>
<p>
Disabled audio & visual ads, unlocked lyrics, suggested track, track info,
unlimited skips
</p>
</div>
<label class="switch">
<input id="adBlock" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
<div class="group">
<p class="group__title">UI</p>
<div class="group__option">
<div class="group__description">
<h4>Fixed menubar</h4>
<p>Always show TIDAL Hi-Fi's menu bar.</p>
</div>
<label class="switch">
<input id="menuBar" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
<div class="group">
<p class="group__title">System</p>
<div class="group__option">
<div class="group__description">
<h4>Tray icon</h4>
<p>Show TIDAL Hi-Fi's tray icon.</p>
</div>
<label class="switch">
<input id="trayIcon" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Minimize on Close</h4>
<p>Minimize window on close instead.</p>
</div>
<label class="switch">
<input id="minimizeOnClose" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Hotkeys</h4>
<p>
Enable extra hotkeys to achieve feature parity with the
<a class="external-link" data-url="https://defkey.com/tidal-desktop-shortcuts">desktop apps</a>.
</p>
</div>
<label class="switch">
<input id="enableCustomHotkeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Single instance</h4>
<p>Prevent opening multiple TIDAL Hi-Fi's instances.</p>
</div>
<label class="switch">
<input id="singleInstance" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="api-section" class="tabs__section">
<div class="group">
<p class="group__title">API</p>
<div class="group__description">
<p>
TIDAL Hi-Fi has a built-in web API to allow users to get current song information.
You can optionally enable playback control as well.
</p>
</div>
<div class="group__option">
<div class="group__description">
<h4>Web API</h4>
<p>Enable the TIDAL Hi-Fi web API.</p>
</div>
<label class="switch">
<input id="apiCheckbox" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<label for="port">API port</label>
<input id="port" type="number" class="text-input" name="port" />
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>Playback control</h4>
<p>Enable playback control from the web API.</p>
</div>
<label class="switch">
<input id="playBackControl" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="integrations-section" class="tabs__section">
<div class="group">
<p class="group__title">Integrations</p>
<div class="group__description">
<p>
TIDAL Hi-Fi is extensible through the use of integrations. You can enable or
disable them here.
</p>
</div>
<div class="group__option">
<div class="group__description">
<h4>MPRIS</h4>
<p>
Enable MPRIS interface which provides a mechanism for discovery, querying and
basic playback control on Linux systems.
</p>
</div>
<label class="switch">
<input id="mprisCheckbox" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Discord RPC</h4>
<p>Show what you're listening to on Discord.</p>
</div>
<label class="switch">
<input id="enableDiscord" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
<div class="group">
<p class="group__title">ListenBrainz</p>
<div class="group__option">
<div class="group__description">
<h4>Enable ListenBrainz</h4>
<p>Scrobble your listens directly to ListenBrainz.</p>
</div>
<label class="switch">
<input id="enableListenBrainz" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div id="listenbrainz__options" hidden="true">
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz API Url</h4>
<p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
</div>
</div>
<textarea id="ListenBrainzAPI" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p>
</div>
</div>
<textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
</div>
</div>
</section>
<section id="advanced-section" class="tabs__section">
<div class="group">
<p class="group__title">Settings</p>
<div class="group__option">
<div class="group__description">
<h4>Update frequency</h4>
<p>
The amount of time, in milliseconds, that tidal-hifi will refresh its playback info by scraping the
website.
The default of 500 seems to work in more cases but if you are fine with a bit more resource usage you
can decrease it as well.
</p>
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
</div>
</div>
<div class="group">
<p class="group__title">Flags</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
</p>
</div>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
</div>
<label class="switch">
<input id="gpuRasterization" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Disable Background Throttling</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
</p>
</div>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="theming-section" class="tabs__section">
<div class="group">
<p class="group__title">Theming</p>
<div class="group__option">
<div class="group__description">
<h4>Custom CSS</h4>
<p>
The css that you put in here will be injected into a style tag in the head of the document.
</p>
</div>
</div>
</div>
<textarea id="customCSS" class="textarea" cols="40" rows="8" spellcheck="false"></textarea>
<div class="group">
<p class="group__title">Theme files</p>
<div class="group__option">
<div class="group__description">
<h4>Current theme</h4>
<p>
Select a theme below or "Tidal - Default" to return to the original Tidal look.
</p>
<select class="select-input" id="themesList" name="themesList">
</select>
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>Upload new themes</h4>
<p>
Click the button and select the css files to import. They will be added to the theme list
automatically.
</p>
<div class="file-drop-area">
<div>
<span class="file-btn">Choose files</span>
<span id="file-message" class="file-msg">or drag and drop files here</span>
<input id="theme-files" class="file-input" type="file" accept=".css" multiple>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="about-section" class="tabs__section about-section">
<img alt="tidal icon" class="about-section__icon" src="./icon.png" />
<p class="about-section__text">
<a class="external-link" data-url="https://github.com/Mastermindzh/tidal-hifi">TIDAL Hi-Fi</a>
is made by
<a class="external-link" data-url="https://www.rickvanlieshout.com">
Rick van Lieshout</a>. <br />It uses
<a class="external-link" data-url="https://castlabs.com/">Castlabs'</a>
version of Electron for widevine support.
</p>
</section>
<footer class="footer">
<p class="footer__note">
Some settings may require a restart of TIDAL Hi-Fi. To do so, click the button below:
</p>
<button class="footer__button" id="restart">Restart TIDAL Hi-Fi</button>
</footer>
</div>
</main>
</div>
</body> </body>
<style>
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
.header a {
-webkit-app-region: no-drag;
}
* {
margin: 0%;
padding: 0%;
color: #ffffff;
font-weight: 400;
font-stretch: normal;
-webkit-font-smoothing: antialiased;
font-family: nationale, nationale-regular, Helvetica, sans-serif;
}
html,
body {
height: 100%;
background-color: black;
display: flex;
flex-direction: column;
}
h2 {
font-size: 1.2rem;
}
small {
font-style: italic;
color: #72777f;
}
.header {
background-color: #242528;
border-bottom: 1px solid #5a5a5a;
height: 50px;
}
.title {
float: left;
line-height: 50px;
margin-left: 15px;
}
.accent {
color: #0ff;
}
.exitWindow {
border: none;
outline: none;
text-decoration: none;
font-size: 1.4rem;
float: right;
margin-right: 15px;
height: 50px;
line-height: 50px;
}
.exitWindow:focus {
border: none;
outline: none;
}
.exitWindow svg {
height: 50px;
color: white;
}
.section {
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(246, 245, 255, .1);
}
.section .option {
margin-bottom: 15px;
}
.section .option p {
max-width: 75%;
float: left
}
.section .option label {
float: right;
}
.section:after,
.section .option:after {
content: "";
display: table;
clear: both;
}
.section h3 {
margin-bottom: 15px;
}
.section h4 {
font-size: 0.9rem;
}
.section p {
color: #72777f;
}
.bottom-border {
border-bottom: 1px solid #0ff;
}
.body {
padding: 15px;
flex: 1 1 auto;
position: relative;
overflow-y: auto;
}
.body::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-color: 2a2a2a;
}
.body::-webkit-scrollbar {
width: 10px;
background-color: #2a2a2a;
}
.body::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #5a5a5a;
}
/* Tabs */
.tabset > input[type="radio"] {
position: absolute;
left: -200vw;
}
.tabset .tab-panel {
display: none;
}
.tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,
.tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2),
.tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3),
.tabset > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4)
{
display: block;
}
.tabset > label {
position: relative;
display: inline-block;
padding: 15px 0px 10px;
border-bottom: 0;
cursor: pointer;
}
.tabset > input + label {
color: #e0e0e0;
margin-right: 30px;
}
.tabset > input:checked + label {
color: #0ff;
border-bottom: 2px solid #0ff;
}
.tab-panel {
padding: 10px 0;
}
/* switches */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(246, 245, 255, .1);
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 24px;
width: 24px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #0ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #0ff;
}
input:checked + .slider:before {
-webkit-transform: translateX(22px);
-ms-transform: translateX(22px);
transform: translateX(22px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/* input field */
input {
background: transparent;
border: 0;
border-bottom: 1px solid rgba(246, 245, 255, .1);
color: rgba(229, 238, 255, .6);
width: 100%;
display: block;
padding: 0 0 12px;
}
.freeTextInput:focus {
outline: none;
border-bottom: 1px solid #0ff;
}
/* buttons */
button{
border:none;
background:none;
align-items: center;
background-color: rgba(229,238,255,.2);
display: inline-flex;
justify-content: center;
border-radius: 12px;
height: 48px;
line-height: 49px;
padding: 0 24px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background .35s ease;
min-height: 0;
min-width: 0;
font-size: 1.14286rem;
font-family: nationale,nationale-regular,Helvetica,sans-serif;
margin-top: 10px;
cursor: pointer;
}
button:hover{
background-color: rgba(229,238,255,.3);
}
textarea {
color: #72777f;
background: #242528;
border: 0;
width: 100%;
color: rgba(229, 238, 255, .6);
padding: 0 0 12px;
display:block;
}
textarea:focus {
outline: none;
border: 0;
border-bottom: 1px solid #0ff;
}
</style>
</html> </html>

View File

@@ -0,0 +1,445 @@
// --- Variables ---
$black: #17171a;
$grey-333: #333;
$white: #f9f9f9;
$tidal-blue: #0ff;
$tidal-grey: #72777f;
$tidal-grey-darker: #404248;
$tidal-grey-darker-focus: #55585f;
$tidal-grey-darkest: #242528;
// --- Fonts ---
@font-face {
font-family: "Noto Sans";
font-weight: 300;
src: url("fonts/NotoSans-Light.ttf") format("truetype");
}
@font-face {
font-family: "Noto Sans";
font-weight: normal;
src: url("fonts/NotoSans-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Noto Sans";
font-weight: 600;
src: url("fonts/NotoSans-SemiBold.ttf") format("truetype");
}
@font-face {
font-family: "Noto Sans";
font-weight: bold;
src: url("fonts/NotoSans-Bold.ttf") format("truetype");
}
$font1: "Noto Sans", helvetica, sans-serif;
// --- Mixins ---
@mixin drag($enabled: true) {
@if $enabled {
-webkit-app-region: drag;
} @else {
-webkit-app-region: no-drag;
}
}
button {
cursor: pointer;
}
// --- Settings window ---
html {
height: 100%;
}
.external-link {
@extend button;
text-decoration: underline;
}
.settings-window {
height: 100%;
margin: 0;
color: $white;
font-family: $font1;
&__wrapper {
height: 100%;
background: $black;
box-shadow: inset 0 0 2px 0 $tidal-grey;
overflow: hidden;
}
&__drag-area {
@include drag;
position: absolute;
width: 100%;
height: 50px;
z-index: 0;
user-select: none;
}
&__close-button {
@extend button;
@include drag(false);
position: absolute;
top: 12px;
right: 10px;
padding: 10px;
border-radius: 100%;
z-index: 1;
&:hover {
background: $grey-333;
}
}
&__svg-icon {
display: block;
width: 18px;
height: 18px;
opacity: 0.7;
}
// --- Settings tabs ---
}
.settings {
height: 100%;
margin: 20px 0;
padding-left: 15px;
font-size: 0;
input {
&[type="radio"] {
margin-right: -18px;
transform: scale(0);
outline: none;
}
& + label {
@include drag(false);
display: inline-block;
position: relative;
margin-right: 35px;
padding-bottom: 8px;
border-bottom: 0;
font-size: 16px;
cursor: pointer;
z-index: 1;
user-select: none;
}
&:checked + label {
border-bottom: 2px solid $tidal-blue;
color: $tidal-blue;
}
}
}
.tabs {
height: calc(100% - 70px);
padding-right: 15px;
font-size: 16px;
overflow: auto;
&__section {
display: none;
}
@for $i from 1 to 7 {
.settings > input:nth-child(#{$i * 2 - 1}):checked ~ & > .tabs__section:nth-child(#{$i}) {
display: block;
}
}
&::-webkit-scrollbar {
width: 10px;
&-thumb {
border-radius: 10px;
background-color: $tidal-grey-darker;
box-shadow: inset 0 0 10px 2px $tidal-grey-darkest;
}
// --- Settings group ---
}
}
.group {
padding: 10px 0;
border-bottom: 1px solid $grey-333;
&:last-child {
border: 0;
}
&__title {
margin-bottom: 10px;
font-size: 16px;
font-weight: bold;
}
&__option {
display: flex;
align-items: center;
}
&__description {
flex-grow: 1;
h4,
label {
display: block;
margin-top: 10px;
margin-bottom: 0;
font-size: 14px;
font-weight: 600;
}
p {
margin-top: 5px;
margin-bottom: 8px;
color: $tidal-grey;
font-size: 14px;
}
.text-input {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 5px 0;
transition: 0.2s;
border: 0;
border-bottom: solid 1px $grey-333;
outline: none;
background: transparent;
color: $tidal-grey;
font-size: 14px;
&:focus {
border-color: $tidal-blue;
color: $white;
}
}
}
}
.switch {
$this: &;
position: relative;
min-width: 50px;
height: 28px;
margin-left: 10px;
input {
transform: scale(0);
outline: none;
&:checked + #{$this}__slider {
background-color: $tidal-blue;
&::before {
transform: translateX(22px);
background-color: $white;
}
}
&:focus + #{$this}__slider {
box-shadow: inset 0 0 0 1px $tidal-blue;
}
}
&__slider {
@extend button;
position: absolute;
inset: 0;
transition: 0.4s;
border-radius: 40px;
background-color: $tidal-grey-darkest;
&::before {
position: absolute;
bottom: 2px;
left: 2px;
width: 24px;
height: 24px;
transition: 0.4s;
border-radius: 50%;
background-color: $white;
content: "";
}
// --- Textarea component
}
}
.textarea {
min-width: 100%;
max-width: 100%;
min-height: 50px;
max-height: 100px;
padding: 8px;
transition: 0.2s;
border: 0;
border-bottom: 1px solid transparent;
background: $tidal-grey-darkest;
color: $tidal-grey;
font-size: 13px;
box-sizing: border-box;
&:focus {
border-color: $tidal-blue;
outline: none;
color: $white;
}
// --- About section ---
}
.about-section {
padding-top: 120px;
text-align: center;
&__icon {
display: inline-block;
width: 100px;
}
&__text {
display: block;
max-width: 350px;
margin: 20px auto 0;
}
// --- Footer ---
}
.footer {
position: sticky;
top: calc(100% - 120px);
height: 100px;
padding-top: 20px;
text-align: center;
&__note {
max-width: 300px;
margin: 0 auto 15px;
color: $tidal-grey;
font-size: 12px;
}
&__button {
@extend button;
display: block;
height: 48px;
margin: auto;
padding: 0 24px;
transition: 0.2s;
border: 0;
border-radius: 12px;
background: $tidal-grey-darker;
color: $white;
font-size: 16px;
&:hover {
background: $tidal-grey-darker-focus;
}
}
}
// file upload
.file-drop-area {
position: relative;
display: flex;
align-items: center;
width: 100%;
max-width: 100%;
padding: 25px 0 25px 0px;
border: 1px dashed $tidal-grey;
border-radius: 3px;
transition: 0.2s;
&.is-active {
background-color: $black;
}
div {
padding-left: 25px;
}
}
.file-btn {
flex-shrink: 0;
background-color: $black;
border: 1px solid $tidal-grey;
border-radius: 3px;
padding: 8px 15px;
margin-right: 10px;
font-size: 12px;
text-transform: uppercase;
}
.file-msg {
font-size: small;
font-weight: 300;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-input {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
cursor: pointer;
opacity: 0;
&:focus {
outline: none;
}
}
.select-input {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 5px 0;
transition: 0.2s;
border: 0;
border-bottom: solid 1px $grey-333;
outline: none;
background: transparent;
color: $tidal-grey;
font-size: 14px;
&:focus {
border-color: $tidal-blue;
color: $white;
}
option {
background-color: $tidal-grey-darkest;
&:disabled {
font-size: 1.2em;
line-height: 1.5em;
text-align: center;
color: $white;
}
}
}

View File

@@ -0,0 +1,54 @@
import fs from "fs";
const cssFilter = (file: string) => file.endsWith(".css");
const sort = (a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase());
/**
* Create an "options header" (disabled option) based on a bit of text
* @param text of the header
* @returns
*/
export const getOptionsHeader = (text: string): HTMLOptionElement => {
const opt = new Option(text, undefined, false, false);
opt.disabled = true;
return opt;
};
/**
* Maps a list of filenames to a list of HTMLOptionElements
* Will strip ".css" from the name but keeps it in the value
* @param array array of filenames
* @returns
*/
export const getOptions = (array: string[]) => {
return array.map((name) => {
return new Option(name.replace(".css", ""), name);
});
};
/**
* Read .css files from a directory and return them in a sorted array.
* @param directory to read from. Will be created if it doesn't exist
* @returns
*/
export const getThemeListFromDirectory = (directory: string): string[] => {
try {
makeUserThemesDirectory(directory);
return fs.readdirSync(directory).filter(cssFilter).sort(sort);
} catch (err) {
console.error(err);
return [];
}
};
/**
* Create the directory to store user themes in
* @param directory directory to create
*/
export const makeUserThemesDirectory = (directory: string) => {
try {
fs.mkdirSync(directory, { recursive: true });
} catch (err) {
console.error(err);
}
};

View File

@@ -1,492 +0,0 @@
const { setTitle } = require("./scripts/window-functions");
const { dialog, process, Notification } = require("electron").remote;
const { store, settings } = require("./scripts/settings");
const { ipcRenderer } = require("electron");
const { app } = require("electron").remote;
const { downloadFile } = require("./scripts/download");
const statuses = require("./constants/statuses");
const hotkeys = require("./scripts/hotkeys");
const globalEvents = require("./constants/globalEvents");
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "Tidal Hifi";
let currentSong = "";
let player;
let currentPlayStatus = statuses.paused;
let progressBarTime;
let currentTimeChanged = false;
let currentTime;
let currentURL = undefined;
let isMutedArtist = false;
const elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[class^="backwardButton"]',
forward: '[class^="forwardButton"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
block: '[class="blockButton"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[data-test="duration-time"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
album_header_title: '.header-details [data-test="title"]',
playing_title: 'span[data-test="table-cell-title"].css-geqnfr',
album_name_cell: '[data-test="table-cell-album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
/**
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function (key) {
return window.document.querySelector(this[key.toLowerCase()]);
},
/**
* Get the icon of the current song
*/
getSongIcon: function () {
const figure = this.get("media");
if (figure) {
const mediaElement = figure.querySelector(this["image"]);
if (mediaElement) {
return mediaElement.src.replace("80x80", "640x640");
}
}
return "";
},
getArtists: function () {
const footer = this.get("footer");
if (footer) {
const artists = footer.querySelector(this["artists"]);
if (artists) {
return artists.innerText;
}
}
return "unknown artist(s)";
},
getAlbumName: function () {
//If listening to an album, get its name from the header title
if (window.location.href.includes("/album/")) {
const albumName = window.document.querySelector(this.album_header_title);
if (albumName) {
return albumName.textContent;
}
//If listening to a playlist or a mix, get album name from the list
} else if (
window.location.href.includes("/playlist/") ||
window.location.href.includes("/mix/")
) {
if (currentPlayStatus === statuses.playing) {
const row = window.document.querySelector(this.playing_title).closest(this.tracklist_row);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
}
}
return "";
},
isMuted: function () {
return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false
},
/**
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function (key) {
const element = this.get(key);
return element ? element.textContent : "";
},
/**
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function (key) {
this.get(key).click();
return this;
},
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key) {
return this.get(key).focus();
},
};
/**
* Play or pause the current song
*/
function playPause() {
const play = elements.get("play");
if (play) {
elements.click("play");
} else {
elements.click("pause");
}
}
/**
* Add hotkeys for when tidal is focused
* Reflects the desktop hotkeys found on:
* https://defkey.com/tidal-desktop-shortcuts
*/
function addHotKeys() {
if (store.get(settings.enableCustomHotkeys)) {
hotkeys.add("Control+p", function () {
elements.click("account").click("settings");
});
hotkeys.add("Control+l", function () {
handleLogout();
});
hotkeys.add("Control+h", function () {
elements.click("home");
});
hotkeys.add("backspace", function () {
elements.click("back");
});
hotkeys.add("shift+backspace", function () {
elements.click("forward");
});
hotkeys.add("control+u", function () {
// reloading window without cache should show the update bar if applicable
window.location.reload(true);
});
hotkeys.add("control+r", function () {
elements.click("repeat");
});
}
// always add the hotkey for the settings window
hotkeys.add("control+=", function () {
ipcRenderer.send(globalEvents.showSettings);
});
hotkeys.add("control+0", function () {
ipcRenderer.send(globalEvents.showSettings);
});
}
/**
* This function will ask the user whether he/she wants to log out.
* It will log the user out if he/she selects "yes"
*/
function handleLogout() {
const logoutOptions = ["Cancel", "Yes, please", "No, thanks"];
dialog.showMessageBox(
null,
{
type: "question",
title: "Logging out",
message: "Are you sure you want to log out?",
buttons: logoutOptions,
defaultId: 2,
},
function (response) {
if (logoutOptions.indexOf("Yes, please") == response) {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) {
window.localStorage.removeItem(key);
i = window.localStorage.length + 1;
}
}
window.location.reload();
}
}
);
}
function addFullScreenListeners() {
window.document.addEventListener("fullscreenchange", () => {
ipcRenderer.send(globalEvents.refreshMenuBar);
});
}
/**
* Add ipc event listeners.
* Some actions triggered outside of the site need info from the site.
*/
function addIPCEventListeners() {
window.addEventListener("DOMContentLoaded", () => {
ipcRenderer.on("globalEvent", (_event, args) => {
switch (args) {
case globalEvents.playPause:
playPause();
break;
case globalEvents.next:
elements.click("next");
break;
case globalEvents.previous:
elements.click("previous");
break;
case globalEvents.play:
elements.click("play");
break;
case globalEvents.pause:
elements.click("pause");
break;
}
});
});
}
/**
* Update the current status of tidal (e.g playing or paused)
*/
function getCurrentlyPlayingStatus() {
let pause = elements.get("pause");
let status = undefined;
// if pause button is visible tidal is playing
if (pause) {
status = statuses.playing;
} else {
status = statuses.paused;
}
return status;
}
/**
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
function convertDuration(duration) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
/**
* Update Tidal-hifi's media info
*
* @param {*} options
*/
function updateMediaInfo(options, notify) {
if (options) {
ipcRenderer.send(globalEvents.updateInfo, options);
if (store.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.message, icon: options.icon }).show();
}
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.message],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
},
};
player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing";
}
}
}
/**
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
* If it's a song it sets the track URL as currentURL, If it's a video it will set currentURL to undefined.
*/
function updateURL() {
const URLelement = elements.get("title").querySelector("a");
switch (URLelement) {
case null:
currentURL = undefined;
break;
default:
const id = URLelement.href.replace(/[^0-9]/g, "");
currentURL = `https://tidal.com/browse/track/${id}`;
break;
}
}
/**
* Watch for song changes and update title + notify
*/
setInterval(function () {
const title = elements.getText("title");
const artists = elements.getArtists();
const album = elements.getAlbumName();
const current = elements.getText("current");
const duration = elements.getText("duration");
const progressBarcurrentTime = elements.get("bar").getAttribute("aria-valuenow");
const songDashArtistTitle = `${title} - ${artists}`;
const currentStatus = getCurrentlyPlayingStatus();
const options = {
title,
message: artists,
album: album,
status: currentStatus,
url: currentURL,
current: current,
duration: duration,
"app-name": appName,
};
const playStatusChanged = currentStatus !== currentPlayStatus;
const progressBarTimeChanged = progressBarcurrentTime !== progressBarTime;
const titleOrArtistChanged = currentSong !== songDashArtistTitle;
muteArtistIfFoundInMutedArtistsList();
if (titleOrArtistChanged || playStatusChanged || progressBarTimeChanged || currentTimeChanged) {
// update title, url and play info with new info
setTitle(songDashArtistTitle);
updateURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
// check progress bar value and make sure current stays up to date after switch
if (progressBarTime != progressBarcurrentTime && !titleOrArtistChanged) {
progressBarTime = progressBarcurrentTime;
currentTime = options.current;
options.duration = duration;
currentTimeChanged = true;
}
if (currentTimeChanged) {
if (options.current == currentTime && currentStatus != "paused") return;
currentTime = options.current;
currentTimeChanged = false;
}
// make sure current is set to 0 if title changes
if (titleOrArtistChanged) {
options.current = "0:00";
currentTime = options.current;
progressBarTime = progressBarcurrentTime;
}
const image = elements.getSongIcon();
new Promise((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(
() => {
updateMediaInfo(options, titleOrArtistChanged);
},
() => {}
);
}
/**
* Checks whether the current artist is included in the "muted artists" list and if so it will automatically mute the player
*/
function muteArtistIfFoundInMutedArtistsList() {
if (store.get(settings.muteArtists)) {
const mutedArtists = store.get(settings.mutedArtists);
if (mutedArtists.find((artist) => artist === artists) !== undefined) {
if (!elements.isMuted()) {
isMutedArtist = true;
elements.click("volume");
}
} else if (currentStatus === statuses.playing && isMutedArtist && elements.isMuted()) {
elements.click("volume");
isMutedArtist = false;
}
}
}
}, 200);
if (process.platform === "linux" && store.get(settings.mpris)) {
try {
const Player = require("mpris-service");
player = Player({
name: "tidal-hifi",
identity: "tidal-hifi",
supportedUriSchemes: ["file"],
supportedMimeTypes: [
"audio/mpeg",
"audio/flac",
"audio/x-flac",
"application/ogg",
"audio/wav",
],
supportedInterfaces: ["player"],
desktopEntry: "tidal-hifi",
});
// Events
var events = {
next: "next",
previous: "previous",
pause: "pause",
playpause: "playpause",
stop: "stop",
play: "play",
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
};
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
switch (events[eventValue]) {
case events.playpause:
playPause();
break;
default:
elements.click(eventValue);
}
});
});
// Override get position function
player.getPosition = function () {
return convertDuration(elements.getText("current")) * 1000 * 1000;
};
player.on("quit", function () {
app.quit();
});
} catch (exception) {
console.log("player api not working");
}
}
addHotKeys();
addIPCEventListeners();
addFullScreenListeners();

585
src/preload.ts Normal file
View File

@@ -0,0 +1,585 @@
import { app, dialog, Notification } from "@electron/remote";
import { clipboard, ipcRenderer } from "electron";
import fs from "fs";
import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings";
import { Songwhip } from "./features/songwhip/songwhip";
import {
ListenBrainz,
ListenBrainzConstants,
ListenBrainzStore,
} from "./features/listenbrainz/listenbrainz";
import { Options } from "./models/options";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys";
import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { MediaStatus } from "./models/mediaStatus";
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "Tidal Hifi";
let currentSong = "";
let player: Player;
let currentPlayStatus = MediaStatus.paused;
const elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[class^="profileOptions"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
/**
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function (key: string) {
return window.document.querySelector(this[key.toLowerCase()]);
},
/**
* Get the icon of the current song
*/
getSongIcon: function () {
const figure = this.get("media");
if (figure) {
const mediaElement = figure.querySelector(this["image"]);
if (mediaElement) {
return mediaElement.src.replace("80x80", "640x640");
}
}
return "";
},
/**
* returns an array of all artists in the current song
* @returns {Array} artists
*/
getArtistsArray: function () {
const footer = this.get("footer");
if (footer) {
const artists = footer.querySelectorAll(this.artists);
if (artists) return Array.from(artists).map((artist) => (artist as HTMLElement).textContent);
}
return [];
},
/**
* unify the artists array into a string separated by commas
* @param {Array} artistsArray
* @returns {String} artists
*/
getArtistsString: function (artistsArray: string[]) {
if (artistsArray.length > 0) return artistsArray.join(", ");
return "unknown artist(s)";
},
getAlbumName: function () {
//If listening to an album, get its name from the header title
if (window.location.href.includes("/album/")) {
const albumName = window.document.querySelector(this.album_header_title);
if (albumName) {
return albumName.textContent;
}
//If listening to a playlist or a mix, get album name from the list
} else if (
window.location.href.includes("/playlist/") ||
window.location.href.includes("/mix/")
) {
if (currentPlayStatus === MediaStatus.playing) {
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
}
}
return "";
},
isMuted: function () {
return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false
},
/**
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function (key: string) {
const element = this.get(key);
return element ? element.textContent : "";
},
/**
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function (key: string) {
this.get(key).click();
return this;
},
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key: string) {
return this.get(key).focus();
},
};
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get(settings.theme);
if (selectedTheme !== "none") {
const themeFile = `${process.resourcesPath}/${selectedTheme}`;
fs.readFile(themeFile, "utf-8", (err, data) => {
if (err) {
alert("An error ocurred reading the theme file.");
return;
}
const themeStyle = document.createElement("style");
themeStyle.innerHTML = data;
document.head.appendChild(themeStyle);
});
}
// read customCSS (it will override the theme)
const style = document.createElement("style");
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
document.head.appendChild(style);
});
}
/**
* Get the update frequency from the store
* make sure it returns a number, if not use the default
*/
function getUpdateFrequency() {
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
const defaultValue = 500;
if (!isNaN(storeValue)) {
return storeValue;
} else {
return defaultValue;
}
}
/**
* Play or pause the current song
*/
function playPause() {
const play = elements.get("play");
if (play) {
elements.click("play");
} else {
elements.click("pause");
}
}
/**
* Clears the old listenbrainz data on launch
*/
ListenBrainzStore.clear();
/**
* Add hotkeys for when tidal is focused
* Reflects the desktop hotkeys found on:
* https://defkey.com/tidal-desktop-shortcuts
*/
function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () {
elements.click("account");
setTimeout(() => {
elements.click("settings");
}, 100);
});
addHotkey("Control+l", function () {
handleLogout();
});
addHotkey("Control+h", function () {
elements.click("home");
});
addHotkey("backspace", function () {
elements.click("back");
});
addHotkey("shift+backspace", function () {
elements.click("forward");
});
addHotkey("control+u", function () {
// reloading window without cache should show the update bar if applicable
window.location.reload();
});
addHotkey("control+r", function () {
elements.click("repeat");
});
addHotkey("control+w", async function () {
const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
clipboard.writeText(url);
new Notification({
title: `Successfully whipped: `,
body: `URL copied to clipboard: ${url}`,
}).show();
});
}
// always add the hotkey for the settings window
addHotkey("control+=", function () {
ipcRenderer.send(globalEvents.showSettings);
});
addHotkey("control+0", function () {
ipcRenderer.send(globalEvents.showSettings);
});
}
/**
* This function will ask the user whether he/she wants to log out.
* It will log the user out if he/she selects "yes"
*/
function handleLogout() {
const logoutOptions = ["Cancel", "Yes, please", "No, thanks"];
dialog
.showMessageBox(null, {
type: "question",
title: "Logging out",
message: "Are you sure you want to log out?",
buttons: logoutOptions,
defaultId: 2,
})
.then((result: { response: number }) => {
if (logoutOptions.indexOf("Yes, please") === result.response) {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) {
window.localStorage.removeItem(key);
break;
}
}
window.location.reload();
}
});
}
function addFullScreenListeners() {
window.document.addEventListener("fullscreenchange", () => {
ipcRenderer.send(globalEvents.refreshMenuBar);
});
}
/**
* Add ipc event listeners.
* Some actions triggered outside of the site need info from the site.
*/
function addIPCEventListeners() {
window.addEventListener("DOMContentLoaded", () => {
ipcRenderer.on("globalEvent", (_event, args) => {
switch (args) {
case globalEvents.playPause:
playPause();
break;
case globalEvents.next:
elements.click("next");
break;
case globalEvents.previous:
elements.click("previous");
break;
case globalEvents.play:
elements.click("play");
break;
case globalEvents.pause:
elements.click("pause");
break;
default:
break;
}
});
});
}
/**
* Update the current status of tidal (e.g playing or paused)
*/
function getCurrentlyPlayingStatus() {
const pause = elements.get("pause");
let status = undefined;
// if pause button is visible tidal is playing
if (pause) {
status = MediaStatus.playing;
} else {
status = MediaStatus.paused;
}
return status;
}
/**
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
function convertDuration(duration: string) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
/**
* Update Tidal-hifi's media info
*
* @param {*} options
*/
function updateMediaInfo(options: Options, notify: boolean) {
if (options) {
ipcRenderer.send(globalEvents.updateInfo, options);
if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
}
updateMpris(options);
updateListenBrainz(options);
}
}
function updateMpris(options: Options) {
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
}
}
function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if (
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
) {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
}
}
}
/**
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
* If it's a song it returns the track URL, if not it will return undefined
*/
function getTrackURL() {
const id = getTrackID();
return `https://tidal.com/browse/track/${id}`;
}
function getTrackID() {
const URLelement = elements.get("title").querySelector("a");
if (URLelement !== null) {
const id = URLelement.href.replace(/\D/g, "");
return id;
}
return window.location;
}
function updateMediaSession(options: Options) {
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: options.title,
artist: options.artists,
album: options.album,
artwork: [
{
src: options.icon,
sizes: "640x640",
type: "image/png",
},
],
});
}
}
/**
* Watch for song changes and update title + notify
*/
setInterval(function () {
const title = elements.getText("title");
const artistsArray = elements.getArtistsArray();
const artistsString = elements.getArtistsString(artistsArray);
skipArtistsIfFoundInSkippedArtistsList(artistsArray);
const album = elements.getAlbumName();
const current = elements.getText("current");
const duration = elements.getText("duration");
const songDashArtistTitle = `${title} - ${artistsString}`;
const currentStatus = getCurrentlyPlayingStatus();
const options = {
title,
artists: artistsString,
album: album,
status: currentStatus,
url: getTrackURL(),
current,
duration,
"app-name": appName,
image: "",
icon: "",
};
const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
// update title, url and play info with new info
setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
const image = elements.getSongIcon();
new Promise<void>((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) {
updateMediaSession(options);
}
});
/**
* automatically skip a song if the artists are found in the list of artists to skip
* @param {*} artists array of artists
*/
function skipArtistsIfFoundInSkippedArtistsList(artists: string[]) {
if (settingsStore.get(settings.skipArtists)) {
const skippedArtists = settingsStore.get<string, string[]>(settings.skippedArtists);
if (skippedArtists.length > 0) {
const artistsToSkip = skippedArtists.map((artist) => artist);
const artistNames = Object.values(artists).map((artist) => artist);
const foundArtist = artistNames.some((artist) => artistsToSkip.includes(artist));
if (foundArtist) {
elements.click("next");
}
}
}
}
}, getUpdateFrequency());
if (process.platform === "linux" && settingsStore.get(settings.mpris)) {
try {
player = Player({
name: "tidal-hifi",
identity: "tidal-hifi",
supportedUriSchemes: ["file"],
supportedMimeTypes: [
"audio/mpeg",
"audio/flac",
"audio/x-flac",
"application/ogg",
"audio/wav",
],
supportedInterfaces: ["player"],
desktopEntry: "tidal-hifi",
});
// Events
const events = {
next: "next",
previous: "previous",
pause: "pause",
playpause: "playpause",
stop: "stop",
play: "play",
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
} as { [key: string]: string };
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
switch (events[eventValue]) {
case events.playpause:
playPause();
break;
default:
elements.click(eventValue);
}
});
});
// Override get position function
player.getPosition = function () {
return convertDuration(elements.getText("current")) * 1000 * 1000;
};
player.on("quit", function () {
app.quit();
});
} catch (exception) {
console.log("player api not working");
}
}
addCustomCss();
addHotKeys();
addIPCEventListeners();
addFullScreenListeners();

View File

@@ -1,95 +0,0 @@
const discordrpc = require("discord-rpc");
const { app, ipcMain } = require("electron");
const globalEvents = require("../constants/globalEvents");
const clientId = "833617820704440341";
const mediaInfoModule = require("./mediaInfo");
const discordModule = [];
function timeToSeconds(timeArray) {
let minutes = timeArray[0] * 1;
let seconds = minutes * 60 + timeArray[1] * 1;
return seconds;
}
let rpc;
const observer = (event, arg) => {
if (mediaInfoModule.mediaInfo.status == "paused" && rpc) {
rpc.setActivity(idleStatus);
} else if (rpc) {
const currentSeconds = timeToSeconds(mediaInfoModule.mediaInfo.current.split(":"));
const durationSeconds = timeToSeconds(mediaInfoModule.mediaInfo.duration.split(":"));
const date = new Date();
const now = (date.getTime() / 1000) | 0;
const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
if (mediaInfoModule.mediaInfo.url) {
rpc.setActivity({
...idleStatus,
...{
details: `Listening to ${mediaInfoModule.mediaInfo.title}`,
state: mediaInfoModule.mediaInfo.artist
? mediaInfoModule.mediaInfo.artist
: "unknown artist(s)",
startTimestamp: parseInt(now),
endTimestamp: parseInt(remaining),
largeImageKey: mediaInfoModule.mediaInfo.image,
largeImageText: mediaInfoModule.mediaInfo.album
? mediaInfoModule.mediaInfo.album
: `${idleStatus.largeImageText}`,
buttons: [{ label: "Play on Tidal", url: mediaInfoModule.mediaInfo.url }],
},
});
} else {
rpc.setActivity({
...idleStatus,
...{
details: `Watching ${mediaInfoModule.mediaInfo.title}`,
state: mediaInfoModule.mediaInfo.artist,
startTimestamp: parseInt(now),
endTimestamp: parseInt(remaining),
},
});
}
}
};
const idleStatus = {
details: `Browsing Tidal`,
largeImageKey: "tidal-hifi-icon",
largeImageText: `Tidal HiFi ${app.getVersion()}`,
instance: false,
};
/**
* Set up the discord rpc and listen on globalEvents.updateInfo
*/
discordModule.initRPC = function () {
rpc = new discordrpc.Client({ transport: "ipc" });
rpc.login({ clientId }).then(
() => {
discordModule.rpc = rpc;
rpc.on("ready", () => {
rpc.setActivity(idleStatus);
});
ipcMain.on(globalEvents.updateInfo, observer);
},
() => {
console.error("Can't connect to Discord, is it running?");
}
);
};
/**
* Remove any RPC connection with discord and remove the event listener on globalEvents.updateInfo
*/
discordModule.unRPC = function () {
if (rpc) {
rpc.clearActivity();
rpc.destroy();
rpc = false;
discordModule.rpc = undefined;
ipcMain.removeListener(globalEvents.updateInfo, observer);
}
};
module.exports = discordModule;

88
src/scripts/discord.ts Normal file
View File

@@ -0,0 +1,88 @@
import { Client } from "discord-rpc";
import { app, ipcMain } from "electron";
import { globalEvents } from "../constants/globalEvents";
import { MediaStatus } from "../models/mediaStatus";
import { mediaInfo } from "./mediaInfo";
const clientId = "833617820704440341";
function timeToSeconds(timeArray: string[]) {
const minutes = parseInt(timeArray[0]) * 1;
const seconds = minutes * 60 + parseInt(timeArray[1]) * 1;
return seconds;
}
export let rpc: Client;
const observer = () => {
if (mediaInfo.status == MediaStatus.paused && rpc) {
rpc.setActivity(idleStatus);
} else if (rpc) {
const currentSeconds = timeToSeconds(mediaInfo.current.split(":"));
const durationSeconds = timeToSeconds(mediaInfo.duration.split(":"));
const date = new Date();
const now = (date.getTime() / 1000) | 0;
const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
if (mediaInfo.url) {
rpc.setActivity({
...idleStatus,
...{
details: `Listening to ${mediaInfo.title}`,
state: mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)",
startTimestamp: now,
endTimestamp: remaining,
largeImageKey: mediaInfo.image,
largeImageText: mediaInfo.album ? mediaInfo.album : `${idleStatus.largeImageText}`,
buttons: [{ label: "Play on Tidal", url: mediaInfo.url }],
},
});
} else {
rpc.setActivity({
...idleStatus,
...{
details: `Watching ${mediaInfo.title}`,
state: mediaInfo.artists,
startTimestamp: now,
endTimestamp: remaining,
},
});
}
}
};
const idleStatus = {
details: `Browsing Tidal`,
largeImageKey: "tidal-hifi-icon",
largeImageText: `Tidal HiFi ${app.getVersion()}`,
instance: false,
};
/**
* Set up the discord rpc and listen on globalEvents.updateInfo
*/
export const initRPC = () => {
rpc = new Client({ transport: "ipc" });
rpc.login({ clientId }).then(
() => {
rpc.on("ready", () => {
rpc.setActivity(idleStatus);
});
ipcMain.on(globalEvents.updateInfo, observer);
},
() => {
console.error("Can't connect to Discord, is it running?");
}
);
};
/**
* Remove any RPC connection with discord and remove the event listener on globalEvents.updateInfo
*/
export const unRPC = () => {
if (rpc) {
rpc.clearActivity();
rpc.destroy();
rpc = null;
ipcMain.removeListener(globalEvents.updateInfo, observer);
}
};

View File

@@ -1,26 +0,0 @@
const download = {};
const request = require("request");
const fs = require("fs");
/**
* download and save a file
* @param {*} fileUrl url to download
* @param {*} targetPath path to save it at
*/
download.downloadFile = function(fileUrl, targetPath) {
return new Promise((resolve, reject) => {
var req = request({
method: "GET",
uri: fileUrl,
});
var out = fs.createWriteStream(targetPath);
req.pipe(out);
req.on("end", resolve);
req.on("error", reject);
});
};
module.exports = download;

23
src/scripts/download.ts Normal file
View File

@@ -0,0 +1,23 @@
import fs from "fs";
import request from "request";
/**
* download and save a file
* @param {string} fileUrl url to download
* @param {string} targetPath path to save it at
*/
export const downloadFile = function (fileUrl: string, targetPath: string) {
return new Promise((resolve, reject) => {
const req = request({
method: "GET",
uri: fileUrl,
});
const out = fs.createWriteStream(targetPath);
req.pipe(out);
req.on("end", resolve);
req.on("error", reject);
});
};

View File

@@ -1,70 +0,0 @@
const express = require("express");
const { mediaInfo } = require("./mediaInfo");
const { store, settings } = require("./settings");
const globalEvents = require("./../constants/globalEvents");
const statuses = require("./../constants/statuses");
const expressModule = {};
const fs = require("fs");
let expressInstance;
/**
* Function to enable tidal-hifi's express api
*/
expressModule.run = function(mainWindow) {
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
function handleGlobalEvent(res, action) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
const expressApp = express();
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json(mediaInfo));
expressApp.get("/image", (req, res) => {
var stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function() {
res.set("Content-Type", "image/png");
stream.pipe(res);
});
stream.on("error", function() {
res.set("Content-Type", "text/plain");
res.status(404).end("Not found");
});
});
if (store.get(settings.playBackControl)) {
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status == statuses.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
}
if (store.get(settings.api)) {
let port = store.get(settings.apiSettings.port);
expressInstance = expressApp.listen(port, "127.0.0.1", () => {});
expressInstance.on("error", function(e) {
let message = e.code;
if (e.code === "EADDRINUSE") {
message = `Port ${port} in use.`;
}
const { dialog } = require("electron");
dialog.showErrorBox("Api failed to start.", message);
});
} else {
expressInstance = undefined;
}
};
module.exports = expressModule;

66
src/scripts/express.ts Normal file
View File

@@ -0,0 +1,66 @@
import { BrowserWindow, dialog } from "electron";
import express, { Response } from "express";
import fs from "fs";
import { settings } from "../constants/settings";
import { MediaStatus } from "../models/mediaStatus";
import { globalEvents } from "./../constants/globalEvents";
import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings";
/**
* Function to enable tidal-hifi's express api
*/
// expressModule.run = function (mainWindow)
export const startExpress = (mainWindow: BrowserWindow) => {
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
function handleGlobalEvent(res: Response, action: string) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
const expressApp = express();
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
expressApp.get("/image", (req, res) => {
const stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function () {
res.set("Content-Type", "image/png");
stream.pipe(res);
});
stream.on("error", function () {
res.set("Content-Type", "text/plain");
res.status(404).end("Not found");
});
});
if (settingsStore.get(settings.playBackControl)) {
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
}
const port = settingsStore.get<string, number>(settings.apiSettings.port);
const expressInstance = expressApp.listen(port, "127.0.0.1");
expressInstance.on("error", function (e: { code: string }) {
let message = e.code;
if (e.code === "EADDRINUSE") {
message = `Port ${port} in use.`;
}
dialog.showErrorBox("Api failed to start.", message);
});
};

View File

@@ -1,11 +0,0 @@
const hotkeyjs = require("hotkeys-js");
const hotkeys = {};
hotkeys.add = function(keys, func) {
hotkeyjs(keys, function(event, args) {
event.preventDefault();
func(event, args);
});
};
module.exports = hotkeys;

11
src/scripts/hotkeys.ts Normal file
View File

@@ -0,0 +1,11 @@
import hotkeyjs, { HotkeysEvent } from "hotkeys-js";
export const addHotkey = function (
keys: string,
func: (event?: KeyboardEvent, args?: HotkeysEvent) => void
) {
hotkeyjs(keys, function (event, args) {
event.preventDefault();
func(event, args);
});
};

View File

@@ -1,26 +1,21 @@
const statuses = require("./../constants/statuses"); import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
const mediaInfo = { export const mediaInfo = {
title: "", title: "",
artist: "", artists: "",
album: "", album: "",
icon: "", icon: "",
status: statuses.paused, status: MediaStatus.paused as string,
url: "", url: "",
current: "", current: "",
duration: "", duration: "",
image: "tidal-hifi-icon" image: "tidal-hifi-icon",
};
const mediaInfoModule = {
mediaInfo,
}; };
/** export const updateMediaInfo = (arg: MediaInfo) => {
* Update artist and song info in the mediaInfo constant
*/
mediaInfoModule.update = function (arg) {
mediaInfo.title = propOrDefault(arg.title); mediaInfo.title = propOrDefault(arg.title);
mediaInfo.artist = propOrDefault(arg.message); mediaInfo.artists = propOrDefault(arg.artists);
mediaInfo.album = propOrDefault(arg.album); mediaInfo.album = propOrDefault(arg.album);
mediaInfo.icon = propOrDefault(arg.icon); mediaInfo.icon = propOrDefault(arg.icon);
mediaInfo.url = propOrDefault(arg.url); mediaInfo.url = propOrDefault(arg.url);
@@ -35,8 +30,6 @@ mediaInfoModule.update = function (arg) {
* @param {*} prop property to check * @param {*} prop property to check
* @param {*} defaultValue defaults to "" * @param {*} defaultValue defaults to ""
*/ */
function propOrDefault(prop, defaultValue = "") { function propOrDefault(prop: string, defaultValue = "") {
return prop ? prop : defaultValue; return prop ? prop : defaultValue;
} }
module.exports = mediaInfoModule;

View File

@@ -1,115 +0,0 @@
const { Menu, app } = require("electron");
const { showSettingsWindow } = require("./settings");
const isMac = process.platform === "darwin";
const settingsMenuEntry = {
label: "Settings",
click() {
showSettingsWindow();
},
accelerator: "Control+/",
};
const quitMenuEntry = {
label: "Quit",
click() {
app.exit(0);
},
accelerator: "Control+Q"
};
const mainMenu = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
settingsMenuEntry,
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
quitMenuEntry,
],
},
]
: []),
// { role: 'fileMenu' }
{
label: "File",
submenu: [settingsMenuEntry, isMac ? { role: "close" } : quitMenuEntry],
},
// { role: 'editMenu' }
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
{ type: "separator" },
settingsMenuEntry,
],
},
// { role: 'viewMenu' }
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "resetzoom" },
{ role: "zoomin" },
{ role: "zoomout" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
// { role: 'windowMenu' }
{
label: "Window",
submenu: [
{ role: "minimize" },
...(isMac
? [{ type: "separator" }, { role: "front" }, { type: "separator" }, { role: "window" }]
: [{ role: "close" }]),
],
},
settingsMenuEntry,
{
label: "About",
click() {
showSettingsWindow("about");
},
},
];
const menuModule = { mainMenu };
menuModule.getMenu = function () {
return Menu.buildFromTemplate(mainMenu);
};
menuModule.addMenu = function () {
Menu.setApplicationMenu(menuModule.getMenu());
};
module.exports = menuModule;

119
src/scripts/menu.ts Normal file
View File

@@ -0,0 +1,119 @@
import { BrowserWindow, Menu, app } from "electron";
import { showSettingsWindow } from "./settings";
const isMac = process.platform === "darwin";
import name from "./../constants/values";
const settingsMenuEntry = {
label: "Settings",
click() {
showSettingsWindow();
},
accelerator: "Control+=",
};
const quitMenuEntry = {
label: "Quit",
click() {
app.exit(0);
},
accelerator: "Control+Q",
};
export const getMenu = function (mainWindow: BrowserWindow) {
const toggleWindow = {
label: "Toggle Window",
click: function () {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
},
};
const mainMenu = [
...(isMac
? [
{
label: name,
submenu: [
{ role: "about" },
settingsMenuEntry,
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
quitMenuEntry,
],
},
]
: []),
{
label: "File",
submenu: [settingsMenuEntry, isMac ? { role: "close" } : quitMenuEntry],
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
{ type: "separator" },
settingsMenuEntry,
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "resetzoom" },
{ role: "zoomin" },
{ role: "zoomout" },
{ type: "separator" },
{ role: "togglefullscreen" },
{ role: "toggledevtools" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
toggleWindow,
...(isMac
? [{ type: "separator" }, { role: "front" }, { type: "separator" }, { role: "window" }]
: [{ role: "close" }]),
],
},
settingsMenuEntry,
{
label: "About",
click() {
showSettingsWindow("about");
},
},
toggleWindow,
quitMenuEntry,
];
return Menu.buildFromTemplate(mainMenu as any);
};
export const addMenu = function (mainWindow: BrowserWindow) {
Menu.setApplicationMenu(getMenu(mainWindow));
};

View File

@@ -1,76 +0,0 @@
const Store = require("electron-store");
const settings = require("./../constants/settings");
const path = require("path");
const { BrowserWindow } = require("electron");
let settingsWindow;
const store = new Store({
defaults: {
notifications: true,
api: true,
playBackControl: true,
muteArtists: false,
mutedArtists: ["TIDAL"],
menuBar: true,
apiSettings: {
port: 47836,
},
trayIcon: true,
minimizeOnClose: false,
mpris: false,
enableCustomHotkeys: false,
enableDiscord: false,
windowBounds: { width: 800, height: 600 },
},
});
const settingsModule = {
store,
settings,
settingsWindow,
};
settingsModule.createSettingsWindow = function () {
settingsWindow = new BrowserWindow({
width: 500,
height: 600,
show: false,
frame: false,
title: "Tidal-hifi - settings",
webPreferences: {
affinity: "window",
preload: path.join(__dirname, "../pages/settings/preload.js"),
plugins: true,
nodeIntegration: true,
},
});
settingsWindow.on("close", (event) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();
}
});
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
settingsModule.settingsWindow = settingsWindow;
};
settingsModule.showSettingsWindow = function (tab = "general") {
settingsWindow.webContents.send("goToTab", tab);
// refresh data just before showing the window
settingsWindow.webContents.send("refreshData");
settingsWindow.show();
};
settingsModule.hideSettingsWindow = function () {
settingsWindow.hide();
};
settingsModule.closeSettingsWindow = function () {
settingsWindow = null;
};
module.exports = settingsModule;

100
src/scripts/settings.ts Normal file
View File

@@ -0,0 +1,100 @@
import Store from "electron-store";
import { settings } from "../constants/settings";
import path from "path";
import { BrowserWindow } from "electron";
let settingsWindow: BrowserWindow;
export const settingsStore = new Store({
defaults: {
adBlock: false,
api: true,
apiSettings: {
port: 47836,
},
customCSS: [],
disableBackgroundThrottle: true,
disableHardwareMediaKeys: false,
enableCustomHotkeys: false,
enableDiscord: false,
ListenBrainz: {
enabled: false,
api: "https://api.listenbrainz.org",
token: "",
},
flags: {
gpuRasterization: true,
disableHardwareMediaKeys: false,
},
menuBar: true,
minimizeOnClose: false,
mpris: false,
notifications: true,
playBackControl: true,
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
theme: "none",
trayIcon: true,
updateFrequency: 500,
windowBounds: { width: 800, height: 600 },
},
migrations: {
"3.1.0": (migrationStore) => {
console.log("running migrations for 3.1.0");
migrationStore.set(
settings.flags.disableHardwareMediaKeys,
migrationStore.get("disableHardwareMediaKeys") ?? false
);
},
},
});
const settingsModule = {
// settings,
settingsWindow,
};
export const createSettingsWindow = function () {
settingsWindow = new BrowserWindow({
width: 700,
height: 600,
resizable: true,
show: false,
transparent: true,
frame: false,
title: "TIDAL Hi-Fi settings",
webPreferences: {
preload: path.join(__dirname, "../pages/settings/preload.js"),
plugins: true,
nodeIntegration: true,
},
});
settingsWindow.on("close", (event: Event) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();
}
});
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
settingsModule.settingsWindow = settingsWindow;
};
export const showSettingsWindow = function (tab = "general") {
settingsWindow.webContents.send("goToTab", tab);
// refresh data just before showing the window
settingsWindow.webContents.send("refreshData");
settingsWindow.show();
};
export const hideSettingsWindow = function () {
settingsWindow.hide();
};
export const closeSettingsWindow = function () {
settingsWindow = null;
};

View File

@@ -1,49 +0,0 @@
const { Tray, app } = require("electron");
const { Menu } = require("electron");
const { getMenu, mainMenu } = require("./menu");
const { store, settings } = require("./settings");
const trayModule = {};
let tray;
trayModule.addTray = function (options = { icon: "" }) {
tray = new Tray(options.icon);
};
trayModule.refreshTray = function (mainWindow) {
if (!tray) {
trayModule.addTray();
}
tray.on("click", function (e) {
if (mainWindow) {
mainWindow.show();
}
});
tray.setToolTip("Tidal-hifi");
if (mainWindow && store.get(settings.minimizeOnClose)) {
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: "Toggle Window",
click: function () {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
},
},
{
label: "Quit",
click: function () {
mainWindow.destroy();
app.quit();
},
},
...mainMenu, //we add menu items from the other context
])
);
} else {
tray.setContextMenu(getMenu());
}
};
module.exports = trayModule;

32
src/scripts/tray.ts Normal file
View File

@@ -0,0 +1,32 @@
import { BrowserWindow, Tray } from "electron";
import { getMenu } from "./menu";
let tray: Tray;
export const addTray = function (mainWindow: BrowserWindow, options = { icon: "" }) {
tray = new Tray(options.icon);
tray.setIgnoreDoubleClickEvents(true);
tray.setToolTip("Tidal-hifi");
const menu = getMenu(mainWindow);
tray.setContextMenu(menu);
tray.on("click", function () {
if (mainWindow.isVisible()) {
if (!mainWindow.isFocused()) {
mainWindow.focus();
} else {
mainWindow.hide();
}
} else {
mainWindow.show();
}
});
};
export const refreshTray = function (mainWindow: BrowserWindow) {
if (!tray) {
addTray(mainWindow);
}
};

View File

@@ -1,11 +0,0 @@
const windowFunctions = {};
windowFunctions.setTitle = function(title) {
window.document.title = title;
};
windowFunctions.getTitle = function() {
return window.document.title;
};
module.exports = windowFunctions;

View File

@@ -0,0 +1,7 @@
export const setTitle = function (title: string) {
window.document.title = title;
};
export const getTitle = function () {
return window.document.title;
};

10
src/themes/Blood.scss Normal file
View File

@@ -0,0 +1,10 @@
$foreground: red;
$background: black;
span {
color: $foreground;
}
.sidebar--WvRg_ {
background-color: $background;
}

View File

@@ -0,0 +1,85 @@
:root {
--footer-player-background: #1a1b26;
--sidebar-background: #1a1b26;
--sidebar-hover-background: #414868;
--sidebar-menu-top-text: #565f89;
--sidebar-menu-playlist-text: #565f89;
--search-background: #1a1b26;
--main-background: #16161e;
--main-navigation-control-background: #1a1b26;
--main-feed-button-background: #1a1b26;
--player-control-background: #24283b;
--player-control-active-button: #ff9e64;
--player-progress-bar: #ff9e64;
--indicator-hifi-background: #9ece6a;
--indicator-hifi-span: #1a1b26;
--player-control-favorite: #f7768e;
--search-dialog-background: #24283b;
--right-queue-background: #24283b;
}
.player--gAOQG.notFullscreen--xbpBL {
background-color: var(--footer-player-background);
}
.sidebar--jVJai {
background-color: var(--sidebar-background);
contain: strict;
flex-grow: 1;
overflow-y: auto;
}
.item--buEQw:hover {
background-color: var(--sidebar-hover-background);
}
.main--jxfcQ {
background-color: var(--main-background);
}
button.button--yO9Cd {
background-color: var(--main-navigation-control-background);
}
.player--gAOQG.lossLess--ON3FI button.withBackground[aria-checked="true"] path {
fill: var(--player-control-active-button);
}
.player--gAOQG.lossLess--ON3FI button.withBackground[aria-checked="true"] {
background-color: var(--player-control-background);
}
.activeItem--kFIk0 .activeItem--kFIk0 .playlistItem--mQrxp .section--PSIay.playingItem--eWkYS {
color: #565f89;
}
.progressBarWrapper--IBBI9 {
color: var(--player-progress-bar);
}
.playbackControls--FhKVf button .tidal-ui__icon {
transform: scale(1);
}
.css-11m9iw3 {
background-color: var(--indicator-hifi-background);
}
.css-11m9iw3 span {
color: var(--indicator-hifi-span);
}
.activeItem--kFIk0 {
color: var(--sidebar-menu-top-text);
}
.activeItem--kFIk0 .playlistItem--mQrxp {
color: var(--sidebar-menu-playlist-text);
}
button.feedBell--kvAbD {
background-color: var(--main-feed-button-background);
}
.baseContainer--jxCbW {
background-color: var(--search-dialog-background);
}
.favoriteButton--Qladw.is-favorite path {
fill: var(--player-control-favorite);
}
.container--PFTHk {
background-color: var(--right-queue-background);
}
.container--cl4MJ{
background-color: var(--search-background);
}
.searchFieldHighlighted--Fitvs {
color: var(--snow-white);
}
.searchField--EGBSq {
background-color: var(--search-background);
}

60
src/types/mpris-service.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
declare class InitOptions {
name: string;
identity: string;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
supportedInterfaces: string[];
desktopEntry: string;
}
declare class Player {
metadata: {
"xesam:title": string;
"xesam:artist": string[];
"xesam:album": string;
"mpris:artUrl": string;
"mpris:length": number;
"mpris:trackid": string;
// other options
[key: string]: string | number | string[] | object;
};
playbackStatus: string;
identity: string;
fullscreen: boolean;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
canQuit: boolean;
canRaise: boolean;
canSetFullscreen: boolean;
hasTrackList: boolean;
desktopEntry: string;
loopStatus: string;
shuffle: boolean;
volume: number;
canControl: boolean;
canPause: boolean;
canPlay: boolean;
canSeek: boolean;
canGoNext: boolean;
canGoPrevious: boolean;
rate: number;
minimumRate: number;
maximumRate: number;
playlists: string[];
activePlaylist: string;
constructor(opts: { name: string; supportedInterfaces?: string[] });
constructor(opts: InitOptions);
getPosition(): number;
seeked(): void;
getTrackIndex(trackId: number): number;
getTrack(trackId: number): string;
addTrack(track: object): void;
removeTrack(trackId: number): number;
getPlaylistIndex(playlistId: number): number;
setPlaylists(playlists: object): void;
setActivePlaylist(playlistId: number): void;
on(event: string | symbol, listener: (...args: object[]) => void): this;
}

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"typeRoots": ["src/types", "node_modules/@types"],
"module": "commonjs",
"target": "ES6",
"lib": ["ES2020", "DOM"],
"noImplicitAny": true,
"sourceMap": true,
"allowJs": true,
"outDir": "ts-dist",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["src/**/*"]
}