diff --git a/config/defaults.js b/config/defaults.js index fb87e566..e90b36a5 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -44,7 +44,8 @@ const defaultConfig = { notifications: { enabled: false, urgency: "normal", - unpauseNotification: false + unpauseNotification: false, + interactive: true } }, }; diff --git a/package.json b/package.json index ce144c38..467217db 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "electron-updater": "^4.3.6", "filenamify": "^4.2.0", "node-fetch": "^2.6.1", + "node-notifier": "^9.0.1", "ytdl-core": "^4.4.5", "ytpl": "^2.0.5" }, diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index dedd9379..9e1b3fd1 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -1,18 +1,17 @@ const { Notification } = require("electron"); +const is = require("electron-is"); const getSongInfo = require("../../providers/song-info"); +const { notificationImage } = require("./utils"); + +const { setup, notifyInteractive } = require("./interactive") const notify = (info, options) => { - let notificationImage = "assets/youtube-music.png"; - - if (info.image) { - notificationImage = info.image.resize({ height: 256, width: 256 }); - } // Fill the notification with content const notification = { title: info.title || "Playing", body: info.artist, - icon: notificationImage, + icon: notificationImage(info), silent: true, urgency: options.urgency, }; @@ -25,6 +24,10 @@ const notify = (info, options) => { }; module.exports = (win, options) => { + //setup interactive notifications for windows + if (is.windows()) { + setup(win); + } const registerCallback = getSongInfo(win); let oldNotification; let oldURL = ""; @@ -39,13 +42,17 @@ module.exports = (win, options) => { } return; } - // If url isn't the same as last one - send notification + // If url isn"t the same as last one - send notification if (songInfo.url !== oldURL) { oldURL = songInfo.url; - // Close the old notification - oldNotification?.close(); - // This fixes a weird bug that would cause the notification to be updated instead of showing - setTimeout(()=>{ oldNotification = notify(songInfo, options) }, 10); + if (is.windows() && options.interactive) { + notifyInteractive(songInfo); + } else { + // Close the old notification + oldNotification?.close(); + // This fixes a weird bug that would cause the notification to be updated instead of showing + setTimeout(() => { oldNotification = notify(songInfo, options) }, 10); + } } }); }); diff --git a/plugins/notifications/interactive.js b/plugins/notifications/interactive.js new file mode 100644 index 00000000..47b7c57c --- /dev/null +++ b/plugins/notifications/interactive.js @@ -0,0 +1,96 @@ +const is = require("electron-is"); +const { app } = require("electron"); +const { notificationImage, icons } = require("./utils"); +const getSongControls = require('../../providers/song-controls'); +const notifier = require("node-notifier"); + +const appID = "com.github.th-ch.youtube-music"; +const shortcutPath = `("Youtube Music" "${app.getPath("exe")}" "${appID}")`; + +//saving controls here avoid errors +let controls; + +//delete old notification +let toDelete; +function Delete() { + if (toDelete === undefined) { + return; + } + const removeNotif = Object.assign(toDelete, { + remove: toDelete.id + }) + notifier.notify(removeNotif) + + toDelete = undefined; +} + +//Setup on launch +module.exports.setup = (win) => { + //save controls + const { playPause, next, previous } = getSongControls(win); + controls = { playPause, next, previous }; + //setup global listeners + notifier.on("dismissed", () => { Delete(); }); + notifier.on("timeout", () => { Delete(); }); + //try installing shortcut + if (!is.dev()) { + notifier.notify({ + title: "installing shortcut", + id: 1337, + install: shortcutPath + }); + } + + //close all listeners on close + win.on("closed", () => { + notifier.removeAllListeners(); + }); +} + +//New notification +module.exports.notifyInteractive = function sendToaster(songInfo) { + Delete(); + //download image and get path + let imgSrc = notificationImage(songInfo, true); + toDelete = { + appID: !is.dev() ? appID : undefined, //(will break action buttons if not installed to start menu) + title: songInfo.title || "Playing", + message: songInfo.artist, + id: parseInt(Math.random() * 1000000, 10), + icon: imgSrc, + actions: [ + icons.previous, // Previous + songInfo.isPaused ? icons.play : icons.pause, + icons.next // Next + ], + sound: false, + }; + //send notification + notifier.notify( + toDelete, + (err, data) => { + // Will also wait until notification is closed. + if (err) { + console.log(`ERROR = ${err}\n DATA = ${data}`); + } + switch (data) { + case icons.previous.normalize(): + controls.previous(); + return; + case icons.next.normalize(): + controls.next(); + return; + case icons.play.normalize(): + controls.playPause(); + toDelete = undefined; // dont delete notification on play/pause + return; + case icons.pause.normalize(): + controls.playPause(); + songInfo.isPaused = true; + toDelete = undefined; // it gets deleted automatically + sendToaster(songInfo); + } + } + + ); +} diff --git a/plugins/notifications/menu.js b/plugins/notifications/menu.js index a61cac67..bc2eab67 100644 --- a/plugins/notifications/menu.js +++ b/plugins/notifications/menu.js @@ -1,4 +1,5 @@ -const {urgencyLevels, setUrgency, setUnpause} = require("./utils"); +const {urgencyLevels, setUrgency, setUnpause, setInteractive} = require("./utils"); +const is = require("electron-is"); module.exports = (win, options) => [ { @@ -15,5 +16,13 @@ module.exports = (win, options) => [ type: "checkbox", checked: options.unpauseNotification, click: (item) => setUnpause(options, item.checked) - } + }, + ...(is.windows() ? + [{ + label: "Interactive", + type: "checkbox", + checked: options.interactive, + click: (item) => setInteractive(options, item.checked) + }] : + []) ]; diff --git a/plugins/notifications/utils.js b/plugins/notifications/utils.js index c43ecb20..d48f8319 100644 --- a/plugins/notifications/utils.js +++ b/plugins/notifications/utils.js @@ -1,19 +1,57 @@ -const {setOptions} = require("../../config/plugins"); +const { setOptions } = require("../../config/plugins"); +const path = require("path"); +const { app } = require("electron"); +const icon = path.join(__dirname, "assets", "youtube-music.png"); +const fs = require("fs"); + +const tempIcon = path.join(app.getPath("userData"), "tempIcon.png"); module.exports.urgencyLevels = [ - {name: "Low", value: "low"}, - {name: "Normal", value: "normal"}, - {name: "High", value: "critical"}, + { name: "Low", value: "low" }, + { name: "Normal", value: "normal" }, + { name: "High", value: "critical" }, ]; module.exports.setUrgency = (options, level) => { - options.urgency = level - setOption(options) + options.urgency = level; + setOption(options); }; module.exports.setUnpause = (options, value) => { - options.unpauseNotification = value - setOption(options) + options.unpauseNotification = value; + setOption(options); +}; +module.exports.setInteractive = (options, value) => { + options.interactive = value; + setOption(options); +} + +module.exports.notificationImage = function (songInfo, url = false) { + //return local url + if (!!songInfo && url) { + try { + fs.writeFileSync(tempIcon, + songInfo.image + .resize({ height: 256, width: 256 }) + .toPNG() + ); + } catch (err) { + console.log(`Error downloading song icon:\n${err.toString()}`) + return icon; + } + return tempIcon; + } + //else: return image + return songInfo.image + ? songInfo.image.resize({ height: 256, width: 256 }) + : icon }; let setOption = options => { setOptions("notifications", options) }; + +module.exports.icons = { + play: "\u{1405}", // ᐅ + pause: "\u{2016}", // ‖ + next: "\u{1433}", // ᐳ + previous: "\u{1438}" // ᐸ +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c969527e..188f3193 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6335,6 +6335,18 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" +node-notifier@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4" + integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + noop-logger@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"