diff --git a/config/defaults.js b/config/defaults.js index 552a4995..5bb74e09 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -30,6 +30,7 @@ const defaultConfig = { // Disabled plugins shortcuts: { enabled: false, + overrideMediaKeys: false, }, downloader: { enabled: false, @@ -59,9 +60,8 @@ const defaultConfig = { steps: 1, //percentage of volume to change arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts globalShortcuts: { - enabled: false, // enable global shortcuts - volumeUp: "Shift+PageUp", // Keybind default can be changed - volumeDown: "Shift+PageDown" + volumeUp: "", + volumeDown: "" }, savedVolume: undefined //plugin save volume between session here }, diff --git a/config/store.js b/config/store.js index f66c1f9a..d6451cee 100644 --- a/config/store.js +++ b/config/store.js @@ -8,6 +8,27 @@ const migrations = { store.set("plugins.discord.listenAlong", true); } }, + ">=1.12.0": (store) => { + const options = store.get("plugins.shortcuts") + let updated = false; + for (const optionType of ["global", "local"]) { + if (Array.isArray(options[optionType])) { + const updatedOptions = {}; + for (const optionObject of options[optionType]) { + if (optionObject.action && optionObject.shortcut) { + updatedOptions[optionObject.action] = optionObject.shortcut; + } + } + + options[optionType] = updatedOptions; + updated = true; + } + } + + if (updated) { + store.set("plugins.shortcuts", options); + } + }, ">=1.11.0": (store) => { if (store.get("options.resumeOnStart") === undefined) { store.set("options.resumeOnStart", true); diff --git a/index.js b/index.js index c54d2555..90096ecf 100644 --- a/index.js +++ b/index.js @@ -39,7 +39,9 @@ if (config.get("options.proxy")) { } // Adds debug features like hotkeys for triggering dev tools and reload -require("electron-debug")(); +require("electron-debug")({ + showDevTools: false //disable automatic devTools on new window +}); // Prevent window being garbage collected let mainWindow; @@ -60,7 +62,7 @@ function onClosed() { function loadPlugins(win) { injectCSS(win.webContents, path.join(__dirname, "youtube-music.css")); - win.webContents.on("did-finish-load", () => { + win.webContents.once("did-finish-load", () => { if (is.dev()) { console.log("did finish load"); win.webContents.openDevTools(); diff --git a/menu.js b/menu.js index 72b2cc66..afea532d 100644 --- a/menu.js +++ b/menu.js @@ -7,6 +7,9 @@ const is = require("electron-is"); const { getAllPlugins } = require("./plugins/utils"); const config = require("./config"); +const prompt = require("custom-electron-prompt"); +const promptOptions = require("./providers/prompt-options"); + // true only if in-app-menu was loaded on launch const inAppMenuActive = config.plugins.isEnabled("in-app-menu"); @@ -76,6 +79,14 @@ const mainMenuTemplate = (win) => { config.set("options.resumeOnStart", item.checked); }, }, + { + label: "Remove upgrade button", + type: "checkbox", + checked: config.get("options.removeUpgradeButton"), + click: (item) => { + config.set("options.removeUpgradeButton", item.checked); + }, + }, ...(is.windows() || is.linux() ? [ { @@ -149,6 +160,14 @@ const mainMenuTemplate = (win) => { { label: "Advanced options", submenu: [ + { + label: "Proxy", + type: "checkbox", + checked: !!config.get("options.proxy"), + click: (item) => { + setProxy(item, win); + }, + }, { label: "Disable hardware acceleration", type: "checkbox", @@ -275,3 +294,25 @@ module.exports.setApplicationMenu = (win) => { const menu = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(menu); }; + +async function setProxy(item, win) { + const output = await prompt({ + title: 'Set Proxy', + label: 'Enter Proxy Address: (leave empty to disable)', + value: config.get("options.proxy"), + type: 'input', + inputAttrs: { + type: 'url', + placeholder: "Example: 'socks5://127.0.0.1:9999" + }, + width: 450, + ...promptOptions() + }, win); + + if (typeof output === "string") { + config.set("options.proxy", output); + item.checked = output !== ""; + } else { //user pressed cancel + item.checked = !item.checked; //reset checkbox + } +} diff --git a/package.json b/package.json index 71063a70..3c2af2eb 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,8 @@ "build:mac": "yarn run clean && electron-builder --mac", "build:win": "yarn run clean && electron-builder --win", "lint": "xo", - "plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm", + "plugins": "yarn run plugin:adblocker", "plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js", - "plugin:autoconfirm": "yarn run generate:package YoutubeNonStop", "release:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github", "release:mac": "yarn run clean && electron-builder --mac -p always", "release:win": "yarn run clean && electron-builder --win -p always" @@ -67,9 +66,9 @@ "@cliqz/adblocker-electron": "^1.22.6", "@ffmpeg/core": "^0.10.0", "@ffmpeg/ffmpeg": "^0.10.0", - "YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0", "async-mutex": "^0.3.2", "browser-id3-writer": "^4.4.0", + "custom-electron-prompt": "^1.1.0", "chokidar": "^3.5.2", "custom-electron-titlebar": "^3.2.7", "discord-rpc": "^3.2.0", diff --git a/plugins/audio-compressor/front.js b/plugins/audio-compressor/front.js new file mode 100644 index 00000000..4f41a4d7 --- /dev/null +++ b/plugins/audio-compressor/front.js @@ -0,0 +1,25 @@ +const applyCompressor = () => { + const videoElement = document.querySelector("video"); + + // If video element is not loaded yet try again + if(videoElement === null) { + setTimeout(applyCompressor, 500); + return; + } + + const audioContext = new AudioContext(); + + let compressor = audioContext.createDynamicsCompressor(); + compressor.threshold.value = -50; + compressor.ratio.value = 12; + compressor.knee.value = 40; + compressor.attack.value = 0; + compressor.release.value = 0.25; + + const source = audioContext.createMediaElementSource(videoElement); + + source.connect(compressor); + compressor.connect(audioContext.destination); +}; + +module.exports = applyCompressor; \ No newline at end of file diff --git a/plugins/auto-confirm-when-paused/front.js b/plugins/auto-confirm-when-paused/front.js deleted file mode 100644 index 0ead302e..00000000 --- a/plugins/auto-confirm-when-paused/front.js +++ /dev/null @@ -1,12 +0,0 @@ -// Define global chrome object to be compliant with the extension code -global.chrome = { - runtime: { - getManifest: () => ({ - version: 1 - }) - } -}; - -module.exports = () => { - require("YoutubeNonStop/autoconfirm.js"); -}; diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 3c3ab5fd..9dc6f9fe 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -145,7 +145,4 @@ module.exports.clear = () => { }; module.exports.connect = connect; module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb); -/** - * @type {Info} - */ -module.exports.info = Object.defineProperties({}, Object.keys(info).reduce((o, k) => ({ ...o, [k]: { enumerable: true, get: () => info[k] } }), {})); +module.exports.isConnected = () => info.rpc !== null; diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js index 9750dabe..49a087d6 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.js @@ -1,6 +1,6 @@ const { setOptions } = require("../../config/plugins"); const { edit } = require("../../config"); -const { clear, info, connect, registerRefresh } = require("./back"); +const { clear, connect, registerRefresh, isConnected } = require("./back"); let hasRegisterred = false; @@ -12,8 +12,8 @@ module.exports = (win, options, refreshMenu) => { return [ { - label: info.rpc !== null ? "Connected" : "Reconnect", - enabled: info.rpc === null, + label: isConnected() ? "Connected" : "Reconnect", + enabled: !isConnected(), click: connect, }, { diff --git a/plugins/precise-volume/back.js b/plugins/precise-volume/back.js index 93ebea9f..f891ce32 100644 --- a/plugins/precise-volume/back.js +++ b/plugins/precise-volume/back.js @@ -1,23 +1,9 @@ -const { isEnabled } = require("../../config/plugins"); - /* This is used to determine if plugin is actually active (not if its only enabled in options) */ let enabled = false; -module.exports = (win) => { - enabled = true; +module.exports = () => enabled = true; - // youtube-music register some of the target listeners after DOMContentLoaded - // did-finish-load is called after all elements finished loading, including said listeners - // Thats the reason the timing is controlled from main - win.webContents.once("did-finish-load", () => { - win.webContents.send("restoreAddEventListener"); - win.webContents.send("setupVideoPlayerVolumeMousewheel", !isEnabled("hide-video-player")); - }); -}; - -module.exports.enabled = () => { - return enabled; -}; +module.exports.enabled = () => enabled; diff --git a/plugins/precise-volume/front.js b/plugins/precise-volume/front.js index 7195b5bf..84241fb8 100644 --- a/plugins/precise-volume/front.js +++ b/plugins/precise-volume/front.js @@ -3,27 +3,73 @@ const { ipcRenderer, remote } = require("electron"); const { setOptions } = require("../../config/plugins"); function $(selector) { return document.querySelector(selector); } +let api; module.exports = (options) => { + document.addEventListener('apiLoaded', e => { + api = e.detail; + firstRun(options); + }) +}; + +/** Restore saved volume and setup tooltip */ +function firstRun(options) { + if (typeof options.savedVolume === "number") { + // Set saved volume as tooltip + setTooltip(options.savedVolume); + + if (api.getVolume() !== options.savedVolume) { + api.setVolume(options.savedVolume); + } + } setupPlaybar(options); - setupSliderObserver(options); - setupLocalArrowShortcuts(options); - if (options.globalShortcuts?.enabled) { - setupGlobalShortcuts(options); + setupGlobalShortcuts(options); + + const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none"; + injectVolumeHud(noVid); + if (!noVid) { + setupVideoPlayerOnwheel(options); + } +} + +function injectVolumeHud(noVid) { + if (noVid) { + const position = "top: 18px; right: 60px; z-index: 999; position: absolute;"; + const mainStyle = "font-size: xx-large; padding: 10px; transition: opacity 1s"; + + $(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend", + ``) + } else { + const position = `top: 10px; left: 10px; z-index: 999; position: absolute;`; + const mainStyle = "font-size: xxx-large; padding: 10px; transition: opacity 0.6s; webkit-text-stroke: 1px black; font-weight: 600;"; + + $("#song-video").insertAdjacentHTML('afterend', + ``) + } +} + +let hudFadeTimeout; + +function showVolumeHud(volume) { + let volumeHud = $("#volumeHud"); + if (!volumeHud) return; + + volumeHud.textContent = volume + '%'; + volumeHud.style.opacity = 1; + + if (hudFadeTimeout) { + clearTimeout(hudFadeTimeout); } - firstRun(options); - - // This way the ipc listener gets cleared either way - ipcRenderer.once("setupVideoPlayerVolumeMousewheel", (_event, toEnable) => { - if (toEnable) - setupVideoPlayerOnwheel(options); - }); -}; + hudFadeTimeout = setTimeout(() => { + volumeHud.style.opacity = 0; + hudFadeTimeout = null; + }, 2000); +} /** Add onwheel event to video player */ function setupVideoPlayerOnwheel(options) { @@ -34,35 +80,20 @@ function setupVideoPlayerOnwheel(options) { }); } -function toPercent(volume) { - return Math.round(Number.parseFloat(volume) * 100); -} - function saveVolume(volume, options) { options.savedVolume = volume; - setOptions("precise-volume", options); + writeOptions(options); } -/** Restore saved volume and setup tooltip */ -function firstRun(options) { - const videoStream = $(".video-stream"); - const slider = $("#volume-slider"); - // Those elements load abit after DOMContentLoaded - if (videoStream && slider) { - // Set saved volume IF it pass checks - if (options.savedVolume - && options.savedVolume >= 0 && options.savedVolume <= 100 - && Math.abs(slider.value - options.savedVolume) < 5 - // If plugin was disabled and volume changed then diff>4 - ) { - videoStream.volume = options.savedVolume / 100; - slider.value = options.savedVolume; - } - // Set current volume as tooltip - setTooltip(toPercent(videoStream.volume)); - } else { - setTimeout(firstRun, 500, options); // Try again in 500 milliseconds - } +//without this function it would rewrite config 20 time when volume change by 20 +let writeTimeout; +function writeOptions(options) { + if (writeTimeout) clearTimeout(writeTimeout); + + writeTimeout = setTimeout(() => { + setOptions("precise-volume", options); + writeTimeout = null; + }, 1500) } /** Add onwheel event to play bar and also track if play bar is hovered*/ @@ -83,32 +114,63 @@ function setupPlaybar(options) { playerbar.addEventListener("mouseleave", () => { playerbar.classList.remove("on-hover"); }); + + setupSliderObserver(options); +} + +/** Save volume + Update the volume tooltip when volume-slider is manually changed */ +function setupSliderObserver(options) { + const sliderObserver = new MutationObserver(mutations => { + for (const mutation of mutations) { + // This checks that volume-slider was manually set + if (mutation.oldValue !== mutation.target.value && + (typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) { + // Diff>4 means it was manually set + setTooltip(mutation.target.value); + saveVolume(mutation.target.value, options); + } + } + }); + + // Observing only changes in 'value' of volume-slider + sliderObserver.observe($("#volume-slider"), { + attributeFilter: ["value"], + attributeOldValue: true + }); } /** if (toIncrease = false) then volume decrease */ function changeVolume(toIncrease, options) { - // Need to change both the actual volume and the slider - const videoStream = $(".video-stream"); - const slider = $("#volume-slider"); // Apply volume change if valid - const steps = (options.steps || 1) / 100; - videoStream.volume = toIncrease ? - Math.min(videoStream.volume + steps, 1) : - Math.max(videoStream.volume - steps, 0); + const steps = (options.steps || 1); + api.setVolume(toIncrease ? + Math.min(api.getVolume() + steps, 100) : + Math.max(api.getVolume() - steps, 0)); // Save the new volume - saveVolume(toPercent(videoStream.volume), options); - // Slider value automatically rounds to multiples of 5 - slider.value = options.savedVolume; + saveVolume(api.getVolume(), options); + + // change slider position (important) + updateVolumeSlider(options); + // Change tooltips to new value setTooltip(options.savedVolume); - // Show volume slider on volume change - showVolumeSlider(slider); + // Show volume slider + showVolumeSlider(); + // Show volume HUD + showVolumeHud(options.savedVolume); +} + +function updateVolumeSlider(options) { + // Slider value automatically rounds to multiples of 5 + $("#volume-slider").value = options.savedVolume > 0 && options.savedVolume < 5 ? + 5 : options.savedVolume; } let volumeHoverTimeoutID; -function showVolumeSlider(slider) { +function showVolumeSlider() { + const slider = $("#volume-slider"); // This class display the volume slider if not in minimized mode slider.classList.add("on-hover"); // Reset timeout if previous one hasn't completed @@ -124,27 +186,6 @@ function showVolumeSlider(slider) { }, 3000); } -/** Save volume + Update the volume tooltip when volume-slider is manually changed */ -function setupSliderObserver(options) { - const sliderObserver = new MutationObserver(mutations => { - for (const mutation of mutations) { - // This checks that volume-slider was manually set - if (mutation.oldValue !== mutation.target.value && - (!options.savedVolume || Math.abs(options.savedVolume - mutation.target.value) > 4)) { - // Diff>4 means it was manually set - setTooltip(mutation.target.value); - saveVolume(mutation.target.value, options); - } - } - }); - - // Observing only changes in 'value' of volume-slider - sliderObserver.observe($("#volume-slider"), { - attributeFilter: ["value"], - attributeOldValue: true - }); -} - // Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small) const tooltipTargets = [ "#volume-slider", diff --git a/plugins/precise-volume/menu.js b/plugins/precise-volume/menu.js index d4ed37a7..06ee84b9 100644 --- a/plugins/precise-volume/menu.js +++ b/plugins/precise-volume/menu.js @@ -1,13 +1,16 @@ const { enabled } = require("./back"); const { setOptions } = require("../../config/plugins"); +const prompt = require("custom-electron-prompt"); +const promptOptions = require("../../providers/prompt-options"); + module.exports = (win, options) => [ { - label: "Arrowkeys controls", + label: "Local Arrowkeys Controls", type: "checkbox", checked: !!options.arrowsShortcut, - click: (item) => { - // Dynamically change setting if plugin enabled + click: item => { + // Dynamically change setting if plugin is enabled if (enabled()) { win.webContents.send("setArrowsShortcut", item.checked); } else { // Fallback to usual method if disabled @@ -15,5 +18,61 @@ module.exports = (win, options) => [ setOptions("precise-volume", options); } } + }, + { + label: "Global Hotkeys", + type: "checkbox", + checked: !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown, + click: item => promptGlobalShortcuts(win, options, item) + }, + { + label: "Set Custom Volume Steps", + click: () => promptVolumeSteps(win, options) } ]; + +// Helper function for globalShortcuts prompt +const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ || undefined }; }; + +async function promptVolumeSteps(win, options) { + const output = await prompt({ + title: "Volume Steps", + label: "Choose Volume Increase/Decrease Steps", + value: options.steps || 1, + type: "counter", + counterOptions: { minimum: 0, maximum: 100, multiFire: true }, + width: 380, + ...promptOptions() + }, win) + + if (output || output === 0) { // 0 is somewhat valid + options.steps = output; + setOptions("precise-volume", options); + } +} + +async function promptGlobalShortcuts(win, options, item) { + const output = await prompt({ + title: "Global Volume Keybinds", + label: "Choose Global Volume Keybinds:", + type: "keybind", + keybindOptions: [ + kb("Increase Volume", "volumeUp", options.globalShortcuts?.volumeUp), + kb("Decrease Volume", "volumeDown", options.globalShortcuts?.volumeDown) + ], + ...promptOptions() + }, win) + + if (output) { + for (const { value, accelerator } of output) { + options.globalShortcuts[value] = accelerator; + } + + setOptions("precise-volume", options); + + item.checked = !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown; + } else { + // Reset checkbox if prompt was canceled + item.checked = !item.checked; + } +} diff --git a/plugins/precise-volume/preload.js b/plugins/precise-volume/preload.js index edc5f20c..6a0fd482 100644 --- a/plugins/precise-volume/preload.js +++ b/plugins/precise-volume/preload.js @@ -24,10 +24,10 @@ function overrideAddEventListener() { module.exports = () => { overrideAddEventListener(); - // Restore original function after did-finish-load to avoid keeping Element.prototype altered - ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded + // Restore original function after finished loading to avoid keeping Element.prototype altered + window.addEventListener('load', () => { Element.prototype.addEventListener = Element.prototype._addEventListener; Element.prototype._addEventListener = undefined; ignored = undefined; - }); + }, { once: true }); }; diff --git a/plugins/shortcuts/back.js b/plugins/shortcuts/back.js index ba12ba42..c32d0f8d 100644 --- a/plugins/shortcuts/back.js +++ b/plugins/shortcuts/back.js @@ -1,9 +1,11 @@ const { globalShortcut } = require("electron"); const is = require("electron-is"); const electronLocalshortcut = require("electron-localshortcut"); - const getSongControls = require("../../providers/song-controls"); const { setupMPRIS } = require("./mpris"); +const registerCallback = require("../../providers/song-info"); + +let player; function _registerGlobalShortcut(webContents, shortcut, action) { globalShortcut.register(shortcut, () => { @@ -21,47 +23,89 @@ function registerShortcuts(win, options) { const songControls = getSongControls(win); const { playPause, next, previous, search } = songControls; - _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause); - _registerGlobalShortcut(win.webContents, "MediaNextTrack", next); - _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous); + if (options.overrideMediaKeys) { + _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause); + _registerGlobalShortcut(win.webContents, "MediaNextTrack", next); + _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous); + } + _registerLocalShortcut(win, "CommandOrControl+F", search); _registerLocalShortcut(win, "CommandOrControl+L", search); + registerCallback(songInfo => { + if (player) { + player.metadata = { + 'mpris:length': songInfo.songDuration * 60 * 1000 * 1000, // In microseconds + 'mpris:artUrl': songInfo.imageSrc, + 'xesam:title': songInfo.title, + 'xesam:artist': songInfo.artist + }; + if (!songInfo.isPaused) { + player.playbackStatus = "Playing" + } + } + } + ) if (is.linux()) { try { - const player = setupMPRIS(); + const MPRISPlayer = setupMPRIS(); - player.on("raise", () => { + MPRISPlayer.on("raise", () => { win.setSkipTaskbar(false); win.show(); }); - player.on("playpause", playPause); - player.on("next", next); - player.on("previous", previous); + MPRISPlayer.on("play", () => { + if (MPRISPlayer.playbackStatus !== 'Playing') { + MPRISPlayer.playbackStatus = 'Playing'; + playPause() + } + }); + MPRISPlayer.on("pause", () => { + if (MPRISPlayer.playbackStatus !== 'Paused') { + MPRISPlayer.playbackStatus = 'Paused'; + playPause() + } + }); + MPRISPlayer.on("next", () => { + next() + }); + MPRISPlayer.on("previous", () => { + previous() + }); + + player = MPRISPlayer + } catch (e) { console.warn("Error in MPRIS", e); } } const { global, local } = options; - (global || []).forEach(({ shortcut, action }) => { - console.debug("Registering global shortcut", shortcut, ":", action); - if (!action || !songControls[action]) { - console.warn("Invalid action", action); - return; - } + const shortcutOptions = { global, local }; - _registerGlobalShortcut(win.webContents, shortcut, songControls[action]); - }); - (local || []).forEach(({ shortcut, action }) => { - console.debug("Registering local shortcut", shortcut, ":", action); - if (!action || !songControls[action]) { - console.warn("Invalid action", action); - return; - } + for (const optionType in shortcutOptions) { + registerAllShortcuts(shortcutOptions[optionType], optionType); + } - _registerLocalShortcut(win, shortcut, songControls[action]); - }); + function registerAllShortcuts(container, type) { + for (const action in container) { + if (!container[action]) { + continue; // Action accelerator is empty + } + + console.debug(`Registering ${type} shortcut`, container[action], ":", action); + if (!songControls[action]) { + console.warn("Invalid action", action); + continue; + } + + if (type === "global") { + _registerGlobalShortcut(win.webContents, container[action], songControls[action]); + } else { // type === "local" + _registerLocalShortcut(win, local[action], songControls[action]); + } + } + } } module.exports = registerShortcuts; diff --git a/plugins/shortcuts/menu.js b/plugins/shortcuts/menu.js new file mode 100644 index 00000000..20f21233 --- /dev/null +++ b/plugins/shortcuts/menu.js @@ -0,0 +1,53 @@ +const { setOptions } = require("../../config/plugins"); +const prompt = require("custom-electron-prompt"); +const promptOptions = require("../../providers/prompt-options"); + +module.exports = (win, options) => [ + { + label: "Set Global Song Controls", + click: () => promptKeybind(options, win) + }, + { + label: "Override MediaKeys", + type: "checkbox", + checked: options.overrideMediaKeys, + click: item => setOption(options, "overrideMediaKeys", item.checked) + } +]; + +function setOption(options, key = null, newValue = null) { + if (key && newValue !== null) { + options[key] = newValue; + } + + setOptions("shortcuts", options); +} + +// Helper function for keybind prompt +const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ }; }; + +async function promptKeybind(options, win) { + const output = await prompt({ + title: "Global Keybinds", + label: "Choose Global Keybinds for Songs Control:", + type: "keybind", + keybindOptions: [ // If default=undefined then no default is used + kb("Previous", "previous", options.global?.previous), + kb("Play / Pause", "playPause", options.global?.playPause), + kb("Next", "next", options.global?.next) + ], + height: 270, + ...promptOptions() + }, win); + + if (output) { + if (!options.global) { + options.global = {}; + } + for (const { value, accelerator } of output) { + options.global[value] = accelerator; + } + setOption(options); + } + // else -> pressed cancel +} diff --git a/preload.js b/preload.js index 5a09c845..d100e954 100644 --- a/preload.js +++ b/preload.js @@ -5,11 +5,12 @@ const { remote } = require("electron"); const config = require("./config"); const { fileExists } = require("./plugins/utils"); const setupFrontLogger = require("./providers/front-logger"); -const setupSongControl = require("./providers/song-controls-front"); const setupSongInfo = require("./providers/song-info-front"); const plugins = config.plugins.getEnabled(); +let api; + plugins.forEach(([plugin, options]) => { const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js"); fileExists(preloadPath, () => { @@ -38,16 +39,46 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + // wait for complete load of youtube api + listenForApiLoad(); + // inject song-info provider setupSongInfo(); - // inject song-control provider - setupSongControl(); - // inject front logger setupFrontLogger(); // Add action for reloading global.reload = () => remote.getCurrentWindow().webContents.loadURL(config.get("url")); + + // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min + setInterval(() => window._lact = Date.now(), 900000); }); + +function listenForApiLoad() { + api = document.querySelector('#movie_player'); + if (api) { + onApiLoaded(); + return; + } + + const observer = new MutationObserver(() => { + api = document.querySelector('#movie_player'); + if (api) { + observer.disconnect(); + onApiLoaded(); + } + }) + + observer.observe(document.documentElement, { childList: true, subtree: true }); +} + +function onApiLoaded() { + document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); + + // Remove upgrade button + if (config.get("options.removeUpgradeButton")) { + document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')?.style.display = "none"; + } +} diff --git a/providers/prompt-custom-titlebar.js b/providers/prompt-custom-titlebar.js new file mode 100644 index 00000000..affa9206 --- /dev/null +++ b/providers/prompt-custom-titlebar.js @@ -0,0 +1,14 @@ +const customTitlebar = require("custom-electron-titlebar"); + +module.exports = () => { + new customTitlebar.Titlebar({ + backgroundColor: customTitlebar.Color.fromHex("#050505"), + minimizable: false, + maximizable: false, + menu: null + }); + const mainStyle = document.querySelector("#container").style; + mainStyle.width = "100%"; + mainStyle.position = "fixed"; + mainStyle.border = "unset"; +}; diff --git a/providers/prompt-options.js b/providers/prompt-options.js new file mode 100644 index 00000000..16f02a99 --- /dev/null +++ b/providers/prompt-options.js @@ -0,0 +1,18 @@ +const path = require("path"); +const is = require("electron-is"); + +const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png"); +const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js"); + +const promptOptions = is.macOS() ? { + customStylesheet: "dark", + icon: iconPath +} : { + customStylesheet: "dark", + // The following are used for custom titlebar + frame: false, + customScript: customTitlebarPath, + enableRemoteModule: true +}; + +module.exports = () => promptOptions; diff --git a/providers/song-controls-front.js b/providers/song-controls-front.js deleted file mode 100644 index 1860e761..00000000 --- a/providers/song-controls-front.js +++ /dev/null @@ -1,18 +0,0 @@ -const { ipcRenderer } = require("electron"); - -let videoStream = document.querySelector(".video-stream"); -module.exports = () => { - ipcRenderer.on("playPause", () => { - if (!videoStream) { - videoStream = document.querySelector(".video-stream"); - } - - if (videoStream.paused) { - videoStream.play(); - } else { - videoStream.yns_pause ? - videoStream.yns_pause() : - videoStream.pause(); - } - }); -}; diff --git a/providers/song-controls.js b/providers/song-controls.js index 5d3ad953..7f43df11 100644 --- a/providers/song-controls.js +++ b/providers/song-controls.js @@ -12,7 +12,7 @@ module.exports = (win) => { // Playback previous: () => pressKey(win, "k"), next: () => pressKey(win, "j"), - playPause: () => win.webContents.send("playPause"), + playPause: () => pressKey(win, "space"), like: () => pressKey(win, "_"), dislike: () => pressKey(win, "+"), go10sBack: () => pressKey(win, "h"), diff --git a/readme.md b/readme.md index 4a704638..e6e57401 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,6 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr ## Available plugins: - **Ad Blocker**: block all ads and tracking out of the box -- **Auto confirm when paused**: when the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) modal appears, automatically click "Yes" - **Blur navigation bar**: makes navigation bar transparent and blurry - **Disable autoplay**: makes every song start in "paused" mode - [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png) @@ -52,6 +51,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr - [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts - **Taskbar media control**: control app from your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png) - **Touchbar**: custom TouchBar layout for macOS +- **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 ## Dev diff --git a/yarn.lock b/yarn.lock index 74e407dc..8230315b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1518,10 +1518,6 @@ dependencies: "@wdio/logger" "6.10.10" -"YoutubeNonStop@git://github.com/lawfx/YoutubeNonStop.git#v0.9.0": - version "0.0.0" - resolved "git://github.com/lawfx/YoutubeNonStop.git#7b6b97b31bb3fd2078179660db0fd3fcc7062259" - abab@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -2960,6 +2956,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +custom-electron-prompt@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/custom-electron-prompt/-/custom-electron-prompt-1.1.0.tgz#611b790047c91f6b532c7861355a0e1f9a81aef2" + integrity sha512-YZYmwZnMOdoWROUlJ+rEMHYsp4XJNNqLj6sZnx5aKBJ8cprEjKP4L5wfo6U+yyX/L9fxVOtvYD0Mp8ki5I9Kow== + custom-electron-titlebar@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-3.2.7.tgz#fb249d6180cbda074b1d392bea755fa0743012a8" diff --git a/youtube-music.css b/youtube-music.css index fe5eb326..67a74dc0 100644 --- a/youtube-music.css +++ b/youtube-music.css @@ -28,3 +28,9 @@ ytmusic-search-box.ytmusic-nav-bar { ytmusic-mealbar-promo-renderer { display: none !important; } + +/* Disable Image Selection */ +img { + -webkit-user-select: none; + user-select: none; +}