From 2c49f6c740de388bc1bb3ccf3fe9c2f48f5fee67 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 7 Jan 2023 19:31:29 +0200 Subject: [PATCH] use Electron with ToastXML instead of SnoreToast * Add support for protocol commands * Remove node-notifier dependency --- .../media-icons-black/next.png | Bin .../media-icons-black}/pause.png | Bin .../media-icons-black}/play.png | Bin .../media-icons-black/previous.png | Bin config/defaults.js | 3 +- index.js | 36 ++-- menu.js | 12 +- package.json | 1 - plugins/notifications/back.js | 2 +- plugins/notifications/interactive.js | 161 ++++++++---------- plugins/notifications/menu.js | 52 +++--- plugins/taskbar-mediacontrol/back.js | 10 +- preload.js | 1 - providers/front-logger.js | 13 -- providers/protocol-handler.js | 44 +++++ providers/song-controls.js | 10 +- providers/song-info.js | 4 +- yarn.lock | 31 +--- 18 files changed, 193 insertions(+), 187 deletions(-) rename plugins/taskbar-mediacontrol/assets/forward.png => assets/media-icons-black/next.png (100%) rename {plugins/taskbar-mediacontrol/assets => assets/media-icons-black}/pause.png (100%) rename {plugins/taskbar-mediacontrol/assets => assets/media-icons-black}/play.png (100%) rename plugins/taskbar-mediacontrol/assets/backward.png => assets/media-icons-black/previous.png (100%) delete mode 100644 providers/front-logger.js create mode 100644 providers/protocol-handler.js diff --git a/plugins/taskbar-mediacontrol/assets/forward.png b/assets/media-icons-black/next.png similarity index 100% rename from plugins/taskbar-mediacontrol/assets/forward.png rename to assets/media-icons-black/next.png diff --git a/plugins/taskbar-mediacontrol/assets/pause.png b/assets/media-icons-black/pause.png similarity index 100% rename from plugins/taskbar-mediacontrol/assets/pause.png rename to assets/media-icons-black/pause.png diff --git a/plugins/taskbar-mediacontrol/assets/play.png b/assets/media-icons-black/play.png similarity index 100% rename from plugins/taskbar-mediacontrol/assets/play.png rename to assets/media-icons-black/play.png diff --git a/plugins/taskbar-mediacontrol/assets/backward.png b/assets/media-icons-black/previous.png similarity index 100% rename from plugins/taskbar-mediacontrol/assets/backward.png rename to assets/media-icons-black/previous.png diff --git a/config/defaults.js b/config/defaults.js index b62432c6..e92a5711 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -55,7 +55,8 @@ const defaultConfig = { enabled: false, unpauseNotification: false, urgency: "normal", //has effect only on Linux - interactive: false //has effect only on Windows + interactive: true, //has effect only on Windows + smallInteractive: false //has effect only on Windows }, "precise-volume": { enabled: false, diff --git a/index.js b/index.js index 7e4e5980..353980b4 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ const { isTesting } = require("./utils/testing"); const { setUpTray } = require("./tray"); const { setupSongInfo } = require("./providers/song-info"); const { setupAppControls, restart } = require("./providers/app-controls"); +const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require("./providers/protocol-handler"); // Catch errors and log them unhandled({ @@ -29,17 +30,9 @@ const app = electron.app; let mainWindow; autoUpdater.autoDownload = false; -if(config.get("options.singleInstanceLock")){ - const gotTheLock = app.requestSingleInstanceLock(); - if (!gotTheLock) app.quit(); - app.on('second-instance', () => { - if (!mainWindow) return; - if (mainWindow.isMinimized()) mainWindow.restore(); - if (!mainWindow.isVisible()) mainWindow.show(); - mainWindow.focus(); - }); -} +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) app.quit(); app.commandLine.appendSwitch( "js-flags", @@ -354,7 +347,7 @@ app.on("ready", () => { // Clear cache after 20s const clearCacheTimeout = setTimeout(() => { if (is.dev()) { - console.log("Clearing app cache."); + console.log("Clearing app cache."); } electron.session.defaultSession.clearCache(); clearTimeout(clearCacheTimeout); @@ -363,9 +356,6 @@ app.on("ready", () => { // Register appID on 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"; app.setAppUserModelId(appID); const appLocation = process.execPath; @@ -391,7 +381,6 @@ app.on("ready", () => { cwd: path.dirname(appLocation), description: "YouTube Music Desktop App - including custom plugins", appUserModelId: appID, - toastActivatorClsid } ); } @@ -402,6 +391,23 @@ app.on("ready", () => { setApplicationMenu(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 app.setLoginItemSettings({ openAtLogin: config.get("options.startAtLogin"), diff --git a/menu.js b/menu.js index 630b48d4..19326c4d 100644 --- a/menu.js +++ b/menu.js @@ -131,16 +131,14 @@ const mainMenuTemplate = (win) => { ], }, { - label: "Single instance lock", + label: "Release single instance lock", type: "checkbox", - checked: config.get("options.singleInstanceLock"), + checked: false, click: (item) => { - config.setMenuOption("options.singleInstanceLock", item.checked); - if (item.checked && !app.hasSingleInstanceLock()) { - app.requestSingleInstanceLock(); - } else if (!item.checked && app.hasSingleInstanceLock()) { + if (item.checked && app.hasSingleInstanceLock()) app.releaseSingleInstanceLock(); - } + else if (!item.checked && !app.hasSingleInstanceLock()) + app.requestSingleInstanceLock(); }, }, { diff --git a/package.json b/package.json index 9827b524..a80dc0cf 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ "md5": "^2.3.0", "mpris-service": "^2.1.2", "node-fetch": "^2.6.7", - "node-notifier": "^10.0.1", "ytdl-core": "^4.11.1", "ytpl": "^2.3.0" }, diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index f3282ed3..98dde0f8 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -39,6 +39,6 @@ const setup = (options) => { module.exports = (win, options) => { // Register the callback for new song information is.windows() && options.interactive ? - require("./interactive")(win, options.unpauseNotification) : + require("./interactive")(win, options) : setup(options); }; diff --git a/plugins/notifications/interactive.js b/plugins/notifications/interactive.js index b7535ea0..0f4efada 100644 --- a/plugins/notifications/interactive.js +++ b/plugins/notifications/interactive.js @@ -1,106 +1,91 @@ const { notificationImage, icons } = require("./utils"); const getSongControls = require('../../providers/song-controls'); const registerCallback = require("../../providers/song-info"); -const is = require("electron-is"); -const WindowsToaster = require('node-notifier').WindowsToaster; +const { changeProtocolHandler } = require("../../providers/protocol-handler"); -const notifier = new WindowsToaster({ withFallback: true }); +const { Notification } = require("electron"); +const path = require('path'); -//store song controls reference on launch -let controls; -let notificationOnUnpause; +let songControls; +let config; +let savedNotification; -module.exports = (win, unpauseNotification) => { - //Save controls and onPause option - const { playPause, next, previous } = getSongControls(win); - controls = { playPause, next, previous }; - notificationOnUnpause = unpauseNotification; +module.exports = (win, _config) => { + songControls = getSongControls(win); + config = _config; - let currentUrl; + let lastSongInfo = { url: undefined }; // Register songInfoCallback - registerCallback(songInfo => { - if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) { - currentUrl = songInfo.url; - sendToaster(songInfo); + registerCallback((songInfo, cause) => { + if (!songInfo.isPaused && (songInfo.url !== lastSongInfo.url || config.unpauseNotification)) { + lastSongInfo = { ...songInfo }; + sendXML(songInfo); } }); win.webContents.once("closed", () => { - deleteNotification() + savedNotification = undefined; }); -} -//delete old notification -let toDelete; -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; - } -} - -//New notification -function sendToaster(songInfo) { - 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(); + changeProtocolHandler( + (cmd) => { + if (Object.keys(songControls).includes(cmd)) { + songControls[cmd](); + if (cmd === 'pause' || (cmd === 'play' && !config.unpauseNotification)) { + setImmediate(() => + sendXML({ ...lastSongInfo, isPaused: cmd === 'pause' }) + ); + } } } - - ); + ) } + + +function sendXML(songInfo) { + const imgSrc = notificationImage(songInfo, true); + + savedNotification?.close(); + + savedNotification = new Notification({ + title: songInfo.title || "Playing", + body: songInfo.artist, + icon: imgSrc, + 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/adaptive-interactive-toasts?tabs=xml + // https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype + toastXml: ` + + `, + }); + + savedNotification.on("close", (_) => { + savedNotification = undefined; + }); + + savedNotification.show(); +} + +const getButton = (kind) => + ``; + +const display = (kind) => + config.smallInteractive ? + `content="${icons[kind]}"` : + `content="" imageUri="file:///${path.resolve(__dirname, "../../assets/media-icons-black", `${kind}.png`)}"`; diff --git a/plugins/notifications/menu.js b/plugins/notifications/menu.js index 3f239909..7c2776f8 100644 --- a/plugins/notifications/menu.js +++ b/plugins/notifications/menu.js @@ -1,30 +1,40 @@ const { urgencyLevels, setOption } = require("./utils"); const is = require("electron-is"); -module.exports = (win, options) => [ - ...(is.linux() ? - [{ - label: "Notification Priority", - submenu: urgencyLevels.map(level => ({ - label: level.name, - type: "radio", - checked: options.urgency === level.value, - click: () => setOption(options, "urgency", level.value) - })), - }] : - []), - ...(is.windows() ? - [{ - label: "Interactive Notifications", - type: "checkbox", - checked: options.interactive, - click: (item) => setOption(options, "interactive", item.checked) - }] : - []), +module.exports = (_win, options) => [ + ...(is.linux() + ? [ + { + label: "Notification Priority", + submenu: urgencyLevels.map((level) => ({ + label: level.name, + type: "radio", + checked: options.urgency === level.value, + click: () => setOption(options, "urgency", level.value), + })), + }, + ] + : []), + ...(is.windows() + ? [ + { + label: "Interactive Notifications", + type: "checkbox", + checked: options.interactive, + click: (item) => setOption(options, "interactive", item.checked), + }, + { + label: "Smaller Interactive Notifications", + type: "checkbox", + checked: options.smallInteractive, + click: (item) => setOption(options, "smallInteractive", item.checked), + }, + ] + : []), { label: "Show notification on unpause", type: "checkbox", checked: options.unpauseNotification, - click: (item) => setOption(options, "unpauseNotification", item.checked) + click: (item) => setOption(options, "unpauseNotification", item.checked), }, ]; diff --git a/plugins/taskbar-mediacontrol/back.js b/plugins/taskbar-mediacontrol/back.js index 26a11732..dce9a6b3 100644 --- a/plugins/taskbar-mediacontrol/back.js +++ b/plugins/taskbar-mediacontrol/back.js @@ -32,22 +32,22 @@ function setThumbar(win, songInfo) { win.setThumbarButtons([ { tooltip: 'Previous', - icon: get('backward.png'), + icon: get('previous'), click() { controls.previous(win.webContents); } }, { tooltip: 'Play/Pause', // 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); } }, { tooltip: 'Next', - icon: get('forward.png'), + icon: get('next'), click() { controls.next(win.webContents); } } ]); } // Util -function get(file) { - return path.join(__dirname, "assets", file); +function get(kind) { + return path.join(__dirname, "../../assets/media-icons-black", `${kind}.png`); } diff --git a/preload.js b/preload.js index 2b9122f1..1354d185 100644 --- a/preload.js +++ b/preload.js @@ -1,4 +1,3 @@ -require("./providers/front-logger")(); const config = require("./config"); const { fileExists } = require("./plugins/utils"); const setupSongInfo = require("./providers/song-info-front"); diff --git a/providers/front-logger.js b/providers/front-logger.js deleted file mode 100644 index 99da2329..00000000 --- a/providers/front-logger.js +++ /dev/null @@ -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)); - }); -}; diff --git a/providers/protocol-handler.js b/providers/protocol-handler.js new file mode 100644 index 00000000..18eaea31 --- /dev/null +++ b/providers/protocol-handler.js @@ -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, +}; + + diff --git a/providers/song-controls.js b/providers/song-controls.js index a23190eb..93f352d1 100644 --- a/providers/song-controls.js +++ b/providers/song-controls.js @@ -8,7 +8,7 @@ const pressKey = (window, key, modifiers = []) => { }; module.exports = (win) => { - return { + const commands = { // Playback previous: () => pressKey(win, "k"), next: () => pressKey(win, "j"), @@ -21,8 +21,7 @@ module.exports = (win) => { go1sForward: () => pressKey(win, "l", ["shift"]), shuffle: () => pressKey(win, "s"), switchRepeat: (n = 1) => { - for (let i = 0; i < n; i++) - pressKey(win, "r"); + for (let i = 0; i < n; i++) pressKey(win, "r"); }, // General volumeMinus10: () => pressKey(win, "-"), @@ -50,4 +49,9 @@ module.exports = (win) => { search: () => pressKey(win, "/"), showShortcuts: () => pressKey(win, "/", ["shift"]), }; + return { + ...commands, + play: commands.playPause, + pause: commands.playPause + }; }; diff --git a/providers/song-info.js b/providers/song-info.js index 88f757a3..8181e6ad 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -95,7 +95,7 @@ const registerProvider = (win) => { await handleData(responseText, win); handlingData = false; callbacks.forEach((c) => { - c(songInfo); + c(songInfo, "video-src-changed"); }); }); ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => { @@ -103,7 +103,7 @@ const registerProvider = (win) => { songInfo.elapsedSeconds = elapsedSeconds; if (handlingData) return; callbacks.forEach((c) => { - c(songInfo); + c(songInfo, "playPaused"); }); }) }; diff --git a/yarn.lock b/yarn.lock index c5a07fde..a73d0b2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3492,11 +3492,6 @@ graceful-fs@^4.2.10: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -4148,7 +4143,7 @@ is-windows@^1.0.1: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -4851,18 +4846,6 @@ node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-notifier@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-10.0.1.tgz#0e82014a15a8456c4cfcdb25858750399ae5f1c7" - integrity sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ== - 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" - node-releases@^1.1.71: version "1.1.72" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" @@ -5896,11 +5879,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -6562,11 +6540,6 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - v8-compile-cache@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" @@ -6625,7 +6598,7 @@ which@^1.2.10: dependencies: isexe "^2.0.0" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==