From 7591f135050eda36489e5264ae00a19b3b3b552d Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Mon, 27 Nov 2023 05:09:33 +0900 Subject: [PATCH] in-app-menu --- src/plugins/in-app-menu/index.ts | 27 ++- src/plugins/in-app-menu/main.ts | 124 +++++----- src/plugins/in-app-menu/menu.ts | 10 +- src/plugins/in-app-menu/renderer.ts | 349 ++++++++++++++-------------- 4 files changed, 256 insertions(+), 254 deletions(-) diff --git a/src/plugins/in-app-menu/index.ts b/src/plugins/in-app-menu/index.ts index be3921a4..52631d54 100644 --- a/src/plugins/in-app-menu/index.ts +++ b/src/plugins/in-app-menu/index.ts @@ -1,21 +1,28 @@ import titlebarStyle from './titlebar.css?inline'; +import { createPlugin } from '@/utils'; +import { onMainLoad } from '@/plugins/in-app-menu/main'; +import { onMenu } from '@/plugins/in-app-menu/menu'; +import { onPlayerApiReady, onRendererLoad } from '@/plugins/in-app-menu/renderer'; -import { createPluginBuilder } from '../utils/builder'; +export interface InAppMenuConfig { + enabled: boolean; + hideDOMWindowControls: boolean; +} -const builder = createPluginBuilder('in-app-menu', { +export default createPlugin({ name: 'In-App Menu', restartNeeded: true, config: { enabled: false, hideDOMWindowControls: false, + } as InAppMenuConfig, + stylesheets: [titlebarStyle], + menu: onMenu, + + backend: onMainLoad, + renderer: { + start: onRendererLoad, + onPlayerApiReady, }, - styles: [titlebarStyle], }); -export default builder; - -declare global { - interface PluginBuilderList { - [builder.id]: typeof builder; - } -} diff --git a/src/plugins/in-app-menu/main.ts b/src/plugins/in-app-menu/main.ts index 5510b015..41b4a332 100644 --- a/src/plugins/in-app-menu/main.ts +++ b/src/plugins/in-app-menu/main.ts @@ -2,75 +2,71 @@ import { register } from 'electron-localshortcut'; import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron'; -import builder from './index'; +import { BackendContext } from '@/types/contexts'; +import { InAppMenuConfig } from '@/plugins/in-app-menu/index'; -export default builder.createMain(({ handle, send }) => { +export const onMainLoad = ({ window: win, ipc: { handle, send } }: BackendContext) => { + win.on('close', () => { + send('close-all-in-app-menu-panel'); + }); - return { - onLoad(win) { - win.on('close', () => { - send('close-all-in-app-menu-panel'); - }); + win.once('ready-to-show', () => { + register(win, '`', () => { + send('toggle-in-app-menu'); + }); + }); - win.once('ready-to-show', () => { - register(win, '`', () => { - send('toggle-in-app-menu'); - }); - }); + handle( + 'get-menu', + () => JSON.parse(JSON.stringify( + Menu.getApplicationMenu(), + (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), + ), + ); - handle( - 'get-menu', - () => JSON.parse(JSON.stringify( - Menu.getApplicationMenu(), - (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), - ), - ); + const getMenuItemById = (commandId: number): MenuItem | null => { + const menu = Menu.getApplicationMenu(); - const getMenuItemById = (commandId: number): MenuItem | null => { - const menu = Menu.getApplicationMenu(); + let target: MenuItem | null = null; + const stack = [...menu?.items ?? []]; + while (stack.length > 0) { + const now = stack.shift(); + now?.submenu?.items.forEach((item) => stack.push(item)); - let target: MenuItem | null = null; - const stack = [...menu?.items ?? []]; - while (stack.length > 0) { - const now = stack.shift(); - now?.submenu?.items.forEach((item) => stack.push(item)); + if (now?.commandId === commandId) { + target = now; + break; + } + } - if (now?.commandId === commandId) { - target = now; - break; - } - } - - return target; - }; - - ipcMain.handle('menu-event', (event, commandId: number) => { - const target = getMenuItemById(commandId); - if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender); - }); - - handle('get-menu-by-id', (commandId: number) => { - const result = getMenuItemById(commandId); - - return JSON.parse(JSON.stringify( - result, - (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), - ); - }); - - handle('window-is-maximized', () => win.isMaximized()); - - handle('window-close', () => win.close()); - handle('window-minimize', () => win.minimize()); - handle('window-maximize', () => win.maximize()); - win.on('maximize', () => send('window-maximize')); - handle('window-unmaximize', () => win.unmaximize()); - win.on('unmaximize', () => send('window-unmaximize')); - - handle('image-path-to-data-url', (imagePath: string) => { - const nativeImageIcon = nativeImage.createFromPath(imagePath); - return nativeImageIcon?.toDataURL(); - }); - }, + return target; }; -}); + + ipcMain.handle('menu-event', (event, commandId: number) => { + const target = getMenuItemById(commandId); + if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender); + }); + + handle('get-menu-by-id', (commandId: number) => { + const result = getMenuItemById(commandId); + + return JSON.parse(JSON.stringify( + result, + (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), + ); + }); + + handle('window-is-maximized', () => win.isMaximized()); + + handle('window-close', () => win.close()); + handle('window-minimize', () => win.minimize()); + handle('window-maximize', () => win.maximize()); + win.on('maximize', () => send('window-maximize')); + handle('window-unmaximize', () => win.unmaximize()); + win.on('unmaximize', () => send('window-unmaximize')); + + handle('image-path-to-data-url', (imagePath: string) => { + const nativeImageIcon = nativeImage.createFromPath(imagePath); + return nativeImageIcon?.toDataURL(); + }); +}; diff --git a/src/plugins/in-app-menu/menu.ts b/src/plugins/in-app-menu/menu.ts index 9c91889d..1f5e7f76 100644 --- a/src/plugins/in-app-menu/menu.ts +++ b/src/plugins/in-app-menu/menu.ts @@ -1,10 +1,12 @@ import is from 'electron-is'; -import builder from './index'; +import { setMenuOptions } from '@/config/plugins'; -import { setMenuOptions } from '../../config/plugins'; +import type { InAppMenuConfig } from './index'; +import type { MenuContext } from '@/types/contexts'; +import type { MenuTemplate } from '@/menu'; -export default builder.createMenu(async ({ getConfig }) => { +export const onMenu = async ({ getConfig }: MenuContext): Promise => { const config = await getConfig(); if (is.linux()) { @@ -22,4 +24,4 @@ export default builder.createMenu(async ({ getConfig }) => { } return []; -}); +}; diff --git a/src/plugins/in-app-menu/renderer.ts b/src/plugins/in-app-menu/renderer.ts index a30f2144..182d22db 100644 --- a/src/plugins/in-app-menu/renderer.ts +++ b/src/plugins/in-app-menu/renderer.ts @@ -6,190 +6,187 @@ import minimizeRaw from './assets/minimize.svg?inline'; import maximizeRaw from './assets/maximize.svg?inline'; import unmaximizeRaw from './assets/unmaximize.svg?inline'; -import builder from './index'; - import type { Menu } from 'electron'; +import type { RendererContext } from '@/types/contexts'; +import type { InAppMenuConfig } from '@/plugins/in-app-menu/index'; + const isMacOS = navigator.userAgent.includes('Macintosh'); const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS; -export default builder.createRenderer(({ getConfig, invoke, on }) => { - return { - async onLoad() { - const config = await getConfig(); +export const onRendererLoad = async ({ getConfig, ipc: { invoke, on } }: RendererContext) => { + const config = await getConfig(); - const hideDOMWindowControls = config.hideDOMWindowControls; + const hideDOMWindowControls = config.hideDOMWindowControls; - let hideMenu = window.mainConfig.get('options.hideMenu'); - const titleBar = document.createElement('title-bar'); - const navBar = document.querySelector('#nav-bar-background'); - let maximizeButton: HTMLButtonElement; - let panelClosers: (() => void)[] = []; - if (isMacOS) titleBar.style.setProperty('--offset-left', '70px'); + let hideMenu = window.mainConfig.get('options.hideMenu'); + const titleBar = document.createElement('title-bar'); + const navBar = document.querySelector('#nav-bar-background'); + let maximizeButton: HTMLButtonElement; + let panelClosers: (() => void)[] = []; + if (isMacOS) titleBar.style.setProperty('--offset-left', '70px'); - const logo = document.createElement('img'); - const close = document.createElement('img'); - const minimize = document.createElement('img'); - const maximize = document.createElement('img'); - const unmaximize = document.createElement('img'); + const logo = document.createElement('img'); + const close = document.createElement('img'); + const minimize = document.createElement('img'); + const maximize = document.createElement('img'); + const unmaximize = document.createElement('img'); - if (window.ELECTRON_RENDERER_URL) { - logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw; - close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw; - minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw; - maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw; - unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw; - } else { - logo.src = logoRaw; - close.src = closeRaw; - minimize.src = minimizeRaw; - maximize.src = maximizeRaw; - unmaximize.src = unmaximizeRaw; - } + if (window.ELECTRON_RENDERER_URL) { + logo.src = window.ELECTRON_RENDERER_URL + '/' + logoRaw; + close.src = window.ELECTRON_RENDERER_URL + '/' + closeRaw; + minimize.src = window.ELECTRON_RENDERER_URL + '/' + minimizeRaw; + maximize.src = window.ELECTRON_RENDERER_URL + '/' + maximizeRaw; + unmaximize.src = window.ELECTRON_RENDERER_URL + '/' + unmaximizeRaw; + } else { + logo.src = logoRaw; + close.src = closeRaw; + minimize.src = minimizeRaw; + maximize.src = maximizeRaw; + unmaximize.src = unmaximizeRaw; + } - logo.classList.add('title-bar-icon'); - const logoClick = () => { - hideMenu = !hideMenu; - let visibilityStyle: string; - if (hideMenu) { - visibilityStyle = 'hidden'; - } else { - visibilityStyle = 'visible'; - } - const menus = document.querySelectorAll('menu-button'); - menus.forEach((menu) => { - menu.style.visibility = visibilityStyle; - }); - }; - logo.onclick = logoClick; - - on('toggle-in-app-menu', logoClick); - - if (!isMacOS) titleBar.appendChild(logo); - document.body.appendChild(titleBar); - - titleBar.appendChild(logo); - - const addWindowControls = async () => { - - // Create window control buttons - const minimizeButton = document.createElement('button'); - minimizeButton.classList.add('window-control'); - minimizeButton.appendChild(minimize); - minimizeButton.onclick = () => invoke('window-minimize'); - - maximizeButton = document.createElement('button'); - if (await invoke('window-is-maximized')) { - maximizeButton.classList.add('window-control'); - maximizeButton.appendChild(unmaximize); - } else { - maximizeButton.classList.add('window-control'); - maximizeButton.appendChild(maximize); - } - maximizeButton.onclick = async () => { - if (await invoke('window-is-maximized')) { - // change icon to maximize - maximizeButton.removeChild(maximizeButton.firstChild!); - maximizeButton.appendChild(maximize); - - // call unmaximize - await invoke('window-unmaximize'); - } else { - // change icon to unmaximize - maximizeButton.removeChild(maximizeButton.firstChild!); - maximizeButton.appendChild(unmaximize); - - // call maximize - await invoke('window-maximize'); - } - }; - - const closeButton = document.createElement('button'); - closeButton.classList.add('window-control'); - closeButton.appendChild(close); - closeButton.onclick = () => invoke('window-close'); - - // Create a container div for the window control buttons - const windowControlsContainer = document.createElement('div'); - windowControlsContainer.classList.add('window-controls-container'); - windowControlsContainer.appendChild(minimizeButton); - windowControlsContainer.appendChild(maximizeButton); - windowControlsContainer.appendChild(closeButton); - - // Add window control buttons to the title bar - titleBar.appendChild(windowControlsContainer); - }; - - if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); - - if (navBar) { - const observer = new MutationObserver((mutations) => { - mutations.forEach(() => { - titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); - document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); - }); - }); - - observer.observe(navBar, { attributes : true, attributeFilter : ['style'] }); - } - - const updateMenu = async () => { - const children = [...titleBar.children]; - children.forEach((child) => { - if (child !== logo) child.remove(); - }); - panelClosers = []; - - const menu = await invoke('get-menu'); - if (!menu) return; - - menu.items.forEach((menuItem) => { - const menu = document.createElement('menu-button'); - const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []); - panelClosers.push(closer); - - menu.append(menuItem.label); - titleBar.appendChild(menu); - if (hideMenu) { - menu.style.visibility = 'hidden'; - } - }); - if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); - }; - await updateMenu(); - - document.title = 'Youtube Music'; - - on('close-all-in-app-menu-panel', () => { - panelClosers.forEach((closer) => closer()); - }); - on('refresh-in-app-menu', () => updateMenu()); - on('window-maximize', () => { - if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { - maximizeButton.removeChild(maximizeButton.firstChild); - maximizeButton.appendChild(unmaximize); - } - }); - on('window-unmaximize', () => { - if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { - maximizeButton.removeChild(maximizeButton.firstChild); - maximizeButton.appendChild(unmaximize); - } - }); - - if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { - on('pip-toggle', () => { - updateMenu(); - }); - } - }, - // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) - onPlayerApiReady() { - const htmlHeadStyle = document.querySelector('head > div > style'); - if (htmlHeadStyle) { - // HACK: This is a hack to remove the scrollbar width - htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); - } - }, + logo.classList.add('title-bar-icon'); + const logoClick = () => { + hideMenu = !hideMenu; + let visibilityStyle: string; + if (hideMenu) { + visibilityStyle = 'hidden'; + } else { + visibilityStyle = 'visible'; + } + const menus = document.querySelectorAll('menu-button'); + menus.forEach((menu) => { + menu.style.visibility = visibilityStyle; + }); }; -}); + logo.onclick = logoClick; + + on('toggle-in-app-menu', logoClick); + + if (!isMacOS) titleBar.appendChild(logo); + document.body.appendChild(titleBar); + + titleBar.appendChild(logo); + + const addWindowControls = async () => { + + // Create window control buttons + const minimizeButton = document.createElement('button'); + minimizeButton.classList.add('window-control'); + minimizeButton.appendChild(minimize); + minimizeButton.onclick = () => invoke('window-minimize'); + + maximizeButton = document.createElement('button'); + if (await invoke('window-is-maximized')) { + maximizeButton.classList.add('window-control'); + maximizeButton.appendChild(unmaximize); + } else { + maximizeButton.classList.add('window-control'); + maximizeButton.appendChild(maximize); + } + maximizeButton.onclick = async () => { + if (await invoke('window-is-maximized')) { + // change icon to maximize + maximizeButton.removeChild(maximizeButton.firstChild!); + maximizeButton.appendChild(maximize); + + // call unmaximize + await invoke('window-unmaximize'); + } else { + // change icon to unmaximize + maximizeButton.removeChild(maximizeButton.firstChild!); + maximizeButton.appendChild(unmaximize); + + // call maximize + await invoke('window-maximize'); + } + }; + + const closeButton = document.createElement('button'); + closeButton.classList.add('window-control'); + closeButton.appendChild(close); + closeButton.onclick = () => invoke('window-close'); + + // Create a container div for the window control buttons + const windowControlsContainer = document.createElement('div'); + windowControlsContainer.classList.add('window-controls-container'); + windowControlsContainer.appendChild(minimizeButton); + windowControlsContainer.appendChild(maximizeButton); + windowControlsContainer.appendChild(closeButton); + + // Add window control buttons to the title bar + titleBar.appendChild(windowControlsContainer); + }; + + if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); + + if (navBar) { + const observer = new MutationObserver((mutations) => { + mutations.forEach(() => { + titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); + document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor); + }); + }); + + observer.observe(navBar, { attributes : true, attributeFilter : ['style'] }); + } + + const updateMenu = async () => { + const children = [...titleBar.children]; + children.forEach((child) => { + if (child !== logo) child.remove(); + }); + panelClosers = []; + + const menu = await invoke('get-menu') as Menu | null; + if (!menu) return; + + menu.items.forEach((menuItem) => { + const menu = document.createElement('menu-button'); + const [, { close: closer }] = createPanel(titleBar, menu, menuItem.submenu?.items ?? []); + panelClosers.push(closer); + + menu.append(menuItem.label); + titleBar.appendChild(menu); + if (hideMenu) { + menu.style.visibility = 'hidden'; + } + }); + if (isNotWindowsOrMacOS && !hideDOMWindowControls) await addWindowControls(); + }; + await updateMenu(); + + document.title = 'Youtube Music'; + + on('close-all-in-app-menu-panel', () => { + panelClosers.forEach((closer) => closer()); + }); + on('refresh-in-app-menu', () => updateMenu()); + on('window-maximize', () => { + if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { + maximizeButton.removeChild(maximizeButton.firstChild); + maximizeButton.appendChild(unmaximize); + } + }); + on('window-unmaximize', () => { + if (isNotWindowsOrMacOS && !hideDOMWindowControls && maximizeButton.firstChild) { + maximizeButton.removeChild(maximizeButton.firstChild); + maximizeButton.appendChild(unmaximize); + } + }); + + if (window.mainConfig.plugins.isEnabled('picture-in-picture')) { + on('pip-toggle', () => { + updateMenu(); + }); + } +}; + +export const onPlayerApiReady = () => { + const htmlHeadStyle = document.querySelector('head > div > style'); + if (htmlHeadStyle) { + // HACK: This is a hack to remove the scrollbar width + htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); + } +};