added settings

This commit is contained in:
Rick van Lieshout 2019-11-03 18:52:15 +01:00
parent 6ef4a0854d
commit f8aa97c15e
10 changed files with 624 additions and 51 deletions

View File

@ -1,6 +1,6 @@
<h1> <h1>
Tidal-hifi Tidal-hifi
<img src = "./build/icon.png" height="30" align="right" /> <img src = "./build/icon.png" height="40" align="right" />
</h1> </h1>
The web version of [listen.tidal.com](listen.tidal.com) running in electron with hifi support thanks to widevine. The web version of [listen.tidal.com](listen.tidal.com) running in electron with hifi support thanks to widevine.
@ -14,6 +14,7 @@ The web version of [listen.tidal.com](listen.tidal.com) running in electron with
- [Installation](#installation) - [Installation](#installation)
- [Using releases](#using-releases) - [Using releases](#using-releases)
- [Using source](#using-source) - [Using source](#using-source)
- [features](#features)
- [Integrations](#integrations) - [Integrations](#integrations)
- [Why](#why) - [Why](#why)
- [Why not extend existing projects?](#why-not-extend-existing-projects) - [Why not extend existing projects?](#why-not-extend-existing-projects)
@ -36,6 +37,15 @@ 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
- Shortcuts ([source](https://defkey.com/tidal-desktop-shortcuts))
- API for status and playback
- [Settings feature](./docs/settings.png) to disable certain functionality.
- Tray player (coming soon)
## Integrations ## Integrations
- [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit) - [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit)

BIN
docs/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

View File

@ -4,6 +4,12 @@ const globalEvents = {
playPause: "playPause", playPause: "playPause",
next: "next", next: "next",
previous: "previous", previous: "previous",
updateInfo: "update-info",
hideSettings: "hideSettings",
showSettings: "showSettings",
updateStatus: "update-status",
storeChanged: "storeChanged",
error: "error",
}; };
module.exports = globalEvents; module.exports = globalEvents;

View File

@ -11,6 +11,8 @@
const settings = { const settings = {
notifications: "notifications", notifications: "notifications",
api: "api", api: "api",
menuBar: "menuBar",
playBackControl: "playBackControl",
apiSettings: { apiSettings: {
root: "apiSettings", root: "apiSettings",
port: "apiSettings.port", port: "apiSettings.port",

View File

@ -1,5 +1,12 @@
const { app, BrowserWindow, globalShortcut, ipcMain } = require("electron"); const { app, BrowserWindow, globalShortcut, ipcMain } = require("electron");
const { settings, store } = require("./scripts/settings"); const {
settings,
store,
createSettingsWindow,
showSettingsWindow,
closeSettingsWindow,
hideSettingsWindow,
} = require("./scripts/settings");
const { addTray, refreshTray } = require("./scripts/tray"); const { addTray, refreshTray } = require("./scripts/tray");
const path = require("path"); const path = require("path");
@ -7,6 +14,7 @@ const tidalUrl = "https://listen.tidal.com";
const expressModule = require("./scripts/express"); const expressModule = require("./scripts/express");
const mediaKeys = require("./constants/mediaKeys"); const mediaKeys = require("./constants/mediaKeys");
const mediaInfoModule = require("./scripts/mediaInfo"); const mediaInfoModule = require("./scripts/mediaInfo");
const globalEvents = require("./constants/globalEvents");
let mainWindow; let mainWindow;
let icon = path.join(__dirname, "../assets/icon.png"); let icon = path.join(__dirname, "../assets/icon.png");
@ -34,10 +42,11 @@ function createWindow(options = {}) {
affinity: "window", affinity: "window",
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
plugins: true, plugins: true,
devTools: !app.isPackaged,
}, },
}); });
mainWindow.setMenuBarVisibility(false); mainWindow.setMenuBarVisibility(store.get(settings.menuBar));
// load the Tidal website // load the Tidal website
mainWindow.loadURL(tidalUrl); mainWindow.loadURL(tidalUrl);
@ -47,7 +56,8 @@ function createWindow(options = {}) {
// Emitted when the window is closed. // Emitted when the window is closed.
mainWindow.on("closed", function() { mainWindow.on("closed", function() {
mainWindow = null; closeSettingsWindow();
app.quit();
}); });
mainWindow.on("resize", () => { mainWindow.on("resize", () => {
let { width, height } = mainWindow.getBounds(); let { width, height } = mainWindow.getBounds();
@ -69,6 +79,7 @@ function addGlobalShortcuts() {
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on("ready", () => { app.on("ready", () => {
createWindow(); createWindow();
createSettingsWindow();
addGlobalShortcuts(); addGlobalShortcuts();
addTray({ icon }); addTray({ icon });
refreshTray(); refreshTray();
@ -85,10 +96,24 @@ app.on("activate", function() {
// IPC // IPC
ipcMain.on("update-info", (event, arg) => { ipcMain.on(globalEvents.updateInfo, (event, arg) => {
mediaInfoModule.update(arg); mediaInfoModule.update(arg);
}); });
ipcMain.on("update-status", (event, arg) => { ipcMain.on(globalEvents.hideSettings, (event, arg) => {
hideSettingsWindow();
});
ipcMain.on(globalEvents.showSettings, (event, arg) => {
showSettingsWindow();
});
ipcMain.on(globalEvents.updateStatus, (event, arg) => {
mediaInfoModule.updateStatus(arg); mediaInfoModule.updateStatus(arg);
}); });
ipcMain.on(globalEvents.storeChanged, (event, arg) => {
mainWindow.setMenuBarVisibility(store.get(settings.menuBar));
});
ipcMain.on(globalEvents.error, (event, arg) => {
console.log(event);
});

View File

@ -0,0 +1,74 @@
let notifications;
let playBackControl;
let api;
let port;
let menuBar;
const { store, settings } = require("./../../scripts/settings");
const { ipcRenderer } = require("electron");
const globalEvents = require("./../../constants/globalEvents");
/**
* Sync the UI forms with the current settings
*/
function refreshSettings() {
notifications.checked = store.get(settings.notifications);
playBackControl.checked = store.get(settings.playBackControl);
api.checked = store.get(settings.api);
port.value = store.get(settings.apiSettings.port);
menuBar.checked = store.get(settings.menuBar);
}
/**
* hide the settings window
*/
window.hide = function() {
ipcRenderer.send(globalEvents.hideSettings);
};
/**
* Restart tidal-hifi after changes
*/
window.restart = function() {
const remote = require("electron").remote;
remote.app.relaunch();
remote.app.exit(0);
};
/**
* Bind UI components to functions after DOMContentLoaded
*/
window.addEventListener("DOMContentLoaded", () => {
function get(id) {
return document.getElementById(id);
}
function addInputListener(source, key) {
source.addEventListener("input", function(event, data) {
if (this.value === "on") {
store.set(key, source.checked);
} else {
store.set(key, this.value);
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
ipcRenderer.on("refreshData", () => {
refreshSettings();
});
notifications = get("notifications");
playBackControl = get("playBackControl");
api = get("apiCheckbox");
port = get("port");
menuBar = get("menuBar");
refreshSettings();
addInputListener(notifications, settings.notifications);
addInputListener(playBackControl, settings.playBackControl);
addInputListener(api, settings.api);
addInputListener(port, settings.apiSettings.port);
addInputListener(menuBar, settings.menuBar);
});

View File

@ -0,0 +1,387 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
</head>
<body>
<div class="header">
<h1 class="title">Settings</h1>
<a href="javascript:hide();" class="exitWindow">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 348.333 348.334">
<g>
<path fill="white" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85
c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563
c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85
l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554
L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" />
</svg>
</a>
</div>
<div class="body">
<div class="tabset">
<!-- Tab 1 -->
<input type="radio" name="tabset" id="tab1" checked />
<label for="tab1">General</label>
<!-- Tab 2 -->
<input type="radio" name="tabset" id="tab2" />
<label for="tab2">Api</label>
<div class="tab-panels">
<section id="general" class="tab-panel">
<div class="section">
<h3>Playback</h3>
<div class="option">
<h4>Notifications</h4>
<p>
Whether to show a notification when a new song starts.
</p>
<label class="switch">
<input id="notifications" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="section">
<h3>UI</h3>
<div class="option">
<h4>Menubar</h4>
<p>
Show Tidal-hifi's menu bar
</p>
<label class="switch">
<input id="menuBar" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="api" class="tab-panel">
<div class="section">
<h3>Api</h3>
<p style="margin-bottom: 15px;">
Tidal-hifi has a web api built in to allow users to get current song information. You can optionally enable playback control as well.
<br />
<br />
<small>* api changes require a restart to update</small>
</p>
<div class="option">
<h4>Web API</h4>
<p>
Whether to enable the Tidal-hifi web api
</p>
<label class="switch">
<input id="apiCheckbox" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4 style="margin-bottom: 5px;">API port</h4>
<input id="port" type="text" class="freeTextInput" name="port">
</div>
<div class="option">
<h4>Playback control</h4>
<p>
Whether to enable playback control from the api
</p>
<label class="switch">
<input id="playBackControl" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
<button onClick="restart()">Restart Tidal-hifi</button>
</section>
</div>
</div>
</div>
</body>
<style>
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
.header a {
-webkit-app-region: no-drag;
}
* {
margin: 0%;
padding: 0%;
color: #ffffff;
font-weight: 400;
font-stretch: normal;
-webkit-font-smoothing: antialiased;
font-family: nationale, nationale-regular, Helvetica, sans-serif;
}
html,
body {
height: 100%;
background-color: black;
display: flex;
flex-direction: column;
}
h2 {
font-size: 1.2rem;
}
small {
font-style: italic;
color: #72777f;
}
.header {
background-color: #242528;
border-bottom: 1px solid #5a5a5a;
height: 50px;
}
.title {
float: left;
line-height: 50px;
margin-left: 15px;
}
.accent {
color: #0ff;
}
.exitWindow {
border: none;
text-decoration: none;
font-size: 1.4rem;
float: right;
margin-right: 15px;
height: 50px;
line-height: 50px;
}
.exitWindow svg {
height: 50px;
color: white;
}
.section {
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(246, 245, 255, .1);
}
.section .option {
margin-bottom: 15px;
}
.section .option p {
max-width: 75%;
float: left
}
.section .option label {
float: right;
}
.section:after,
.section .option:after {
content: "";
display: table;
clear: both;
}
.section h3 {
margin-bottom: 15px;
}
.section h4 {
font-size: 0.9rem;
}
.section p {
color: #72777f;
}
.bottom-border {
border-bottom: 1px solid #0ff;
}
.body {
padding: 15px;
flex: 1 1 auto;
position: relative;
overflow-y: auto;
}
.body::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-color: 2a2a2a;
}
.body::-webkit-scrollbar {
width: 10px;
background-color: #2a2a2a;
}
.body::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #5a5a5a;
}
/* Tabs */
.tabset > input[type="radio"] {
position: absolute;
left: -200vw;
}
.tabset .tab-panel {
display: none;
}
.tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,
.tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2),
.tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3) {
display: block;
}
.tabset > label {
position: relative;
display: inline-block;
padding: 15px 0px 10px;
border-bottom: 0;
cursor: pointer;
}
.tabset > input + label {
color: #e0e0e0;
margin-right: 30px;
}
.tabset > input:checked + label {
color: #0ff;
border-bottom: 2px solid #0ff;
}
.tab-panel {
padding: 10px 0;
}
/* switches */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(246, 245, 255, .1);
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 24px;
width: 24px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #0ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #0ff;
}
input:checked + .slider:before {
-webkit-transform: translateX(22px);
-ms-transform: translateX(22px);
transform: translateX(22px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/* input field */
input {
background: transparent;
border: 0;
border-bottom: 1px solid rgba(246, 245, 255, .1);
color: rgba(229, 238, 255, .6);
width: 100%;
display: block;
padding: 0 0 12px;
}
.freeTextInput:focus {
outline: none;
border-bottom: 1px solid #0ff;
}
/* buttons */
button{
border:none;
background:none;
align-items: center;
background-color: rgba(229,238,255,.2);
display: inline-flex;
justify-content: center;
border-radius: 12px;
height: 48px;
line-height: 49px;
padding: 0 24px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background .35s ease;
min-height: 0;
min-width: 0;
font-size: 1.14286rem;
font-family: nationale,nationale-regular,Helvetica,sans-serif;
margin-top: 10px;
cursor: pointer;
}
button:hover{
background-color: rgba(229,238,255,.3);
}
</style>
</html>

View File

@ -9,6 +9,7 @@ const hotkeys = require("./scripts/hotkeys");
const globalEvents = require("./constants/globalEvents"); const globalEvents = require("./constants/globalEvents");
const notifier = require("node-notifier"); const notifier = require("node-notifier");
const notificationPath = `${app.getPath("userData")}/notification.jpg`; const notificationPath = `${app.getPath("userData")}/notification.jpg`;
let currentSong = "";
const elements = { const elements = {
play: '*[data-test="play"]', play: '*[data-test="play"]',
@ -146,6 +147,10 @@ function addHotKeys() {
hotkeys.add("control+r", function() { hotkeys.add("control+r", function() {
elements.click("repeat"); elements.click("repeat");
}); });
hotkeys.add("control+/", function() {
ipcRenderer.send(globalEvents.showSettings);
});
} }
/** /**
@ -185,10 +190,6 @@ function handleLogout() {
*/ */
function addIPCEventListeners() { function addIPCEventListeners() {
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
ipcRenderer.on("getPlayInfo", () => {
alert(`${elements.getText("title")} - ${elements.getText("artists")}`);
});
ipcRenderer.on("globalEvent", (event, args) => { ipcRenderer.on("globalEvent", (event, args) => {
switch (args) { switch (args) {
case globalEvents.playPause: case globalEvents.playPause:
@ -221,7 +222,7 @@ function updateStatus() {
if (!play) { if (!play) {
status = statuses.playing; status = statuses.playing;
} }
ipcRenderer.send("update-status", status); ipcRenderer.send(globalEvents.updateStatus, status);
} }
/** /**
@ -237,33 +238,36 @@ setInterval(function() {
if (getTitle() !== songDashArtistTitle) { if (getTitle() !== songDashArtistTitle) {
setTitle(songDashArtistTitle); setTitle(songDashArtistTitle);
const image = elements.getSongIcon(); if (currentSong !== songDashArtistTitle) {
currentSong = songDashArtistTitle;
const image = elements.getSongIcon();
const options = { const options = {
title, title,
message: artists, message: artists,
}; };
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (image.startsWith("http")) { if (image.startsWith("http")) {
downloadFile(image, notificationPath).then( downloadFile(image, notificationPath).then(
() => { () => {
options.icon = notificationPath; options.icon = notificationPath;
resolve(); resolve();
}, },
() => { () => {
reject(); reject();
} }
); );
} else { } else {
reject(); reject();
} }
}).then( }).then(
() => { () => {
ipcRenderer.send("update-info", options); ipcRenderer.send(globalEvents.updateInfo, options);
store.get(settings.notifications) && notifier.notify(options); store.get(settings.notifications) && notifier.notify(options);
}, },
() => {} () => {}
); );
}
} }
}, 200); }, 200);

View File

@ -3,9 +3,10 @@ const { mediaInfo } = require("./mediaInfo");
const { store, settings } = require("./settings"); const { store, settings } = require("./settings");
const globalEvents = require("./../constants/globalEvents"); const globalEvents = require("./../constants/globalEvents");
const statuses = require("./../constants/statuses"); const statuses = require("./../constants/statuses");
const expressModule = {}; const expressModule = {};
var fs = require("fs"); const fs = require("fs");
let expressInstance;
/** /**
* Function to enable tidal-hifi's express api * Function to enable tidal-hifi's express api
@ -24,17 +25,6 @@ expressModule.run = function(mainWindow) {
const expressApp = express(); const expressApp = express();
expressApp.get("/", (req, res) => res.send("Hello World!")); expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json(mediaInfo)); expressApp.get("/current", (req, res) => res.json(mediaInfo));
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status == statuses.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
expressApp.get("/image", (req, res) => { expressApp.get("/image", (req, res) => {
var stream = fs.createReadStream(mediaInfo.icon); var stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function() { stream.on("open", function() {
@ -47,7 +37,34 @@ expressModule.run = function(mainWindow) {
}); });
}); });
expressApp.listen(store.get(settings.apiSettings.port), () => {}); if (store.get(settings.playBackControl)) {
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status == statuses.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
}
if (store.get(settings.api)) {
let port = store.get(settings.apiSettings.port);
expressInstance = expressApp.listen(port, () => {});
expressInstance.on("error", function(e) {
let message = e.code;
if (e.code === "EADDRINUSE") {
message = `Port ${port} in use.`;
}
const { dialog } = require("electron");
dialog.showErrorBox("Api failed to start.", message);
});
} else {
expressInstance = undefined;
}
}; };
module.exports = expressModule; module.exports = expressModule;

View File

@ -1,13 +1,20 @@
const Store = require("electron-store"); const Store = require("electron-store");
const settings = require("./../constants/settings"); const settings = require("./../constants/settings");
const path = require("path");
const { BrowserWindow } = require("electron");
let settingsWindow;
const store = new Store({ const store = new Store({
defaults: { defaults: {
notifications: true, notifications: true,
api: true, api: true,
playBackControl: true,
menuBar: false,
apiSettings: { apiSettings: {
port: 47836, port: 47836,
}, },
windowBounds: { width: 800, height: 600 }, windowBounds: { width: 800, height: 600 },
}, },
}); });
@ -15,6 +22,47 @@ const store = new Store({
const settingsModule = { const settingsModule = {
store, store,
settings, settings,
settingsWindow,
};
settingsModule.createSettingsWindow = function() {
settingsWindow = new BrowserWindow({
width: 500,
height: 600,
show: false,
frame: false,
title: "Tidal-hifi - settings",
webPreferences: {
affinity: "window",
preload: path.join(__dirname, "../pages/settings/preload.js"),
plugins: true,
nodeIntegration: true,
},
});
settingsWindow.on("close", (event) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();
}
});
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
settingsModule.settingsWindow = settingsWindow;
};
settingsModule.showSettingsWindow = function() {
// refresh data just before showing the window
settingsWindow.webContents.send("refreshData");
settingsWindow.show();
};
settingsModule.hideSettingsWindow = function() {
settingsWindow.hide();
};
settingsModule.closeSettingsWindow = function() {
settingsWindow = null;
}; };
module.exports = settingsModule; module.exports = settingsModule;