mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 02:31:45 +00:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b843825f72 | |||
| b66d3bc3d4 | |||
| 9adabd41d9 | |||
| 3f3df09819 | |||
| 1f5f597561 | |||
| 91e4433aba | |||
| 2d3ce4a8b3 | |||
| 971b7f05c5 | |||
| bb6115fec1 | |||
| 2a6dc30366 | |||
| 5e2d843742 | |||
| 7aaef26cc8 | |||
| 0a08eaaa3c | |||
| 0d22446f20 | |||
| a0543d15a6 | |||
| e62ee35b42 | |||
| ef6fb402bf | |||
| a8301f44be | |||
| 1ead86a220 | |||
| 03e716fe17 | |||
| f0bb328981 | |||
| f40183f0ca | |||
| 5b004acdc1 | |||
| 0f96da9928 | |||
| dfba3d9c2d | |||
| d9c51063f4 | |||
| cd9012691a | |||
| 2499f574ef | |||
| e7e873866d | |||
| 4ccbc741b8 | |||
| 8ec965a1a4 | |||
| 0936e9a258 | |||
| 32a5597573 | |||
| 9932fd7647 | |||
| 68429be1ce | |||
| d7ac493337 | |||
| 686a0a340e | |||
| bba499044b | |||
| 6e1c50ede1 | |||
| 6e739e2723 | |||
| 86029a0a73 | |||
| b458925aa6 | |||
| 86a1c3c850 | |||
| 8666f934cd | |||
| 59a93916a8 | |||
| f06a3c8c70 | |||
| 7fe937b21e | |||
| a4aa22aae9 | |||
| 54d25a26c7 | |||
| f5622970c6 | |||
| ea09825ece | |||
| 7bd69e447a | |||
| f6de5c7c22 | |||
| 77d4e9cb84 | |||
| b420998458 | |||
| feb06b015e | |||
| 0f192aab2b | |||
| 30840804fa | |||
| d23bfe9368 | |||
| 768ec7bda7 | |||
| c25a6f9d2a | |||
| cb910a6fd7 | |||
| 28b5645a56 | |||
| f4df6fceee | |||
| d69c8a754e | |||
| c3d90d8b27 | |||
| bed8d0a7f2 | |||
| 704fba9aba | |||
| 407887254f | |||
| 7088941179 | |||
| 1eeaf1dd0a | |||
| 9abf7a77d8 | |||
| 5bd97685b9 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -91,6 +91,8 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
with:
|
||||
run: yarn test
|
||||
|
||||
|
||||
24
changelog.md
24
changelog.md
@ -2,8 +2,32 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0)
|
||||
|
||||
- Bump ytdl-core (bug fix) [`#816`](https://github.com/th-ch/youtube-music/pull/816)
|
||||
- Bump electron and fix tests in CI [`#813`](https://github.com/th-ch/youtube-music/pull/813)
|
||||
- Allow user to pass custom CSS file [`#800`](https://github.com/th-ch/youtube-music/pull/800)
|
||||
- [Snyk] Upgrade html-to-text from 8.2.0 to 8.2.1 [`#799`](https://github.com/th-ch/youtube-music/pull/799)
|
||||
- [Snyk] Upgrade electron-store from 8.0.1 to 8.0.2 [`#772`](https://github.com/th-ch/youtube-music/pull/772)
|
||||
- Bump jpeg-js from 0.4.3 to 0.4.4 [`#756`](https://github.com/th-ch/youtube-music/pull/756)
|
||||
- Support MPRIS loop and volume change [`#749`](https://github.com/th-ch/youtube-music/pull/749)
|
||||
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8 [`#742`](https://github.com/th-ch/youtube-music/pull/742)
|
||||
- Use ; instead of space for play/pause. [`#745`](https://github.com/th-ch/youtube-music/pull/745)
|
||||
- Update readme.md [`#750`](https://github.com/th-ch/youtube-music/pull/750)
|
||||
- fix lyrics font size [`#753`](https://github.com/th-ch/youtube-music/pull/753)
|
||||
- fix top gap between nav-bar and browse-page [`#734`](https://github.com/th-ch/youtube-music/pull/734)
|
||||
- migrate from remote to ipc + fix restart in portable app [`#605`](https://github.com/th-ch/youtube-music/pull/605)
|
||||
- [Snyk] Upgrade custom-electron-prompt from 1.4.2 to 1.5.0 [`#717`](https://github.com/th-ch/youtube-music/pull/717)
|
||||
- Picture in Picture v2 [`#685`](https://github.com/th-ch/youtube-music/pull/685)
|
||||
- Add MPRIS volume control [`#776`](https://github.com/th-ch/youtube-music/issues/776)
|
||||
- Remove jest [`bb6115f`](https://github.com/th-ch/youtube-music/commit/bb6115fec1a18a416edb365a442eb0b0ee330768)
|
||||
- migrate from remote to ipc [`5bd9768`](https://github.com/th-ch/youtube-music/commit/5bd97685b9e07c656e0b57a9e02819afc70af1b1)
|
||||
- v3 [`d23bfe9`](https://github.com/th-ch/youtube-music/commit/d23bfe936840b947ca101fd304464f65d36e88cc)
|
||||
|
||||
#### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0)
|
||||
|
||||
> 16 May 2022
|
||||
|
||||
- Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712)
|
||||
- fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693)
|
||||
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to 1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689)
|
||||
|
||||
@ -83,6 +83,13 @@ const defaultConfig = {
|
||||
mode: "custom",
|
||||
forceHide: false,
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"enabled": false,
|
||||
"alwaysOnTop": true,
|
||||
"savePosition": true,
|
||||
"saveSize": false,
|
||||
"hotkey": "P"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -2,8 +2,16 @@ const Store = require("electron-store");
|
||||
|
||||
const defaults = require("./defaults");
|
||||
|
||||
const setDefaultPluginOptions = (store, plugin) => {
|
||||
if (!store.get(`plugins.${plugin}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
}
|
||||
}
|
||||
|
||||
const migrations = {
|
||||
">=1.17.0": (store) => {
|
||||
setDefaultPluginOptions(store, "picture-in-picture");
|
||||
|
||||
if (store.get("plugins.video-toggle.mode") === undefined) {
|
||||
store.set("plugins.video-toggle.mode", "custom");
|
||||
}
|
||||
|
||||
70
index.js
70
index.js
@ -2,8 +2,6 @@
|
||||
const path = require("path");
|
||||
|
||||
const electron = require("electron");
|
||||
const remote = require('@electron/remote/main');
|
||||
remote.initialize();
|
||||
const enhanceWebRequest = require("electron-better-web-request").default;
|
||||
const is = require("electron-is");
|
||||
const unhandled = require("electron-unhandled");
|
||||
@ -15,6 +13,7 @@ const { fileExists, injectCSS } = require("./plugins/utils");
|
||||
const { isTesting } = require("./utils/testing");
|
||||
const { setUpTray } = require("./tray");
|
||||
const { setupSongInfo } = require("./providers/song-info");
|
||||
const { setupAppControls, restart } = require("./providers/app-controls");
|
||||
|
||||
// Catch errors and log them
|
||||
unhandled({
|
||||
@ -85,6 +84,22 @@ function onClosed() {
|
||||
|
||||
function loadPlugins(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||
// Load user CSS
|
||||
const themes = config.get("options.themes");
|
||||
if (Array.isArray(themes)) {
|
||||
themes.forEach((cssFile) => {
|
||||
fileExists(
|
||||
cssFile,
|
||||
() => {
|
||||
injectCSS(win.webContents, cssFile);
|
||||
},
|
||||
() => {
|
||||
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
win.webContents.once("did-finish-load", () => {
|
||||
if (is.dev()) {
|
||||
console.log("did finish load");
|
||||
@ -120,14 +135,13 @@ function createMainWindow() {
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
||||
affinity: "main-window", // main window, and addition windows should work in one process
|
||||
...(isTesting()
|
||||
...(!isTesting()
|
||||
? {
|
||||
// Only necessary when testing with Spectron
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
}
|
||||
// Sandbox is only enabled in tests for now
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
||||
sandbox: false,
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
frame: !is.macOS() && !useInlineMenu,
|
||||
@ -138,7 +152,6 @@ function createMainWindow() {
|
||||
: "default",
|
||||
autoHideMenuBar: config.get("options.hideMenu"),
|
||||
});
|
||||
remote.enable(win.webContents);
|
||||
|
||||
if (windowPosition) {
|
||||
const { x, y } = windowPosition;
|
||||
@ -175,6 +188,10 @@ function createMainWindow() {
|
||||
win.webContents.loadURL(urlToLoad);
|
||||
win.on("closed", onClosed);
|
||||
|
||||
const setPiPOptions = config.plugins.isEnabled("picture-in-picture")
|
||||
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value })
|
||||
: () => {};
|
||||
|
||||
win.on("move", () => {
|
||||
if (win.isMaximized()) return;
|
||||
let position = win.getPosition();
|
||||
@ -183,6 +200,8 @@ function createMainWindow() {
|
||||
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||
if (!isPiPEnabled) {
|
||||
lateSave("window-position", { x: position[0], y: position[1] });
|
||||
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) {
|
||||
lateSave("pip-position", position, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
@ -190,31 +209,36 @@ function createMainWindow() {
|
||||
|
||||
win.on("resize", () => {
|
||||
const windowSize = win.getSize();
|
||||
|
||||
const isMaximized = win.isMaximized();
|
||||
if (winWasMaximized !== isMaximized) {
|
||||
winWasMaximized = isMaximized;
|
||||
config.set("window-maximized", isMaximized);
|
||||
}
|
||||
|
||||
const isPiPEnabled =
|
||||
config.plugins.isEnabled("picture-in-picture") &&
|
||||
config.plugins.getOptions("picture-in-picture")["isInPiP"];
|
||||
if (!isMaximized && !isPiPEnabled) {
|
||||
|
||||
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
||||
winWasMaximized = isMaximized;
|
||||
config.set("window-maximized", isMaximized);
|
||||
}
|
||||
if (isMaximized) return;
|
||||
|
||||
if (!isPiPEnabled) {
|
||||
lateSave("window-size", {
|
||||
width: windowSize[0],
|
||||
height: windowSize[1],
|
||||
});
|
||||
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) {
|
||||
lateSave("pip-size", windowSize, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
let savedTimeouts = {};
|
||||
function lateSave(key, value) {
|
||||
function lateSave(key, value, fn = config.set) {
|
||||
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
|
||||
|
||||
savedTimeouts[key] = setTimeout(() => {
|
||||
config.set(key, value);
|
||||
fn(key, value);
|
||||
savedTimeouts[key] = undefined;
|
||||
}, 1000)
|
||||
}, 600);
|
||||
}
|
||||
|
||||
win.webContents.on("render-process-gone", (event, webContents, details) => {
|
||||
@ -261,6 +285,7 @@ app.once("browser-window-created", (event, win) => {
|
||||
|
||||
setupSongInfo(win);
|
||||
loadPlugins(win);
|
||||
setupAppControls();
|
||||
|
||||
win.webContents.on("did-fail-load", (
|
||||
_event,
|
||||
@ -448,13 +473,8 @@ function showUnresponsiveDialog(win, details) {
|
||||
cancelId: 0
|
||||
}).then( result => {
|
||||
switch (result.response) {
|
||||
case 1: //if relaunch - relaunch+exit
|
||||
app.relaunch();
|
||||
case 2:
|
||||
app.quit();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case 1: restart(); break;
|
||||
case 2: app.quit(); break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
34
menu.js
34
menu.js
@ -3,6 +3,7 @@ const path = require("path");
|
||||
|
||||
const { app, Menu, dialog } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const { restart } = require("./providers/app-controls");
|
||||
|
||||
const { getAllPlugins } = require("./plugins/utils");
|
||||
const config = require("./config");
|
||||
@ -99,6 +100,34 @@ const mainMenuTemplate = (win) => {
|
||||
config.set("options.ForceShowLikeButtons", item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Theme",
|
||||
submenu: [
|
||||
{
|
||||
label: "No theme",
|
||||
type: "radio",
|
||||
checked: !config.get("options.themes"), // todo rename "themes"
|
||||
click: () => {
|
||||
config.set("options.themes", []);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Import custom CSS file",
|
||||
type: "radio",
|
||||
checked: false,
|
||||
click: async () => {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
filters: [{ name: "CSS Files", extensions: ["css"] }],
|
||||
properties: ["openFile", "multiSelections"],
|
||||
});
|
||||
if (filePaths) {
|
||||
config.set("options.themes", filePaths);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -302,10 +331,7 @@ const mainMenuTemplate = (win) => {
|
||||
},
|
||||
{
|
||||
label: "Restart App",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
click: restart
|
||||
},
|
||||
{ role: "quit" },
|
||||
],
|
||||
|
||||
24
package.json
24
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"license": "MIT",
|
||||
"repository": "th-ch/youtube-music",
|
||||
@ -67,7 +67,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test": "playwright test",
|
||||
"test:debug": "DEBUG=pw:browser* playwright test",
|
||||
"start": "electron .",
|
||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
||||
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
||||
@ -92,42 +93,41 @@
|
||||
"npm": "Please use yarn and not npm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.23.7",
|
||||
"@electron/remote": "^2.0.8",
|
||||
"@cliqz/adblocker-electron": "^1.23.8",
|
||||
"@ffmpeg/core": "^0.10.0",
|
||||
"@ffmpeg/ffmpeg": "^0.10.1",
|
||||
"Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.4.6",
|
||||
"async-mutex": "^0.3.2",
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"custom-electron-prompt": "^1.4.2",
|
||||
"custom-electron-prompt": "^1.5.0",
|
||||
"custom-electron-titlebar": "^4.1.0",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-better-web-request": "^1.0.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-store": "^8.0.1",
|
||||
"electron-unhandled": "^3.0.2",
|
||||
"electron-store": "^8.0.2",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^4.6.3",
|
||||
"filenamify": "^4.3.0",
|
||||
"hark": "^1.2.3",
|
||||
"html-to-text": "^8.2.0",
|
||||
"html-to-text": "^8.2.1",
|
||||
"md5": "^2.3.0",
|
||||
"mpris-service": "^2.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-notifier": "^10.0.1",
|
||||
"ytdl-core": "^4.11.0",
|
||||
"ytdl-core": "^4.11.1",
|
||||
"ytpl": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.25.1",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"electron": "^17.0.0",
|
||||
"electron": "^20.1.1",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-icon-maker": "0.0.5",
|
||||
"jest": "^27.3.1",
|
||||
"playwright": "^1.17.1",
|
||||
"playwright": "^1.25.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"xo": "^0.45.0"
|
||||
},
|
||||
|
||||
@ -3,7 +3,6 @@ const { join } = require("path");
|
||||
|
||||
const Mutex = require("async-mutex").Mutex;
|
||||
const { ipcRenderer } = require("electron");
|
||||
const remote = require('@electron/remote');
|
||||
const is = require("electron-is");
|
||||
const filenamify = require("filenamify");
|
||||
|
||||
@ -137,7 +136,7 @@ const toMP3 = async (
|
||||
safeVideoName + "." + extension
|
||||
);
|
||||
|
||||
const folder = options.downloadFolder || remote.app.getPath("downloads");
|
||||
const folder = options.downloadFolder || await ipcRenderer.invoke('getDownloadsFolder');
|
||||
const name = metadata.title
|
||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||
: videoName;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const config = require("../../config");
|
||||
const { Titlebar, Color } = require("custom-electron-titlebar");
|
||||
const { isEnabled } = require("../../config/plugins");
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
module.exports = (options) => {
|
||||
@ -29,6 +30,12 @@ module.exports = (options) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnabled("picture-in-picture")) {
|
||||
ipcRenderer.on("pip-toggle", (_, pipEnabled) => {
|
||||
bar.refreshMenu();
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
|
||||
|
||||
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||
|
||||
@ -13,6 +13,11 @@
|
||||
height: 75px !important;
|
||||
}
|
||||
|
||||
/* fixes top gap between nav-bar and browse-page */
|
||||
#browse-page {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||
ytmusic-nav-bar,
|
||||
.tab-titleiron-icon,
|
||||
|
||||
@ -7,6 +7,6 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1vw !important;
|
||||
font-size: clamp(1.4rem, 1.1vmax, 3rem) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"] {
|
||||
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"],
|
||||
.sign-in-link {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
37
plugins/picture-in-picture/adaptors/in-app-menu.js
Normal file
37
plugins/picture-in-picture/adaptors/in-app-menu.js
Normal file
@ -0,0 +1,37 @@
|
||||
const { Menu, app } = require("electron");
|
||||
const { setApplicationMenu } = require("../../../menu");
|
||||
|
||||
module.exports = (win, options, setOptions, togglePip, isInPip) => {
|
||||
if (isInPip) {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
{
|
||||
label: "App",
|
||||
submenu: [
|
||||
{
|
||||
label: "Exit Picture in Picture",
|
||||
click: togglePip,
|
||||
},
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: options.alwaysOnTop,
|
||||
click: (item) => {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
]));
|
||||
} else {
|
||||
setApplicationMenu(win);
|
||||
}
|
||||
};
|
||||
@ -1,79 +1,109 @@
|
||||
const path = require("path");
|
||||
|
||||
const { app, ipcMain } = require("electron");
|
||||
const electronLocalshortcut = require("electron-localshortcut");
|
||||
|
||||
const { setOptions } = require("../../config/plugins");
|
||||
const { setOptions, isEnabled } = require("../../config/plugins");
|
||||
const { injectCSS } = require("../utils");
|
||||
|
||||
let isInPiPMode = false;
|
||||
let isInPiP = false;
|
||||
let originalPosition;
|
||||
let originalSize;
|
||||
let originalFullScreen;
|
||||
let originalMaximized;
|
||||
|
||||
const pipPosition = [10, 10];
|
||||
const pipSize = [450, 275];
|
||||
let win;
|
||||
let options;
|
||||
|
||||
const togglePiP = async (win) => {
|
||||
isInPiPMode = !isInPiPMode;
|
||||
setOptions("picture-in-picture", { isInPiP: isInPiPMode });
|
||||
const pipPosition = () => (options.savePosition && options["pip-position"]) || [10, 10];
|
||||
const pipSize = () => (options.saveSize && options["pip-size"]) || [450, 275];
|
||||
|
||||
if (isInPiPMode) {
|
||||
const setLocalOptions = (_options) => {
|
||||
options = { ...options, ..._options };
|
||||
setOptions("picture-in-picture", _options);
|
||||
}
|
||||
|
||||
|
||||
const adaptors = [];
|
||||
const runAdaptors = () => adaptors.forEach(a => a());
|
||||
|
||||
if (isEnabled("in-app-menu")) {
|
||||
let adaptor = require("./adaptors/in-app-menu");
|
||||
adaptors.push(() => adaptor(win, options, setLocalOptions, togglePiP, isInPiP));
|
||||
}
|
||||
|
||||
const togglePiP = async () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
if (originalFullScreen) win.setFullScreen(false);
|
||||
originalMaximized = win.isMaximized();
|
||||
if (originalMaximized) win.unmaximize();
|
||||
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
|
||||
win.webContents.on("before-input-event", blockShortcutsInPiP);
|
||||
|
||||
win.setFullScreenable(false);
|
||||
await win.webContents.executeJavaScript(
|
||||
// Go fullscreen
|
||||
`
|
||||
if (!document.querySelector("ytmusic-player-page").playerPageOpen_) {
|
||||
document.querySelector(".toggle-player-page-button").click();
|
||||
}
|
||||
document.querySelector(".fullscreen-button").click();
|
||||
document.querySelector("ytmusic-player-bar").classList.add("pip");
|
||||
`
|
||||
);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", true);
|
||||
|
||||
app.dock?.hide();
|
||||
win.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
app.dock?.show();
|
||||
win.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
if (options.alwaysOnTop) {
|
||||
win.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
await win.webContents.executeJavaScript(
|
||||
// Exit fullscreen
|
||||
`
|
||||
document.querySelector(".exit-fullscreen-button").click();
|
||||
document.querySelector("ytmusic-player-bar").classList.remove("pip");
|
||||
`
|
||||
);
|
||||
runAdaptors();
|
||||
win.webContents.send("pip-toggle", false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
win.setAlwaysOnTop(false);
|
||||
|
||||
if (originalFullScreen) win.setFullScreen(true);
|
||||
if (originalMaximized) win.maximize();
|
||||
}
|
||||
|
||||
const [x, y] = isInPiPMode ? pipPosition : originalPosition;
|
||||
const [w, h] = isInPiPMode ? pipSize : originalSize;
|
||||
const [x, y] = isInPiP ? pipPosition() : originalPosition;
|
||||
const [w, h] = isInPiP ? pipSize() : originalSize;
|
||||
win.setPosition(x, y);
|
||||
win.setSize(w, h);
|
||||
|
||||
win.setWindowButtonVisibility?.(!isInPiPMode);
|
||||
};
|
||||
|
||||
module.exports = (win) => {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
ipcMain.on("picture-in-picture", async () => {
|
||||
await togglePiP(win);
|
||||
});
|
||||
win.setWindowButtonVisibility?.(!isInPiP);
|
||||
};
|
||||
|
||||
const blockShortcutsInPiP = (event, input) => {
|
||||
const blockedShortcuts = ["f", "escape"];
|
||||
if (blockedShortcuts.includes(input.key.toLowerCase())) {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === "f") {
|
||||
event.preventDefault();
|
||||
} else if (key === 'escape') {
|
||||
togglePiP();
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (_win, _options) => {
|
||||
options ??= _options;
|
||||
win ??= _win;
|
||||
setLocalOptions({ isInPiP });
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
ipcMain.on("picture-in-picture", async () => {
|
||||
await togglePiP();
|
||||
});
|
||||
if (options.hotkey) {
|
||||
electronLocalshortcut.register(win, options.hotkey, togglePiP);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setOptions = setLocalOptions;
|
||||
|
||||
@ -3,6 +3,8 @@ const { ipcRenderer } = require("electron");
|
||||
const { getSongMenu } = require("../../providers/dom-elements");
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
let menu = null;
|
||||
const pipButton = ElementFromFile(
|
||||
templatePath(__dirname, "picture-in-picture.html")
|
||||
@ -14,7 +16,7 @@ const observer = new MutationObserver(() => {
|
||||
if (!menu) return;
|
||||
}
|
||||
if (menu.contains(pipButton)) return;
|
||||
const menuUrl = document.querySelector(
|
||||
const menuUrl = $(
|
||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
|
||||
)?.href;
|
||||
if (menuUrl && !menuUrl.includes("watch?")) return;
|
||||
@ -26,11 +28,57 @@ global.togglePictureInPicture = () => {
|
||||
ipcRenderer.send("picture-in-picture");
|
||||
};
|
||||
|
||||
const listenForToggle = () => {
|
||||
const originalExitButton = $(".exit-fullscreen-button");
|
||||
const clonedExitButton = originalExitButton.cloneNode(true);
|
||||
clonedExitButton.onclick = () => togglePictureInPicture();
|
||||
|
||||
const appLayout = $("ytmusic-app-layout");
|
||||
const expandMenu = $('#expanding-menu');
|
||||
const middleControls = $('.middle-controls');
|
||||
const playerPage = $("ytmusic-player-page");
|
||||
const togglePlayerPageButton = $(".toggle-player-page-button");
|
||||
const fullScreenButton = $(".fullscreen-button");
|
||||
const player = $('#player');
|
||||
const onPlayerDblClick = player.onDoubleClick_;
|
||||
|
||||
ipcRenderer.on('pip-toggle', (_, isPip) => {
|
||||
if (isPip) {
|
||||
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
|
||||
player.onDoubleClick_ = () => {};
|
||||
expandMenu.onmouseleave = () => middleControls.click();
|
||||
if (!playerPage.playerPageOpen_) {
|
||||
togglePlayerPageButton.click();
|
||||
}
|
||||
fullScreenButton.click();
|
||||
appLayout.classList.add("pip");
|
||||
} else {
|
||||
$(".exit-fullscreen-button").replaceWith(originalExitButton);
|
||||
player.onDoubleClick_ = onPlayerDblClick;
|
||||
expandMenu.onmouseleave = undefined;
|
||||
originalExitButton.click();
|
||||
appLayout.classList.remove("pip");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function observeMenu(options) {
|
||||
document.addEventListener(
|
||||
"apiLoaded",
|
||||
() => {
|
||||
observer.observe(document.querySelector("ytmusic-popup-container"), {
|
||||
listenForToggle();
|
||||
const minButton = $(".player-minimize-button");
|
||||
// remove native listeners
|
||||
minButton.replaceWith(minButton.cloneNode(true));
|
||||
$(".player-minimize-button").onclick = () => {
|
||||
global.togglePictureInPicture();
|
||||
setTimeout(() => $('#player').click());
|
||||
};
|
||||
|
||||
// allows easily closing the menu by programmatically clicking outside of it
|
||||
$("#expanding-menu").removeAttribute("no-cancel-on-outside-click");
|
||||
// TODO: think about wether an additional button in songMenu is needed
|
||||
observer.observe($("ytmusic-popup-container"), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
60
plugins/picture-in-picture/menu.js
Normal file
60
plugins/picture-in-picture/menu.js
Normal file
@ -0,0 +1,60 @@
|
||||
const prompt = require("custom-electron-prompt");
|
||||
|
||||
const promptOptions = require("../../providers/prompt-options");
|
||||
const { setOptions } = require("./back.js");
|
||||
|
||||
module.exports = (win, options) => [
|
||||
{
|
||||
label: "Always on top",
|
||||
type: "checkbox",
|
||||
checked: options.alwaysOnTop,
|
||||
click: (item) => {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Save window position",
|
||||
type: "checkbox",
|
||||
checked: options.savePosition,
|
||||
click: (item) => {
|
||||
setOptions({ savePosition: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Save window size",
|
||||
type: "checkbox",
|
||||
checked: options.saveSize,
|
||||
click: (item) => {
|
||||
setOptions({ saveSize: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Hotkey",
|
||||
type: "checkbox",
|
||||
checked: options.hotkey,
|
||||
click: async (item) => {
|
||||
const output = await prompt({
|
||||
title: "Picture in Picture Hotkey",
|
||||
label: "Choose a hotkey for toggling Picture in Picture",
|
||||
type: "keybind",
|
||||
keybindOptions: [{
|
||||
value: "hotkey",
|
||||
label: "Hotkey",
|
||||
default: options.hotkey
|
||||
}],
|
||||
...promptOptions()
|
||||
}, win)
|
||||
|
||||
if (output) {
|
||||
const { value, accelerator } = output[0];
|
||||
setOptions({ [value]: accelerator });
|
||||
|
||||
item.checked = !!accelerator;
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
@ -1,11 +1,26 @@
|
||||
ytmusic-player-bar.pip svg,
|
||||
ytmusic-player-bar.pip yt-formatted-string {
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
color: white;
|
||||
/* improve visibility of the player bar elements */
|
||||
ytmusic-app-layout.pip ytmusic-player-bar svg,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .time-info,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
|
||||
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
color: white !important;
|
||||
fill: white !important;
|
||||
}
|
||||
|
||||
ytmusic-player-bar.pip ytmusic-player-expanding-menu {
|
||||
/* improve the style of the player bar expanding menu */
|
||||
ytmusic-app-layout.pip ytmusic-player-expanding-menu {
|
||||
border-radius: 30px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(5px) brightness(20%);
|
||||
}
|
||||
|
||||
/* fix volumeHud position when both in-app-menu and PiP are active */
|
||||
.cet-container ytmusic-app-layout.pip #volumeHud {
|
||||
top: 22px !important;
|
||||
}
|
||||
|
||||
/* disable the video-toggle button when in PiP mode */
|
||||
ytmusic-app-layout.pip .video-switch-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@ -7,9 +7,18 @@ This is used to determine if plugin is actually active
|
||||
*/
|
||||
let enabled = false;
|
||||
|
||||
module.exports = (win) => {
|
||||
const { globalShortcut } = require('electron');
|
||||
|
||||
module.exports = (win, options) => {
|
||||
enabled = true;
|
||||
injectCSS(win.webContents, path.join(__dirname, "volume-hud.css"));
|
||||
|
||||
if (options.globalShortcuts?.volumeUp) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
|
||||
}
|
||||
if (options.globalShortcuts?.volumeDown) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.enabled = () => enabled;
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { globalShortcut } = require('@electron/remote');
|
||||
|
||||
const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins");
|
||||
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
let api;
|
||||
|
||||
module.exports = (options) => {
|
||||
let api, options;
|
||||
|
||||
module.exports = (_options) => {
|
||||
options = _options;
|
||||
document.addEventListener('apiLoaded', e => {
|
||||
api = e.detail;
|
||||
firstRun(options);
|
||||
ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease));
|
||||
ipcRenderer.on('setVolume', (_, value) => setVolume(value));
|
||||
firstRun();
|
||||
}, { once: true, passive: true })
|
||||
};
|
||||
|
||||
module.exports.moveVolumeHud = moveVolumeHud;
|
||||
|
||||
/** Restore saved volume and setup tooltip */
|
||||
function firstRun(options) {
|
||||
function firstRun() {
|
||||
if (typeof options.savedVolume === "number") {
|
||||
// Set saved volume as tooltip
|
||||
setTooltip(options.savedVolume);
|
||||
@ -26,16 +29,14 @@ function firstRun(options) {
|
||||
}
|
||||
}
|
||||
|
||||
setupPlaybar(options);
|
||||
setupPlaybar();
|
||||
|
||||
setupLocalArrowShortcuts(options);
|
||||
|
||||
setupGlobalShortcuts(options);
|
||||
setupLocalArrowShortcuts();
|
||||
|
||||
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
|
||||
injectVolumeHud(noVid);
|
||||
if (!noVid) {
|
||||
setupVideoPlayerOnwheel(options);
|
||||
setupVideoPlayerOnwheel();
|
||||
if (!isEnabled('video-toggle')) {
|
||||
//video-toggle handles hud positioning on its own
|
||||
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
||||
@ -96,22 +97,22 @@ function showVolumeHud(volume) {
|
||||
}
|
||||
|
||||
/** Add onwheel event to video player */
|
||||
function setupVideoPlayerOnwheel(options) {
|
||||
function setupVideoPlayerOnwheel() {
|
||||
$("#main-panel").addEventListener("wheel", event => {
|
||||
event.preventDefault();
|
||||
// Event.deltaY < 0 means wheel-up
|
||||
changeVolume(event.deltaY < 0, options);
|
||||
changeVolume(event.deltaY < 0);
|
||||
});
|
||||
}
|
||||
|
||||
function saveVolume(volume, options) {
|
||||
function saveVolume(volume) {
|
||||
options.savedVolume = volume;
|
||||
writeOptions(options);
|
||||
writeOptions();
|
||||
}
|
||||
|
||||
//without this function it would rewrite config 20 time when volume change by 20
|
||||
let writeTimeout;
|
||||
function writeOptions(options) {
|
||||
function writeOptions() {
|
||||
if (writeTimeout) clearTimeout(writeTimeout);
|
||||
|
||||
writeTimeout = setTimeout(() => {
|
||||
@ -121,13 +122,13 @@ function writeOptions(options) {
|
||||
}
|
||||
|
||||
/** Add onwheel event to play bar and also track if play bar is hovered*/
|
||||
function setupPlaybar(options) {
|
||||
function setupPlaybar() {
|
||||
const playerbar = $("ytmusic-player-bar");
|
||||
|
||||
playerbar.addEventListener("wheel", event => {
|
||||
event.preventDefault();
|
||||
// Event.deltaY < 0 means wheel-up
|
||||
changeVolume(event.deltaY < 0, options);
|
||||
changeVolume(event.deltaY < 0);
|
||||
});
|
||||
|
||||
// Keep track of mouse position for showVolumeSlider()
|
||||
@ -139,11 +140,11 @@ function setupPlaybar(options) {
|
||||
playerbar.classList.remove("on-hover");
|
||||
});
|
||||
|
||||
setupSliderObserver(options);
|
||||
setupSliderObserver();
|
||||
}
|
||||
|
||||
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
||||
function setupSliderObserver(options) {
|
||||
function setupSliderObserver() {
|
||||
const sliderObserver = new MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
// This checks that volume-slider was manually set
|
||||
@ -151,7 +152,7 @@ function setupSliderObserver(options) {
|
||||
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
||||
// Diff>4 means it was manually set
|
||||
setTooltip(mutation.target.value);
|
||||
saveVolume(mutation.target.value, options);
|
||||
saveVolume(mutation.target.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -163,29 +164,32 @@ function setupSliderObserver(options) {
|
||||
});
|
||||
}
|
||||
|
||||
/** if (toIncrease = false) then volume decrease */
|
||||
function changeVolume(toIncrease, options) {
|
||||
// Apply volume change if valid
|
||||
const steps = Number(options.steps || 1);
|
||||
api.setVolume(toIncrease ?
|
||||
Math.min(api.getVolume() + steps, 100) :
|
||||
Math.max(api.getVolume() - steps, 0));
|
||||
|
||||
function setVolume(value) {
|
||||
api.setVolume(value);
|
||||
// Save the new volume
|
||||
saveVolume(api.getVolume(), options);
|
||||
saveVolume(value);
|
||||
|
||||
// change slider position (important)
|
||||
updateVolumeSlider(options);
|
||||
updateVolumeSlider();
|
||||
|
||||
// Change tooltips to new value
|
||||
setTooltip(options.savedVolume);
|
||||
setTooltip(value);
|
||||
// Show volume slider
|
||||
showVolumeSlider();
|
||||
// Show volume HUD
|
||||
showVolumeHud(options.savedVolume);
|
||||
showVolumeHud(value);
|
||||
}
|
||||
|
||||
function updateVolumeSlider(options) {
|
||||
/** if (toIncrease = false) then volume decrease */
|
||||
function changeVolume(toIncrease) {
|
||||
// Apply volume change if valid
|
||||
const steps = Number(options.steps || 1);
|
||||
setVolume(toIncrease ?
|
||||
Math.min(api.getVolume() + steps, 100) :
|
||||
Math.max(api.getVolume() - steps, 0));
|
||||
}
|
||||
|
||||
function updateVolumeSlider() {
|
||||
// Slider value automatically rounds to multiples of 5
|
||||
for (const slider of ["#volume-slider", "#expand-volume-slider"]) {
|
||||
$(slider).value =
|
||||
@ -228,26 +232,17 @@ function setTooltip(volume) {
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalShortcuts(options) {
|
||||
if (options.globalShortcuts.volumeUp) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
|
||||
}
|
||||
if (options.globalShortcuts.volumeDown) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
|
||||
}
|
||||
}
|
||||
|
||||
function setupLocalArrowShortcuts(options) {
|
||||
function setupLocalArrowShortcuts() {
|
||||
if (options.arrowsShortcut) {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
switch (event.code) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
changeVolume(true, options);
|
||||
changeVolume(true);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
changeVolume(false, options);
|
||||
changeVolume(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
15
plugins/quality-changer/back.js
Normal file
15
plugins/quality-changer/back.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { ipcMain, dialog } = require("electron");
|
||||
|
||||
module.exports = () => {
|
||||
ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => {
|
||||
return await dialog.showMessageBox({
|
||||
type: "question",
|
||||
buttons: qualityLabels,
|
||||
defaultId: currentIndex,
|
||||
title: "Choose Video Quality",
|
||||
message: "Choose Video Quality:",
|
||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||
cancelId: -1
|
||||
})
|
||||
})
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
const { dialog } = require('@electron/remote');
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
@ -18,24 +18,17 @@ function setup(event) {
|
||||
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
|
||||
|
||||
qualitySettingsButton.onclick = function chooseQuality() {
|
||||
if (api.getPlayerState() === 2) api.playVideo();
|
||||
else if (api.getPlayerState() === 1) api.pauseVideo();
|
||||
setTimeout(() => $('#player').click());
|
||||
|
||||
const currentIndex = api.getAvailableQualityLevels().indexOf(api.getPlaybackQuality())
|
||||
const qualityLevels = api.getAvailableQualityLevels();
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: "question",
|
||||
buttons: api.getAvailableQualityLabels(),
|
||||
defaultId: currentIndex,
|
||||
title: "Choose Video Quality",
|
||||
message: "Choose Video Quality:",
|
||||
detail: `Current Quality: ${api.getAvailableQualityLabels()[currentIndex]}`,
|
||||
cancelId: -1
|
||||
}).then((promise) => {
|
||||
const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
|
||||
|
||||
ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then(promise => {
|
||||
if (promise.response === -1) return;
|
||||
const newQuality = api.getAvailableQualityLevels()[promise.response];
|
||||
const newQuality = qualityLevels[promise.response];
|
||||
api.setPlaybackQualityRange(newQuality);
|
||||
api.setPlaybackQuality(newQuality)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ const mpris = require("mpris-service");
|
||||
const { ipcMain } = require("electron");
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const getSongControls = require("../../providers/song-controls");
|
||||
const config = require("../../config");
|
||||
|
||||
function setupMPRIS() {
|
||||
const player = mpris({
|
||||
@ -19,7 +20,7 @@ function setupMPRIS() {
|
||||
|
||||
function registerMPRIS(win) {
|
||||
const songControls = getSongControls(win);
|
||||
const { playPause, next, previous } = songControls;
|
||||
const { playPause, next, previous, volumeMinus10, volumePlus10 } = songControls;
|
||||
try {
|
||||
const secToMicro = n => Math.round(Number(n) * 1e6);
|
||||
const microToSec = n => Math.round(Number(n) / 1e6);
|
||||
@ -34,6 +35,35 @@ function registerMPRIS(win) {
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||
|
||||
let currentLoopStatus = undefined;
|
||||
let manuallySwitchingStatus = false;
|
||||
ipcMain.on("repeatChanged", (_, mode) => {
|
||||
if (manuallySwitchingStatus)
|
||||
return;
|
||||
|
||||
if (mode === "Repeat off")
|
||||
currentLoopStatus = "None";
|
||||
else if (mode === "Repeat one")
|
||||
currentLoopStatus = "Track";
|
||||
else if (mode === "Repeat all")
|
||||
currentLoopStatus = "Playlist";
|
||||
|
||||
player.loopStatus = currentLoopStatus;
|
||||
});
|
||||
player.on("loopStatus", (status) => {
|
||||
// switchRepeat cycles between states in that order
|
||||
const switches = ["None", "Playlist", "Track"];
|
||||
const currentIndex = switches.indexOf(currentLoopStatus);
|
||||
const targetIndex = switches.indexOf(status);
|
||||
|
||||
// Get a delta in the range [0,2]
|
||||
const delta = (targetIndex - currentIndex + 3) % 3;
|
||||
|
||||
manuallySwitchingStatus = true;
|
||||
songControls.switchRepeat(delta);
|
||||
manuallySwitchingStatus = false;
|
||||
})
|
||||
|
||||
player.getPosition = () => secToMicro(currentSeconds)
|
||||
|
||||
player.on("raise", () => {
|
||||
@ -53,21 +83,45 @@ function registerMPRIS(win) {
|
||||
playPause()
|
||||
}
|
||||
});
|
||||
player.on("playpause", () => {
|
||||
player.playbackStatus = player.playbackStatus === 'Playing' ? "Paused" : "Playing";
|
||||
playPause();
|
||||
});
|
||||
|
||||
player.on("playpause", playPause);
|
||||
player.on("next", next);
|
||||
player.on("previous", previous);
|
||||
|
||||
player.on('seek', seekBy);
|
||||
player.on('position', seekTo);
|
||||
|
||||
registerCallback(songInfo => {
|
||||
if (player) {
|
||||
const data = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
'mpris:artUrl': songInfo.imageSrc,
|
||||
'xesam:title': songInfo.title,
|
||||
'xesam:artist': [songInfo.artist],
|
||||
ipcMain.on('volumeChanged', (_, value) => {
|
||||
player.volume = value;
|
||||
});
|
||||
player.on('volume', (newVolume) => {
|
||||
if (config.plugins.isEnabled('precise-volume')) {
|
||||
// With precise volume we can set the volume to the exact value.
|
||||
win.webContents.send('setVolume', newVolume)
|
||||
} else {
|
||||
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
|
||||
const deltaVolume = Math.round((newVolume - player.volume) / 10);
|
||||
|
||||
if (deltaVolume > 0) {
|
||||
for (let i = 0; i < deltaVolume; i++)
|
||||
volumePlus10();
|
||||
} else {
|
||||
for (let i = 0; i < -deltaVolume; i++)
|
||||
volumeMinus10();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerCallback(songInfo => {
|
||||
if (player) {
|
||||
const data = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
'mpris:artUrl': songInfo.imageSrc,
|
||||
'xesam:title': songInfo.title,
|
||||
'xesam:artist': [songInfo.artist],
|
||||
'mpris:trackid': '/'
|
||||
};
|
||||
if (songInfo.album) data['xesam:album'] = songInfo.album;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
const { sortSegments } = require("../segments");
|
||||
|
||||
test("Segment sorting", () => {
|
||||
|
||||
@ -32,9 +32,16 @@ module.exports.listenAction = (channel, callback) => {
|
||||
return ipcMain.on(channel, callback);
|
||||
};
|
||||
|
||||
module.exports.fileExists = (path, callbackIfExists) => {
|
||||
module.exports.fileExists = (
|
||||
path,
|
||||
callbackIfExists,
|
||||
callbackIfError = undefined
|
||||
) => {
|
||||
fs.access(path, fs.F_OK, (err) => {
|
||||
if (err) {
|
||||
if (callbackIfError) {
|
||||
callbackIfError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
41
preload.js
41
preload.js
@ -1,25 +1,34 @@
|
||||
const path = require("path");
|
||||
|
||||
const remote = require('@electron/remote');
|
||||
|
||||
require("./providers/front-logger")();
|
||||
const config = require("./config");
|
||||
const { fileExists } = require("./plugins/utils");
|
||||
const setupFrontLogger = require("./providers/front-logger");
|
||||
const setupSongInfo = require("./providers/song-info-front");
|
||||
const { setupSongControls } = require("./providers/song-controls-front");
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const plugins = config.plugins.getEnabled();
|
||||
|
||||
let api;
|
||||
|
||||
plugins.forEach(([plugin, options]) => {
|
||||
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
|
||||
plugins.forEach(async ([plugin, options]) => {
|
||||
const preloadPath = await ipcRenderer.invoke(
|
||||
"getPath",
|
||||
__dirname,
|
||||
"plugins",
|
||||
plugin,
|
||||
"preload.js"
|
||||
);
|
||||
fileExists(preloadPath, () => {
|
||||
const run = require(preloadPath);
|
||||
run(options);
|
||||
});
|
||||
|
||||
const actionPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
||||
const actionPath = await ipcRenderer.invoke(
|
||||
"getPath",
|
||||
__dirname,
|
||||
"plugins",
|
||||
plugin,
|
||||
"actions.js"
|
||||
);
|
||||
fileExists(actionPath, () => {
|
||||
const actions = require(actionPath).actions || {};
|
||||
|
||||
@ -32,8 +41,14 @@ plugins.forEach(([plugin, options]) => {
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
plugins.forEach(([plugin, options]) => {
|
||||
const pluginPath = path.join(__dirname, "plugins", plugin, "front.js");
|
||||
plugins.forEach(async ([plugin, options]) => {
|
||||
const pluginPath = await ipcRenderer.invoke(
|
||||
"getPath",
|
||||
__dirname,
|
||||
"plugins",
|
||||
plugin,
|
||||
"front.js"
|
||||
);
|
||||
fileExists(pluginPath, () => {
|
||||
const run = require(pluginPath);
|
||||
run(options);
|
||||
@ -49,12 +64,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// inject song-controls
|
||||
setupSongControls();
|
||||
|
||||
// inject front logger
|
||||
setupFrontLogger();
|
||||
|
||||
// Add action for reloading
|
||||
global.reload = () =>
|
||||
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
||||
global.reload = () => ipcRenderer.send('reload');
|
||||
|
||||
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||
setInterval(() => window._lact = Date.now(), 900000);
|
||||
|
||||
@ -1,6 +1,23 @@
|
||||
const app = require("electron").app || require('@electron/remote').app;
|
||||
const path = require("path");
|
||||
|
||||
const is = require("electron-is");
|
||||
|
||||
const { app, BrowserWindow, ipcMain, ipcRenderer } = require("electron");
|
||||
const config = require("../config");
|
||||
|
||||
module.exports.restart = () => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
is.main() ? restart() : ipcRenderer.send('restart');
|
||||
};
|
||||
|
||||
module.exports.setupAppControls = () => {
|
||||
ipcMain.on('restart', restart);
|
||||
ipcMain.handle('getDownloadsFolder', () => app.getPath("downloads"));
|
||||
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get("url")));
|
||||
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
|
||||
}
|
||||
|
||||
function restart() {
|
||||
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
||||
// execPath will be undefined if not running portable app, resulting in default behavior
|
||||
app.quit();
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ module.exports = (win) => {
|
||||
// Playback
|
||||
previous: () => pressKey(win, "k"),
|
||||
next: () => pressKey(win, "j"),
|
||||
playPause: () => pressKey(win, "space"),
|
||||
playPause: () => pressKey(win, ";"),
|
||||
like: () => pressKey(win, "+"),
|
||||
dislike: () => pressKey(win, "_"),
|
||||
go10sBack: () => pressKey(win, "h"),
|
||||
@ -20,7 +20,10 @@ module.exports = (win) => {
|
||||
go1sBack: () => pressKey(win, "h", ["shift"]),
|
||||
go1sForward: () => pressKey(win, "l", ["shift"]),
|
||||
shuffle: () => pressKey(win, "s"),
|
||||
switchRepeat: () => pressKey(win, "r"),
|
||||
switchRepeat: (n = 1) => {
|
||||
for (let i = 0; i < n; i++)
|
||||
pressKey(win, "r");
|
||||
},
|
||||
// General
|
||||
volumeMinus10: () => pressKey(win, "-"),
|
||||
volumePlus10: () => pressKey(win, "="),
|
||||
|
||||
@ -22,6 +22,8 @@ module.exports = () => {
|
||||
if (config.plugins.isEnabled('tuna-obs') ||
|
||||
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
||||
setupTimeChangeListener();
|
||||
setupRepeatChangeListener();
|
||||
setupVolumeChangeListener(apiEvent.detail);
|
||||
}
|
||||
const video = $('video');
|
||||
// name = "dataloaded" and abit later "dataupdated"
|
||||
@ -63,3 +65,21 @@ function setupTimeChangeListener() {
|
||||
});
|
||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
|
||||
}
|
||||
|
||||
function setupRepeatChangeListener() {
|
||||
const repeatObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('repeatChanged', mutations[0].target.title);
|
||||
});
|
||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('repeatChanged', $('#right-controls .repeat').title);
|
||||
}
|
||||
|
||||
function setupVolumeChangeListener(api) {
|
||||
$('video').addEventListener('volumechange', (_) => {
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
}
|
||||
|
||||
12
readme.md
12
readme.md
@ -33,6 +33,10 @@ You can check out the [latest release](https://github.com/th-ch/youtube-music/re
|
||||
|
||||
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### MacOS
|
||||
|
||||
If you get an error "is damaged and can’t be opened." when launching the app, run `xattr -cr /Applications/YouTube\ Music.app` in Terminal.
|
||||
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
@ -85,7 +89,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
||||
|
||||
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Video Toggle**: Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab
|
||||
- **Video Toggle**: Adds a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to switch between Video/Song mode. can also optionally remove the whole video tab
|
||||
|
||||
---
|
||||
|
||||
@ -93,6 +97,12 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
|
||||
|
||||
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
|
||||
|
||||
## Themes
|
||||
|
||||
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
|
||||
|
||||
Some predefined themes are available in https://github.com/OceanicSquirrel/themes-for-ytmdesktop-player.
|
||||
|
||||
## Dev
|
||||
|
||||
```sh
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const NodeEnvironment = require("jest-environment-node");
|
||||
const { _electron: electron } = require("playwright");
|
||||
|
||||
class TestEnvironment extends NodeEnvironment {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await super.setup();
|
||||
|
||||
const appPath = path.resolve(__dirname, "..");
|
||||
this.global.__APP__ = await electron.launch({ args: [appPath] });
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
if (this.global.__APP__) {
|
||||
await this.global.__APP__.close();
|
||||
}
|
||||
await super.teardown();
|
||||
}
|
||||
|
||||
runScript(script) {
|
||||
return super.runScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestEnvironment;
|
||||
@ -1,16 +1,29 @@
|
||||
/**
|
||||
* @jest-environment ./tests/environment
|
||||
*/
|
||||
const path = require("path");
|
||||
|
||||
describe("YouTube Music App", () => {
|
||||
const app = global.__APP__;
|
||||
const { _electron: electron } = require("playwright");
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
test("With default settings, app is launched and visible", async () => {
|
||||
const window = await app.firstWindow();
|
||||
const title = await window.title();
|
||||
expect(title).toEqual("YouTube Music");
|
||||
process.env.NODE_ENV = "test";
|
||||
|
||||
const url = window.url();
|
||||
expect(url.startsWith("https://music.youtube.com")).toBe(true);
|
||||
const appPath = path.resolve(__dirname, "..");
|
||||
|
||||
test("YouTube Music App - With default settings, app is launched and visible", async () => {
|
||||
const app = await electron.launch({
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-gpu",
|
||||
"--whitelisted-ips=",
|
||||
"--disable-dev-shm-usage",
|
||||
appPath,
|
||||
],
|
||||
});
|
||||
|
||||
const window = await app.firstWindow();
|
||||
const title = await window.title();
|
||||
expect(title).toEqual("YouTube Music");
|
||||
|
||||
const url = window.url();
|
||||
expect(url.startsWith("https://music.youtube.com")).toBe(true);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
6
tray.js
6
tray.js
@ -2,6 +2,7 @@ const path = require("path");
|
||||
|
||||
const { app, Menu, nativeImage, Tray } = require("electron");
|
||||
|
||||
const { restart } = require("./providers/app-controls");
|
||||
const config = require("./config");
|
||||
const getSongControls = require("./providers/song-controls");
|
||||
|
||||
@ -65,10 +66,7 @@ module.exports.setUpTray = (app, win) => {
|
||||
},
|
||||
{
|
||||
label: "Restart App",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
click: restart
|
||||
},
|
||||
{ role: "quit" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user