Merge pull request #227 from Mastermindzh/feature/theming
Feature/theming
2
.gitignore
vendored
@ -14,4 +14,6 @@ build/linux/arch/*
|
|||||||
.idea
|
.idea
|
||||||
ts-dist/**
|
ts-dist/**
|
||||||
ts-dist
|
ts-dist
|
||||||
|
themes
|
||||||
|
!src/themes
|
||||||
.sass-cache
|
.sass-cache
|
||||||
|
@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- moved from Javascript to Typescript for all files
|
- moved from Javascript to Typescript for all files
|
||||||
- use `npm run watch` to watch for changes & recompile typescript and sass files
|
- use `npm run watch` to watch for changes & recompile typescript and sass files
|
||||||
|
|
||||||
|
- Added support for theming the application
|
||||||
|
|
||||||
## 5.1.0
|
## 5.1.0
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
109
README.md
@ -1,16 +1,22 @@
|
|||||||
# Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
|
# Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
|
||||||
|
|
||||||
![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg)
|
![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg) [![Discord logo](./docs/images/discord.png)](https://discord.gg/yhNwf4v4He)
|
||||||
|
|
||||||
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
|
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
|
||||||
|
|
||||||
![tidal-hifi preview](./docs/preview.png)
|
![tidal-hifi preview](./docs/images/preview.png)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
<!-- toc -->
|
<!-- toc -->
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Tidal-hifi](#tidal-hifi)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Contributions](#contributions)
|
||||||
|
- [Why did I create tidal-hifi?](#why-did-i-create-tidal-hifi)
|
||||||
|
- [Why not extend existing projects?](#why-not-extend-existing-projects)
|
||||||
|
- [Installation](#installation)
|
||||||
- [Dependencies](#dependencies)
|
- [Dependencies](#dependencies)
|
||||||
- [Using releases](#using-releases)
|
- [Using releases](#using-releases)
|
||||||
- [Snap](#snap)
|
- [Snap](#snap)
|
||||||
@ -18,25 +24,57 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
|
|||||||
- [Flatpak](#flatpak)
|
- [Flatpak](#flatpak)
|
||||||
- [Nix](#nix)
|
- [Nix](#nix)
|
||||||
- [Using source](#using-source)
|
- [Using source](#using-source)
|
||||||
- [Features](#features)
|
- [Integrations](#integrations)
|
||||||
- [Integrations](#integrations)
|
|
||||||
- [Known bugs](#known-bugs)
|
- [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)
|
- [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)
|
- [Special thanks to](#special-thanks-to)
|
||||||
- [Why not extend existing projects?](#why-not-extend-existing-projects)
|
- [Donations](#donations)
|
||||||
- [Special thanks to](#special-thanks-to)
|
- [Images](#images)
|
||||||
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont)
|
|
||||||
- [Images](#images)
|
|
||||||
- [Settings window](#settings-window)
|
- [Settings window](#settings-window)
|
||||||
- [User setups](#user-setups)
|
- [User setups](#user-setups)
|
||||||
|
|
||||||
<!-- tocstop -->
|
<!-- tocstop -->
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- HiFi playback
|
||||||
|
- Notifications
|
||||||
|
- Custom [theming](./docs/theming.md)
|
||||||
|
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
- ![Discord logo](./docs/images/discord.png) [Join the Discord server](https://discord.gg/yhNwf4v4He)
|
||||||
|
|
||||||
|
## Why did I create tidal-hifi?
|
||||||
|
|
||||||
|
I moved from Spotify over to Tidal and found Linux support to be lacking.
|
||||||
|
|
||||||
|
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
|
||||||
|
|
||||||
|
### Why not extend existing projects?
|
||||||
|
|
||||||
|
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
|
||||||
|
|
||||||
|
- Lack of maintainers/developers. (no hotfixes, no issues being handled etc)
|
||||||
|
- Most are simple web wrappers, not my cup of tea.
|
||||||
|
- Some are DE oriented. I want this to work on WM's too.
|
||||||
|
- None have widevine working at the moment
|
||||||
|
|
||||||
|
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Dependencies
|
### 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.
|
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
|
||||||
|
|
||||||
@ -48,15 +86,15 @@ To install with `snap` you need to download the pre-packaged snap-package from t
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -91,23 +129,12 @@ 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
|
|
||||||
- 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))
|
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
Tidal-hifi comes with several integrations out of the box.
|
Tidal-hifi comes with several integrations out of the box.
|
||||||
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
|
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
|
||||||
|
|
||||||
![integrations menu, showing a list of integrations](./docs/integrations.png)
|
![integrations menu, showing a list of integrations](./docs/images/integrations.png)
|
||||||
|
|
||||||
It currently includes:
|
It currently includes:
|
||||||
|
|
||||||
@ -126,38 +153,20 @@ The last.fm login doesn't work, as is evident from the following issue: [Last.fm
|
|||||||
However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled).
|
However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled).
|
||||||
For now that will be the default workaround.
|
For now that will be the default workaround.
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
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 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
|
## Special thanks to
|
||||||
|
|
||||||
- [Castlabs](https://castlabs.com/)
|
- [Castlabs](https://castlabs.com/)
|
||||||
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)
|
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)
|
||||||
|
|
||||||
## Buy me a coffee? Please don't
|
## Donations
|
||||||
|
|
||||||
Instead spend some money on a charity I care for: [kwf.nl](https://www.kwf.nl/donatie/donation).
|
You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh)
|
||||||
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
|
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
### Settings window
|
### Settings window
|
||||||
|
|
||||||
![settings window](./docs/settings-preview.png)
|
![settings window](./docs/images/settings-preview.png)
|
||||||
|
|
||||||
### User setups
|
### User setups
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ snap:
|
|||||||
plugs:
|
plugs:
|
||||||
- default
|
- default
|
||||||
- screen-inhibit-control
|
- screen-inhibit-control
|
||||||
|
extraResources:
|
||||||
|
- "themes/**"
|
||||||
linux:
|
linux:
|
||||||
category: AudioVideo
|
category: AudioVideo
|
||||||
icon: assets/icons
|
icon: assets/icons
|
||||||
|
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 |
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.
|
||||||
|
|
||||||
|
![Settings window with the theming tab opened](./images/theming.png)
|
||||||
|
|
||||||
|
## Custom CSS
|
||||||
|
|
||||||
|
The custom CSS will be added to the HTML document last.
|
||||||
|
This means that it will overwrite any existing CSS, even that of themes, unless the original has an access modifier such as `$important`.
|
||||||
|
|
||||||
|
![settings window on the theming tab with a custom CSS override](./images/customcss.png)
|
||||||
|
|
||||||
|
## config
|
||||||
|
|
||||||
|
The theme selector and customCSS are stored in the config file.
|
||||||
|
The custom CSS is stored as a list of lines.
|
||||||
|
|
||||||
|
![settings window on the theming tab next to the config file](./images/customcss-config.png)
|
||||||
|
|
||||||
|
## Warning! Themes might break
|
||||||
|
|
||||||
|
Themes might break at any point. Tidal changes their webpage structure a ton (they probably generate classNames and don't provide roles/ids/attributes.)
|
||||||
|
|
||||||
|
If one breaks you can create an Issue on GitHub or ask for assistance in the [Discord channel](https://discord.gg/yhNwf4v4He).
|
@ -8,7 +8,8 @@
|
|||||||
"compile": "tsc && npm run sass-and-copy",
|
"compile": "tsc && npm run sass-and-copy",
|
||||||
"watch": "tsc-watch --onSuccess \"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",
|
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
|
||||||
"sass-and-copy": "npm run sass && npm run copy-files",
|
"copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources",
|
||||||
|
"sass-and-copy": "npm run sass && npm run copy-files && npm run copy-themes-dev",
|
||||||
"build": "npm run builder -- -c ./build/electron-builder.yml",
|
"build": "npm run builder -- -c ./build/electron-builder.yml",
|
||||||
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
|
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
|
||||||
"build-unpacked": "npm run builder -- -c ./build/electron-builder.unpacked.yml",
|
"build-unpacked": "npm run builder -- -c ./build/electron-builder.unpacked.yml",
|
||||||
@ -20,7 +21,7 @@
|
|||||||
"build-base": "npm run builder -- -c ./build/electron-builder.base.yml",
|
"build-base": "npm run builder -- -c ./build/electron-builder.base.yml",
|
||||||
"prebuilder": "npm run compile",
|
"prebuilder": "npm run compile",
|
||||||
"builder": "electron-builder --publish=never",
|
"builder": "electron-builder --publish=never",
|
||||||
"sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css",
|
"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": "npx stylelint **/*.scss",
|
||||||
"style-lint-fix": "npx stylelint --fix **/*.scss"
|
"style-lint-fix": "npx stylelint --fix **/*.scss"
|
||||||
},
|
},
|
||||||
|
@ -33,6 +33,7 @@ export const settings = {
|
|||||||
singleInstance: "singleInstance",
|
singleInstance: "singleInstance",
|
||||||
skipArtists: "skipArtists",
|
skipArtists: "skipArtists",
|
||||||
skippedArtists: "skippedArtists",
|
skippedArtists: "skippedArtists",
|
||||||
|
theme: "theme",
|
||||||
trayIcon: "trayIcon",
|
trayIcon: "trayIcon",
|
||||||
updateFrequency: "updateFrequency",
|
updateFrequency: "updateFrequency",
|
||||||
windowBounds: {
|
windowBounds: {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import remote from "@electron/remote";
|
import remote, { app } from "@electron/remote";
|
||||||
import { ipcRenderer, shell } from "electron";
|
import { ipcRenderer, shell } from "electron";
|
||||||
|
import fs from "fs";
|
||||||
import { globalEvents } from "../../constants/globalEvents";
|
import { globalEvents } from "../../constants/globalEvents";
|
||||||
import { settings } from "../../constants/settings";
|
import { settings } from "../../constants/settings";
|
||||||
import { settingsStore } from "./../../scripts/settings";
|
import { settingsStore } from "./../../scripts/settings";
|
||||||
|
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
|
||||||
|
|
||||||
let adBlock: HTMLInputElement,
|
let adBlock: HTMLInputElement,
|
||||||
api: HTMLInputElement,
|
api: HTMLInputElement,
|
||||||
@ -21,8 +23,45 @@ let adBlock: HTMLInputElement,
|
|||||||
singleInstance: HTMLInputElement,
|
singleInstance: HTMLInputElement,
|
||||||
skipArtists: HTMLInputElement,
|
skipArtists: HTMLInputElement,
|
||||||
skippedArtists: HTMLInputElement,
|
skippedArtists: HTMLInputElement,
|
||||||
|
theme: HTMLSelectElement,
|
||||||
trayIcon: HTMLInputElement,
|
trayIcon: HTMLInputElement,
|
||||||
updateFrequency: HTMLInputElement;
|
updateFrequency: 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
|
* Sync the UI forms with the current settings
|
||||||
@ -30,7 +69,7 @@ let adBlock: HTMLInputElement,
|
|||||||
function refreshSettings() {
|
function refreshSettings() {
|
||||||
adBlock.checked = settingsStore.get(settings.adBlock);
|
adBlock.checked = settingsStore.get(settings.adBlock);
|
||||||
api.checked = settingsStore.get(settings.api);
|
api.checked = settingsStore.get(settings.api);
|
||||||
customCSS.value = settingsStore.get(settings.customCSS);
|
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
|
||||||
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
|
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
|
||||||
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
|
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
|
||||||
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
|
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
|
||||||
@ -44,6 +83,7 @@ function refreshSettings() {
|
|||||||
port.value = settingsStore.get(settings.apiSettings.port);
|
port.value = settingsStore.get(settings.apiSettings.port);
|
||||||
singleInstance.checked = settingsStore.get(settings.singleInstance);
|
singleInstance.checked = settingsStore.get(settings.singleInstance);
|
||||||
skipArtists.checked = settingsStore.get(settings.skipArtists);
|
skipArtists.checked = settingsStore.get(settings.skipArtists);
|
||||||
|
theme.value = settingsStore.get(settings.theme);
|
||||||
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
|
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
|
||||||
trayIcon.checked = settingsStore.get(settings.trayIcon);
|
trayIcon.checked = settingsStore.get(settings.trayIcon);
|
||||||
updateFrequency.value = settingsStore.get(settings.updateFrequency);
|
updateFrequency.value = settingsStore.get(settings.updateFrequency);
|
||||||
@ -75,10 +115,13 @@ function restart() {
|
|||||||
* Bind UI components to functions after DOMContentLoaded
|
* Bind UI components to functions after DOMContentLoaded
|
||||||
*/
|
*/
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
function get(id: string): HTMLInputElement {
|
function get<T = HTMLInputElement>(id: string): T {
|
||||||
return document.getElementById(id) as HTMLInputElement;
|
return document.getElementById(id) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getThemeFiles();
|
||||||
|
handleFileUploads();
|
||||||
|
|
||||||
document.getElementById("close").addEventListener("click", hide);
|
document.getElementById("close").addEventListener("click", hide);
|
||||||
document.getElementById("restart").addEventListener("click", restart);
|
document.getElementById("restart").addEventListener("click", restart);
|
||||||
document.querySelectorAll(".external-link").forEach((elem) =>
|
document.querySelectorAll(".external-link").forEach((elem) =>
|
||||||
@ -105,6 +148,13 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSelectListener(source: HTMLSelectElement, key: string) {
|
||||||
|
source.addEventListener("change", () => {
|
||||||
|
settingsStore.set(key, source.value);
|
||||||
|
ipcRenderer.send(globalEvents.storeChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ipcRenderer.on("refreshData", () => {
|
ipcRenderer.on("refreshData", () => {
|
||||||
refreshSettings();
|
refreshSettings();
|
||||||
});
|
});
|
||||||
@ -127,6 +177,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||||||
notifications = get("notifications");
|
notifications = get("notifications");
|
||||||
playBackControl = get("playBackControl");
|
playBackControl = get("playBackControl");
|
||||||
port = get("port");
|
port = get("port");
|
||||||
|
theme = get<HTMLSelectElement>("themesList");
|
||||||
trayIcon = get("trayIcon");
|
trayIcon = get("trayIcon");
|
||||||
skipArtists = get("skipArtists");
|
skipArtists = get("skipArtists");
|
||||||
skippedArtists = get("skippedArtists");
|
skippedArtists = get("skippedArtists");
|
||||||
@ -152,6 +203,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||||||
addInputListener(skipArtists, settings.skipArtists);
|
addInputListener(skipArtists, settings.skipArtists);
|
||||||
addTextAreaListener(skippedArtists, settings.skippedArtists);
|
addTextAreaListener(skippedArtists, settings.skippedArtists);
|
||||||
addInputListener(singleInstance, settings.singleInstance);
|
addInputListener(singleInstance, settings.singleInstance);
|
||||||
|
addSelectListener(theme, settings.theme);
|
||||||
addInputListener(trayIcon, settings.trayIcon);
|
addInputListener(trayIcon, settings.trayIcon);
|
||||||
addInputListener(updateFrequency, settings.updateFrequency);
|
addInputListener(updateFrequency, settings.updateFrequency);
|
||||||
});
|
});
|
||||||
|
@ -35,6 +35,9 @@
|
|||||||
<input type="radio" name="tab" id="advanced" />
|
<input type="radio" name="tab" id="advanced" />
|
||||||
<label for="advanced">Advanced</label>
|
<label for="advanced">Advanced</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="theming" />
|
||||||
|
<label for="theming">Theming</label>
|
||||||
|
|
||||||
<input type="radio" name="tab" id="about" />
|
<input type="radio" name="tab" id="about" />
|
||||||
<label for="about">About</label>
|
<label for="about">About</label>
|
||||||
|
|
||||||
@ -226,17 +229,6 @@
|
|||||||
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
|
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div class="group">
|
||||||
<p class="group__title">Flags</p>
|
<p class="group__title">Flags</p>
|
||||||
<div class="group__option">
|
<div class="group__option">
|
||||||
@ -277,6 +269,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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">
|
<p class="about-section__text">
|
||||||
|
@ -156,7 +156,7 @@ html {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@for $i from 1 to 6 {
|
@for $i from 1 to 7 {
|
||||||
.settings > input:nth-child(#{$i * 2 - 1}):checked ~ & > .tabs__section:nth-child(#{$i}) {
|
.settings > input:nth-child(#{$i * 2 - 1}):checked ~ & > .tabs__section:nth-child(#{$i}) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -230,8 +230,6 @@ html {
|
|||||||
border-color: $tidal-blue;
|
border-color: $tidal-blue;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Switch slider component ---
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -361,3 +359,87 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { Notification, app, dialog } from "@electron/remote";
|
import { Notification, app, dialog } from "@electron/remote";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
|
import fs from "fs";
|
||||||
import Player from "mpris-service";
|
import Player from "mpris-service";
|
||||||
import { globalEvents } from "./constants/globalEvents";
|
import { globalEvents } from "./constants/globalEvents";
|
||||||
import { settings } from "./constants/settings";
|
import { settings } from "./constants/settings";
|
||||||
@ -147,8 +148,24 @@ const elements = {
|
|||||||
|
|
||||||
function addCustomCss() {
|
function addCustomCss() {
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
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");
|
const style = document.createElement("style");
|
||||||
style.innerHTML = settingsStore.get(settings.customCSS);
|
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export const settingsStore = new Store({
|
|||||||
apiSettings: {
|
apiSettings: {
|
||||||
port: 47836,
|
port: 47836,
|
||||||
},
|
},
|
||||||
customCSS: "",
|
customCSS: [],
|
||||||
disableBackgroundThrottle: true,
|
disableBackgroundThrottle: true,
|
||||||
disableHardwareMediaKeys: false,
|
disableHardwareMediaKeys: false,
|
||||||
enableCustomHotkeys: false,
|
enableCustomHotkeys: false,
|
||||||
@ -30,6 +30,7 @@ export const settingsStore = new Store({
|
|||||||
singleInstance: true,
|
singleInstance: true,
|
||||||
skipArtists: false,
|
skipArtists: false,
|
||||||
skippedArtists: [""],
|
skippedArtists: [""],
|
||||||
|
theme: "none",
|
||||||
trayIcon: true,
|
trayIcon: true,
|
||||||
updateFrequency: 500,
|
updateFrequency: 500,
|
||||||
windowBounds: { width: 800, height: 600 },
|
windowBounds: { width: 800, height: 600 },
|
||||||
@ -54,7 +55,7 @@ export const createSettingsWindow = function () {
|
|||||||
settingsWindow = new BrowserWindow({
|
settingsWindow = new BrowserWindow({
|
||||||
width: 700,
|
width: 700,
|
||||||
height: 600,
|
height: 600,
|
||||||
resizable: false,
|
resizable: true,
|
||||||
show: false,
|
show: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
|
10
src/themes/Blood.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
$foreground: red;
|
||||||
|
$background: black;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar--WvRg_ {
|
||||||
|
background-color: $background;
|
||||||
|
}
|
82
src/themes/Tokyo Night.scss
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
: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--fNPGt.notFullscreen--ugyc2 {
|
||||||
|
background-color: var(--footer-player-background);
|
||||||
|
}
|
||||||
|
.sidebar--WvRg_ {
|
||||||
|
background-color: var(--sidebar-background);
|
||||||
|
contain: strict;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.item--VTpWS:hover {
|
||||||
|
background-color: var(--sidebar-hover-background);
|
||||||
|
}
|
||||||
|
.main--LUnJp {
|
||||||
|
background-color: var(--main-background);
|
||||||
|
}
|
||||||
|
button.button--ncJwL {
|
||||||
|
background-color: var(--main-navigation-control-background);
|
||||||
|
}
|
||||||
|
.player--fNPGt.lossLess--g5Jss button.withBackground[aria-checked="true"] path {
|
||||||
|
fill: var(--player-control-active-button);
|
||||||
|
}
|
||||||
|
.player--fNPGt.lossLess--g5Jss button.withBackground[aria-checked="true"] {
|
||||||
|
background-color: var(--player-control-background);
|
||||||
|
}
|
||||||
|
.activeItem--qV6eL .activeItem--qV6eL .playlistItem--YARJh .section--FI41E.playingItem--eWkYS {
|
||||||
|
color: #565f89;
|
||||||
|
}
|
||||||
|
.progressBarWrapper--WZfox {
|
||||||
|
color: var(--player-progress-bar);
|
||||||
|
}
|
||||||
|
.playbackControls--FLeZA button .tidal-ui__icon {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
.css-11m9iw3 {
|
||||||
|
background-color: var(--indicator-hifi-background);
|
||||||
|
}
|
||||||
|
.css-11m9iw3 span {
|
||||||
|
color: var(--indicator-hifi-span);
|
||||||
|
}
|
||||||
|
.activeItem--qV6eL {
|
||||||
|
color: var(--sidebar-menu-top-text);
|
||||||
|
}
|
||||||
|
.activeItem--qV6eL .playlistItem--YARJh {
|
||||||
|
color: var(--sidebar-menu-playlist-text);
|
||||||
|
}
|
||||||
|
button.feedBell--B8anb {
|
||||||
|
background-color: var(--main-feed-button-background);
|
||||||
|
}
|
||||||
|
.baseContainer--cbf17 {
|
||||||
|
background-color: var(--search-dialog-background);
|
||||||
|
}
|
||||||
|
.favoriteButton--TtBlM.is-favorite path {
|
||||||
|
fill: var(--player-control-favorite);
|
||||||
|
}
|
||||||
|
.container--mkEWd {
|
||||||
|
background-color: var(--right-queue-background);
|
||||||
|
}
|
||||||
|
.container--vJVjO {
|
||||||
|
background-color: var(--search-background);
|
||||||
|
}
|
||||||
|
.searchFieldHighlighted--Fitvs {
|
||||||
|
color: var(--snow-white);
|
||||||
|
}
|