Compare commits

...

14 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
41 changed files with 3105 additions and 1924 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"
]
}

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

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,13 @@ 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

View File

@@ -7,6 +7,8 @@ snap:
plugs:
- default
- screen-inhibit-control
extraResources:
- "themes/**"
linux:
category: AudioVideo
icon: assets/icons

3669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,14 @@
{
"name": "tidal-hifi",
"version": "5.1.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",
@@ -42,13 +45,24 @@
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"@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.7",
"sass-lint": "^1.13.1",
"sass-lint-auto-fix": "^0.21.2"
"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,7 +8,7 @@
* },
* windowBounds: { width: 800, height: 600 },
*/
const settings = {
export const settings = {
adBlock: "adBlock",
api: "api",
apiSettings: {
@@ -21,6 +21,7 @@ const settings = {
enableCustomHotkeys: "enableCustomHotkeys",
enableDiscord: "enableDiscord",
flags: {
root: "flags",
disableHardwareMediaKeys: "flags.disableHardwareMediaKeys",
gpuRasterization: "flags.gpuRasterization",
},
@@ -40,5 +41,3 @@ const settings = {
height: "windowBounds.height",
},
};
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,44 +1,47 @@
require("@electron/remote/main").initialize();
const {
app,
import { enable, initialize } from "@electron/remote/main";
import {
BrowserWindow,
app,
components,
globalShortcut,
ipcMain,
protocol,
session,
} = require("electron");
const {
settings,
store,
createSettingsWindow,
showSettingsWindow,
} 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);
});
@@ -57,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);
@@ -70,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) {
@@ -80,13 +83,13 @@ 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,
@@ -97,23 +100,20 @@ function createWindow(options = {}) {
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);
@@ -126,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)) {
@@ -144,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]}`);
});
});
}
@@ -157,7 +156,7 @@ app.on("ready", async () => {
await components.whenReady();
// Adblock
if (store.get(settings.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 });
@@ -169,9 +168,12 @@ app.on("ready", async () => {
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();
if (settingsStore.get(settings.trayIcon)) {
addTray(mainWindow, { icon });
refreshTray(mainWindow);
}
settingsStore.get(settings.api) && startExpress(mainWindow);
settingsStore.get(settings.enableDiscord) && initRPC();
} else {
app.quit();
}
@@ -186,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,157 +0,0 @@
let adBlock,
api,
customCSS,
disableBackgroundThrottle,
disableHardwareMediaKeys,
enableCustomHotkeys,
enableDiscord,
gpuRasterization,
menuBar,
minimizeOnClose,
mpris,
notifications,
playBackControl,
port,
singleInstance,
skipArtists,
skippedArtists,
trayIcon,
updateFrequency;
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() {
adBlock.checked = store.get(settings.adBlock);
api.checked = store.get(settings.api);
customCSS.value = store.get(settings.customCSS);
disableBackgroundThrottle.checked = store.get("disableBackgroundThrottle");
disableHardwareMediaKeys.checked = store.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = store.get(settings.enableCustomHotkeys);
enableDiscord.checked = store.get(settings.enableDiscord);
gpuRasterization.checked = store.get(settings.flags.gpuRasterization);
menuBar.checked = store.get(settings.menuBar);
minimizeOnClose.checked = store.get(settings.minimizeOnClose);
mpris.checked = store.get(settings.mpris);
notifications.checked = store.get(settings.notifications);
playBackControl.checked = store.get(settings.playBackControl);
port.value = store.get(settings.apiSettings.port);
singleInstance.checked = store.get(settings.singleInstance);
skipArtists.checked = store.get(settings.skipArtists);
skippedArtists.value = store.get(settings.skippedArtists).join("\n");
trayIcon.checked = store.get(settings.trayIcon);
updateFrequency.value = store.get(settings.updateFrequency);
}
/**
* 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.exit();
}
/**
* 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();
});
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

@@ -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>
@@ -226,6 +229,49 @@
<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>
@@ -238,41 +284,34 @@
<textarea id="customCSS" class="textarea" cols="40" rows="8" spellcheck="false"></textarea>
<div class="group">
<p class="group__title">Flags</p>
<p class="group__title">Theme files</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<h4>Current theme</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
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="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>
<h4>Upload new themes</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
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>

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,17 +1,19 @@
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, updateFrequency, customCSS } = 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;
const elements = {
@@ -45,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()]);
},
@@ -74,7 +76,7 @@ const elements = {
if (footer) {
const artists = footer.querySelectorAll(this.artists);
if (artists) return Array.from(artists).map((artist) => artist.textContent);
if (artists) return Array.from(artists).map((artist) => (artist as HTMLElement).textContent);
}
return [];
},
@@ -84,7 +86,7 @@ const elements = {
* @param {Array} artistsArray
* @returns {String} artists
*/
getArtistsString: function (artistsArray) {
getArtistsString: function (artistsArray: string[]) {
if (artistsArray.length > 0) return artistsArray.join(", ");
return "unknown artist(s)";
},
@@ -120,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 : "";
},
@@ -129,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;
},
@@ -138,7 +140,7 @@ 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();
},
};
@@ -146,7 +148,7 @@ const elements = {
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const style = document.createElement("style");
style.innerHTML = store.get(customCSS);
style.innerHTML = settingsStore.get(settings.customCSS);
document.head.appendChild(style);
});
}
@@ -156,7 +158,7 @@ function addCustomCss() {
* make sure it returns a number, if not use the default
*/
function getUpdateFrequency() {
const storeValue = store.get(updateFrequency);
const storeValue = settingsStore.get(settings.updateFrequency) as number;
const defaultValue = 500;
if (!isNaN(storeValue)) {
@@ -185,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);
});
}
@@ -231,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() {
@@ -293,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
@@ -309,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]);
}
@@ -319,10 +319,10 @@ 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) {
if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
}
if (player) {
@@ -361,7 +361,7 @@ function getTrackID() {
return window.location;
}
function updateMediaSession(options) {
function updateMediaSession(options: Options) {
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: options.title,
@@ -401,6 +401,8 @@ setInterval(function () {
current,
duration,
"app-name": appName,
image: "",
icon: "",
};
const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
@@ -413,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(
@@ -430,23 +432,20 @@ setInterval(function () {
// if the image can't be found on the page continue without it
resolve();
}
}).then(
() => {
updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) {
updateMediaSession(options);
}
},
() => {}
);
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) {
updateMediaSession(options);
}
});
/**
* automatically skip a song if the artists are found in the list of artists to skip
* @param {*} artists array of artists
*/
function skipArtistsIfFoundInSkippedArtistsList(artists) {
if (store.get(skipArtists)) {
const skippedArtists = store.get(settings.skippedArtists);
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);
@@ -459,9 +458,8 @@ setInterval(function () {
}
}, 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",
@@ -478,7 +476,7 @@ if (process.platform === "linux" && store.get(settings.mpris)) {
});
// Events
var events = {
const events = {
next: "next",
previous: "previous",
pause: "pause",
@@ -488,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];

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.artists
? mediaInfoModule.mediaInfo.artists
: "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.artists,
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,23 +1,24 @@
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;
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) {
// 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, action) {
function handleGlobalEvent(res: Response, action: any) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
@@ -26,7 +27,7 @@ expressModule.run = function (mainWindow) {
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
expressApp.get("/image", (req, res) => {
var stream = fs.createReadStream(mediaInfo.icon);
const stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function () {
res.set("Content-Type", "image/png");
stream.pipe(res);
@@ -37,7 +38,7 @@ expressModule.run = function (mainWindow) {
});
});
if (store.get(settings.playBackControl)) {
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));
@@ -50,21 +51,16 @@ expressModule.run = function (mainWindow) {
}
});
}
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;
}
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);
});
};
module.exports = expressModule;

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,6 +1,7 @@
const statuses = require("./../constants/statuses");
import { MediaInfo } from "../models/mediaInfo";
import { statuses } from "./../constants/statuses";
const mediaInfo = {
export const mediaInfo = {
title: "",
artists: "",
album: "",
@@ -11,14 +12,8 @@ const mediaInfo = {
duration: "",
image: "tidal-hifi-icon",
};
const mediaInfoModule = {
mediaInfo,
};
/**
* Update artist and song info in the mediaInfo constant
*/
mediaInfoModule.update = function (arg) {
export const updateMediaInfo = (arg: MediaInfo) => {
mediaInfo.title = propOrDefault(arg.title);
mediaInfo.artists = propOrDefault(arg.artists);
mediaInfo.album = propOrDefault(arg.album);
@@ -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,11 +1,12 @@
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: {
adBlock: false,
api: true,
@@ -45,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,
@@ -66,7 +66,7 @@ settingsModule.createSettingsWindow = function () {
},
});
settingsWindow.on("close", (event) => {
settingsWindow.on("close", (event: any) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();
@@ -78,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,9 +1,9 @@
const { Tray } = require("electron");
const { getMenu } = require("./menu");
const trayModule = {};
let tray;
import { BrowserWindow, Tray } from "electron";
import { getMenu } from "./menu";
trayModule.addTray = function (mainWindow, options = { icon: "" }) {
let tray: Tray;
export const addTray = function (mainWindow: BrowserWindow, options = { icon: "" }) {
tray = new Tray(options.icon);
tray.setIgnoreDoubleClickEvents(true);
tray.setToolTip("Tidal-hifi");
@@ -25,10 +25,8 @@ trayModule.addTray = function (mainWindow, options = { icon: "" }) {
});
};
trayModule.refreshTray = function (mainWindow) {
export const refreshTray = function (mainWindow: BrowserWindow) {
if (!tray) {
trayModule.addTray(mainWindow);
addTray(mainWindow);
}
};
module.exports = trayModule;

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/**/*"]
}