Merge branch 'local-upstream/master' into new-downloader
4
.github/workflows/build.yml
vendored
@ -1,7 +1,9 @@
|
|||||||
name: Build YouTube Music
|
name: Build YouTube Music
|
||||||
|
|
||||||
on:
|
on:
|
||||||
- push
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: "16.x"
|
NODE_VERSION: "16.x"
|
||||||
|
|||||||
26
.github/workflows/winget-submission.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
name: Submit to Windows Package Manager Community Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "Specific tag name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
winget:
|
||||||
|
name: Publish winget package
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Submit package to Windows Package Manager Community Repository
|
||||||
|
uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
|
with:
|
||||||
|
identifier: th-ch.YouTubeMusic
|
||||||
|
installers-regex: '^YouTube-Music-Setup-[\d\.]+\.exe$'
|
||||||
|
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||||
|
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||||
|
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||||
|
fork-user: youtube-music-winget
|
||||||
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 192 B After Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B |
@ -55,8 +55,13 @@ const defaultConfig = {
|
|||||||
notifications: {
|
notifications: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
unpauseNotification: false,
|
unpauseNotification: false,
|
||||||
urgency: "normal", //has effect only on Linux
|
urgency: "normal", //has effect only on Linux
|
||||||
interactive: false //has effect only on Windows
|
// the following has effect only on Windows
|
||||||
|
interactive: true,
|
||||||
|
toastStyle: 1, // see plugins/notifications/utils for more info
|
||||||
|
refreshOnPlayPause: false,
|
||||||
|
trayControls: true,
|
||||||
|
hideButtonText: false
|
||||||
},
|
},
|
||||||
"precise-volume": {
|
"precise-volume": {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
139
config/dynamic.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const { ipcRenderer, ipcMain } = require("electron");
|
||||||
|
|
||||||
|
const defaultConfig = require("./defaults");
|
||||||
|
const { getOptions, setOptions, setMenuOptions } = require("./plugins");
|
||||||
|
|
||||||
|
const activePlugins = {};
|
||||||
|
/**
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The method is **sync** in the main process and **async** in the renderer process.
|
||||||
|
*/
|
||||||
|
module.exports.getActivePlugins =
|
||||||
|
process.type === "renderer"
|
||||||
|
? async () => ipcRenderer.invoke("get-active-plugins")
|
||||||
|
: () => activePlugins;
|
||||||
|
|
||||||
|
if (process.type === "browser") {
|
||||||
|
ipcMain.handle("get-active-plugins", this.getActivePlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The method is **sync** in the main process and **async** in the renderer process.
|
||||||
|
*/
|
||||||
|
module.exports.isActive =
|
||||||
|
process.type === "renderer"
|
||||||
|
? async (plugin) =>
|
||||||
|
plugin in (await ipcRenderer.invoke("get-active-plugins"))
|
||||||
|
: (plugin) => plugin in activePlugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create a dynamic synced config for plugins.
|
||||||
|
*
|
||||||
|
* [!IMPORTANT!]
|
||||||
|
* The methods are **sync** in the main process and **async** in the renderer process.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the plugin.
|
||||||
|
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
|
||||||
|
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { PluginConfig } = require("../../config/dynamic");
|
||||||
|
* const config = new PluginConfig("plugin-name", { enableFront: true });
|
||||||
|
* module.exports = { ...config };
|
||||||
|
*
|
||||||
|
* // or
|
||||||
|
*
|
||||||
|
* module.exports = (win, options) => {
|
||||||
|
* const config = new PluginConfig("plugin-name", {
|
||||||
|
* enableFront: true,
|
||||||
|
* initialOptions: options,
|
||||||
|
* });
|
||||||
|
* setupMyPlugin(win, config);
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
module.exports.PluginConfig = class PluginConfig {
|
||||||
|
#name;
|
||||||
|
#config;
|
||||||
|
#defaultConfig;
|
||||||
|
#enableFront;
|
||||||
|
|
||||||
|
constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
|
||||||
|
const pluginDefaultConfig = defaultConfig.plugins[name] || {};
|
||||||
|
const pluginConfig = initialOptions || getOptions(name) || {};
|
||||||
|
|
||||||
|
this.#name = name;
|
||||||
|
this.#enableFront = enableFront;
|
||||||
|
this.#defaultConfig = pluginDefaultConfig;
|
||||||
|
this.#config = { ...pluginDefaultConfig, ...pluginConfig };
|
||||||
|
|
||||||
|
if (this.#enableFront) {
|
||||||
|
this.#setupFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
activePlugins[name] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get = (option) => {
|
||||||
|
return this.#config[option];
|
||||||
|
};
|
||||||
|
|
||||||
|
set = (option, value) => {
|
||||||
|
this.#config[option] = value;
|
||||||
|
this.#save();
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle = (option) => {
|
||||||
|
this.#config[option] = !this.#config[option];
|
||||||
|
this.#save();
|
||||||
|
};
|
||||||
|
|
||||||
|
getAll = () => {
|
||||||
|
return { ...this.#config };
|
||||||
|
};
|
||||||
|
|
||||||
|
setAll = (options) => {
|
||||||
|
this.#config = { ...this.#config, ...options };
|
||||||
|
this.#save();
|
||||||
|
};
|
||||||
|
|
||||||
|
getDefaultConfig = () => {
|
||||||
|
return this.#defaultConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
|
||||||
|
*
|
||||||
|
* Used for options that require a restart to take effect.
|
||||||
|
*/
|
||||||
|
setAndMaybeRestart = (option, value) => {
|
||||||
|
this.#config[option] = value;
|
||||||
|
setMenuOptions(this.#name, this.#config);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Called only from back */
|
||||||
|
#save() {
|
||||||
|
setOptions(this.#name, this.#config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupFront() {
|
||||||
|
if (process.type === "renderer") {
|
||||||
|
for (const [fnName, fn] of Object.entries(this)) {
|
||||||
|
if (typeof fn !== "function") return;
|
||||||
|
this[fnName] = async (...args) => {
|
||||||
|
return await ipcRenderer.invoke(
|
||||||
|
`${this.name}-config-${fnName}`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (process.type === "browser") {
|
||||||
|
for (const [fnName, fn] of Object.entries(this)) {
|
||||||
|
if (typeof fn !== "function") return;
|
||||||
|
ipcMain.handle(`${this.name}-config-${fnName}`, (_, ...args) => {
|
||||||
|
return fn(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -11,6 +11,14 @@ const setDefaultPluginOptions = (store, plugin) => {
|
|||||||
const migrations = {
|
const migrations = {
|
||||||
">=1.20.0": (store) => {
|
">=1.20.0": (store) => {
|
||||||
setDefaultPluginOptions(store, "visualizer");
|
setDefaultPluginOptions(store, "visualizer");
|
||||||
|
|
||||||
|
if (store.get("plugins.notifications.toastStyle") === undefined) {
|
||||||
|
const pluginOptions = store.get("plugins.notifications") || {};
|
||||||
|
store.set("plugins.notifications", {
|
||||||
|
...defaults.plugins.notifications,
|
||||||
|
...pluginOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
">=1.17.0": (store) => {
|
">=1.17.0": (store) => {
|
||||||
setDefaultPluginOptions(store, "picture-in-picture");
|
setDefaultPluginOptions(store, "picture-in-picture");
|
||||||
|
|||||||
38
index.js
@ -14,6 +14,7 @@ const { isTesting } = require("./utils/testing");
|
|||||||
const { setUpTray } = require("./tray");
|
const { setUpTray } = require("./tray");
|
||||||
const { setupSongInfo } = require("./providers/song-info");
|
const { setupSongInfo } = require("./providers/song-info");
|
||||||
const { setupAppControls, restart } = require("./providers/app-controls");
|
const { setupAppControls, restart } = require("./providers/app-controls");
|
||||||
|
const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require("./providers/protocol-handler");
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -29,17 +30,9 @@ const app = electron.app;
|
|||||||
let mainWindow;
|
let mainWindow;
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
|
|
||||||
if(config.get("options.singleInstanceLock")){
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
if (!gotTheLock) app.quit();
|
|
||||||
|
|
||||||
app.on('second-instance', () => {
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!mainWindow) return;
|
if (!gotTheLock) app.exit();
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
||||||
if (!mainWindow.isVisible()) mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
|
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
|
||||||
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
||||||
@ -77,6 +70,7 @@ function onClosed() {
|
|||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Electron.BrowserWindow} win */
|
||||||
function loadPlugins(win) {
|
function loadPlugins(win) {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||||
// Load user CSS
|
// Load user CSS
|
||||||
@ -347,9 +341,6 @@ app.on("ready", () => {
|
|||||||
|
|
||||||
// Register appID on windows
|
// Register appID on windows
|
||||||
if (is.windows()) {
|
if (is.windows()) {
|
||||||
// Depends on SnoreToast version https://github.com/KDE/snoretoast/blob/master/CMakeLists.txt#L5
|
|
||||||
const toastActivatorClsid = "eb1fdd5b-8f70-4b5a-b230-998a2dc19303";
|
|
||||||
|
|
||||||
const appID = "com.github.th-ch.youtube-music";
|
const appID = "com.github.th-ch.youtube-music";
|
||||||
app.setAppUserModelId(appID);
|
app.setAppUserModelId(appID);
|
||||||
const appLocation = process.execPath;
|
const appLocation = process.execPath;
|
||||||
@ -361,8 +352,7 @@ app.on("ready", () => {
|
|||||||
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
|
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
|
||||||
if (
|
if (
|
||||||
shortcutDetails.target !== appLocation ||
|
shortcutDetails.target !== appLocation ||
|
||||||
shortcutDetails.appUserModelId !== appID ||
|
shortcutDetails.appUserModelId !== appID
|
||||||
shortcutDetails.toastActivatorClsid !== toastActivatorClsid
|
|
||||||
) {
|
) {
|
||||||
throw "needUpdate";
|
throw "needUpdate";
|
||||||
}
|
}
|
||||||
@ -375,7 +365,6 @@ app.on("ready", () => {
|
|||||||
cwd: path.dirname(appLocation),
|
cwd: path.dirname(appLocation),
|
||||||
description: "YouTube Music Desktop App - including custom plugins",
|
description: "YouTube Music Desktop App - including custom plugins",
|
||||||
appUserModelId: appID,
|
appUserModelId: appID,
|
||||||
toastActivatorClsid
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -386,6 +375,23 @@ app.on("ready", () => {
|
|||||||
setApplicationMenu(mainWindow);
|
setApplicationMenu(mainWindow);
|
||||||
setUpTray(app, mainWindow);
|
setUpTray(app, mainWindow);
|
||||||
|
|
||||||
|
setupProtocolHandler(mainWindow);
|
||||||
|
|
||||||
|
app.on('second-instance', (_event, commandLine, _workingDirectory) => {
|
||||||
|
const uri = `${APP_PROTOCOL}://`;
|
||||||
|
const protocolArgv = commandLine.find(arg => arg.startsWith(uri));
|
||||||
|
if (protocolArgv) {
|
||||||
|
const command = protocolArgv.slice(uri.length, -1);
|
||||||
|
if (is.dev()) console.debug(`Received command over protocol: "${command}"`);
|
||||||
|
handleProtocol(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mainWindow) return;
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
if (!mainWindow.isVisible()) mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
});
|
||||||
|
|
||||||
// Autostart at login
|
// Autostart at login
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
openAtLogin: config.get("options.startAtLogin"),
|
openAtLogin: config.get("options.startAtLogin"),
|
||||||
|
|||||||
12
menu.js
@ -133,14 +133,12 @@ const mainMenuTemplate = (win) => {
|
|||||||
{
|
{
|
||||||
label: "Single instance lock",
|
label: "Single instance lock",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: config.get("options.singleInstanceLock"),
|
checked: false,
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
config.setMenuOption("options.singleInstanceLock", item.checked);
|
if (item.checked && app.hasSingleInstanceLock())
|
||||||
if (item.checked && !app.hasSingleInstanceLock()) {
|
|
||||||
app.requestSingleInstanceLock();
|
|
||||||
} else if (!item.checked && app.hasSingleInstanceLock()) {
|
|
||||||
app.releaseSingleInstanceLock();
|
app.releaseSingleInstanceLock();
|
||||||
}
|
else if (!item.checked && !app.hasSingleInstanceLock())
|
||||||
|
app.requestSingleInstanceLock();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -163,7 +161,7 @@ const mainMenuTemplate = (win) => {
|
|||||||
if (item.checked && !config.get("options.hideMenuWarned")) {
|
if (item.checked && !config.get("options.hideMenuWarned")) {
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info', title: 'Hide Menu Enabled',
|
type: 'info', title: 'Hide Menu Enabled',
|
||||||
message: "Menu will be hidden on next launch, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
|
message: "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
17
package.json
@ -87,11 +87,11 @@
|
|||||||
"generate:package": "node utils/generate-package-json.js",
|
"generate:package": "node utils/generate-package-json.js",
|
||||||
"postinstall": "yarn run icon && yarn run plugins",
|
"postinstall": "yarn run icon && yarn run plugins",
|
||||||
"clean": "del-cli dist",
|
"clean": "del-cli dist",
|
||||||
"build": "yarn run clean && electron-builder --win --mac --linux",
|
"build": "yarn run clean && electron-builder --win --mac --linux -p never",
|
||||||
"build:linux": "yarn run clean && electron-builder --linux",
|
"build:linux": "yarn run clean && electron-builder --linux -p never",
|
||||||
"build:mac": "yarn run clean && electron-builder --mac dmg:x64",
|
"build:mac": "yarn run clean && electron-builder --mac dmg:x64 -p never",
|
||||||
"build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64",
|
"build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64 -p never",
|
||||||
"build:win": "yarn run clean && electron-builder --win",
|
"build:win": "yarn run clean && electron-builder --win -p never",
|
||||||
"lint": "xo",
|
"lint": "xo",
|
||||||
"changelog": "auto-changelog",
|
"changelog": "auto-changelog",
|
||||||
"plugins": "yarn run plugin:adblocker && yarn run plugin:bypass-age-restrictions",
|
"plugins": "yarn run plugin:adblocker && yarn run plugin:bypass-age-restrictions",
|
||||||
@ -115,8 +115,8 @@
|
|||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"butterchurn": "^2.6.7",
|
"butterchurn": "^2.6.7",
|
||||||
"butterchurn-presets": "^2.4.7",
|
"butterchurn-presets": "^2.4.7",
|
||||||
"custom-electron-prompt": "^1.5.1",
|
"custom-electron-prompt": "^1.5.4",
|
||||||
"custom-electron-titlebar": "^4.1.5",
|
"custom-electron-titlebar": "^4.1.6",
|
||||||
"electron-better-web-request": "^1.0.1",
|
"electron-better-web-request": "^1.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
@ -126,13 +126,12 @@
|
|||||||
"electron-updater": "^5.3.0",
|
"electron-updater": "^5.3.0",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"howler": "^2.2.3",
|
"howler": "^2.2.3",
|
||||||
"html-to-text": "^9.0.3",
|
"html-to-text": "^9.0.4",
|
||||||
"keyboardevent-from-electron-accelerator": "^2.0.0",
|
"keyboardevent-from-electron-accelerator": "^2.0.0",
|
||||||
"keyboardevents-areequal": "^0.2.2",
|
"keyboardevents-areequal": "^0.2.2",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.8",
|
"node-fetch": "^2.6.8",
|
||||||
"node-notifier": "^10.0.1",
|
|
||||||
"simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4",
|
"simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4",
|
||||||
"vudio": "^2.1.1",
|
"vudio": "^2.1.1",
|
||||||
"youtubei.js": "^3.1.1",
|
"youtubei.js": "^3.1.1",
|
||||||
|
|||||||
@ -11,28 +11,32 @@ module.exports = (options) => {
|
|||||||
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
|
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If captions are disabled by default,
|
|
||||||
* unload "captions" module when video changes.
|
|
||||||
*/
|
|
||||||
const videoChanged = (api, options) => {
|
|
||||||
if (options.disableCaptions) {
|
|
||||||
setTimeout(() => api.unloadModule("captions"), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup(event, options) {
|
function setup(event, options) {
|
||||||
const api = event.detail;
|
const api = event.detail;
|
||||||
|
|
||||||
$("video").addEventListener("srcChanged", () => videoChanged(api, options));
|
|
||||||
|
|
||||||
$(".right-controls-buttons").append(captionsSettingsButton);
|
$(".right-controls-buttons").append(captionsSettingsButton);
|
||||||
|
|
||||||
|
let captionTrackList = api.getOption("captions", "tracklist");
|
||||||
|
|
||||||
|
$("video").addEventListener("srcChanged", () => {
|
||||||
|
if (options.disableCaptions) {
|
||||||
|
setTimeout(() => api.unloadModule("captions"), 100);
|
||||||
|
captionsSettingsButton.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.loadModule("captions");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
captionTrackList = api.getOption("captions", "tracklist");
|
||||||
|
|
||||||
|
captionsSettingsButton.style.display = captionTrackList?.length
|
||||||
|
? "inline-block"
|
||||||
|
: "none";
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
|
||||||
captionsSettingsButton.onclick = async () => {
|
captionsSettingsButton.onclick = async () => {
|
||||||
api.loadModule("captions");
|
|
||||||
|
|
||||||
const captionTrackList = api.getOption("captions", "tracklist");
|
|
||||||
|
|
||||||
if (captionTrackList?.length) {
|
if (captionTrackList?.length) {
|
||||||
const currentCaptionTrack = api.getOption("captions", "track");
|
const currentCaptionTrack = api.getOption("captions", "track");
|
||||||
let currentIndex = !currentCaptionTrack ?
|
let currentIndex = !currentCaptionTrack ?
|
||||||
|
|||||||
@ -2,14 +2,12 @@ const path = require("path");
|
|||||||
|
|
||||||
const electronLocalshortcut = require("electron-localshortcut");
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
const config = require("../../config");
|
|
||||||
const { injectCSS } = require("../utils");
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
||||||
setupTitlebar();
|
setupTitlebar();
|
||||||
|
|
||||||
//tracks menu visibility
|
//tracks menu visibility
|
||||||
let visible = !config.get("options.hideMenu");
|
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win) => {
|
||||||
// css for custom scrollbar + disable drag area(was causing bugs)
|
// css for custom scrollbar + disable drag area(was causing bugs)
|
||||||
@ -18,16 +16,8 @@ module.exports = (win) => {
|
|||||||
win.once("ready-to-show", () => {
|
win.once("ready-to-show", () => {
|
||||||
attachTitlebarToWindow(win);
|
attachTitlebarToWindow(win);
|
||||||
|
|
||||||
//register keyboard shortcut && hide menu if hideMenu is enabled
|
electronLocalshortcut.register(win, "`", () => {
|
||||||
if (config.get("options.hideMenu")) {
|
win.webContents.send("toggleMenu");
|
||||||
electronLocalshortcut.register(win, "Esc", () => {
|
});
|
||||||
setMenuVisibility(!visible);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setMenuVisibility(value) {
|
|
||||||
visible = value;
|
|
||||||
win.webContents.send("refreshMenu", visible);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,28 +5,31 @@ const { isEnabled } = require("../../config/plugins");
|
|||||||
function $(selector) { return document.querySelector(selector); }
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
module.exports = (options) => {
|
module.exports = (options) => {
|
||||||
let visible = !config.get("options.hideMenu");
|
let visible = () => !!$('.cet-menubar').firstChild;
|
||||||
const bar = new Titlebar({
|
const bar = new Titlebar({
|
||||||
|
icon: "https://cdn-icons-png.flaticon.com/512/5358/5358672.png",
|
||||||
backgroundColor: Color.fromHex("#050505"),
|
backgroundColor: Color.fromHex("#050505"),
|
||||||
itemBackgroundColor: Color.fromHex("#1d1d1d"),
|
itemBackgroundColor: Color.fromHex("#1d1d1d"),
|
||||||
svgColor: Color.WHITE,
|
svgColor: Color.WHITE,
|
||||||
menu: visible ? undefined : null
|
menu: config.get("options.hideMenu") ? null : undefined
|
||||||
});
|
});
|
||||||
bar.updateTitle(" ");
|
bar.updateTitle(" ");
|
||||||
document.title = "Youtube Music";
|
document.title = "Youtube Music";
|
||||||
|
|
||||||
const hideIcon = hide => $('.cet-window-icon').style.display = hide ? 'none' : 'flex';
|
const toggleMenu = () => {
|
||||||
|
if (visible()) {
|
||||||
if (options.hideIcon) hideIcon(true);
|
|
||||||
|
|
||||||
ipcRenderer.on("refreshMenu", (_, showMenu) => {
|
|
||||||
if (showMenu === undefined && !visible) return;
|
|
||||||
if (showMenu === false) {
|
|
||||||
bar.updateMenu(null);
|
bar.updateMenu(null);
|
||||||
visible = false;
|
|
||||||
} else {
|
} else {
|
||||||
bar.refreshMenu();
|
bar.refreshMenu();
|
||||||
visible = true;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.cet-window-icon').addEventListener('click', toggleMenu);
|
||||||
|
ipcRenderer.on("toggleMenu", toggleMenu);
|
||||||
|
|
||||||
|
ipcRenderer.on("refreshMenu", () => {
|
||||||
|
if (visible()) {
|
||||||
|
bar.refreshMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -36,14 +39,13 @@ module.exports = (options) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||||
document.addEventListener('apiLoaded', () => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
setNavbarMargin();
|
setNavbarMargin();
|
||||||
const playPageObserver = new MutationObserver(setNavbarMargin);
|
const playPageObserver = new MutationObserver(setNavbarMargin);
|
||||||
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
|
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
|
||||||
setupSearchOpenObserver();
|
setupSearchOpenObserver();
|
||||||
|
setupMenuOpenObserver();
|
||||||
}, { once: true, passive: true })
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,6 +57,15 @@ function setupSearchOpenObserver() {
|
|||||||
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ["opened"] })
|
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ["opened"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupMenuOpenObserver() {
|
||||||
|
const menuOpenObserver = new MutationObserver(mutations => {
|
||||||
|
$('#nav-bar-background').style.webkitAppRegion =
|
||||||
|
Array.from($('.cet-menubar').childNodes).some(c => c.classList.contains('open')) ?
|
||||||
|
'no-drag' : 'drag';
|
||||||
|
});
|
||||||
|
menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ["class"] })
|
||||||
|
}
|
||||||
|
|
||||||
function setNavbarMargin() {
|
function setNavbarMargin() {
|
||||||
$('#nav-bar-background').style.right =
|
$('#nav-bar-background').style.right =
|
||||||
$('ytmusic-app-layout').playerPageOpen_ ?
|
$('ytmusic-app-layout').playerPageOpen_ ?
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
const { setOptions } = require("../../config/plugins");
|
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
|
||||||
{
|
|
||||||
label: "Hide Icon",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: options.hideIcon,
|
|
||||||
click: (item) => {
|
|
||||||
win.webContents.send("hideIcon", item.checked);
|
|
||||||
options.hideIcon = item.checked;
|
|
||||||
setOptions("in-app-menu", options);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -12,11 +12,17 @@
|
|||||||
height: 75px !important;
|
height: 75px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fixes top gap between nav-bar and browse-page */
|
/* fix top gap between nav-bar and browse-page */
|
||||||
#browse-page {
|
#browse-page {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fix navbar hiding library items */
|
||||||
|
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"] {
|
||||||
|
top: 50px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||||
ytmusic-nav-bar,
|
ytmusic-nav-bar,
|
||||||
.tab-titleiron-icon,
|
.tab-titleiron-icon,
|
||||||
@ -46,7 +52,7 @@ yt-page-navigation-progress,
|
|||||||
top: 30px !important;
|
top: 30px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
background-color: #030303;
|
background-color: #030303;
|
||||||
@ -59,7 +65,7 @@ yt-page-navigation-progress,
|
|||||||
background-color: rgba(15, 15, 15, 0.699);
|
background-color: rgba(15, 15, 15, 0.699);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */
|
||||||
::-webkit-scrollbar-thumb:vertical {
|
::-webkit-scrollbar-thumb:vertical {
|
||||||
border: 2px solid rgba(0, 0, 0, 0);
|
border: 2px solid rgba(0, 0, 0, 0);
|
||||||
|
|
||||||
@ -70,7 +76,7 @@ yt-page-navigation-progress,
|
|||||||
-webkit-border-radius: 100px;
|
-webkit-border-radius: 100px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:vertical:active {
|
::-webkit-scrollbar-thumb:vertical:active {
|
||||||
background: #4d4c4c; /* Some darker color when you click it */
|
background: #4d4c4c; /* some darker color when you click it */
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
-moz-border-radius: 100px;
|
-moz-border-radius: 100px;
|
||||||
-webkit-border-radius: 100px;
|
-webkit-border-radius: 100px;
|
||||||
@ -80,6 +86,17 @@ yt-page-navigation-progress,
|
|||||||
background-color: inherit
|
background-color: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** hideMenu toggler **/
|
||||||
|
.cet-window-icon {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cet-window-icon img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
filter: invert(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** make navbar draggable **/
|
||||||
#nav-bar-background {
|
#nav-bar-background {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,13 @@ const fetch = require("node-fetch");
|
|||||||
|
|
||||||
const { cleanupName } = require("../../providers/song-info");
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
const { injectCSS } = require("../utils");
|
const { injectCSS } = require("../utils");
|
||||||
|
let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||||
|
let revRomanized = false;
|
||||||
|
|
||||||
module.exports = async (win) => {
|
module.exports = async (win, options) => {
|
||||||
|
if(options.romanizedLyrics) {
|
||||||
|
revRomanized = true;
|
||||||
|
}
|
||||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
|
|
||||||
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
||||||
@ -17,17 +22,51 @@ module.exports = async (win) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleRomanized = () => {
|
||||||
|
revRomanized = !revRomanized;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchFromGenius = async (metadata) => {
|
const fetchFromGenius = async (metadata) => {
|
||||||
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
|
const songTitle = `${cleanupName(metadata.title)}`;
|
||||||
metadata.title
|
const songArtist = `${cleanupName(metadata.artist)}`;
|
||||||
)}`;
|
let lyrics;
|
||||||
|
|
||||||
|
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
|
||||||
|
Genius Lyrics behavior is observed.
|
||||||
|
*/
|
||||||
|
let hasAsianChars = false;
|
||||||
|
if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) {
|
||||||
|
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
|
||||||
|
hasAsianChars = true;
|
||||||
|
} else {
|
||||||
|
lyrics = await getLyricsList(`${songArtist} ${songTitle}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
|
||||||
|
for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
|
||||||
|
*/
|
||||||
|
if(revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) {
|
||||||
|
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
|
||||||
|
}
|
||||||
|
return lyrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song
|
||||||
|
* @param {*} queryString
|
||||||
|
* @returns The lyrics of the first song found using the Genius-Lyrics API
|
||||||
|
*/
|
||||||
|
const getLyricsList = async (queryString) => {
|
||||||
let response = await fetch(
|
let response = await fetch(
|
||||||
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(queryString)}`
|
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fetch the first URL with the api, giving a collection of song results.
|
||||||
|
Pick the first song, parsing the json given by the API.
|
||||||
|
*/
|
||||||
const info = await response.json();
|
const info = await response.json();
|
||||||
let url = "";
|
let url = "";
|
||||||
try {
|
try {
|
||||||
@ -36,16 +75,23 @@ const fetchFromGenius = async (metadata) => {
|
|||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
let lyrics = await getLyrics(url);
|
||||||
|
return lyrics;
|
||||||
|
}
|
||||||
|
|
||||||
if (is.dev()) {
|
/**
|
||||||
console.log("Fetching lyrics from Genius:", url);
|
*
|
||||||
}
|
* @param {*} url
|
||||||
|
* @returns The lyrics of the song URL provided, null if none
|
||||||
|
*/
|
||||||
|
const getLyrics = async (url) => {
|
||||||
response = await fetch(url);
|
response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("Fetching lyrics from Genius:", url);
|
||||||
|
}
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const lyrics = convert(html, {
|
const lyrics = convert(html, {
|
||||||
baseElements: {
|
baseElements: {
|
||||||
@ -64,8 +110,8 @@ const fetchFromGenius = async (metadata) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return lyrics;
|
return lyrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.fetchFromGenius = fetchFromGenius;
|
module.exports.toggleRomanized = toggleRomanized;
|
||||||
|
module.exports.fetchFromGenius = fetchFromGenius;
|
||||||
@ -2,7 +2,7 @@ const { ipcRenderer } = require("electron");
|
|||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
|
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => setTimeout(() => {
|
||||||
const tabList = document.querySelectorAll("tp-yt-paper-tab");
|
const tabList = document.querySelectorAll("tp-yt-paper-tab");
|
||||||
const tabs = {
|
const tabs = {
|
||||||
upNext: tabList[0],
|
upNext: tabList[0],
|
||||||
@ -90,5 +90,5 @@ module.exports = () => {
|
|||||||
tabs.lyrics.removeAttribute("disabled");
|
tabs.lyrics.removeAttribute("disabled");
|
||||||
tabs.lyrics.removeAttribute("aria-disabled");
|
tabs.lyrics.removeAttribute("aria-disabled");
|
||||||
}
|
}
|
||||||
});
|
}, 500));
|
||||||
};
|
};
|
||||||
|
|||||||
17
plugins/lyrics-genius/menu.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
const { toggleRomanized } = require("./back");
|
||||||
|
|
||||||
|
module.exports = (win, options, refreshMenu) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "Romanized Lyrics",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.romanizedLyrics,
|
||||||
|
click: (item) => {
|
||||||
|
options.romanizedLyrics = item.checked;
|
||||||
|
setOptions('lyrics-genius', options);
|
||||||
|
toggleRomanized();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -2,8 +2,9 @@ const { Notification } = require("electron");
|
|||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { notificationImage } = require("./utils");
|
const { notificationImage } = require("./utils");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
const notify = (info, options) => {
|
const notify = (info) => {
|
||||||
|
|
||||||
// Fill the notification with content
|
// Fill the notification with content
|
||||||
const notification = {
|
const notification = {
|
||||||
@ -11,7 +12,7 @@ const notify = (info, options) => {
|
|||||||
body: info.artist,
|
body: info.artist,
|
||||||
icon: notificationImage(info),
|
icon: notificationImage(info),
|
||||||
silent: true,
|
silent: true,
|
||||||
urgency: options.urgency,
|
urgency: config.get('urgency'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the notification
|
// Send the notification
|
||||||
@ -21,24 +22,25 @@ const notify = (info, options) => {
|
|||||||
return currentNotification;
|
return currentNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setup = (options) => {
|
const setup = () => {
|
||||||
let oldNotification;
|
let oldNotification;
|
||||||
let currentUrl;
|
let currentUrl;
|
||||||
|
|
||||||
registerCallback(songInfo => {
|
registerCallback(songInfo => {
|
||||||
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
|
if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) {
|
||||||
// Close the old notification
|
// Close the old notification
|
||||||
oldNotification?.close();
|
oldNotification?.close();
|
||||||
currentUrl = songInfo.url;
|
currentUrl = songInfo.url;
|
||||||
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
||||||
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
|
setTimeout(() => { oldNotification = notify(songInfo) }, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Electron.BrowserWindow} win */
|
||||||
module.exports = (win, options) => {
|
module.exports = (win, options) => {
|
||||||
// Register the callback for new song information
|
// Register the callback for new song information
|
||||||
is.windows() && options.interactive ?
|
is.windows() && options.interactive ?
|
||||||
require("./interactive")(win, options.unpauseNotification) :
|
require("./interactive")(win) :
|
||||||
setup(options);
|
setup();
|
||||||
};
|
};
|
||||||
|
|||||||
5
plugins/notifications/config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const { PluginConfig } = require("../../config/dynamic");
|
||||||
|
|
||||||
|
const config = new PluginConfig("notifications");
|
||||||
|
|
||||||
|
module.exports = { ...config };
|
||||||
@ -1,106 +1,235 @@
|
|||||||
const { notificationImage, icons } = require("./utils");
|
const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require("./utils");
|
||||||
const getSongControls = require('../../providers/song-controls');
|
const getSongControls = require('../../providers/song-controls');
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const is = require("electron-is");
|
const { changeProtocolHandler } = require("../../providers/protocol-handler");
|
||||||
const WindowsToaster = require('node-notifier').WindowsToaster;
|
const { setTrayOnClick, setTrayOnDoubleClick } = require("../../tray");
|
||||||
|
|
||||||
const notifier = new WindowsToaster({ withFallback: true });
|
const { Notification, app, ipcMain } = require("electron");
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
//store song controls reference on launch
|
const config = require("./config");
|
||||||
let controls;
|
|
||||||
let notificationOnUnpause;
|
|
||||||
|
|
||||||
module.exports = (win, unpauseNotification) => {
|
let songControls;
|
||||||
//Save controls and onPause option
|
let savedNotification;
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
|
||||||
controls = { playPause, next, previous };
|
|
||||||
notificationOnUnpause = unpauseNotification;
|
|
||||||
|
|
||||||
let currentUrl;
|
/** @param {Electron.BrowserWindow} win */
|
||||||
|
module.exports = (win) => {
|
||||||
|
songControls = getSongControls(win);
|
||||||
|
|
||||||
|
let currentSeconds = 0;
|
||||||
|
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||||
|
|
||||||
|
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||||
|
|
||||||
|
if (app.isPackaged) save_temp_icons();
|
||||||
|
|
||||||
|
let savedSongInfo;
|
||||||
|
let lastUrl;
|
||||||
|
|
||||||
// Register songInfoCallback
|
// Register songInfoCallback
|
||||||
registerCallback(songInfo => {
|
registerCallback(songInfo => {
|
||||||
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
|
if (!songInfo.artist && !songInfo.title) return;
|
||||||
currentUrl = songInfo.url;
|
savedSongInfo = { ...songInfo };
|
||||||
sendToaster(songInfo);
|
if (!songInfo.isPaused &&
|
||||||
|
(songInfo.url !== lastUrl || config.get("unpauseNotification"))
|
||||||
|
) {
|
||||||
|
lastUrl = songInfo.url
|
||||||
|
sendNotification(songInfo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
win.webContents.once("closed", () => {
|
if (config.get("trayControls")) {
|
||||||
deleteNotification()
|
setTrayOnClick(() => {
|
||||||
|
if (savedNotification) {
|
||||||
|
savedNotification.close();
|
||||||
|
savedNotification = undefined;
|
||||||
|
} else if (savedSongInfo) {
|
||||||
|
sendNotification({
|
||||||
|
...savedSongInfo,
|
||||||
|
elapsedSeconds: currentSeconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTrayOnDoubleClick(() => {
|
||||||
|
if (win.isVisible()) {
|
||||||
|
win.hide();
|
||||||
|
} else win.show();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app.once("before-quit", () => {
|
||||||
|
savedNotification?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
changeProtocolHandler(
|
||||||
|
(cmd) => {
|
||||||
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
|
songControls[cmd]();
|
||||||
|
if (config.get("refreshOnPlayPause") && (
|
||||||
|
cmd === 'pause' ||
|
||||||
|
(cmd === 'play' && !config.get("unpauseNotification"))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setImmediate(() =>
|
||||||
|
sendNotification({
|
||||||
|
...savedSongInfo,
|
||||||
|
isPaused: cmd === 'pause',
|
||||||
|
elapsedSeconds: currentSeconds
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//delete old notification
|
function sendNotification(songInfo) {
|
||||||
let toDelete;
|
const iconSrc = notificationImage(songInfo);
|
||||||
function deleteNotification() {
|
|
||||||
if (toDelete !== undefined) {
|
|
||||||
// To remove the notification it has to be done this way
|
|
||||||
const removeNotif = Object.assign(toDelete, {
|
|
||||||
remove: toDelete.id
|
|
||||||
})
|
|
||||||
notifier.notify(removeNotif)
|
|
||||||
|
|
||||||
toDelete = undefined;
|
savedNotification?.close();
|
||||||
|
|
||||||
|
savedNotification = new Notification({
|
||||||
|
title: songInfo.title || "Playing",
|
||||||
|
body: songInfo.artist,
|
||||||
|
icon: iconSrc,
|
||||||
|
silent: true,
|
||||||
|
// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root
|
||||||
|
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
|
||||||
|
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
|
||||||
|
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
|
||||||
|
toastXml: get_xml(songInfo, iconSrc),
|
||||||
|
});
|
||||||
|
|
||||||
|
savedNotification.on("close", (_) => {
|
||||||
|
savedNotification = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
savedNotification.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
const get_xml = (songInfo, iconSrc) => {
|
||||||
|
switch (config.get("toastStyle")) {
|
||||||
|
default:
|
||||||
|
case ToastStyles.logo:
|
||||||
|
case ToastStyles.legacy:
|
||||||
|
return xml_logo(songInfo, iconSrc);
|
||||||
|
case ToastStyles.banner_top_custom:
|
||||||
|
return xml_banner_top_custom(songInfo, iconSrc);
|
||||||
|
case ToastStyles.hero:
|
||||||
|
return xml_hero(songInfo, iconSrc);
|
||||||
|
case ToastStyles.banner_bottom:
|
||||||
|
return xml_banner_bottom(songInfo, iconSrc);
|
||||||
|
case ToastStyles.banner_centered_bottom:
|
||||||
|
return xml_banner_centered_bottom(songInfo, iconSrc);
|
||||||
|
case ToastStyles.banner_centered_top:
|
||||||
|
return xml_banner_centered_top(songInfo, iconSrc);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconLocation = app.isPackaged ?
|
||||||
|
path.resolve(app.getPath("userData"), 'icons') :
|
||||||
|
path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
|
||||||
|
|
||||||
|
const display = (kind) => {
|
||||||
|
if (config.get("toastStyle") === ToastStyles.legacy) {
|
||||||
|
return `content="${icons[kind]}"`;
|
||||||
|
} else {
|
||||||
|
return `\
|
||||||
|
content="${config.get("hideButtonText") ? "" : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
|
||||||
|
imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//New notification
|
const getButton = (kind) =>
|
||||||
function sendToaster(songInfo) {
|
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||||
deleteNotification();
|
|
||||||
//download image and get path
|
|
||||||
let imgSrc = notificationImage(songInfo, true);
|
|
||||||
toDelete = {
|
|
||||||
appID: "com.github.th-ch.youtube-music",
|
|
||||||
title: songInfo.title || "Playing",
|
|
||||||
message: songInfo.artist,
|
|
||||||
id: parseInt(Math.random() * 1000000, 10),
|
|
||||||
icon: imgSrc,
|
|
||||||
actions: [
|
|
||||||
icons.previous,
|
|
||||||
songInfo.isPaused ? icons.play : icons.pause,
|
|
||||||
icons.next
|
|
||||||
],
|
|
||||||
sound: false,
|
|
||||||
};
|
|
||||||
//send notification
|
|
||||||
notifier.notify(
|
|
||||||
toDelete,
|
|
||||||
(err, data) => {
|
|
||||||
// Will also wait until notification is closed.
|
|
||||||
if (err) {
|
|
||||||
console.log(`ERROR = ${err.toString()}\n DATA = ${data}`);
|
|
||||||
}
|
|
||||||
switch (data) {
|
|
||||||
//buttons
|
|
||||||
case icons.previous.normalize():
|
|
||||||
controls.previous();
|
|
||||||
return;
|
|
||||||
case icons.next.normalize():
|
|
||||||
controls.next();
|
|
||||||
return;
|
|
||||||
case icons.play.normalize():
|
|
||||||
controls.playPause();
|
|
||||||
// dont delete notification on play/pause
|
|
||||||
toDelete = undefined;
|
|
||||||
//manually send notification if not sending automatically
|
|
||||||
if (!notificationOnUnpause) {
|
|
||||||
songInfo.isPaused = false;
|
|
||||||
sendToaster(songInfo);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case icons.pause.normalize():
|
|
||||||
controls.playPause();
|
|
||||||
songInfo.isPaused = true;
|
|
||||||
toDelete = undefined;
|
|
||||||
sendToaster(songInfo);
|
|
||||||
return;
|
|
||||||
//Native datatype
|
|
||||||
case "dismissed":
|
|
||||||
case "timeout":
|
|
||||||
deleteNotification();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
);
|
const getButtons = (isPaused) => `\
|
||||||
|
<actions>
|
||||||
|
${getButton('previous')}
|
||||||
|
${isPaused ? getButton('play') : getButton('pause')}
|
||||||
|
${getButton('next')}
|
||||||
|
</actions>\
|
||||||
|
`;
|
||||||
|
|
||||||
|
const toast = (content, isPaused) => `\
|
||||||
|
<toast>
|
||||||
|
<audio silent="true" />
|
||||||
|
<visual>
|
||||||
|
<binding template="ToastGeneric">
|
||||||
|
${content}
|
||||||
|
</binding>
|
||||||
|
</visual>
|
||||||
|
|
||||||
|
${getButtons(isPaused)}
|
||||||
|
</toast>`;
|
||||||
|
|
||||||
|
const xml_image = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
|
||||||
|
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||||
|
<text id="1">${title}</text>
|
||||||
|
<text id="2">${artist}</text>\
|
||||||
|
`, isPaused);
|
||||||
|
|
||||||
|
|
||||||
|
const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"');
|
||||||
|
|
||||||
|
const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"');
|
||||||
|
|
||||||
|
const xml_banner_bottom = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, '');
|
||||||
|
|
||||||
|
const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
|
||||||
|
<image id="1" src="${imgSrc}" name="Image" />
|
||||||
|
<text>ㅤ</text>
|
||||||
|
<group>
|
||||||
|
<subgroup>
|
||||||
|
<text hint-style="body">${songInfo.title}</text>
|
||||||
|
<text hint-style="captionSubtle">${songInfo.artist}</text>
|
||||||
|
</subgroup>
|
||||||
|
${xml_more_data(songInfo)}
|
||||||
|
</group>\
|
||||||
|
`, songInfo.isPaused);
|
||||||
|
|
||||||
|
const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\
|
||||||
|
<subgroup hint-textStacking="bottom">
|
||||||
|
${album ?
|
||||||
|
`<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
||||||
|
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)}</text>
|
||||||
|
</subgroup>\
|
||||||
|
`;
|
||||||
|
|
||||||
|
const xml_banner_centered_bottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
||||||
|
<text>ㅤ</text>
|
||||||
|
<group>
|
||||||
|
<subgroup hint-weight="1" hint-textStacking="center">
|
||||||
|
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||||
|
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||||
|
</subgroup>
|
||||||
|
</group>
|
||||||
|
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||||
|
`, isPaused);
|
||||||
|
|
||||||
|
const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => toast(`\
|
||||||
|
<image id="1" src="${imgSrc}" name="Image" />
|
||||||
|
<text>ㅤ</text>
|
||||||
|
<group>
|
||||||
|
<subgroup hint-weight="1" hint-textStacking="center">
|
||||||
|
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||||
|
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||||
|
</subgroup>
|
||||||
|
</group>\
|
||||||
|
`, isPaused);
|
||||||
|
|
||||||
|
const titleFontPicker = (title) => {
|
||||||
|
if (title.length <= 13) {
|
||||||
|
return 'Header';
|
||||||
|
} else if (title.length <= 22) {
|
||||||
|
return 'Subheader';
|
||||||
|
} else if (title.length <= 26) {
|
||||||
|
return 'Title';
|
||||||
|
} else {
|
||||||
|
return 'Subtitle';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,80 @@
|
|||||||
const { urgencyLevels, setOption } = require("./utils");
|
const { urgencyLevels, ToastStyles, snakeToCamel } = require("./utils");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
module.exports = (_win, options) => [
|
||||||
...(is.linux() ?
|
...(is.linux()
|
||||||
[{
|
? [
|
||||||
label: "Notification Priority",
|
{
|
||||||
submenu: urgencyLevels.map(level => ({
|
label: "Notification Priority",
|
||||||
label: level.name,
|
submenu: urgencyLevels.map((level) => ({
|
||||||
type: "radio",
|
label: level.name,
|
||||||
checked: options.urgency === level.value,
|
type: "radio",
|
||||||
click: () => setOption(options, "urgency", level.value)
|
checked: options.urgency === level.value,
|
||||||
})),
|
click: () => config.set("urgency", level.value),
|
||||||
}] :
|
})),
|
||||||
[]),
|
},
|
||||||
...(is.windows() ?
|
]
|
||||||
[{
|
: []),
|
||||||
label: "Interactive Notifications",
|
...(is.windows()
|
||||||
type: "checkbox",
|
? [
|
||||||
checked: options.interactive,
|
{
|
||||||
click: (item) => setOption(options, "interactive", item.checked)
|
label: "Interactive Notifications",
|
||||||
}] :
|
type: "checkbox",
|
||||||
[]),
|
checked: options.interactive,
|
||||||
|
// doesn't update until restart
|
||||||
|
click: (item) => config.setAndMaybeRestart("interactive", item.checked),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// submenu with settings for interactive notifications (name shouldn't be too long)
|
||||||
|
label: "Interactive Settings",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Open/Close on tray click",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.trayControls,
|
||||||
|
click: (item) => config.set("trayControls", item.checked),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hide Button Text",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.hideButtonText,
|
||||||
|
click: (item) => config.set("hideButtonText", item.checked),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Refresh on Play/Pause",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.refreshOnPlayPause,
|
||||||
|
click: (item) => config.set("refreshOnPlayPause", item.checked),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Style",
|
||||||
|
submenu: getToastStyleMenuItems(options)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: "Show notification on unpause",
|
label: "Show notification on unpause",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: options.unpauseNotification,
|
checked: options.unpauseNotification,
|
||||||
click: (item) => setOption(options, "unpauseNotification", item.checked)
|
click: (item) => config.set("unpauseNotification", item.checked),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getToastStyleMenuItems(options) {
|
||||||
|
const arr = new Array(Object.keys(ToastStyles).length);
|
||||||
|
|
||||||
|
// ToastStyles index starts from 1
|
||||||
|
for (const [name, index] of Object.entries(ToastStyles)) {
|
||||||
|
arr[index - 1] = {
|
||||||
|
label: snakeToCamel(name),
|
||||||
|
type: "radio",
|
||||||
|
checked: options.toastStyle === index,
|
||||||
|
click: () => config.set("toastStyle", index),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,22 @@
|
|||||||
const { setMenuOptions } = require("../../config/plugins");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
const icon = "assets/youtube-music.png";
|
const icon = "assets/youtube-music.png";
|
||||||
const tempIcon = path.join(app.getPath("userData"), "tempIcon.png");
|
const userData = app.getPath("userData");
|
||||||
|
const tempIcon = path.join(userData, "tempIcon.png");
|
||||||
|
const tempBanner = path.join(userData, "tempBanner.png");
|
||||||
|
|
||||||
|
module.exports.ToastStyles = {
|
||||||
|
logo: 1,
|
||||||
|
banner_centered_top: 2,
|
||||||
|
hero: 3,
|
||||||
|
banner_top_custom: 4,
|
||||||
|
banner_centered_bottom: 5,
|
||||||
|
banner_bottom: 6,
|
||||||
|
legacy: 7
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.icons = {
|
module.exports.icons = {
|
||||||
play: "\u{1405}", // ᐅ
|
play: "\u{1405}", // ᐅ
|
||||||
@ -13,38 +25,37 @@ module.exports.icons = {
|
|||||||
previous: "\u{1438}" // ᐸ
|
previous: "\u{1438}" // ᐸ
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.setOption = (options, option, value) => {
|
|
||||||
options[option] = value;
|
|
||||||
setMenuOptions("notifications", options)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.urgencyLevels = [
|
module.exports.urgencyLevels = [
|
||||||
{ name: "Low", value: "low" },
|
{ name: "Low", value: "low" },
|
||||||
{ name: "Normal", value: "normal" },
|
{ name: "Normal", value: "normal" },
|
||||||
{ name: "High", value: "critical" },
|
{ name: "High", value: "critical" },
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports.notificationImage = function (songInfo, saveIcon = false) {
|
module.exports.notificationImage = (songInfo) => {
|
||||||
//return local path to temp icon
|
if (!songInfo.image) return icon;
|
||||||
if (saveIcon && !!songInfo.image) {
|
if (!config.get("interactive")) return nativeImageToLogo(songInfo.image);
|
||||||
try {
|
|
||||||
fs.writeFileSync(tempIcon,
|
switch (config.get("toastStyle")) {
|
||||||
centerNativeImage(songInfo.image)
|
case module.exports.ToastStyles.logo:
|
||||||
.toPNG()
|
case module.exports.ToastStyles.legacy:
|
||||||
);
|
return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon);
|
||||||
} catch (err) {
|
default:
|
||||||
console.log(`Error writing song icon to disk:\n${err.toString()}`)
|
return this.saveImage(songInfo.image, tempBanner);
|
||||||
return icon;
|
};
|
||||||
}
|
|
||||||
return tempIcon;
|
|
||||||
}
|
|
||||||
//else: return image
|
|
||||||
return songInfo.image
|
|
||||||
? centerNativeImage(songInfo.image)
|
|
||||||
: icon
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function centerNativeImage(nativeImage) {
|
module.exports.saveImage = (img, save_path) => {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(save_path, img.toPNG());
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error writing song icon to disk:\n${err.toString()}`)
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
return save_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function nativeImageToLogo(nativeImage) {
|
||||||
const tempImage = nativeImage.resize({ height: 256 });
|
const tempImage = nativeImage.resize({ height: 256 });
|
||||||
const margin = Math.max((tempImage.getSize().width - 256), 0);
|
const margin = Math.max((tempImage.getSize().width - 256), 0);
|
||||||
|
|
||||||
@ -54,3 +65,27 @@ function centerNativeImage(nativeImage) {
|
|||||||
width: 256, height: 256
|
width: 256, height: 256
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.save_temp_icons = () => {
|
||||||
|
for (const kind of Object.keys(module.exports.icons)) {
|
||||||
|
const destinationPath = path.join(userData, 'icons', `${kind}.png`);
|
||||||
|
if (fs.existsSync(destinationPath)) continue;
|
||||||
|
const iconPath = path.resolve(__dirname, "../../assets/media-icons-black", `${kind}.png`);
|
||||||
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||||
|
fs.copyFile(iconPath, destinationPath, () => { });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.snakeToCamel = (str) => {
|
||||||
|
return str.replace(/([-_][a-z]|^[a-z])/g, (group) =>
|
||||||
|
group.toUpperCase()
|
||||||
|
.replace('-', ' ')
|
||||||
|
.replace('_', ' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.secondsToMinutes = (seconds) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secondsLeft = seconds % 60;
|
||||||
|
return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const observer = new MutationObserver(() => {
|
|||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
}
|
}
|
||||||
if (menu.contains(pipButton)) return;
|
if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) return;
|
||||||
const menuUrl = $(
|
const menuUrl = $(
|
||||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
|
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
|
||||||
)?.href;
|
)?.href;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const observePopupContainer = () => {
|
|||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) {
|
if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) {
|
||||||
menu.prepend(slider);
|
menu.prepend(slider);
|
||||||
if (!observingSlider) {
|
if (!observingSlider) {
|
||||||
setupSliderListener();
|
setupSliderListener();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const mpris = require("mpris-service");
|
const mpris = require("mpris-service");
|
||||||
const {ipcMain} = require("electron");
|
const { ipcMain } = require("electron");
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
const config = require("../../config");
|
const config = require("../../config");
|
||||||
@ -18,9 +18,10 @@ function setupMPRIS() {
|
|||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Electron.BrowserWindow} win */
|
||||||
function registerMPRIS(win) {
|
function registerMPRIS(win) {
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const {playPause, next, previous, volumeMinus10, volumePlus10, shuffle} = songControls;
|
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
|
||||||
try {
|
try {
|
||||||
const secToMicro = n => Math.round(Number(n) * 1e6);
|
const secToMicro = n => Math.round(Number(n) * 1e6);
|
||||||
const microToSec = n => Math.round(Number(n) / 1e6);
|
const microToSec = n => Math.round(Number(n) / 1e6);
|
||||||
@ -30,6 +31,13 @@ function registerMPRIS(win) {
|
|||||||
|
|
||||||
const player = setupMPRIS();
|
const player = setupMPRIS();
|
||||||
|
|
||||||
|
ipcMain.on("apiLoaded", () => {
|
||||||
|
win.webContents.send("setupSeekedListener", "mpris");
|
||||||
|
win.webContents.send("setupTimeChangedListener", "mpris");
|
||||||
|
win.webContents.send("setupRepeatChangedListener", "mpris");
|
||||||
|
win.webContents.send("setupVolumeChangedListener", "mpris");
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
|
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
|
||||||
|
|
||||||
let currentSeconds = 0;
|
let currentSeconds = 0;
|
||||||
@ -109,7 +117,7 @@ function registerMPRIS(win) {
|
|||||||
// With precise volume we can set the volume to the exact value.
|
// With precise volume we can set the volume to the exact value.
|
||||||
let newVol = parseInt(newVolume * 100);
|
let newVol = parseInt(newVolume * 100);
|
||||||
if (parseInt(player.volume * 100) !== newVol) {
|
if (parseInt(player.volume * 100) !== newVol) {
|
||||||
if (!autoUpdate){
|
if (!autoUpdate) {
|
||||||
mprisVolNewer = true;
|
mprisVolNewer = true;
|
||||||
autoUpdate = false;
|
autoUpdate = false;
|
||||||
win.webContents.send('setVolume', newVol);
|
win.webContents.send('setVolume', newVol);
|
||||||
|
|||||||
@ -32,22 +32,22 @@ function setThumbar(win, songInfo) {
|
|||||||
win.setThumbarButtons([
|
win.setThumbarButtons([
|
||||||
{
|
{
|
||||||
tooltip: 'Previous',
|
tooltip: 'Previous',
|
||||||
icon: get('backward.png'),
|
icon: get('previous'),
|
||||||
click() { controls.previous(win.webContents); }
|
click() { controls.previous(win.webContents); }
|
||||||
}, {
|
}, {
|
||||||
tooltip: 'Play/Pause',
|
tooltip: 'Play/Pause',
|
||||||
// Update icon based on play state
|
// Update icon based on play state
|
||||||
icon: songInfo.isPaused ? get('play.png') : get('pause.png'),
|
icon: songInfo.isPaused ? get('play') : get('pause'),
|
||||||
click() { controls.playPause(win.webContents); }
|
click() { controls.playPause(win.webContents); }
|
||||||
}, {
|
}, {
|
||||||
tooltip: 'Next',
|
tooltip: 'Next',
|
||||||
icon: get('forward.png'),
|
icon: get('next'),
|
||||||
click() { controls.next(win.webContents); }
|
click() { controls.next(win.webContents); }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util
|
// Util
|
||||||
function get(file) {
|
function get(kind) {
|
||||||
return path.join(__dirname, "assets", file);
|
return path.join(__dirname, "../../assets/media-icons-black", `${kind}.png`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,9 @@ const post = async (data) => {
|
|||||||
fetch(url, { method: 'POST', headers, body: JSON.stringify({ data }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
|
fetch(url, { method: 'POST', headers, body: JSON.stringify({ data }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Electron.BrowserWindow} win */
|
||||||
module.exports = async (win) => {
|
module.exports = async (win) => {
|
||||||
|
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||||
ipcMain.on('timeChanged', async (_, t) => {
|
ipcMain.on('timeChanged', async (_, t) => {
|
||||||
if (!data.title) return;
|
if (!data.title) return;
|
||||||
data.progress = secToMilisec(t);
|
data.progress = secToMilisec(t);
|
||||||
|
|||||||
10
preload.js
@ -1,9 +1,9 @@
|
|||||||
require("./providers/front-logger")();
|
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
const setupSongInfo = require("./providers/song-info-front");
|
const setupSongInfo = require("./providers/song-info-front");
|
||||||
const { setupSongControls } = require("./providers/song-controls-front");
|
const { setupSongControls } = require("./providers/song-controls-front");
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
@ -69,6 +69,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||||
setInterval(() => window._lact = Date.now(), 900000);
|
setInterval(() => window._lact = Date.now(), 900000);
|
||||||
|
|
||||||
|
// setup back to front logger
|
||||||
|
if (is.dev()) {
|
||||||
|
ipcRenderer.on("log", (_event, log) => {
|
||||||
|
console.log(JSON.parse(log));
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function listenForApiLoad() {
|
function listenForApiLoad() {
|
||||||
@ -118,6 +125,7 @@ function onApiLoaded() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||||
|
ipcRenderer.send('apiLoaded');
|
||||||
|
|
||||||
// Remove upgrade button
|
// Remove upgrade button
|
||||||
if (config.get("options.removeUpgradeButton")) {
|
if (config.get("options.removeUpgradeButton")) {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
|
||||||
|
|
||||||
function logToString(log) {
|
|
||||||
return (typeof log === "string") ?
|
|
||||||
log :
|
|
||||||
JSON.stringify(log, null, "\t");
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
ipcRenderer.on("log", (_event, log) => {
|
|
||||||
console.log(logToString(log));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
44
providers/protocol-handler.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const { app } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const getSongControls = require("./song-controls");
|
||||||
|
|
||||||
|
const APP_PROTOCOL = "youtubemusic";
|
||||||
|
|
||||||
|
let protocolHandler;
|
||||||
|
|
||||||
|
function setupProtocolHandler(win) {
|
||||||
|
if (process.defaultApp && process.argv.length >= 2) {
|
||||||
|
app.setAsDefaultProtocolClient(
|
||||||
|
APP_PROTOCOL,
|
||||||
|
process.execPath,
|
||||||
|
[path.resolve(process.argv[1])]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
app.setAsDefaultProtocolClient(APP_PROTOCOL)
|
||||||
|
}
|
||||||
|
|
||||||
|
const songControls = getSongControls(win);
|
||||||
|
|
||||||
|
protocolHandler = (cmd) => {
|
||||||
|
if (Object.keys(songControls).includes(cmd)) {
|
||||||
|
songControls[cmd]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProtocol(cmd) {
|
||||||
|
protocolHandler(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeProtocolHandler(f) {
|
||||||
|
protocolHandler = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
APP_PROTOCOL,
|
||||||
|
setupProtocolHandler,
|
||||||
|
handleProtocol,
|
||||||
|
changeProtocolHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1,13 +1,8 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
const config = require("../config");
|
|
||||||
const is = require("electron-is");
|
|
||||||
|
|
||||||
module.exports.setupSongControls = () => {
|
module.exports.setupSongControls = () => {
|
||||||
document.addEventListener('apiLoaded', e => {
|
document.addEventListener('apiLoaded', e => {
|
||||||
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
|
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
|
||||||
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
|
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
|
||||||
if (is.linux() && config.plugins.isEnabled('shortcuts')) { // MPRIS Enabled
|
|
||||||
document.querySelector('video').addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
|
||||||
}
|
|
||||||
}, { once: true, passive: true })
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const pressKey = (window, key, modifiers = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win) => {
|
||||||
return {
|
const commands = {
|
||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, "k"),
|
previous: () => pressKey(win, "k"),
|
||||||
next: () => pressKey(win, "j"),
|
next: () => pressKey(win, "j"),
|
||||||
@ -21,8 +21,7 @@ module.exports = (win) => {
|
|||||||
go1sForward: () => pressKey(win, "l", ["shift"]),
|
go1sForward: () => pressKey(win, "l", ["shift"]),
|
||||||
shuffle: () => pressKey(win, "s"),
|
shuffle: () => pressKey(win, "s"),
|
||||||
switchRepeat: (n = 1) => {
|
switchRepeat: (n = 1) => {
|
||||||
for (let i = 0; i < n; i++)
|
for (let i = 0; i < n; i++) pressKey(win, "r");
|
||||||
pressKey(win, "r");
|
|
||||||
},
|
},
|
||||||
// General
|
// General
|
||||||
volumeMinus10: () => pressKey(win, "-"),
|
volumeMinus10: () => pressKey(win, "-"),
|
||||||
@ -50,4 +49,9 @@ module.exports = (win) => {
|
|||||||
search: () => pressKey(win, "/"),
|
search: () => pressKey(win, "/"),
|
||||||
showShortcuts: () => pressKey(win, "/", ["shift"]),
|
showShortcuts: () => pressKey(win, "/", ["shift"]),
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
...commands,
|
||||||
|
play: commands.playPause,
|
||||||
|
pause: commands.playPause
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
const {ipcRenderer} = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
const is = require('electron-is');
|
const { getImage } = require("./song-info");
|
||||||
const {getImage} = require("./song-info");
|
|
||||||
|
|
||||||
const config = require("../config");
|
|
||||||
|
|
||||||
global.songInfo = {};
|
global.songInfo = {};
|
||||||
|
|
||||||
@ -17,20 +14,69 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
|||||||
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||||
const srcChangedEvent = new CustomEvent('srcChanged');
|
const srcChangedEvent = new CustomEvent('srcChanged');
|
||||||
|
|
||||||
|
const singleton = (fn) => {
|
||||||
|
let called = false;
|
||||||
|
return (...args) => {
|
||||||
|
if (called) return;
|
||||||
|
called = true;
|
||||||
|
return fn(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.setupSeekedListener = singleton(() => {
|
||||||
|
document.querySelector('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setupTimeChangedListener = singleton(() => {
|
||||||
|
const progressObserver = new MutationObserver(mutations => {
|
||||||
|
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||||
|
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||||
|
});
|
||||||
|
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setupRepeatChangedListener = singleton(() => {
|
||||||
|
const repeatObserver = new MutationObserver(mutations => {
|
||||||
|
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
|
||||||
|
});
|
||||||
|
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||||
|
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.setupVolumeChangedListener = singleton((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());
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
document.addEventListener('apiLoaded', apiEvent => {
|
document.addEventListener('apiLoaded', apiEvent => {
|
||||||
if (config.plugins.isEnabled('tuna-obs') ||
|
ipcRenderer.on("setupTimeChangedListener", async () => {
|
||||||
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
this.setupTimeChangedListener();
|
||||||
setupTimeChangeListener();
|
});
|
||||||
setupRepeatChangeListener();
|
|
||||||
setupVolumeChangeListener(apiEvent.detail);
|
ipcRenderer.on("setupRepeatChangedListener", async () => {
|
||||||
}
|
this.setupRepeatChangedListener();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on("setupVolumeChangedListener", async () => {
|
||||||
|
this.setupVolumeChangedListener(apiEvent.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on("setupSeekedListener", async () => {
|
||||||
|
this.setupSeekedListener();
|
||||||
|
});
|
||||||
|
|
||||||
const video = $('video');
|
const video = $('video');
|
||||||
// name = "dataloaded" and abit later "dataupdated"
|
// name = "dataloaded" and abit later "dataupdated"
|
||||||
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
||||||
if (name !== 'dataloaded') return;
|
if (name !== 'dataloaded') return;
|
||||||
video.dispatchEvent(srcChangedEvent);
|
video.dispatchEvent(srcChangedEvent);
|
||||||
sendSongInfo();
|
setTimeout(sendSongInfo());
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const status of ['playing', 'pause']) {
|
for (const status of ['playing', 'pause']) {
|
||||||
@ -49,37 +95,14 @@ module.exports = () => {
|
|||||||
|
|
||||||
data.videoDetails.album = $$(
|
data.videoDetails.album = $$(
|
||||||
".byline.ytmusic-player-bar > .yt-simple-endpoint"
|
".byline.ytmusic-player-bar > .yt-simple-endpoint"
|
||||||
).find(e => e.href?.includes("browse"))?.textContent;
|
).find(e =>
|
||||||
|
e.href?.includes("browse/FEmusic_library_privately_owned_release")
|
||||||
|
|| e.href?.includes("browse/MPREb")
|
||||||
|
)?.textContent;
|
||||||
|
|
||||||
data.videoDetails.elapsedSeconds = Math.floor(video.currentTime);
|
data.videoDetails.elapsedSeconds = Math.floor(video.currentTime);
|
||||||
data.videoDetails.isPaused = false;
|
data.videoDetails.isPaused = false;
|
||||||
ipcRenderer.send("video-src-changed", JSON.stringify(data));
|
ipcRenderer.send("video-src-changed", JSON.stringify(data));
|
||||||
}
|
}
|
||||||
}, {once: true, passive: true});
|
}, { once: true, passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupTimeChangeListener() {
|
|
||||||
const progressObserver = new MutationObserver(mutations => {
|
|
||||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
|
||||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
|
||||||
});
|
|
||||||
progressObserver.observe($('#progress-bar'), {attributeFilter: ["value"]})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupRepeatChangeListener() {
|
|
||||||
const repeatObserver = new MutationObserver(mutations => {
|
|
||||||
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode)
|
|
||||||
});
|
|
||||||
repeatObserver.observe($('#right-controls .repeat'), {attributeFilter: ["title"]});
|
|
||||||
|
|
||||||
// Emit the initial value as well; as it's persistent between launches.
|
|
||||||
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|||||||
@ -61,7 +61,8 @@ const handleData = async (responseText, win) => {
|
|||||||
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
||||||
|
|
||||||
const oldUrl = songInfo.imageSrc;
|
const oldUrl = songInfo.imageSrc;
|
||||||
songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0];
|
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||||
|
songInfo.imageSrc = thumbnails[thumbnails.length - 1]?.url.split("?")[0];
|
||||||
if (oldUrl !== songInfo.imageSrc) {
|
if (oldUrl !== songInfo.imageSrc) {
|
||||||
songInfo.image = await getImage(songInfo.imageSrc);
|
songInfo.image = await getImage(songInfo.imageSrc);
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ const registerProvider = (win) => {
|
|||||||
await handleData(responseText, win);
|
await handleData(responseText, win);
|
||||||
handlingData = false;
|
handlingData = false;
|
||||||
callbacks.forEach((c) => {
|
callbacks.forEach((c) => {
|
||||||
c(songInfo);
|
c(songInfo, "video-src-changed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
|
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
|
||||||
@ -103,7 +104,7 @@ const registerProvider = (win) => {
|
|||||||
songInfo.elapsedSeconds = elapsedSeconds;
|
songInfo.elapsedSeconds = elapsedSeconds;
|
||||||
if (handlingData) return;
|
if (handlingData) return;
|
||||||
callbacks.forEach((c) => {
|
callbacks.forEach((c) => {
|
||||||
c(songInfo);
|
c(songInfo, "playPaused");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@ -118,7 +118,7 @@ winget install th-ch.YouTubeMusic
|
|||||||
|
|
||||||
- **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time
|
- **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time
|
||||||
|
|
||||||
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
|
> If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using the in-app-menu plugin)
|
||||||
|
|
||||||
## Themes
|
## Themes
|
||||||
|
|
||||||
|
|||||||
25
tray.js
@ -1,14 +1,29 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { app, Menu, nativeImage, Tray } = require("electron");
|
const { Menu, nativeImage, Tray } = require("electron");
|
||||||
|
|
||||||
const { restart } = require("./providers/app-controls");
|
const { restart } = require("./providers/app-controls");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const getSongControls = require("./providers/song-controls");
|
const getSongControls = require("./providers/song-controls");
|
||||||
|
|
||||||
// Prevent tray being garbage collected
|
// Prevent tray being garbage collected
|
||||||
|
|
||||||
|
/** @type {Electron.Tray} */
|
||||||
let tray;
|
let tray;
|
||||||
|
|
||||||
|
module.exports.setTrayOnClick = (fn) => {
|
||||||
|
if (!tray) return;
|
||||||
|
tray.removeAllListeners('click');
|
||||||
|
tray.on("click", fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
// wont do anything on macos since its disabled
|
||||||
|
module.exports.setTrayOnDoubleClick = (fn) => {
|
||||||
|
if (!tray) return;
|
||||||
|
tray.removeAllListeners('double-click');
|
||||||
|
tray.on("double-click", fn);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.setUpTray = (app, win) => {
|
module.exports.setUpTray = (app, win) => {
|
||||||
if (!config.get("options.tray")) {
|
if (!config.get("options.tray")) {
|
||||||
tray = undefined;
|
tray = undefined;
|
||||||
@ -17,13 +32,19 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
|
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
|
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
|
||||||
|
|
||||||
let trayIcon = nativeImage.createFromPath(iconPath).resize({
|
let trayIcon = nativeImage.createFromPath(iconPath).resize({
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
tray = new Tray(trayIcon);
|
tray = new Tray(trayIcon);
|
||||||
tray.setToolTip("Youtube Music");
|
|
||||||
|
tray.setToolTip("YouTube Music");
|
||||||
|
|
||||||
|
// macOS only
|
||||||
tray.setIgnoreDoubleClickEvents(true);
|
tray.setIgnoreDoubleClickEvents(true);
|
||||||
|
|
||||||
tray.on("click", () => {
|
tray.on("click", () => {
|
||||||
if (config.get("options.trayClickPlayPause")) {
|
if (config.get("options.trayClickPlayPause")) {
|
||||||
playPause();
|
playPause();
|
||||||
|
|||||||
87
yarn.lock
@ -2382,16 +2382,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"custom-electron-prompt@npm:^1.5.1":
|
"custom-electron-prompt@npm:^1.5.4":
|
||||||
version: 1.5.1
|
version: 1.5.4
|
||||||
resolution: "custom-electron-prompt@npm:1.5.1"
|
resolution: "custom-electron-prompt@npm:1.5.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
electron: ">=10.0.0"
|
electron: ">=10.0.0"
|
||||||
checksum: 43a0d72a7a3471135822cb210d580285f70080d9d3a7b03f82cd4be403059fe20ea05ebdd1f9534928c386ab25a353e678f2cfb3f4ca016b41f3366bff700767
|
checksum: 93995b5f0e9d14401a8c4fdd358af32d8b7585b59b111667cfa55f9505109c08914f3140953125b854e5d09e811de8c76c7fec718934c13e8a1ad09fe1b85270
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"custom-electron-titlebar@npm:^4.1.5":
|
"custom-electron-titlebar@npm:^4.1.6":
|
||||||
version: 4.1.6
|
version: 4.1.6
|
||||||
resolution: "custom-electron-titlebar@npm:4.1.6"
|
resolution: "custom-electron-titlebar@npm:4.1.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2528,7 +2528,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"deepmerge@npm:^4.2.2":
|
"deepmerge@npm:^4.3.0":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "deepmerge@npm:4.3.0"
|
resolution: "deepmerge@npm:4.3.0"
|
||||||
checksum: c7980eb5c5be040b371f1df0d566473875cfabed9f672ccc177b81ba8eee5686ce2478de2f1d0076391621cbe729e5eacda397179a59ef0f68901849647db126
|
checksum: c7980eb5c5be040b371f1df0d566473875cfabed9f672ccc177b81ba8eee5686ce2478de2f1d0076391621cbe729e5eacda397179a59ef0f68901849647db126
|
||||||
@ -3017,15 +3017,15 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^22.0.2":
|
"electron@npm:^22.0.2":
|
||||||
version: 22.2.0
|
version: 22.2.1
|
||||||
resolution: "electron@npm:22.2.0"
|
resolution: "electron@npm:22.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get": ^2.0.0
|
"@electron/get": ^2.0.0
|
||||||
"@types/node": ^16.11.26
|
"@types/node": ^16.11.26
|
||||||
extract-zip: ^2.0.1
|
extract-zip: ^2.0.1
|
||||||
bin:
|
bin:
|
||||||
electron: cli.js
|
electron: cli.js
|
||||||
checksum: 096434fe95408928c86de4782e87fcad8b933043a9a7b5447e87964c6c597352584d413157cca43a9f1fd4bf669d2e344d6604ff5c499367e3ca6f1008e2fd5f
|
checksum: d7331f1e4fbdaf7cb2e5093c3636cb2b64bd437a31b4664f67d4353caf1d021ab582f88584dd2e170a282ebf11158b17cc2f6846432beae3a4b5bc371555fd6d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -4122,7 +4122,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3":
|
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "get-intrinsic@npm:1.2.0"
|
resolution: "get-intrinsic@npm:1.2.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4355,13 +4355,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"growly@npm:^1.3.0":
|
|
||||||
version: 1.3.0
|
|
||||||
resolution: "growly@npm:1.3.0"
|
|
||||||
checksum: 53cdecd4c16d7d9154a9061a9ccb87d602e957502ca69b529d7d1b2436c2c0b700ec544fc6b3e4cd115d59b81e62e44ce86bd0521403b579d3a2a97d7ce72a44
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"handlebars@npm:^4.7.7":
|
"handlebars@npm:^4.7.7":
|
||||||
version: 4.7.7
|
version: 4.7.7
|
||||||
resolution: "handlebars@npm:4.7.7"
|
resolution: "handlebars@npm:4.7.7"
|
||||||
@ -4531,16 +4524,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"html-to-text@npm:^9.0.3":
|
"html-to-text@npm:^9.0.4":
|
||||||
version: 9.0.3
|
version: 9.0.4
|
||||||
resolution: "html-to-text@npm:9.0.3"
|
resolution: "html-to-text@npm:9.0.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@selderee/plugin-htmlparser2": ^0.10.0
|
"@selderee/plugin-htmlparser2": ^0.10.0
|
||||||
deepmerge: ^4.2.2
|
deepmerge: ^4.3.0
|
||||||
dom-serializer: ^2.0.0
|
dom-serializer: ^2.0.0
|
||||||
htmlparser2: ^8.0.1
|
htmlparser2: ^8.0.1
|
||||||
selderee: ^0.10.0
|
selderee: ^0.10.0
|
||||||
checksum: 4509edd5c03a9aae163a3362a05b26aa0f9e7deaaa856a5f64c6885d9d9d7235190efb905c6794e5a696bdd2536d6e19432953670e3243b1399048727c232134
|
checksum: 5431f7fa5501ba05cdc7e7eb90b9d3f7607e9779f313abc6a48bf493e144947f3bde63426679ca153e085ca77d7c0983bb2cf160a30b68b1598d1fb174a0ca05
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -4756,13 +4749,13 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"internal-slot@npm:^1.0.4":
|
"internal-slot@npm:^1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.5
|
||||||
resolution: "internal-slot@npm:1.0.4"
|
resolution: "internal-slot@npm:1.0.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: ^1.1.3
|
get-intrinsic: ^1.2.0
|
||||||
has: ^1.0.3
|
has: ^1.0.3
|
||||||
side-channel: ^1.0.4
|
side-channel: ^1.0.4
|
||||||
checksum: 8974588d06bab4f675573a3b52975370facf6486df51bc0567a982c7024fa29495f10b76c0d4dc742dd951d1b72024fdc1e31bb0bedf1678dc7aacacaf5a4f73
|
checksum: 97e84046bf9e7574d0956bd98d7162313ce7057883b6db6c5c7b5e5f05688864b0978ba07610c726d15d66544ffe4b1050107d93f8a39ebc59b15d8b429b497a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -5652,11 +5645,11 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"locate-path@npm:^7.1.0":
|
"locate-path@npm:^7.1.0":
|
||||||
version: 7.1.1
|
version: 7.2.0
|
||||||
resolution: "locate-path@npm:7.1.1"
|
resolution: "locate-path@npm:7.2.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: ^6.0.0
|
p-locate: ^6.0.0
|
||||||
checksum: 1d88af5b512d6e6398026252e17382907126683ab09ae5d6b8918d0bc72ca2642e1ad6e2fe635c5920840e369618e5d748c08deb57ba537fdd3f78e87ca993e0
|
checksum: c1b653bdf29beaecb3d307dfb7c44d98a2a98a02ebe353c9ad055d1ac45d6ed4e1142563d222df9b9efebc2bcb7d4c792b507fad9e7150a04c29530b7db570f8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -6238,20 +6231,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"node-notifier@npm:^10.0.1":
|
|
||||||
version: 10.0.1
|
|
||||||
resolution: "node-notifier@npm:10.0.1"
|
|
||||||
dependencies:
|
|
||||||
growly: ^1.3.0
|
|
||||||
is-wsl: ^2.2.0
|
|
||||||
semver: ^7.3.5
|
|
||||||
shellwords: ^0.1.1
|
|
||||||
uuid: ^8.3.2
|
|
||||||
which: ^2.0.2
|
|
||||||
checksum: ac09456152e433462dd3ca277048de7a60c6d63fc657e00ac72805841baf9bb2573e8d3f64c4b64af73546d1ed39733af6b0036c38b57a83c883aa33fff35a2e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"nopt@npm:^6.0.0":
|
"nopt@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "nopt@npm:6.0.0"
|
resolution: "nopt@npm:6.0.0"
|
||||||
@ -7586,13 +7565,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"shellwords@npm:^0.1.1":
|
|
||||||
version: 0.1.1
|
|
||||||
resolution: "shellwords@npm:0.1.1"
|
|
||||||
checksum: 8d73a5e9861f5e5f1068e2cfc39bc0002400fe58558ab5e5fa75630d2c3adf44ca1fac81957609c8320d5533e093802fcafc72904bf1a32b95de3c19a0b1c0d4
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"side-channel@npm:^1.0.4":
|
"side-channel@npm:^1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "side-channel@npm:1.0.4"
|
resolution: "side-channel@npm:1.0.4"
|
||||||
@ -8295,9 +8267,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"type-fest@npm:^3.1.0":
|
"type-fest@npm:^3.1.0":
|
||||||
version: 3.5.6
|
version: 3.5.7
|
||||||
resolution: "type-fest@npm:3.5.6"
|
resolution: "type-fest@npm:3.5.7"
|
||||||
checksum: ce80d778c35280e967796b99989b796a75f8c2b6c8f1b88eb62990107f5fa90c30fdb1826fe5e2ab192e5c1eef7de6f29d133b443e80c753cbc020b01a32487a
|
checksum: 06358352daa706d6f582d2041945e629fdd236c3c94678c4d87efb5d2e77bab78740337449f44bbd09a736c966e70570e901e2e2576b59b369891ffc1bf87bb6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -8495,7 +8467,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"uuid@npm:^8.3.1, uuid@npm:^8.3.2":
|
"uuid@npm:^8.3.1":
|
||||||
version: 8.3.2
|
version: 8.3.2
|
||||||
resolution: "uuid@npm:8.3.2"
|
resolution: "uuid@npm:8.3.2"
|
||||||
bin:
|
bin:
|
||||||
@ -8911,8 +8883,8 @@ __metadata:
|
|||||||
browser-id3-writer: ^4.4.0
|
browser-id3-writer: ^4.4.0
|
||||||
butterchurn: ^2.6.7
|
butterchurn: ^2.6.7
|
||||||
butterchurn-presets: ^2.4.7
|
butterchurn-presets: ^2.4.7
|
||||||
custom-electron-prompt: ^1.5.1
|
custom-electron-prompt: ^1.5.4
|
||||||
custom-electron-titlebar: ^4.1.5
|
custom-electron-titlebar: ^4.1.6
|
||||||
del-cli: ^5.0.0
|
del-cli: ^5.0.0
|
||||||
electron: ^22.0.2
|
electron: ^22.0.2
|
||||||
electron-better-web-request: ^1.0.1
|
electron-better-web-request: ^1.0.1
|
||||||
@ -8927,14 +8899,13 @@ __metadata:
|
|||||||
electron-updater: ^5.3.0
|
electron-updater: ^5.3.0
|
||||||
filenamify: ^4.3.0
|
filenamify: ^4.3.0
|
||||||
howler: ^2.2.3
|
howler: ^2.2.3
|
||||||
html-to-text: ^9.0.3
|
html-to-text: ^9.0.4
|
||||||
keyboardevent-from-electron-accelerator: ^2.0.0
|
keyboardevent-from-electron-accelerator: ^2.0.0
|
||||||
keyboardevents-areequal: ^0.2.2
|
keyboardevents-areequal: ^0.2.2
|
||||||
md5: ^2.3.0
|
md5: ^2.3.0
|
||||||
mpris-service: ^2.1.2
|
mpris-service: ^2.1.2
|
||||||
node-fetch: ^2.6.8
|
node-fetch: ^2.6.8
|
||||||
node-gyp: ^9.3.1
|
node-gyp: ^9.3.1
|
||||||
node-notifier: ^10.0.1
|
|
||||||
playwright: ^1.29.2
|
playwright: ^1.29.2
|
||||||
simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4"
|
simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4"
|
||||||
vudio: ^2.1.1
|
vudio: ^2.1.1
|
||||||
|
|||||||
@ -44,3 +44,9 @@ ytmusic-cast-button {
|
|||||||
.ytp-chrome-top-buttons {
|
.ytp-chrome-top-buttons {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make youtube-music logo un-draggable */
|
||||||
|
ytmusic-nav-bar>div.left-content>a,
|
||||||
|
ytmusic-nav-bar>div.left-content>a>picture>img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|||||||