Compare commits

...

49 Commits

Author SHA1 Message Date
77a853e980 feat: add .css theme file upload and a unstyled theme selector 2023-05-08 22:31:22 +02:00
757f8511c0 ci: added theme files to resources 2023-05-08 00:03:05 +02:00
2ef457be2c Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-07 23:47:21 +02:00
2c5d2b9530 ci: cross-platform copy-files 2023-05-07 23:46:18 +02:00
757bd0da80 Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-07 23:28:02 +02:00
d823f07ed8 last files transformed from js -> ts 2023-05-07 23:27:46 +02:00
32ade76ae3 chore: compile sass themes 2023-05-07 23:25:35 +02:00
a1c02dfed3 last files transformed from js -> ts 2023-05-07 16:13:30 +02:00
21d6e57cb9 set up css folder 2023-05-07 15:48:00 +02:00
53e4711c39 chore: more typescript 2023-05-07 15:45:45 +02:00
e8509d42e7 organize imports 2023-05-01 23:31:37 +02:00
46d030cf8e transitioning to ts 2023-05-01 23:23:56 +02:00
412f1ae3e3 feat: added first typescript support
Didn't add many types yet. Just used to test out typescript compiler, copying files and building.
Now that all that seems to go well I can start converting all files to .ts and then adding proper typing everywhere
2023-05-01 13:44:02 +02:00
68f0c89ec2 replaced sass-lint with style-lint 2023-05-01 13:43:07 +02:00
8d44ff8afb Merge pull request #223 from Mastermindzh/release/5.1.0
Release/5.1.0
2023-04-27 15:30:09 +02:00
bccc979f43 fixed pacman icon 2023-04-27 15:07:40 +02:00
6849952c41 feat: added proper updates through the mediasession api. fixes #198 2023-04-27 14:29:28 +02:00
07be74af9f feat: added custom CSS settings. fixes #213 2023-04-27 14:13:32 +02:00
fc6adc25ca release: docs 2023-04-27 11:35:10 +02:00
4498e8a73e feat: you can now set updateFrequency in the settings window 2023-04-27 11:35:00 +02:00
3d2a9c3992 Merge pull request #219 from thanasistrisp/artists
[Bug]: get multiple artists instead of a single one
2023-04-27 10:51:35 +02:00
af6bfaf55e Merge pull request #222 from mdh34/mdh34-desktop-icon-fix
Fix Linux Desktop Icons
2023-04-27 10:46:44 +02:00
Matt Harris
8bac90e0f1 Fix Linux Icons 2023-04-26 21:34:17 +01:00
Matt Harris
887c75f61a change icon name in desktop file 2023-04-25 21:46:20 +01:00
Thanasis Trispiotis
cde7408cc4 fix: get multiple artists
- in mrpis multiple names are showing
- also at title
- skipping from settings any artist that is present in current artists at Tidal
2023-04-23 22:24:04 +03:00
05b422e045 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi 2023-04-22 21:19:42 +02:00
35289d8216 ci: updated workflow node versions 2023-04-22 21:19:36 +02:00
ea42b79cd8 ci: updated workflow node versions 2023-04-22 21:14:56 +02:00
6d859cf780 chore: updated deps. fixes #203 (kinda.. as much as we can) 2023-04-22 20:57:38 +02:00
af20092053 fix: fixed tray click bug. fixes #196 2023-04-22 20:51:20 +02:00
166ca353cf chore: updating deps 2023-04-22 16:56:23 +02:00
b807aa2f76 Merge pull request #215 from Mastermindzh/dependabot/npm_and_yarn/minimatch-and-electron-builder-3.1.2
Bump minimatch and electron-builder
2023-04-21 11:47:10 +02:00
dependabot[bot]
ef8ffe47f5 Bump minimatch and electron-builder
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependency [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder). These dependencies need to be updated together.


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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-03 05:12:03 +00:00
58 changed files with 5526 additions and 7755 deletions

12
.eslintrc Normal file
View File

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

View File

@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 16
node-version: 19
- run: npm install
- run: npm run build
@@ -26,7 +26,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 16
node-version: 19
- run: npm install
- run: npm run build
@@ -36,6 +36,6 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 16
node-version: 19
- run: npm install
- run: npm run build

4
.gitignore vendored
View File

