diff --git a/index.js b/index.js index 0d576231..96de5491 100644 --- a/index.js +++ b/index.js @@ -178,7 +178,12 @@ function createMainWindow() { win.on("move", () => { if (win.isMaximized()) return; let position = win.getPosition(); - lateSave("window-position", { x: position[0], y: position[1] }); + const isPiPEnabled = + config.plugins.isEnabled("picture-in-picture") && + config.plugins.getOptions("picture-in-picture")["isInPiP"]; + if (!isPiPEnabled) { + lateSave("window-position", { x: position[0], y: position[1] }); + } }); let winWasMaximized; @@ -191,7 +196,10 @@ function createMainWindow() { winWasMaximized = isMaximized; config.set("window-maximized", isMaximized); } - if (!isMaximized) { + const isPiPEnabled = + config.plugins.isEnabled("picture-in-picture") && + config.plugins.getOptions("picture-in-picture")["isInPiP"]; + if (!isMaximized && !isPiPEnabled) { lateSave("window-size", { width: windowSize[0], height: windowSize[1], diff --git a/plugins/picture-in-picture/back.js b/plugins/picture-in-picture/back.js new file mode 100644 index 00000000..7cda241f --- /dev/null +++ b/plugins/picture-in-picture/back.js @@ -0,0 +1,79 @@ +const path = require("path"); + +const { app, ipcMain } = require("electron"); + +const { setOptions } = require("../../config/plugins"); +const { injectCSS } = require("../utils"); + +let isInPiPMode = false; +let originalPosition; +let originalSize; + +const pipPosition = [10, 10]; +const pipSize = [450, 275]; + +const togglePiP = async (win) => { + isInPiPMode = !isInPiPMode; + setOptions("picture-in-picture", { isInPiP: isInPiPMode }); + + if (isInPiPMode) { + originalPosition = win.getPosition(); + originalSize = win.getSize(); + + win.webContents.on("before-input-event", blockShortcutsInPiP); + + win.setFullScreenable(false); + await win.webContents.executeJavaScript( + // Go fullscreen + ` + if (!document.querySelector("ytmusic-player-page").playerPageOpen_) { + document.querySelector(".toggle-player-page-button").click(); + } + document.querySelector(".fullscreen-button").click(); + document.querySelector("ytmusic-player-bar").classList.add("pip"); + ` + ); + win.setFullScreenable(true); + + app.dock?.hide(); + win.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + }); + app.dock?.show(); + win.setAlwaysOnTop(true, "screen-saver", 1); + } else { + win.webContents.removeListener("before-input-event", blockShortcutsInPiP); + + await win.webContents.executeJavaScript( + // Exit fullscreen + ` + document.querySelector(".exit-fullscreen-button").click(); + document.querySelector("ytmusic-player-bar").classList.remove("pip"); + ` + ); + + win.setVisibleOnAllWorkspaces(false); + win.setAlwaysOnTop(false); + } + + const [x, y] = isInPiPMode ? pipPosition : originalPosition; + const [w, h] = isInPiPMode ? pipSize : originalSize; + win.setPosition(x, y); + win.setSize(w, h); + + win.setWindowButtonVisibility?.(!isInPiPMode); +}; + +module.exports = (win) => { + injectCSS(win.webContents, path.join(__dirname, "style.css")); + ipcMain.on("picture-in-picture", async () => { + await togglePiP(win); + }); +}; + +const blockShortcutsInPiP = (event, input) => { + const blockedShortcuts = ["f", "escape"]; + if (blockedShortcuts.includes(input.key.toLowerCase())) { + event.preventDefault(); + } +}; diff --git a/plugins/picture-in-picture/front.js b/plugins/picture-in-picture/front.js new file mode 100644 index 00000000..637c7fb6 --- /dev/null +++ b/plugins/picture-in-picture/front.js @@ -0,0 +1,42 @@ +const { ipcRenderer } = require("electron"); + +const { getSongMenu } = require("../../providers/dom-elements"); +const { ElementFromFile, templatePath } = require("../utils"); + +let menu = null; +const pipButton = ElementFromFile( + templatePath(__dirname, "picture-in-picture.html") +); + +const observer = new MutationObserver(() => { + if (!menu) { + menu = getSongMenu(); + if (!menu) return; + } + if (menu.contains(pipButton)) return; + const menuUrl = document.querySelector( + 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint' + )?.href; + if (menuUrl && !menuUrl.includes("watch?")) return; + + menu.prepend(pipButton); +}); + +global.togglePictureInPicture = () => { + ipcRenderer.send("picture-in-picture"); +}; + +function observeMenu(options) { + document.addEventListener( + "apiLoaded", + () => { + observer.observe(document.querySelector("ytmusic-popup-container"), { + childList: true, + subtree: true, + }); + }, + { once: true, passive: true } + ); +} + +module.exports = observeMenu; diff --git a/plugins/picture-in-picture/style.css b/plugins/picture-in-picture/style.css new file mode 100644 index 00000000..adb962d8 --- /dev/null +++ b/plugins/picture-in-picture/style.css @@ -0,0 +1,11 @@ +ytmusic-player-bar.pip svg, +ytmusic-player-bar.pip yt-formatted-string { + filter: drop-shadow(2px 4px 6px black); + color: white; +} + +ytmusic-player-bar.pip ytmusic-player-expanding-menu { + border-radius: 30px; + background-color: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(5px) brightness(20%); +} diff --git a/plugins/picture-in-picture/templates/picture-in-picture.html b/plugins/picture-in-picture/templates/picture-in-picture.html new file mode 100644 index 00000000..6dd0440c --- /dev/null +++ b/plugins/picture-in-picture/templates/picture-in-picture.html @@ -0,0 +1,51 @@ + diff --git a/readme.md b/readme.md index 2700d9f8..10b6a279 100644 --- a/readme.md +++ b/readme.md @@ -63,6 +63,8 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr - **Notifications**: Display a notification when a song starts playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) are available on windows) +- **Picture in picture**: allows to switch the app to picture-in-picture mode + - **Playback Speed**: Listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png) - **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume steps