Compare commits
	
		
			151 Commits
		
	
	
		
			2.2.1
			...
			ed6f04b6d4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ed6f04b6d4 | ||
|  | ffe8278c8c | ||
|  | e9434cc5ea | ||
|  | d81912db0c | ||
|  | c0110632e6 | ||
|  | 3571289d28 | ||
| 11cc209025 | |||
| f5ccbda7d9 | |||
| e8cf1783e8 | |||
| 8037a73e57 | |||
| 45e191dae0 | |||
| f147536b12 | |||
| d03bb58afa | |||
| a39fef8d49 | |||
| 41ca1d5a43 | |||
| 6969de8270 | |||
| ad05b767d8 | |||
|  | 6d873ce287 | ||
| 63d123f96a | |||
| f038412c50 | |||
| ff02287df7 | |||
|  | f221ded108 | ||
| 1440f70100 | |||
| 439333e15a | |||
| b9854e0595 | |||
| 8b56c28d75 | |||
| 700a14fe88 | |||
| 3c835077d5 | |||
| 194de286c8 | |||
| a7dee5c2c9 | |||
| 8036cbb919 | |||
| 90cf231c76 | |||
| 42a70534f2 | |||
| b07865d98b | |||
| cc26bfa080 | |||
| 822bdf401e | |||
| a169c57a52 | |||
| 60eb1bbef9 | |||
| 1761c8dd40 | |||
| 62244f432a | |||
| a408a6a8cc | |||
| 6e5a2c626c | |||
| 4350ab9bd9 | |||
| 77a853e980 | |||
| 757f8511c0 | |||
| 2ef457be2c | |||
| 2c5d2b9530 | |||
| 757bd0da80 | |||
| d823f07ed8 | |||
| 32ade76ae3 | |||
| a1c02dfed3 | |||
| 21d6e57cb9 | |||
| 53e4711c39 | |||
| e8509d42e7 | |||
| 46d030cf8e | |||
| 412f1ae3e3 | |||
| 68f0c89ec2 | |||
| 8d44ff8afb | |||
| bccc979f43 | |||
| 6849952c41 | |||
| 07be74af9f | |||
| fc6adc25ca | |||
| 4498e8a73e | |||
| 3d2a9c3992 | |||
| af6bfaf55e | |||
|  | 8bac90e0f1 | ||
|  | 887c75f61a | ||
|  | cde7408cc4 | ||
| 05b422e045 | |||
| 35289d8216 | |||
| ea42b79cd8 | |||
| 6d859cf780 | |||
| af20092053 | |||
| 166ca353cf | |||
| b807aa2f76 | |||
|  | ef8ffe47f5 | ||
| ba7b2a5717 | |||
| 0d93bedb4d | |||
|  | 1de71aa82b | ||
|  | b2e68f5a8f | ||
|  | a2a2023853 | ||
|  | 26c8a38350 | ||
| eb93fbc35d | |||
|  | d3c56fa445 | ||
| 6998992011 | |||
| 108e1d65d4 | |||
| 1097f83911 | |||
| 8c734777cc | |||
|  | de17ac6113 | ||
|  | ced41c00d7 | ||
|  | 744016f307 | ||
|  | ad8ef71c6b | ||
|  | 0120391418 | ||
| d0f9a34f9c | |||
| 3b316f2301 | |||
| c0d9cd2834 | |||
| 0620d87d8b | |||
| 57b7f9148f | |||
| 63ccff97ea | |||
| 3a4d23738f | |||
| c96bdb0d28 | |||
|  | 115d8c6c5c | ||
|  | cd2a068470 | ||
| bf260b14e0 | |||
| d161a68c95 | |||
|  | 9de8cea50e | ||
| 5f330a7c48 | |||
| 732710c3ef | |||
| 4941aae950 | |||
| 1439a11969 | |||
|  | 3a3e0e1a2d | ||
| fa9ab22867 | |||
| 207a61d199 | |||
|  | 7b18322e17 | ||
| 8f47756244 | |||
|  | cdcf9431bf | ||
| 374f3da740 | |||
| 3965ada0a2 | |||
| 79ff02d06c | |||
| 7b2afd2290 | |||
| 5fde20ace1 | |||
| 7f5f5e7f62 | |||
| 6a1a1efe74 | |||
| 94e1bb1780 | |||
| d66dd8cc9e | |||
|  | de97ac8a00 | ||
|  | 82ac5edf22 | ||
|  | 909c8ee8ba | ||
|  | 15b6b13e14 | ||
| 89589b75e1 | |||
| 6a7b3eefd4 | |||
|  | 0583c4a188 | ||
|  | 8855e7f89f | ||
|  | 4fbb598c50 | ||
| 101fe967d3 | |||
|  | 53468e0dc3 | ||
| 53cecbcd18 | |||
| 5a65f60cc5 | |||
| d51d5cdc24 | |||
| 0dec967e71 | |||
|  | c940d0991d | ||
| 662ef6ad7b | |||
| 5313ab13d3 | |||
| f43f227191 | |||
|  | ae25d88e94 | ||
| 5ef6074015 | |||
| 8fea5265e7 | |||
| 8d2e03ca6b | |||
| 1074de228b | |||
|  | 64d1aa4041 | ||
|  | 6608330ed3 | 
							
								
								
									
										16
									
								
								.drone.yml
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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 | ||||||
| @@ -50,4 +54,4 @@ trim_trailing_whitespace = ignore | |||||||
| charset = ignore | charset = ignore | ||||||
|  |  | ||||||
| [{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**] | [{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**] | ||||||
| insert_final_newline = false | insert_final_newline = false | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								.eslintrc
									
									
									
									
									
										Normal 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" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,24 +5,32 @@ 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 | ||||||
|     steps: |     steps: | ||||||
|  |       - name: update apt | ||||||
|  |         run: sudo apt-get update | ||||||
|  |       - name: Install libarchive-tools | ||||||
|  |         run: sudo apt-get install -y libarchive-tools | ||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 12 |           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: 12 |           node-version: 19 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|  |  | ||||||
| @@ -32,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: 12 |           node-version: 19 | ||||||
|       - run: npm install |       - run: npm install | ||||||
|       - run: npm run build |       - run: npm run build | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,14 +5,22 @@ 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 | ||||||
|     steps: |     steps: | ||||||
|  |       - name: update apt | ||||||
|  |         run: sudo apt-get update | ||||||
|  |       - name: Install libarchive-tools | ||||||
|  |         run: sudo apt-get install -y libarchive-tools | ||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|       - uses: actions/setup-node@master |       - uses: actions/setup-node@master | ||||||
|         with: |         with: | ||||||
|           node-version: 12 |           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 | ||||||
| @@ -21,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: 12 |           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 | ||||||
| @@ -40,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: 12 |           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
									
									
								
							
							
						
						| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								.stylelintrc.json
									
									
									
									
									
										Normal 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 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | { | ||||||
|  |   "cSpell.words": [ | ||||||
|  |     "flac", | ||||||
|  |     "geqnfr", | ||||||
|  |     "hifi", | ||||||
|  |     "playpause", | ||||||
|  |     "rescrobbler", | ||||||
|  |     "Songwhip", | ||||||
|  |     "trackid", | ||||||
|  |     "tracklist", | ||||||
|  |     "widevine", | ||||||
|  |     "xesam" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -4,6 +4,184 @@ 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.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: | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## 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 | ||||||
|  |  | ||||||
|  | - Mar0xy fixed some build issues (thanks!) | ||||||
|  | - vincens2005 fixed the quit button in the menubar | ||||||
|  |  | ||||||
|  | ## 2.8.0 | ||||||
|  |  | ||||||
|  | - Added the ability to mute artists automatically | ||||||
|  | - Added better error handling for discord rpc | ||||||
|  |  | ||||||
|  | ## 2.7.2 | ||||||
|  |  | ||||||
|  | - Disabled sandboxing to fix a display compositor issue on Linux. | ||||||
|  |  | ||||||
|  | ## 2.7.1 | ||||||
|  |  | ||||||
|  | - Fixed bug: Triggering full screen from the Tidal web app would cause the menubar to be visible even if it was disabled in the settings | ||||||
|  |  | ||||||
|  | ## 2.7.0 | ||||||
|  |  | ||||||
|  | - Switched to the native Notifier (removed node-notifier) | ||||||
|  | - Album art now also has a name, based on [best effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847) | ||||||
|  |  | ||||||
|  | ## 2.6.0 | ||||||
|  |  | ||||||
|  | - Add album images to media info and discord | ||||||
|  |  | ||||||
|  | ## 2.5.0 | ||||||
|  |  | ||||||
|  | - Notify-send now correctly shows "Tidal HiFi" as the program name | ||||||
|  | - Updated dependencies (including electron itself) | ||||||
|  |  | ||||||
|  | ### known issues | ||||||
|  |  | ||||||
|  | - Requires older version of nodejs due to electron-builder (use lts/gallium) | ||||||
|  |  | ||||||
|  | ### builds | ||||||
|  |  | ||||||
|  | updated to nodejs 16 in actions | ||||||
|  |  | ||||||
|  | ## 2.4.0 | ||||||
|  |  | ||||||
|  | - Added more MPRIS settings | ||||||
|  | - Added instruction for rescrobbler to get last.fm working without sandbox mode | ||||||
|  |  | ||||||
|  | ## 2.3.0 | ||||||
|  |  | ||||||
|  | - Added a setting to minimize to tray on app close (off by default) | ||||||
|  | - Added the main menu to the tray icon | ||||||
|  |  | ||||||
| ## 2.2.1 | ## 2.2.1 | ||||||
|  |  | ||||||
| - artists is now gotten specifically from the footer. This fixes the [unknown artists bug](https://github.com/Mastermindzh/tidal-hifi/issues/45). | - artists is now gotten specifically from the footer. This fixes the [unknown artists bug](https://github.com/Mastermindzh/tidal-hifi/issues/45). | ||||||
| @@ -16,10 +194,10 @@ moved to: [https://github.com/Mastermindzh/tidal-hifi-aur](https://github.com/Ma | |||||||
| ## 2.2.0 | ## 2.2.0 | ||||||
|  |  | ||||||
| - The discord integration now adds a time remaining field based on the song duration | - The discord integration now adds a time remaining field based on the song duration | ||||||
| - All fields (current, remaining, and url are also available in the API*) | - All fields (current, remaining, and url are also available in the API\*) | ||||||
| - the artist field is now correctly identified | - the artist field is now correctly identified | ||||||
|  |  | ||||||
| * current time only updates on play/pause. | \* current time only updates on play/pause. | ||||||
|  |  | ||||||
| ## 2.1.1 | ## 2.1.1 | ||||||
|  |  | ||||||
| @@ -44,7 +222,7 @@ moved to: [https://github.com/Mastermindzh/tidal-hifi-aur](https://github.com/Ma | |||||||
|  |  | ||||||
| ## 1.3.0 | ## 1.3.0 | ||||||
|  |  | ||||||
| -- re-enabled mpris-service wit the electron downloader fixes | -- re-enabled MPRIS-service wit the electron downloader fixes | ||||||
|  |  | ||||||
| ## 1.2.0 | ## 1.2.0 | ||||||
|  |  | ||||||
| @@ -59,7 +237,7 @@ Bugfixes: | |||||||
| ## 1.1.0 | ## 1.1.0 | ||||||
|  |  | ||||||
| - updated to electron 8.0.0 | - updated to electron 8.0.0 | ||||||
| - Added a beta-version of the mpris service | - Added a beta-version of the MPRIS service | ||||||
|  |  | ||||||
| - Bugfixes: | - Bugfixes: | ||||||
|   - icon on gnome not showing in launcher |   - icon on gnome not showing in launcher | ||||||
|   | |||||||
							
								
								
									
										172
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,63 +1,125 @@ | |||||||
| <h1> | # Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/> | ||||||
| Tidal-hifi |  | ||||||
| <img src = "./build/icon.png" height="40" align="right" /> |  [](https://github.com/Mastermindzh/tidal-hifi/actions) [](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [](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. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 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) | ||||||
| - [Why](#why) |     - [Snap](#snap) | ||||||
| - [Why not extend existing projects?](#why-not-extend-existing-projects) |     - [Arch Linux](#arch-linux) | ||||||
| - [Special thanks to...](#special-thanks-to) |     - [Flatpak](#flatpak) | ||||||
| - [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont) |     - [Nix](#nix) | ||||||
| - [Images](#images) |     - [Using source](#using-source) | ||||||
|   - [settings window](#settings-window) |   - [Integrations](#integrations) | ||||||
|   - [user setups](#user-setups) |     - [Known bugs](#known-bugs) | ||||||
|  |       - [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)) | ||||||
|  |  | ||||||
|  | ## Contributions | ||||||
|  |  | ||||||
|  | To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals. | ||||||
|  |  | ||||||
|  | -  [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 | ||||||
|  |  | ||||||
| Arch Linux users can use the AUR to install tidal-hifi: | Arch Linux users can use the AUR to install tidal-hifi: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| trizen tidal-hifi | 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 | ||||||
| @@ -69,69 +131,51 @@ 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 |  | ||||||
| - Custom [integrations](#integrations) |  | ||||||
| - [Settings feature](./docs/settings.png) to disable certain functionality. (`ctrl+=`) |  | ||||||
|  |  | ||||||
| ## 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. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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) | ||||||
|  |  | ||||||
| ### Known bugs | ### Known bugs | ||||||
|  |  | ||||||
| - [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4). | #### last.fm doesn't work out of the box. Use rescrobbler as a workaround | ||||||
|  |  | ||||||
| ## Why | 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). | ||||||
|  | For now that will be the default workaround. | ||||||
|  |  | ||||||
| I moved from Spotify over to Tidal and found Linux support to be lacking. | #### DRM not working on Windows | ||||||
|  |  | ||||||
| When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it. | 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. | ||||||
|  |  | ||||||
| ## Why not extend existing projects? | ## Special thanks to | ||||||
|  |  | ||||||
| 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://secure.kwf.nl/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 | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### 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
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/22x22.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/24x24.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/256x256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 29 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/384x384.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 46 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/48x48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icons/64x64.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										42
									
								
								build/electron-builder.base.yml
									
									
									
									
									
										Normal 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"] | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								build/electron-builder.unpacked.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | extends: ./build/electron-builder.base.yml | ||||||
|  | linux: | ||||||
|  |   target: | ||||||
|  |     - dir | ||||||
| @@ -1,36 +1,16 @@ | |||||||
| appId: com.rickvanlieshout.tidal-hifi | extends: ./build/electron-builder.base.yml | ||||||
| electronVersion: 8.5.2 |  | ||||||
| electronDownload: |  | ||||||
|   version: 8.5.2-wvvmp |  | ||||||
|   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 | ||||||
|     - deb |     - deb | ||||||
|     - rpm |     - rpm | ||||||
|     - 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: icon.png | ||||||
|  |   artifactName: "tidalhifi" | ||||||
|  |   appId: com.rickvanlieshout.tidalhifi | ||||||
|  |   executableName: tidalhifi | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								build/icon-inverted.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 102 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icon.icns
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								docs/images/customcss-config.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 38 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/customcss.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/discord.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.5 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 800 KiB After Width: | Height: | Size: 800 KiB | 
| Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 726 KiB | 
| Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/theming.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 49 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/images/tokyo-night.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/no-dutch-music.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										38
									
								
								docs/theming.md
									
									
									
									
									
										Normal 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. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## 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`. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## config | ||||||
|  |  | ||||||
|  | The theme selector and customCSS are stored in the config file. | ||||||
|  | The custom CSS is stored as a list of lines. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## 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). | ||||||
							
								
								
									
										43
									
								
								listen.tidal.com-parsing-scripts/verifyElements.js
									
									
									
									
									
										Normal 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; | ||||||
|  | })(); | ||||||
							
								
								
									
										12852
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										78
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,43 +1,75 @@ | |||||||
| { | { | ||||||
|   "name": "tidal-hifi", |   "name": "tidal-hifi", | ||||||
|   "version": "2.2.1", |   "version": "5.4.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": { | ||||||
|     "discord-rpc": "^3.2.0", |     "@electron/remote": "^2.0.10", | ||||||
|     "electron-store": "^5.1.1", |     "axios": "^1.4.0", | ||||||
|     "express": "^4.17.1", |     "discord-rpc": "^4.0.1", | ||||||
|     "hotkeys-js": "^3.7.6", |     "electron-store": "^8.1.0", | ||||||
|     "mpris-service": "^2.1.0", |     "express": "^4.18.2", | ||||||
|     "node-notifier": "^9.0.1", |     "hotkeys-js": "^3.11.2", | ||||||
|     "request": "^2.88.2" |     "mpris-service": "^2.1.2", | ||||||
|  |     "request": "^2.88.2", | ||||||
|  |     "sass": "^1.64.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@mastermindzh/prettier-config": "^1.0.0", |     "@mastermindzh/prettier-config": "^1.0.0", | ||||||
|     "dot-prop": ">=4.2.1", |     "@types/discord-rpc": "^4.0.5", | ||||||
|     "electron": "git+https://github.com/castlabs/electron-releases.git#v10.4.3-wvvmp", |     "@types/express": "^4.17.17", | ||||||
|     "electron-builder": "^21.2.0", |     "@types/node": "^20.4.4", | ||||||
|     "electron-reload": "^1.5.0", |     "@types/request": "^2.48.8", | ||||||
|     "prettier": "^2.0.4" |     "@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
									
								
							
							
						
						| @@ -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" }], | ||||||
|  | }; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| const globalEvents = { | export const globalEvents = { | ||||||
|   play: "play", |   play: "play", | ||||||
|   pause: "pause", |   pause: "pause", | ||||||
|   playPause: "playPause", |   playPause: "playPause", | ||||||
| @@ -6,9 +6,10 @@ const globalEvents = { | |||||||
|   previous: "previous", |   previous: "previous", | ||||||
|   updateInfo: "update-info", |   updateInfo: "update-info", | ||||||
|   hideSettings: "hideSettings", |   hideSettings: "hideSettings", | ||||||
|  |   refreshMenuBar: "refreshMenubar", | ||||||
|   showSettings: "showSettings", |   showSettings: "showSettings", | ||||||
|   storeChanged: "storeChanged", |   storeChanged: "storeChanged", | ||||||
|   error: "error", |   error: "error", | ||||||
|  |   whip: "whip", | ||||||
|  |   log: "log", | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| module.exports = globalEvents; |  | ||||||
| @@ -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; |  | ||||||
| @@ -1,31 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Object to type my settings file: |  | ||||||
|  * |  | ||||||
|  *    notifications: true, |  | ||||||
|  *    api: true, |  | ||||||
|  *    apiSettings: { |  | ||||||
|  *      port: 47836, |  | ||||||
|  *    }, |  | ||||||
|  *    windowBounds: { width: 800, height: 600 }, |  | ||||||
|  */ |  | ||||||
| const settings = { |  | ||||||
|   notifications: "notifications", |  | ||||||
|   api: "api", |  | ||||||
|   menuBar: "menuBar", |  | ||||||
|   playBackControl: "playBackControl", |  | ||||||
|   apiSettings: { |  | ||||||
|     root: "apiSettings", |  | ||||||
|     port: "apiSettings.port", |  | ||||||
|   }, |  | ||||||
|   mpris: "mpris", |  | ||||||
|   enableCustomHotkeys: "enableCustomHotkeys", |  | ||||||
|   trayIcon: "trayIcon", |  | ||||||
|   enableDiscord: "enableDiscord", |  | ||||||
|   windowBounds: { |  | ||||||
|     root: "windowBounds", |  | ||||||
|     width: "windowBounds.width", |  | ||||||
|     height: "windowBounds.height", |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = settings; |  | ||||||
							
								
								
									
										50
									
								
								src/constants/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | |||||||
|  | /** | ||||||
|  |  * Object to type my settings file: | ||||||
|  |  * | ||||||
|  |  *    notifications: true, | ||||||
|  |  *    api: true, | ||||||
|  |  *    apiSettings: { | ||||||
|  |  *      port: 47836, | ||||||
|  |  *    }, | ||||||
|  |  *    windowBounds: { width: 800, height: 600 }, | ||||||
|  |  */ | ||||||
|  | export const settings = { | ||||||
|  |   adBlock: "adBlock", | ||||||
|  |   api: "api", | ||||||
|  |   apiSettings: { | ||||||
|  |     root: "apiSettings", | ||||||
|  |     port: "apiSettings.port", | ||||||
|  |   }, | ||||||
|  |   customCSS: "customCSS", | ||||||
|  |   disableBackgroundThrottle: "disableBackgroundThrottle", | ||||||
|  |   disableHardwareMediaKeys: "disableHardwareMediaKeys", | ||||||
|  |   enableCustomHotkeys: "enableCustomHotkeys", | ||||||
|  |   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: { | ||||||
|  |     root: "windowBounds", | ||||||
|  |     width: "windowBounds.width", | ||||||
|  |     height: "windowBounds.height", | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| module.exports = { | export const statuses = { | ||||||
|   playing: "playing", |   playing: "playing", | ||||||
|   paused: "paused", |   paused: "paused", | ||||||
| }; | }; | ||||||
							
								
								
									
										3
									
								
								src/constants/values.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | export default { | ||||||
|  |   name: "tidal-hifi", | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | declare module "mpris-service"; | ||||||
							
								
								
									
										83
									
								
								src/features/listenbrainz/listenbrainz.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,83 @@ | |||||||
|  | import axios from "axios"; | ||||||
|  | import { settingsStore } from "../../scripts/settings"; | ||||||
|  | import { settings } from "../../constants/settings"; | ||||||
|  | import { MediaStatus } from "../../models/mediaStatus"; | ||||||
|  | import Store from "electron-store"; | ||||||
|  |  | ||||||
|  | const ListenBrainzStore = new Store({name: "listenbrainz"}); | ||||||
|  |  | ||||||
|  | export class ListenBrainz { | ||||||
|  |   /** | ||||||
|  |    * 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<any> { | ||||||
|  |     try { | ||||||
|  |       if (status == MediaStatus.paused) { | ||||||
|  |         return false; | ||||||
|  |       } 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("OldData") as string[]; | ||||||
|  |         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(settings.ListenBrainz.api)}/1/submit-listens`, playing_data, { | ||||||
|  |             headers:{ | ||||||
|  |                 "Content-Type": "application/json", | ||||||
|  |                 "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}` | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         if (!OldData) { | ||||||
|  |             ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]); | ||||||
|  |         } else if (OldData[1] != title) { | ||||||
|  |             // This constructs the data required to scrobble the data after the song finishes | ||||||
|  |             const scrobble_data = { | ||||||
|  |                 listen_type: "single", | ||||||
|  |                 payload: [ | ||||||
|  |                     { | ||||||
|  |                         listened_at: OldData[0], | ||||||
|  |                         track_metadata: { | ||||||
|  |                             additional_info: { | ||||||
|  |                                 media_player: "Tidal Hi-Fi", | ||||||
|  |                                 submission_client: "Tidal Hi-Fi", | ||||||
|  |                                 music_service: "listen.tidal.com", | ||||||
|  |                                 duration: OldData[3], | ||||||
|  |                             }, | ||||||
|  |                             artist_name: OldData[2], | ||||||
|  |                             track_name: OldData[1], | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }; | ||||||
|  |             await axios.post(`${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, scrobble_data, { | ||||||
|  |                 headers:{ | ||||||
|  |                     "Content-Type": "application/json", | ||||||
|  |                     "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}` | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log(JSON.stringify(error)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								src/features/logger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | |||||||
|  | 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 }); | ||||||
|  |     } else { | ||||||
|  |       console.log(`${content} \n ${JSON.stringify(object, null, 2)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/features/songwhip/models/Artist.ts
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/features/songwhip/models/ServiceLinks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | export interface ServiceLinks { | ||||||
|  |   link: string; | ||||||
|  |   countries: string[]; | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								src/features/songwhip/models/whip.ts
									
									
									
									
									
										Normal 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[]; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								src/features/songwhip/songwhip.ts
									
									
									
									
									
										Normal 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}`; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										124
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						| @@ -1,124 +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`), |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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, |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   mainWindow.setMenuBarVisibility(store.get(settings.menuBar)); |  | ||||||
|  |  | ||||||
|   // load the Tidal website |  | ||||||
|   mainWindow.loadURL(tidalUrl); |  | ||||||
|  |  | ||||||
|   // run stuff after first load |  | ||||||
|   mainWindow.webContents.once("did-finish-load", () => {}); |  | ||||||
|  |  | ||||||
|   // 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(); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| 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.storeChanged, (event, arg) => { |  | ||||||
|   mainWindow.setMenuBarVisibility(store.get(settings.menuBar)); |  | ||||||
|  |  | ||||||
|   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
									
								
							
							
						
						| @@ -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 menuBarVisbility 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
									
								
							
							
						
						| @@ -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; | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/models/mediaStatus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | export enum MediaStatus { | ||||||
|  |   playing = "playing", | ||||||
|  |   paused = "paused", | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/models/options.ts
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/pages/settings/fonts/NotoSans-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/pages/settings/fonts/NotoSans-Light.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/pages/settings/fonts/NotoSans-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/pages/settings/fonts/NotoSans-SemiBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -1,98 +0,0 @@ | |||||||
| let notifications; |  | ||||||
| let playBackControl; |  | ||||||
| let api; |  | ||||||
| let port; |  | ||||||
| let menuBar; |  | ||||||
|  |  | ||||||
| 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); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 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); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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"); |  | ||||||
|   mpris = get("mprisCheckbox"); |  | ||||||
|   enableCustomHotkeys = get("enableCustomHotkeys"); |  | ||||||
|   enableDiscord = get("enableDiscord"); |  | ||||||
|  |  | ||||||
|   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); |  | ||||||
| }); |  | ||||||
							
								
								
									
										227
									
								
								src/pages/settings/preload.ts
									
									
									
									
									
										Normal 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); | ||||||
|  | }); | ||||||
| @@ -2,462 +2,367 @@ | |||||||
| <html lang="en"> | <html lang="en"> | ||||||
|  |  | ||||||
| <head> | <head> | ||||||
|     <meta charset="UTF-8" /> |   <title>Tidal Hi-Fi settings</title> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |   <meta charset="UTF-8" /> | ||||||
|     <meta http-equiv="X-UA-Compatible" content="ie=edge" /> |   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|  |   <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> |  | ||||||
|                     <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>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 trough 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 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 class="group__option"> | ||||||
|  |               <div class="group__description"> | ||||||
|  |                 <h4>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> | </html> | ||||||
|     .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); |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
| </html> |  | ||||||
							
								
								
									
										445
									
								
								src/pages/settings/settings.scss
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								src/pages/settings/theming.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |   } | ||||||
|  | }; | ||||||
							
								
								
									
										410
									
								
								src/preload.js
									
									
									
									
									
								
							
							
						
						| @@ -1,410 +0,0 @@ | |||||||
| const { setTitle } = require("./scripts/window-functions"); |  | ||||||
| const { dialog, process } = 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 notifier = require("node-notifier"); |  | ||||||
| const notificationPath = `${app.getPath("userData")}/notification.jpg`; |  | ||||||
| let currentSong = ""; |  | ||||||
| let player; |  | ||||||
| let currentPlayStatus = statuses.paused; |  | ||||||
| let progressBarTime; |  | ||||||
| let currentTimeChanged = false; |  | ||||||
| let currentTime; |  | ||||||
| let currentURL = undefined; |  | ||||||
|  |  | ||||||
| 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", |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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)"; |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 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(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Update Tidal-hifi's media info |  | ||||||
|  * |  | ||||||
|  * @param {*} options |  | ||||||
|  */ |  | ||||||
| function updateMediaInfo(options, notify) { |  | ||||||
|   if (options) { |  | ||||||
|     ipcRenderer.send(globalEvents.updateInfo, options); |  | ||||||
|     store.get(settings.notifications) && notify && notifier.notify(options); |  | ||||||
|  |  | ||||||
|     if (player) { |  | ||||||
|       player.metadata = { |  | ||||||
|         ...player.metadata, |  | ||||||
|         ...{ |  | ||||||
|           "xesam:title": options.title, |  | ||||||
|           "xesam:artist": [options.artists], |  | ||||||
|           "mpris:artUrl": options.image, |  | ||||||
|         }, |  | ||||||
|       }; |  | ||||||
|       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 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, |  | ||||||
|     status: currentStatus, |  | ||||||
|     url: currentURL, |  | ||||||
|     current: current, |  | ||||||
|     duration: duration, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const playStatusChanged = currentStatus !== currentPlayStatus; |  | ||||||
|   const progressBarTimeChanged = progressBarcurrentTime !== progressBarTime; |  | ||||||
|   const titleOrArtistChanged = currentSong !== songDashArtistTitle; |  | ||||||
|  |  | ||||||
|   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")) { |  | ||||||
|         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); |  | ||||||
|       }, |  | ||||||
|       () => {} |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| }, 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); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     player.on("quit", function () { |  | ||||||
|       app.quit(); |  | ||||||
|     }); |  | ||||||
|   } catch (exception) { |  | ||||||
|     console.log("player api not working"); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| addHotKeys(); |  | ||||||
| addIPCEventListeners(); |  | ||||||
							
								
								
									
										554
									
								
								src/preload.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,554 @@ | |||||||
|  | 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 { statuses } from "./constants/statuses"; | ||||||
|  | import { Songwhip } from "./features/songwhip/songwhip"; | ||||||
|  | import { ListenBrainz } 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"; | ||||||
|  |  | ||||||
|  | const notificationPath = `${app.getPath("userData")}/notification.jpg`; | ||||||
|  | const appName = "Tidal Hifi"; | ||||||
|  | let currentSong = ""; | ||||||
|  | let player: Player; | ||||||
|  | let currentPlayStatus = statuses.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 === statuses.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(settings.updateFrequency) as number; | ||||||
|  |   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"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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 = statuses.playing; | ||||||
|  |   } else { | ||||||
|  |     status = statuses.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(); | ||||||
|  |     } | ||||||
|  |     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 == 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 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); | ||||||
|  |       if (settingsStore.get(settings.ListenBrainz.enabled)) { | ||||||
|  |         ListenBrainz.scrobble(options.title, options.artists, options.status, convertDuration(options.duration)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 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(); | ||||||
| @@ -1,83 +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), |  | ||||||
|           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 }).catch(console.error); |  | ||||||
|   discordModule.rpc = rpc; |  | ||||||
|  |  | ||||||
|   rpc.on("ready", () => { |  | ||||||
|     rpc.setActivity(idleStatus); |  | ||||||
|   }); |  | ||||||
|   ipcMain.on(globalEvents.updateInfo, observer); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Remove any RPC connection with discord and remove the event listener on globalEvents.updateInfo |  | ||||||
|  */ |  | ||||||
| discordModule.unRPC = function () { |  | ||||||
|   rpc.clearActivity(); |  | ||||||
|   rpc.destroy(); |  | ||||||
|   rpc = false; |  | ||||||
|   discordModule.rpc = rpc; |  | ||||||
|   ipcMain.removeListener(globalEvents.updateInfo, observer); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = discordModule; |  | ||||||
							
								
								
									
										88
									
								
								src/scripts/discord.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -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
									
								
							
							
						
						| @@ -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); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -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
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | |||||||
|  | import { BrowserWindow, dialog } from "electron"; | ||||||
|  | import express, { Response } from "express"; | ||||||
|  | import fs from "fs"; | ||||||
|  | import { globalEvents } from "./../constants/globalEvents"; | ||||||
|  | import { statuses } from "./../constants/statuses"; | ||||||
|  | import { mediaInfo } from "./mediaInfo"; | ||||||
|  | import { settingsStore } from "./settings"; | ||||||
|  | import { settings } from "../constants/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 == statuses.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); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -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
									
								
							
							
						
						| @@ -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); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -1,29 +1,28 @@ | |||||||
| const statuses = require("./../constants/statuses"); | import { MediaInfo } from "../models/mediaInfo"; | ||||||
|  | import { statuses } from "./../constants/statuses"; | ||||||
| 
 | 
 | ||||||
| const mediaInfo = { | export const mediaInfo = { | ||||||
|   title: "", |   title: "", | ||||||
|   artist: "", |   artists: "", | ||||||
|  |   album: "", | ||||||
|   icon: "", |   icon: "", | ||||||
|   status: statuses.paused, |   status: statuses.paused, | ||||||
|   url: "", |   url: "", | ||||||
|   current: "", |   current: "", | ||||||
|   duration: "" |   duration: "", | ||||||
| }; |   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.icon = propOrDefault(arg.icon); |   mediaInfo.icon = propOrDefault(arg.icon); | ||||||
|   mediaInfo.url = propOrDefault(arg.url); |   mediaInfo.url = propOrDefault(arg.url); | ||||||
|   mediaInfo.status = propOrDefault(arg.status); |   mediaInfo.status = propOrDefault(arg.status); | ||||||
|   mediaInfo.current = propOrDefault(arg.current); |   mediaInfo.current = propOrDefault(arg.current); | ||||||
|   mediaInfo.duration = propOrDefault(arg.duration); |   mediaInfo.duration = propOrDefault(arg.duration); | ||||||
|  |   mediaInfo.image = propOrDefault(arg.image); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @@ -31,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; |  | ||||||
| @@ -1,107 +0,0 @@ | |||||||
| const { Menu } = require("electron"); |  | ||||||
| const { showSettingsWindow } = require("./settings"); |  | ||||||
| const isMac = process.platform === "darwin"; |  | ||||||
|  |  | ||||||
| const settingsMenuEntry = { |  | ||||||
|   label: "Settings", |  | ||||||
|   click() { |  | ||||||
|     showSettingsWindow(); |  | ||||||
|   }, |  | ||||||
|   accelerator: "Control+/", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const mainMenu = [ |  | ||||||
|   ...(isMac |  | ||||||
|     ? [ |  | ||||||
|         { |  | ||||||
|           label: app.name, |  | ||||||
|           submenu: [ |  | ||||||
|             { role: "about" }, |  | ||||||
|             settingsMenuEntry, |  | ||||||
|             { type: "separator" }, |  | ||||||
|             { role: "services" }, |  | ||||||
|             { type: "separator" }, |  | ||||||
|             { role: "hide" }, |  | ||||||
|             { role: "hideothers" }, |  | ||||||
|             { role: "unhide" }, |  | ||||||
|             { type: "separator" }, |  | ||||||
|             { role: "quit" }, |  | ||||||
|           ], |  | ||||||
|         }, |  | ||||||
|       ] |  | ||||||
|     : []), |  | ||||||
|   // { role: 'fileMenu' } |  | ||||||
|   { |  | ||||||
|     label: "File", |  | ||||||
|     submenu: [settingsMenuEntry, isMac ? { role: "close" } : { role: "quit" }], |  | ||||||
|   }, |  | ||||||
|   // { 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
									
								
							
							
						
						| @@ -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)); | ||||||
|  | }; | ||||||
| @@ -1,73 +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, |  | ||||||
|     menuBar: true, |  | ||||||
|     apiSettings: { |  | ||||||
|       port: 47836, |  | ||||||
|     }, |  | ||||||
|     trayIcon: true, |  | ||||||
|     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
									
								
							
							
						
						| @@ -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; | ||||||
|  | }; | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| const { Tray } = require("electron"); |  | ||||||
| const { getMenu } = require("./menu"); |  | ||||||
| const trayModule = {}; |  | ||||||
| let tray; |  | ||||||
|  |  | ||||||
| trayModule.addTray = function (options = { icon: "" }) { |  | ||||||
|   tray = new Tray(options.icon); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| trayModule.refreshTray = function () { |  | ||||||
|   if (!tray) { |  | ||||||
|     trayModule.addTray(); |  | ||||||
|   } |  | ||||||
|   tray.on("click", function (e) { |  | ||||||
|     // do nothing on click |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   tray.setToolTip("Tidal-hifi"); |  | ||||||
|   tray.setContextMenu(getMenu()); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = trayModule; |  | ||||||
							
								
								
									
										32
									
								
								src/scripts/tray.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| const windowFunctions = {}; |  | ||||||
|  |  | ||||||
| windowFunctions.setTitle = function(title) { |  | ||||||
|   window.document.title = title; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| windowFunctions.getTitle = function() { |  | ||||||
|   return window.document.title; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = windowFunctions; |  | ||||||
							
								
								
									
										7
									
								
								src/scripts/window-functions.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | $foreground: red; | ||||||
|  | $background: black; | ||||||
|  |  | ||||||
|  | span { | ||||||
|  |   color: $foreground; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .sidebar--WvRg_ { | ||||||
|  |   background-color: $background; | ||||||
|  | } | ||||||
							
								
								
									
										85
									
								
								src/themes/Tokyo Night.scss
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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/**/*"] | ||||||
|  | } | ||||||