@@ -9,6 +9,10 @@ build/linux/arch/*
!build/linux/arch/install.sh
*.css
*.css.map
!src/themes/**/**.css
# JetBrains IDE configuration
.idea
ts-dist/**
ts-dist
themes

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
19.8.1

13
.stylelintrc.json Normal file
View File

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

12
.vscode/settings.json vendored
View File

@@ -1,3 +1,13 @@
{
"cSpell.words": ["hifi", "rescrobbler", "widevine"]
"cSpell.words": [
"flac",
"geqnfr",
"hifi",
"playpause",
"rescrobbler",
"trackid",
"tracklist",
"widevine",
"xesam"
]
}

View File

@@ -4,6 +4,35 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 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
## 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.

View File

@@ -1,39 +1,43 @@
<h1>
Tidal-hifi
<img src = "./build/icon.png" height="40" align="right" />
</h1>
# Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg)
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
![tidal-hifi preview](./docs/preview.png)
## Table of contents
## Table of Contents
<!-- toc -->
- [Installation](#installation)
* [Using releases](#using-releases)
* [Snap](#snap)
* [Arch Linux](#arch-linux)
* [Flatpak](#flatpak)
* [Nix](#nix)
* [Using source](#using-source)
- [Dependencies](#dependencies)
- [Using releases](#using-releases)
- [Snap](#snap)
- [Arch Linux](#arch-linux)
- [Flatpak](#flatpak)
- [Nix](#nix)
- [Using source](#using-source)
- [Features](#features)
- [Integrations](#integrations)
* [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)
- [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)
- [Why](#why)
- [Why not extend existing projects?](#why-not-extend-existing-projects)
- [Special thanks to...](#special-thanks-to)
- [Special thanks to](#special-thanks-to)
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont)
- [Images](#images)
* [Settings window](#settings-window)
* [User setups](#user-setups)
- [Settings window](#settings-window)
- [User setups](#user-setups)
<!-- tocstop -->
## 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) in order for the software to work properly.
### Using releases
Various packaged versions of the software are available on the [releases](https://github.com/Mastermindzh/tidal-hifi/releases) tab.
@@ -93,7 +97,7 @@ To install and work with the code on this project follow these steps:
- Notifications
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- API for status and playback
- [Mute artists automatically (defaults to "Tidal")]("./docs/muting-artists.md")
- 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/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))
@@ -139,7 +143,7 @@ Whilst there are a handful of projects attempting to run Tidal on Electron they
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)
## Special thanks to...
## Special thanks to
- [Castlabs](https://castlabs.com/)
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)

BIN
assets/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/icons/22x22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
assets/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/icons/384x384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
assets/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,15 +1,17 @@
appId: com.rickvanlieshout.tidal-hifi
electronVersion: 19.0.5
electronVersion: 24.1.2
electronDownload:
version: 19.0.5+wvcus
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: icon.png
icon: assets/icons
target:
- dir
executableName: tidal-hifi
@@ -18,7 +20,7 @@ linux:
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: icon.png
Icon: tidal-hifi
StartupNotify: true
Terminal: false
Type: Application

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

11422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,14 @@
{
"name": "tidal-hifi",
"version": "4.4.0",
"version": "5.2.0",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "src/main.js",
"main": "ts-dist/main.js",
"scripts": {
"start": "electron .",
"compile": "tsc && npm run sass-and-copy",
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
"sass-and-copy": "npm run sass && npm run copy-files",
"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",
@@ -14,12 +18,11 @@
"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",
"prestart": "npm run sass",
"prebuilder": "npm run sass",
"prebuilder": "npm run compile",
"builder": "electron-builder --publish=never",
"sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css",
"sass-lint": "sass-lint -vc ./sass-lint.yml ./src/pages/settings/settings.scss",
"sass-lint-fix": "sass-lint-auto-fix ./src/pages/settings/settings.scss --config-sass-lint ./sass-lint.yml"
"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": [
"electron",
@@ -31,23 +34,35 @@
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT",
"dependencies": {
"@electron/remote": "^2.0.8",
"@electron/remote": "^2.0.9",
"discord-rpc": "^4.0.1",
"electron-store": "^8.1.0",
"express": "^4.18.1",
"hotkeys-js": "^3.9.4",
"express": "^4.18.2",
"hotkeys-js": "^3.10.2",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.54.9"
"sass": "^1.62.0"
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"electron": "git+https://github.com/castlabs/electron-releases.git#v19.0.5+wvcus",
"electron-builder": "^23.3.3",
"js-yaml": "^3.14.1",
"prettier": "^2.7.1",
"sass-lint": "^1.13.1",
"sass-lint-auto-fix": "^0.21.2"
"@types/discord-rpc": "^4.0.4",
"@types/express": "^4.17.17",
"@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"copyfiles": "^2.4.1",
"electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus",
"electron-builder": "^24.2.1",
"eslint": "^8.39.0",
"js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0",
"prettier": "^2.8.8",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-standard-scss": "^9.0.0",
"stylelint-prettier": "^3.0.0",
"tsc-watch": "^6.0.4",
"typescript": "^5.0.4"
},
"prettier": "@mastermindzh/prettier-config"
}
}

View File

@@ -1,21 +0,0 @@
rules:
property-sort-order:
- 1
- order: "smacss"
class-name-format:
- 1
- convention: "hyphenatedbem"
quotes:
- 1
- style: "double"
nesting-depth:
- 1
- max-depth: 3
placeholder-in-extend:
- 0
no-vendor-prefixes:
- 0
empty-line-between-blocks:
- 0
force-pseudo-nesting:
- 0

View File

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

View File

@@ -1,4 +1,4 @@
const globalEvents = {
export const globalEvents = {
play: "play",
pause: "pause",
playPause: "playPause",
@@ -11,5 +11,3 @@ const globalEvents = {
storeChanged: "storeChanged",
error: "error",
};
module.exports = globalEvents;

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
module.exports = {
export default {
name: "tidal-hifi",
};

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

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

View File

@@ -1,36 +1,47 @@
require("@electron/remote/main").initialize();
const { app, BrowserWindow, components, globalShortcut, ipcMain, protocol } = require("electron");
const {
settings,
store,
createSettingsWindow,
showSettingsWindow,
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,
} = require("./scripts/settings");
const { addTray, refreshTray } = require("./scripts/tray");
const { addMenu } = require("./scripts/menu");
const path = require("path");
showSettingsWindow,
settingsStore,
} from "./scripts/settings";
import { settings } from "./constants/settings";
import { addTray, refreshTray } from "./scripts/tray";
import { MediaInfo } from "./models/mediaInfo";
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");
const flagValues = require("./constants/flags");
let mainWindow;
let icon = path.join(__dirname, "../assets/icon.png");
initialize();
let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal";
setFlags();
function setFlags() {
const flags = store.get().flags;
if (flags) {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flags)) {
if (value) {
flagValues[key].forEach((flag) => {
flags[key].forEach((flag) => {
console.log(`enabling command line switch ${flag.flag} with value ${flag.value}`);
app.commandLine.appendSwitch(flag.flag, flag.value);
});
@@ -49,7 +60,7 @@ function setFlags() {
*
*/
function syncMenuBarWithStore() {
const fixedMenuBar = store.get(settings.menuBar);
const fixedMenuBar = !!settingsStore.get(settings.menuBar);
mainWindow.autoHideMenuBar = !fixedMenuBar;
mainWindow.setMenuBarVisibility(fixedMenuBar);
@@ -62,7 +73,7 @@ function syncMenuBarWithStore() {
* @returns true if singInstance is not requested, otherwise true/false based on whether the current window is the main window
*/
function isMainInstanceOrMultipleInstancesAllowed() {
if (store.get(settings.singleInstance)) {
if (settingsStore.get(settings.singleInstance)) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -72,39 +83,37 @@ function isMainInstanceOrMultipleInstancesAllowed() {
return true;
}
function createWindow(options = {}) {
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
// 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),
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
},
});
require("@electron/remote/main").enable(mainWindow.webContents);
enable(mainWindow.webContents);
registerHttpProtocols();
syncMenuBarWithStore();
// load the Tidal website
mainWindow.loadURL(tidalUrl);
if (store.get(settings.disableBackgroundThrottle)) {
if (settingsStore.get(settings.disableBackgroundThrottle)) {
// prevent setInterval lag
mainWindow.webContents.setBackgroundThrottling(false);
}
// run stuff after first load
mainWindow.webContents.once("did-finish-load", () => {});
mainWindow.on("close", function (event) {
if (!app.isQuiting && store.get(settings.minimizeOnClose)) {
mainWindow.on("close", function (event: CloseEvent) {
if (settingsStore.get(settings.minimizeOnClose)) {
event.preventDefault();
mainWindow.hide();
refreshTray(mainWindow);
@@ -117,14 +126,13 @@ function createWindow(options = {}) {
app.quit();
});
mainWindow.on("resize", () => {
let { width, height } = mainWindow.getBounds();
store.set(settings.windowBounds.root, { width, height });
const { width, height } = mainWindow.getBounds();
settingsStore.set(settings.windowBounds.root, { width, height });
});
}
function registerHttpProtocols() {
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request, _callback) => {
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request) => {
mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`);
});
if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) {
@@ -135,7 +143,7 @@ function registerHttpProtocols() {
function addGlobalShortcuts() {
Object.keys(mediaKeys).forEach((key) => {
globalShortcut.register(`${key}`, () => {
mainWindow.webContents.send("globalEvent", `${mediaKeys[key]}`);
mainWindow.webContents.send("globalEvent", `${(mediaKeys as any)[key]}`);
});
});
}
@@ -146,14 +154,26 @@ function addGlobalShortcuts() {
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();
store.get(settings.trayIcon) && addTray(mainWindow, { icon }) && refreshTray();
store.get(settings.api) && expressModule.run(mainWindow);
store.get(settings.enableDiscord) && discordModule.initRPC();
// mainWindow.webContents.openDevTools();
if (settingsStore.get(settings.trayIcon)) {
addTray(mainWindow, { icon });
refreshTray(mainWindow);
}
settingsStore.get(settings.api) && startExpress(mainWindow);
settingsStore.get(settings.enableDiscord) && initRPC();
} else {
app.quit();
}
@@ -168,35 +188,35 @@ app.on("activate", function () {
});
app.on("browser-window-created", (_, window) => {
require("@electron/remote/main").enable(window.webContents);
enable(window.webContents);
});
// IPC
ipcMain.on(globalEvents.updateInfo, (_event, arg) => {
mediaInfoModule.update(arg);
ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => {
updateMediaInfo(arg);
});
ipcMain.on(globalEvents.hideSettings, (_event, _arg) => {
ipcMain.on(globalEvents.hideSettings, () => {
hideSettingsWindow();
});
ipcMain.on(globalEvents.showSettings, (_event, _arg) => {
ipcMain.on(globalEvents.showSettings, () => {
showSettingsWindow();
});
ipcMain.on(globalEvents.refreshMenuBar, (_event, _arg) => {
ipcMain.on(globalEvents.refreshMenuBar, () => {
syncMenuBarWithStore();
});
ipcMain.on(globalEvents.storeChanged, (_event, _arg) => {
ipcMain.on(globalEvents.storeChanged, () => {
syncMenuBarWithStore();
if (store.get(settings.enableDiscord) && !discordModule.rpc) {
discordModule.initRPC();
} else if (!store.get(settings.enableDiscord) && discordModule.rpc) {
discordModule.unRPC();
if (settingsStore.get(settings.enableDiscord) && !rpc) {
initRPC();
} else if (!settingsStore.get(settings.enableDiscord) && rpc) {
unRPC();
}
});
ipcMain.on(globalEvents.error, (event, _arg) => {
ipcMain.on(globalEvents.error, (event) => {
console.log(event);
});

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

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,191 @@
import remote 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";
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,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement;
function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const fileNames = fs.readdirSync(process.resourcesPath).filter((file) => file.endsWith(".css"));
const options = fileNames.map((name) => {
return new Option(name, name);
});
// empty old options
const oldOptions = document.querySelectorAll("#themesList option");
oldOptions.forEach((o) => o.remove());
[new Option("Tidal - Default", "none")].concat(options).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 = `${process.resourcesPath}/${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(settings.customCSS);
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);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
}
/**
* 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() {
remote.app.relaunch();
remote.app.exit();
}
/**
* Bind UI components to functions after DOMContentLoaded
*/
window.addEventListener("DOMContentLoaded", () => {
function get(id: string): HTMLInputElement {
return document.getElementById(id) as HTMLInputElement;
}
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);
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
function addTextAreaListener(source: HTMLInputElement, key: string) {
source.addEventListener("input", () => {
settingsStore.set(key, source.value.split("\n"));
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");
trayIcon = get("trayIcon");
skipArtists = get("skipArtists");
skippedArtists = get("skippedArtists");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
refreshSettings();
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);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(updateFrequency, settings.updateFrequency);
});

View File

@@ -35,6 +35,9 @@
<input type="radio" name="tab" id="advanced" />
<label for="advanced">Advanced</label>
<input type="radio" name="tab" id="theming" />
<label for="theming">Theming</label>
<input type="radio" name="tab" id="about" />
<label for="about">About</label>
@@ -48,33 +51,34 @@
<p>Show a notification when a new song starts.</p>
</div>
<label class="switch">
<input id="notifications" type="checkbox">
<input id="notifications" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Mute Artists automatically</h4>
<p>The following list of artists (1 per line) will be muted automatically.</p>
</div>
<label class="switch">
<input id="muteArtists" type="checkbox">
<span class="switch__slider"></span>
</label>
</div>
<textarea id="mutedArtists" class="textarea" cols="40" rows="5" spellcheck="false"></textarea>
<div class="group__option">
<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">
<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>
@@ -84,7 +88,7 @@
<p>Always show TIDAL Hi-Fi's menu bar.</p>
</div>
<label class="switch">
<input id="menuBar" type="checkbox">
<input id="menuBar" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -97,7 +101,7 @@
<p>Show TIDAL Hi-Fi's tray icon.</p>
</div>
<label class="switch">
<input id="trayIcon" type="checkbox">
<input id="trayIcon" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -107,7 +111,7 @@
<p>Minimize window on close instead.</p>
</div>
<label class="switch">
<input id="minimizeOnClose" type="checkbox">
<input id="minimizeOnClose" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -120,7 +124,7 @@
</p>
</div>
<label class="switch">
<input id="enableCustomHotkeys" type="checkbox">
<input id="enableCustomHotkeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -130,7 +134,7 @@
<p>Prevent opening multiple TIDAL Hi-Fi's instances.</p>
</div>
<label class="switch">
<input id="singleInstance" type="checkbox">
<input id="singleInstance" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -142,8 +146,8 @@
<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.
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">
@@ -152,14 +156,14 @@
<p>Enable the TIDAL Hi-Fi web API.</p>
</div>
<label class="switch">
<input id="apiCheckbox" type="checkbox">
<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="text" class="text-input" name="port">
<input id="port" type="number" class="text-input" name="port" />
</div>
</div>
<div class="group__option">
@@ -168,7 +172,7 @@
<p>Enable playback control from the web API.</p>
</div>
<label class="switch">
<input id="playBackControl" type="checkbox">
<input id="playBackControl" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -180,18 +184,20 @@
<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.
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>
<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">
<input id="mprisCheckbox" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -201,7 +207,7 @@
<p>Show what you're listening to on Discord.</p>
</div>
<label class="switch">
<input id="enableDiscord" type="checkbox">
<input id="enableDiscord" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
@@ -210,55 +216,121 @@
<section id="advanced-section" class="tabs__section">
<div class="group">
<p class="group__title">Flags</p>
<p class="group__title">Settings</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<h4>Update frequency</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome
MPRIS client separately from the custom MPRIS client.
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>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox">
<span class="switch__slider"></span>
</label>
</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>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
<h4>Current theme</h4>
<p>
Select a theme below or "Tidal - Default" to return to the original Tidal look.
</p>
<select id="themesList" name="themesList">
</select>
</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>
<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>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox">
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="about-section" class="tabs__section about-section">
<img alt="tidal icon" class="about-section__icon" src="./icon.png">
<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>
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 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>
@@ -267,4 +339,4 @@
</div>
</body>
</html>
</html>

View File

@@ -3,7 +3,6 @@
$black: #17171a;
$grey-333: #333;
$white: #f9f9f9;
$tidal-blue: #0ff;
$tidal-grey: #72777f;
$tidal-grey-darker: #404248;
@@ -36,16 +35,14 @@ $tidal-grey-darkest: #242528;
src: url("fonts/NotoSans-Bold.ttf") format("truetype");
}
$font1: "Noto Sans", Helvetica, sans-serif;
$font1: "Noto Sans", helvetica, sans-serif;
// --- Mixins ---
@mixin drag($enabled: true) {
@if $enabled {
-webkit-app-region: drag;
}
@else {
} @else {
-webkit-app-region: no-drag;
}
}
@@ -62,6 +59,7 @@ html {
.external-link {
@extend button;
text-decoration: underline;
}
@@ -80,6 +78,7 @@ html {
&__drag-area {
@include drag;
position: absolute;
width: 100%;
height: 50px;
@@ -90,6 +89,7 @@ html {
&__close-button {
@extend button;
@include drag(false);
position: absolute;
top: 12px;
right: 10px;
@@ -106,7 +106,7 @@ html {
display: block;
width: 18px;
height: 18px;
opacity: .7;
opacity: 0.7;
}
// --- Settings tabs ---
@@ -125,8 +125,9 @@ html {
outline: none;
}
&+label {
& + label {
@include drag(false);
display: inline-block;
position: relative;
margin-right: 35px;
@@ -138,7 +139,7 @@ html {
user-select: none;
}
&:checked+label {
&:checked + label {
border-bottom: 2px solid $tidal-blue;
color: $tidal-blue;
}
@@ -155,8 +156,8 @@ html {
display: none;
}
@for $i from 1 to 6 {
.settings>input:nth-child(#{$i*2-1}):checked~&>.tabs__section:nth-child(#{$i}) {
@for $i from 1 to 7 {
.settings > input:nth-child(#{$i * 2 - 1}):checked ~ & > .tabs__section:nth-child(#{$i}) {
display: block;
}
}
@@ -217,7 +218,7 @@ html {
width: 100%;
margin-bottom: 10px;
padding: 5px 0;
transition: .2s;
transition: 0.2s;
border: 0;
border-bottom: solid 1px $grey-333;
outline: none;
@@ -237,38 +238,36 @@ html {
.switch {
$this: &;
position: relative;
min-width: 50px;
height: 28px;
margin-left: 10px;
input {
transform: scale(0);
outline: none;
&:checked+#{$this}__slider {
&:checked + #{$this}__slider {
background-color: $tidal-blue;
&::before {
transform: translateX(22px);
background-color: white;
background-color: $white;
}
}
&:focus+#{$this}__slider {
&:focus + #{$this}__slider {
box-shadow: inset 0 0 0 1px $tidal-blue;
}
}
&__slider {
@extend button;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: .4s;
inset: 0;
transition: 0.4s;
border-radius: 40px;
background-color: $tidal-grey-darkest;
@@ -278,7 +277,7 @@ html {
left: 2px;
width: 24px;
height: 24px;
transition: .4s;
transition: 0.4s;
border-radius: 50%;
background-color: $white;
content: "";
@@ -294,7 +293,7 @@ html {
min-height: 50px;
max-height: 100px;
padding: 8px;
transition: .2s;
transition: 0.2s;
border: 0;
border-bottom: 1px solid transparent;
background: $tidal-grey-darkest;
@@ -345,11 +344,12 @@ html {
&__button {
@extend button;
display: block;
height: 48px;
margin: auto;
padding: 0 24px;
transition: .2s;
transition: 0.2s;
border: 0;
border-radius: 12px;
background: $tidal-grey-darker;
@@ -361,3 +361,57 @@ html {
}
}
}
// 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;
}
}

View File

@@ -1,19 +1,20 @@
const { setTitle } = require("./scripts/window-functions");
const { dialog, process, Notification } = require("@electron/remote");
const { store, settings } = require("./scripts/settings");
const { ipcRenderer } = require("electron");
const { app } = require("@electron/remote");
const { downloadFile } = require("./scripts/download");
const statuses = require("./constants/statuses");
const hotkeys = require("./scripts/hotkeys");
const globalEvents = require("./constants/globalEvents");
const { skipArtists } = require("./constants/settings");
import { Notification, app, dialog } from "@electron/remote";
import { ipcRenderer } from "electron";
import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings";
import { statuses } from "./constants/statuses";
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;
let player: any;
let currentPlayStatus = statuses.paused;
let isMutedArtist = true;
const elements = {
play: '*[data-test="play"]',
@@ -46,7 +47,7 @@ const elements = {
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function (key) {
get: function (key: string) {
return window.document.querySelector(this[key.toLowerCase()]);
},
@@ -66,16 +67,27 @@ const elements = {
return "";
},
getArtists: function () {
/**
* 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.querySelector(this["artists"]);
if (artists) {
return artists.innerText;
}
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)";
},
@@ -110,7 +122,7 @@ const elements = {
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function (key) {
getText: function (key: string) {
const element = this.get(key);
return element ? element.textContent : "";
},
@@ -119,7 +131,7 @@ const elements = {
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function (key) {
click: function (key: string) {
this.get(key).click();
return this;
},
@@ -128,11 +140,34 @@ const elements = {
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key) {
focus: function (key: string) {
return this.get(key).focus();
},
};
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const style = document.createElement("style");
style.innerHTML = settingsStore.get(settings.customCSS);
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
*/
@@ -152,41 +187,41 @@ function playPause() {
* https://defkey.com/tidal-desktop-shortcuts
*/
function addHotKeys() {
if (store.get(settings.enableCustomHotkeys)) {
hotkeys.add("Control+p", function () {
if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () {
elements.click("account").click("settings");
});
hotkeys.add("Control+l", function () {
addHotkey("Control+l", function () {
handleLogout();
});
hotkeys.add("Control+h", function () {
addHotkey("Control+h", function () {
elements.click("home");
});
hotkeys.add("backspace", function () {
addHotkey("backspace", function () {
elements.click("back");
});
hotkeys.add("shift+backspace", function () {
addHotkey("shift+backspace", function () {
elements.click("forward");
});
hotkeys.add("control+u", function () {
addHotkey("control+u", function () {
// reloading window without cache should show the update bar if applicable
window.location.reload(true);
window.location.reload();
});
hotkeys.add("control+r", function () {
addHotkey("control+r", function () {
elements.click("repeat");
});
}
// always add the hotkey for the settings window
hotkeys.add("control+=", function () {
addHotkey("control+=", function () {
ipcRenderer.send(globalEvents.showSettings);
});
hotkeys.add("control+0", function () {
addHotkey("control+0", function () {
ipcRenderer.send(globalEvents.showSettings);
});
}
@@ -198,28 +233,26 @@ function addHotKeys() {
function handleLogout() {
const logoutOptions = ["Cancel", "Yes, please", "No, thanks"];
dialog.showMessageBox(
null,
{
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) {
})
.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);
i = window.localStorage.length + 1;
break;
}
}
window.location.reload();
}
}
);
});
}
function addFullScreenListeners() {
@@ -260,7 +293,7 @@ function addIPCEventListeners() {
* Update the current status of tidal (e.g playing or paused)
*/
function getCurrentlyPlayingStatus() {
let pause = elements.get("pause");
const pause = elements.get("pause");
let status = undefined;
// if pause button is visible tidal is playing
@@ -276,7 +309,7 @@ function getCurrentlyPlayingStatus() {
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
function convertDuration(duration) {
function convertDuration(duration: string) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
@@ -286,21 +319,22 @@ function convertDuration(duration) {
*
* @param {*} options
*/
function updateMediaInfo(options, notify) {
function updateMediaInfo(options: Options, notify: boolean) {
if (options) {
ipcRenderer.send(globalEvents.updateInfo, options);
if (store.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.message, icon: options.icon }).show();
if (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.message],
"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";
@@ -313,41 +347,65 @@ function updateMediaInfo(options, notify) {
* 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(/[^0-9]/g, "");
return `https://tidal.com/browse/track/${id}`;
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 artists = elements.getArtists();
skipArtistsIfFoundInSkippedArtistsList(artists);
muteArtistIfFoundInMutedArtistsList(artists); // doing this here so that nothing can possibly fail before we call this function
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} - ${artists}`;
const songDashArtistTitle = `${title} - ${artistsString}`;
const currentStatus = getCurrentlyPlayingStatus();
const options = {
title,
message: artists,
artists: artistsString,
album: album,
status: currentStatus,
url: getTrackURL(),
current,
duration,
"app-name": appName,
image: "",
icon: "",
};
const titleOrArtistChanged = currentSong !== songDashArtistTitle;
const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
// update title, url and play info with new info
setTitle(songDashArtistTitle);
@@ -357,7 +415,7 @@ setInterval(function () {
const image = elements.getSongIcon();
new Promise((resolve) => {
new Promise<void>((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
@@ -374,48 +432,34 @@ setInterval(function () {
// if the image can't be found on the page continue without it
resolve();
}
}).then(
() => {
updateMediaInfo(options, titleOrArtistChanged);
},
() => {}
);
/**
* Checks whether the current artist is included in the "muted artists" list and if so it will automatically mute the player
*/
function muteArtistIfFoundInMutedArtistsList(artists) {
if (store.get(settings.muteArtists)) {
const mutedArtists = store.get(settings.mutedArtists);
if (mutedArtists.find((artist) => artist === artists) !== undefined) {
if (!elements.isMuted()) {
isMutedArtist = true;
elements.click("volume");
}
} else if (isMutedArtist && elements.isMuted()) {
elements.click("volume");
isMutedArtist = false;
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) {
updateMediaSession(options);
}
}
});
/**
* automatically skip a song if the artists are found in the list of artists to skip
* @param {*} artists list of artists to skip
* @param {*} artists array of artists
*/
function skipArtistsIfFoundInSkippedArtistsList(artists) {
if (store.get(skipArtists)) {
const skippedArtists = store.get(settings.skippedArtists);
if (skippedArtists.find((artist) => artist === artists) !== undefined) {
elements.click("next");
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");
}
}
}
}
}, 1000);
}, getUpdateFrequency());
if (process.platform === "linux" && store.get(settings.mpris)) {
if (process.platform === "linux" && settingsStore.get(settings.mpris)) {
try {
const Player = require("mpris-service");
player = Player({
name: "tidal-hifi",
identity: "tidal-hifi",
@@ -432,7 +476,7 @@ if (process.platform === "linux" && store.get(settings.mpris)) {
});
// Events
var events = {
const events = {
next: "next",
previous: "previous",
pause: "pause",
@@ -442,7 +486,7 @@ if (process.platform === "linux" && store.get(settings.mpris)) {
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
};
} as { [key: string]: string };
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
@@ -468,7 +512,7 @@ if (process.platform === "linux" && store.get(settings.mpris)) {
console.log("player api not working");
}
}
addCustomCss();
addHotKeys();
addIPCEventListeners();
addFullScreenListeners();

View File

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

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

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

View File

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

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

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

View File

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

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

@@ -0,0 +1,66 @@
import { BrowserWindow, dialog } from "electron";
import express, { Response } from "express";
import fs from "fs";
import { 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: any) {
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);
});
};

View File

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

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

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

View File

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

View File

@@ -1,7 +1,7 @@
const { Menu, app } = require("electron");
const { showSettingsWindow } = require("./settings");
import { BrowserWindow, Menu, app } from "electron";
import { showSettingsWindow } from "./settings";
const isMac = process.platform === "darwin";
const { name } = require("./../constants/values");
import name from "./../constants/values";
const settingsMenuEntry = {
label: "Settings",
@@ -19,9 +19,7 @@ const quitMenuEntry = {
accelerator: "Control+Q",
};
const menuModule = {};
menuModule.getMenu = function (mainWindow) {
export const getMenu = function (mainWindow: BrowserWindow) {
const toggleWindow = {
label: "Toggle Window",
click: function () {
@@ -113,11 +111,9 @@ menuModule.getMenu = function (mainWindow) {
quitMenuEntry,
];
return Menu.buildFromTemplate(mainMenu);
return Menu.buildFromTemplate(mainMenu as any);
};
menuModule.addMenu = function (mainWindow) {
Menu.setApplicationMenu(menuModule.getMenu(mainWindow));
export const addMenu = function (mainWindow: BrowserWindow) {
Menu.setApplicationMenu(getMenu(mainWindow));
};
module.exports = menuModule;

View File

@@ -1,36 +1,38 @@
const Store = require("electron-store");
const settings = require("./../constants/settings");
const path = require("path");
const { BrowserWindow } = require("electron");
import Store from "electron-store";
let settingsWindow;
import { settings } from "../constants/settings";
import path from "path";
import { BrowserWindow } from "electron";
const store = new Store({
let settingsWindow: BrowserWindow;
export const settingsStore = new Store({
defaults: {
notifications: true,
adBlock: false,
api: true,
playBackControl: true,
muteArtists: false,
mutedArtists: ["TIDAL"],
skipArtists: false,
skippedArtists: [""],
disableBackgroundThrottle: true,
menuBar: true,
apiSettings: {
port: 47836,
},
singleInstance: true,
customCSS: "",
disableBackgroundThrottle: true,
disableHardwareMediaKeys: false,
trayIcon: true,
minimizeOnClose: false,
mpris: false,
enableCustomHotkeys: false,
enableDiscord: false,
windowBounds: { width: 800, height: 600 },
flags: {
gpuRasterization: true,
disableHardwareMediaKeys: false,
},
menuBar: true,
minimizeOnClose: false,
mpris: false,
notifications: true,
playBackControl: true,
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
trayIcon: true,
updateFrequency: 500,
windowBounds: { width: 800, height: 600 },
},
migrations: {
"3.1.0": (migrationStore) => {
@@ -44,12 +46,11 @@ const store = new Store({
});
const settingsModule = {
store,
settings,
// settings,
settingsWindow,
};
settingsModule.createSettingsWindow = function () {
export const createSettingsWindow = function () {
settingsWindow = new BrowserWindow({
width: 700,
height: 600,
@@ -65,7 +66,7 @@ settingsModule.createSettingsWindow = function () {
},
});
settingsWindow.on("close", (event) => {
settingsWindow.on("close", (event: any) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();
@@ -77,19 +78,17 @@ settingsModule.createSettingsWindow = function () {
settingsModule.settingsWindow = settingsWindow;
};
settingsModule.showSettingsWindow = function (tab = "general") {
export const 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 () {
export const hideSettingsWindow = function () {
settingsWindow.hide();
};
settingsModule.closeSettingsWindow = function () {
export const closeSettingsWindow = function () {
settingsWindow = null;
};
module.exports = settingsModule;

View File

@@ -1,26 +0,0 @@
const { Tray } = require("electron");
const { getMenu } = require("./menu");
const trayModule = {};
let tray;
trayModule.addTray = function (mainWindow, 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 () {
mainWindow.isVisible() ? mainWindow.focus() : mainWindow.show();
});
};
trayModule.refreshTray = function (mainWindow) {
if (!tray) {
trayModule.addTray(mainWindow);
}
};
module.exports = trayModule;

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

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

View File

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

View File

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

3
src/themes/csstest.css Normal file
View File

@@ -0,0 +1,3 @@
h2 {
color: black;
}

7
src/themes/test.scss Normal file
View File

@@ -0,0 +1,7 @@
h1 {
color: black;
.title {
color: blue;
}
}

16
tsconfig.json Normal file
View File

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