diff --git a/assets/youtube-music-tray.png b/assets/youtube-music-tray.png new file mode 100644 index 00000000..5e947ca1 Binary files /dev/null and b/assets/youtube-music-tray.png differ diff --git a/index.js b/index.js index fe18f419..f7dddafb 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,19 @@ "use strict"; const path = require("path"); -const electron = require("electron"); -const is = require("electron-is"); +const electron = require("electron"); +const is = require("electron-is"); const { autoUpdater } = require("electron-updater"); -const { setApplicationMenu } = require("./menu"); -const { getEnabledPlugins, store } = require("./store"); -const { fileExists, injectCSS } = require("./plugins/utils"); +const { setApplicationMenu } = require("./menu"); +const { + autoUpdate, + getEnabledPlugins, + isAppVisible, + store, +} = require("./store"); +const { fileExists, injectCSS } = require("./plugins/utils"); +const { setUpTray } = require("./tray"); const app = electron.app; @@ -32,23 +38,23 @@ function onClosed() { } function createMainWindow() { - const windowSize = store.get("window-size"); + const windowSize = store.get("window-size"); const windowMaximized = store.get("window-maximized"); const win = new electron.BrowserWindow({ - icon : icon, - width : windowSize.width, - height : windowSize.height, + icon: icon, + width: windowSize.width, + height: windowSize.height, backgroundColor: "#000", - show : false, - webPreferences : { - nodeIntegration : false, - preload : path.join(__dirname, "preload.js"), - nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy - affinity : "main-window" // main window, and addition windows should work in one process + show: false, + webPreferences: { + nodeIntegration: false, + preload: path.join(__dirname, "preload.js"), + nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy + affinity: "main-window", // main window, and addition windows should work in one process }, - frame : !is.macOS(), - titleBarStyle: is.macOS() ? "hiddenInset": "default" + frame: !is.macOS(), + titleBarStyle: is.macOS() ? "hiddenInset" : "default", }); if (windowMaximized) { win.maximize(); @@ -65,7 +71,7 @@ function createMainWindow() { } }); - getEnabledPlugins().forEach(plugin => { + getEnabledPlugins().forEach((plugin) => { console.log("Loaded plugin - " + plugin); const pluginPath = path.join(__dirname, "plugins", plugin, "back.js"); fileExists(pluginPath, () => { @@ -114,7 +120,9 @@ function createMainWindow() { }); win.once("ready-to-show", () => { - win.show(); + if (isAppVisible()) { + win.show(); + } }); return win; @@ -142,16 +150,18 @@ app.on("activate", () => { app.on("ready", () => { setApplicationMenu(); mainWindow = createMainWindow(); - if (!is.dev()) { + setUpTray(app, mainWindow); + + if (!is.dev() && autoUpdate()) { autoUpdater.checkForUpdatesAndNotify(); autoUpdater.on("update-available", () => { const dialogOpts = { - type : "info", + type: "info", buttons: ["OK"], - title : "Application Update", + title: "Application Update", message: "A new version is available", - detail : - "A new version is available and can be downloaded at https://github.com/th-ch/youtube-music/releases/latest" + detail: + "A new version is available and can be downloaded at https://github.com/th-ch/youtube-music/releases/latest", }; electron.dialog.showMessageBox(dialogOpts); }); @@ -159,11 +169,15 @@ app.on("ready", () => { // Optimized for Mac OS X if (process.platform === "darwin") { + if (!isAppVisible()) { + app.dock.hide(); + } + var forceQuit = false; app.on("before-quit", () => { forceQuit = true; }); - mainWindow.on("close", event => { + mainWindow.on("close", (event) => { if (!forceQuit) { event.preventDefault(); mainWindow.hide(); diff --git a/menu.js b/menu.js index 6a864d33..2c72da2e 100644 --- a/menu.js +++ b/menu.js @@ -1,33 +1,79 @@ const { app, Menu } = require("electron"); -const { getAllPlugins } = require("./plugins/utils"); -const { isPluginEnabled, enablePlugin, disablePlugin } = require("./store"); +const { getAllPlugins } = require("./plugins/utils"); +const { + isPluginEnabled, + enablePlugin, + disablePlugin, + autoUpdate, + isAppVisible, + isTrayEnabled, + setOptions, +} = require("./store"); -module.exports.setApplicationMenu = () => { - const menuTemplate = [ - { - label : "Plugins", - submenu: getAllPlugins().map(plugin => { - return { - label : plugin, - type : "checkbox", - checked: isPluginEnabled(plugin), - click : item => { - if (item.checked) { - enablePlugin(plugin); - } else { - disablePlugin(plugin); - } +const mainMenuTemplate = [ + { + label: "Plugins", + submenu: getAllPlugins().map((plugin) => { + return { + label: plugin, + type: "checkbox", + checked: isPluginEnabled(plugin), + click: (item) => { + if (item.checked) { + enablePlugin(plugin); + } else { + disablePlugin(plugin); } - }; - }) - } - ]; + }, + }; + }), + }, + { + label: "Options", + submenu: [ + { + label: "Auto-update", + type: "checkbox", + checked: autoUpdate(), + click: (item) => { + setOptions({ autoUpdates: item.checked }); + }, + }, + { + label: "Tray", + submenu: [ + { + label: "Disabled", + type: "radio", + checked: !isTrayEnabled(), + click: () => setOptions({ tray: false, appVisible: true }), + }, + { + label: "Enabled + app visible", + type: "radio", + checked: isTrayEnabled() && isAppVisible(), + click: () => setOptions({ tray: true, appVisible: true }), + }, + { + label: "Enabled + app hidden", + type: "radio", + checked: isTrayEnabled() && !isAppVisible(), + click: () => setOptions({ tray: true, appVisible: false }), + }, + ], + }, + ], + }, +]; +module.exports.mainMenuTemplate = mainMenuTemplate; +module.exports.setApplicationMenu = () => { + const menuTemplate = [...mainMenuTemplate]; if (process.platform === "darwin") { const name = app.getName(); menuTemplate.unshift({ - label : name, + label: name, submenu: [ { role: "about" }, { type: "separator" }, @@ -36,9 +82,9 @@ module.exports.setApplicationMenu = () => { { role: "unhide" }, { type: "separator" }, { - label : "Select All", + label: "Select All", accelerator: "CmdOrCtrl+A", - selector : "selectAll:" + selector: "selectAll:", }, { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, @@ -46,8 +92,8 @@ module.exports.setApplicationMenu = () => { { type: "separator" }, { role: "minimize" }, { role: "close" }, - { role: "quit" } - ] + { role: "quit" }, + ], }); } diff --git a/store/index.js b/store/index.js index 8d34c783..4ae9af01 100644 --- a/store/index.js +++ b/store/index.js @@ -4,18 +4,30 @@ const plugins = require("./plugins"); const store = new Store({ defaults: { "window-size": { - width : 1100, + width: 1100, height: 550 }, - url : "https://music.youtube.com", - plugins: ["navigation", "shortcuts", "adblocker"] + url: "https://music.youtube.com", + plugins: ["navigation", "shortcuts", "adblocker"], + options: { + tray: false, + appVisible: true, + autoUpdates: true + } } }); module.exports = { - store : store, - isPluginEnabled : plugin => plugins.isEnabled(store, plugin), + store: store, + // Plugins + isPluginEnabled: plugin => plugins.isEnabled(store, plugin), getEnabledPlugins: () => plugins.getEnabledPlugins(store), - enablePlugin : plugin => plugins.enablePlugin(store, plugin), - disablePlugin : plugin => plugins.disablePlugin(store, plugin) + enablePlugin: plugin => plugins.enablePlugin(store, plugin), + disablePlugin: plugin => plugins.disablePlugin(store, plugin), + // Options + setOptions: options => + store.set("options", { ...store.get("options"), ...options }), + isTrayEnabled: () => store.get("options.tray"), + isAppVisible: () => store.get("options.appVisible"), + autoUpdate: () => store.get("options.autoUpdates") }; diff --git a/tray.js b/tray.js new file mode 100644 index 00000000..d36c0911 --- /dev/null +++ b/tray.js @@ -0,0 +1,69 @@ +const path = require("path"); + +const { Menu, nativeImage, Tray } = require("electron"); + +const { mainMenuTemplate } = require("./menu"); +const { isTrayEnabled } = require("./store"); +const { clickInYoutubeMusic } = require("./utils/youtube-music"); + +// Prevent tray being garbage collected +let tray; + +module.exports.setUpTray = (app, win) => { + if (!isTrayEnabled()) { + tray = undefined; + return; + } + + const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png"); + let trayIcon = nativeImage.createFromPath(iconPath).resize({ + width: 16, + height: 16, + }); + tray = new Tray(trayIcon); + tray.setToolTip("Youtube Music"); + + const trayMenu = Menu.buildFromTemplate([ + { + label: "Play/Pause", + click: () => { + clickInYoutubeMusic( + win, + "#left-controls > div > paper-icon-button.play-pause-button.style-scope.ytmusic-player-bar" + ); + }, + }, + { + label: "Next", + click: () => { + clickInYoutubeMusic( + win, + "#left-controls > div > paper-icon-button.next-button.style-scope.ytmusic-player-bar" + ); + }, + }, + { + label: "Previous", + click: () => { + clickInYoutubeMusic( + win, + "#left-controls > div > paper-icon-button.previous-button.style-scope.ytmusic-player-bar" + ); + }, + }, + { + label: "Show", + click: () => { + win.show(); + }, + }, + ...mainMenuTemplate, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + tray.setContextMenu(trayMenu); +}; diff --git a/utils/youtube-music.js b/utils/youtube-music.js new file mode 100644 index 00000000..3a7e848a --- /dev/null +++ b/utils/youtube-music.js @@ -0,0 +1,8 @@ +const clickInYoutubeMusic = (win, selector) => { + win.webContents.executeJavaScript( + `document.querySelector("${selector}").click();`, + true + ); +}; + +module.exports = { clickInYoutubeMusic };