16
.drone.yml
Normal file
@ -0,0 +1,16 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: install
|
||||
image: node:19.4.0
|
||||
commands:
|
||||
- npm install
|
||||
|
||||
- name: build_with_linux
|
||||
image: node:19.4.0
|
||||
commands:
|
||||
- apt-get update && apt-get upgrade -y
|
||||
- apt-get install -y libarchive-tools rpm
|
||||
- npm run build
|
2
.gitignore
vendored
@ -14,4 +14,6 @@ build/linux/arch/*
|
||||
.idea
|
||||
ts-dist/**
|
||||
ts-dist
|
||||
themes
|
||||
!src/themes
|
||||
.sass-cache
|
||||
|
@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## 5.2.0
|
||||
|
||||
- moved from Javascript to Typescript for all files
|
||||
|
||||
- use `npm run watch` to watch for changes & recompile typescript and sass files
|
||||
|
||||
- Added support for theming the application
|
||||
- Added drone build file use `drone exec` or drone.ci to build it
|
||||
|
||||
## 5.1.0
|
||||
|
||||
### New features
|
||||
|
89
README.md
@ -1,15 +1,21 @@
|
||||
# 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) [![github builds](https://github.com/mastermindzh/tidal-hifi/actions/workflows/build.yml/badge.svg)](https://github.com/Mastermindzh/tidal-hifi/actions) [![Build Status](https://ci.mastermindzh.tech/api/badges/Mastermindzh/tidal-hifi/status.svg)](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [![Discord logo](./docs/images/discord.png)](https://discord.gg/yhNwf4v4He)
|
||||
|
||||
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
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [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)
|
||||
- [Using releases](#using-releases)
|
||||
@ -18,25 +24,57 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
|
||||
- [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)
|
||||
- [Why](#why)
|
||||
- [Why not extend existing projects?](#why-not-extend-existing-projects)
|
||||
- [Special thanks to](#special-thanks-to)
|
||||
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont)
|
||||
- [Donations](#donations)
|
||||
- [Images](#images)
|
||||
- [Settings window](#settings-window)
|
||||
- [User setups](#user-setups)
|
||||
|
||||
<!-- 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
|
||||
|
||||
### 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
|
||||
|
||||
@ -91,23 +129,12 @@ To install and work with the code on this project follow these steps:
|
||||
- npm install
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
![integrations menu, showing a list of integrations](./docs/integrations.png)
|
||||
![integrations menu, showing a list of integrations](./docs/images/integrations.png)
|
||||
|
||||
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).
|
||||
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
|
||||
|
||||
- [Castlabs](https://castlabs.com/)
|
||||
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).
|
||||
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
|
||||
You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh)
|
||||
|
||||
## Images
|
||||
|
||||
### Settings window
|
||||
|
||||
![settings window](./docs/settings-preview.png)
|
||||
![settings window](./docs/images/settings-preview.png)
|
||||
|
||||
### User setups
|
||||
|
||||
|
@ -7,6 +7,8 @@ snap:
|
||||
plugs:
|
||||
- default
|
||||
- screen-inhibit-control
|
||||
extraResources:
|
||||
- "themes/**"
|
||||
linux:
|
||||
category: AudioVideo
|
||||
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",
|
||||
"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",
|
||||
"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-deb": "npm run builder -- -c ./build/electron-builder.deb.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",
|
||||
"prebuilder": "npm run compile",
|
||||
"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-fix": "npx stylelint --fix **/*.scss"
|
||||
},
|
||||
|
@ -33,6 +33,7 @@ export const settings = {
|
||||
singleInstance: "singleInstance",
|
||||
skipArtists: "skipArtists",
|
||||
skippedArtists: "skippedArtists",
|
||||
theme: "theme",
|
||||
trayIcon: "trayIcon",
|
||||
updateFrequency: "updateFrequency",
|
||||
windowBounds: {
|
||||
|
@ -1,8 +1,10 @@
|
||||
import remote from "@electron/remote";
|
||||
import remote, { app } from "@electron/remote";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import fs from "fs";
|
||||
import { globalEvents } from "../../constants/globalEvents";
|
||||
import { settings } from "../../constants/settings";
|
||||
import { settingsStore } from "./../../scripts/settings";
|
||||
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
|
||||
|
||||
let adBlock: HTMLInputElement,
|
||||
api: HTMLInputElement,
|
||||
@ -21,8 +23,45 @@ let adBlock: HTMLInputElement,
|
||||
singleInstance: HTMLInputElement,
|
||||
skipArtists: HTMLInputElement,
|
||||
skippedArtists: HTMLInputElement,
|
||||
theme: HTMLSelectElement,
|
||||
trayIcon: 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
|
||||
@ -30,7 +69,7 @@ let adBlock: HTMLInputElement,
|
||||
function refreshSettings() {
|
||||
adBlock.checked = settingsStore.get(settings.adBlock);
|
||||
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);
|
||||
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
|
||||
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
|
||||
@ -44,6 +83,7 @@ function refreshSettings() {
|
||||
port.value = settingsStore.get(settings.apiSettings.port);
|
||||
singleInstance.checked = settingsStore.get(settings.singleInstance);
|
||||
skipArtists.checked = settingsStore.get(settings.skipArtists);
|
||||
theme.value = settingsStore.get(settings.theme);
|
||||
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
|
||||
trayIcon.checked = settingsStore.get(settings.trayIcon);
|
||||
updateFrequency.value = settingsStore.get(settings.updateFrequency);
|
||||
@ -75,10 +115,13 @@ function restart() {
|
||||
* Bind UI components to functions after DOMContentLoaded
|
||||
*/
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
function get(id: string): HTMLInputElement {
|
||||
return document.getElementById(id) as HTMLInputElement;
|
||||
function get<T = HTMLInputElement>(id: string): T {
|
||||
return document.getElementById(id) as T;
|
||||
}
|
||||
|
||||
getThemeFiles();
|
||||
handleFileUploads();
|
||||
|
||||
document.getElementById("close").addEventListener("click", hide);
|
||||
document.getElementById("restart").addEventListener("click", restart);
|
||||
document.querySelectorAll(".external-link").forEach((elem) =>
|
||||
@ -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", () => {
|
||||
refreshSettings();
|
||||
});
|
||||
@ -127,6 +177,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
notifications = get("notifications");
|
||||
playBackControl = get("playBackControl");
|
||||
port = get("port");
|
||||
theme = get<HTMLSelectElement>("themesList");
|
||||
trayIcon = get("trayIcon");
|
||||
skipArtists = get("skipArtists");
|
||||
skippedArtists = get("skippedArtists");
|
||||
@ -152,6 +203,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
addInputListener(skipArtists, settings.skipArtists);
|
||||
addTextAreaListener(skippedArtists, settings.skippedArtists);
|
||||
addInputListener(singleInstance, settings.singleInstance);
|
||||
addSelectListener(theme, settings.theme);
|
||||
addInputListener(trayIcon, settings.trayIcon);
|
||||
addInputListener(updateFrequency, settings.updateFrequency);
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
@ -226,17 +229,6 @@
|
||||
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
|
||||
</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">
|
||||
<p class="group__title">Flags</p>
|
||||
<div class="group__option">
|
||||
@ -277,6 +269,53 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="theming-section" class="tabs__section">
|
||||
<div class="group">
|
||||
<p class="group__title">Theming</p>
|
||||
<div class="group__option">
|
||||
<div class="group__description">
|
||||
<h4>Custom CSS</h4>
|
||||
<p>
|
||||
The css that you put in here will be injected into a style tag in the head of the document.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="customCSS" class="textarea" cols="40" rows="8" spellcheck="false"></textarea>
|
||||
|
||||
<div class="group">
|
||||
<p class="group__title">Theme files</p>
|
||||
<div class="group__option">
|
||||
<div class="group__description">
|
||||
<h4>Current theme</h4>
|
||||
<p>
|
||||
Select a theme below or "Tidal - Default" to return to the original Tidal look.
|
||||
</p>
|
||||
<select class="select-input" id="themesList" name="themesList">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group__option">
|
||||
<div class="group__description">
|
||||
<h4>Upload new themes</h4>
|
||||
<p>
|
||||
Click the button and select the css files to import. They will be added to the theme list
|
||||
automatically.
|
||||
</p>
|
||||
<div class="file-drop-area">
|
||||
<div>
|
||||
<span class="file-btn">Choose files</span>
|
||||
<span id="file-message" class="file-msg">or drag and drop files here</span>
|
||||
<input id="theme-files" class="file-input" type="file" accept=".css" multiple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about-section" class="tabs__section about-section">
|
||||
<img alt="tidal icon" class="about-section__icon" src="./icon.png" />
|
||||
<p class="about-section__text">
|
||||
|
@ -156,7 +156,7 @@ html {
|
||||
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}) {
|
||||
display: block;
|
||||
}
|
||||
@ -230,8 +230,6 @@ html {
|
||||
border-color: $tidal-blue;
|
||||
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 { ipcRenderer } from "electron";
|
||||
import fs from "fs";
|
||||
import Player from "mpris-service";
|
||||
import { globalEvents } from "./constants/globalEvents";
|
||||
import { settings } from "./constants/settings";
|
||||
@ -39,7 +40,7 @@ const elements = {
|
||||
bar: '*[data-test="progress-bar"]',
|
||||
footer: "#footerPlayer",
|
||||
album_header_title: '.header-details [data-test="title"]',
|
||||
playing_title: 'span[data-test="table-cell-title"].css-geqnfr',
|
||||
playing_title: 'span[data-test="table-cell-title"].css-1vjc1xk',
|
||||
album_name_cell: '[data-test="table-cell-album"]',
|
||||
tracklist_row: '[data-test="tracklist-row"]',
|
||||
volume: '*[data-test="volume"]',
|
||||
@ -147,8 +148,24 @@ const elements = {
|
||||
|
||||
function addCustomCss() {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const selectedTheme = settingsStore.get(settings.theme);
|
||||
if (selectedTheme !== "none") {
|
||||
const themeFile = `${process.resourcesPath}/${selectedTheme}`;
|
||||
fs.readFile(themeFile, "utf-8", (err, data) => {
|
||||
if (err) {
|
||||
alert("An error ocurred reading the theme file.");
|
||||
return;
|
||||
}
|
||||
|
||||
const themeStyle = document.createElement("style");
|
||||
themeStyle.innerHTML = data;
|
||||
document.head.appendChild(themeStyle);
|
||||
});
|
||||
}
|
||||
|
||||
// read customCSS (it will override the theme)
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = settingsStore.get(settings.customCSS);
|
||||
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export const settingsStore = new Store({
|
||||
apiSettings: {
|
||||
port: 47836,
|
||||
},
|
||||
customCSS: "",
|
||||
customCSS: [],
|
||||
disableBackgroundThrottle: true,
|
||||
disableHardwareMediaKeys: false,
|
||||
enableCustomHotkeys: false,
|
||||
@ -30,6 +30,7 @@ export const settingsStore = new Store({
|
||||
singleInstance: true,
|
||||
skipArtists: false,
|
||||
skippedArtists: [""],
|
||||
theme: "none",
|
||||
trayIcon: true,
|
||||
updateFrequency: 500,
|
||||
windowBounds: { width: 800, height: 600 },
|
||||
@ -54,7 +55,7 @@ export const createSettingsWindow = function () {
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 700,
|
||||
height: 600,
|
||||
resizable: false,
|
||||
resizable: true,
|
||||
show: false,
|
||||
transparent: true,
|
||||
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);
|
||||
}
|