diff --git a/assets/youtube-music.svg b/assets/youtube-music.svg new file mode 100644 index 0000000000..ae7217862e --- /dev/null +++ b/assets/youtube-music.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.ts b/index.ts index 2eb343ce08..8a46ad284c 100644 --- a/index.ts +++ b/index.ts @@ -136,7 +136,12 @@ function createMainWindow() { sandbox: false, }), }, - frame: !is.macOS() && !useInlineMenu, + frame: !is.macOS() && !is.linux() && !useInlineMenu, + titleBarOverlay: { + color: '#00000000', + symbolColor: '#ffffff', + height: 36, + }, titleBarStyle: useInlineMenu ? 'hidden' : (is.macOS() diff --git a/package-lock.json b/package-lock.json index d050629417..c5e2b95f17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "butterchurn-presets": "2.4.7", "conf": "10.2.0", "custom-electron-prompt": "1.5.7", - "custom-electron-titlebar": "4.1.6", "electron-better-web-request": "1.0.1", "electron-debug": "3.2.0", "electron-is": "3.0.0", @@ -3184,14 +3183,6 @@ "electron": ">=10.0.0" } }, - "node_modules/custom-electron-titlebar": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/custom-electron-titlebar/-/custom-electron-titlebar-4.1.6.tgz", - "integrity": "sha512-AGULUZMxhEZDpl0Z1jfZzXgQEdhAPe8YET0dYQA/19t8oCrTFzF2PzdvJNCmxGU4Ai3jPWVeCPKg4vM7ffU0Mg==", - "peerDependencies": { - "electron": ">20" - } - }, "node_modules/dbus-next": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/dbus-next/-/dbus-next-0.9.2.tgz", diff --git a/package.json b/package.json index 0d764e97fc..ddd74ba545 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,6 @@ "butterchurn-presets": "2.4.7", "conf": "10.2.0", "custom-electron-prompt": "1.5.7", - "custom-electron-titlebar": "4.1.6", "electron-better-web-request": "1.0.1", "electron-debug": "3.2.0", "electron-is": "3.0.0", diff --git a/plugins/in-app-menu/back.ts b/plugins/in-app-menu/back.ts index 61a00cbd24..ff13e30d4a 100644 --- a/plugins/in-app-menu/back.ts +++ b/plugins/in-app-menu/back.ts @@ -1,27 +1,58 @@ import path from 'node:path'; import { register } from 'electron-localshortcut'; -// eslint-disable-next-line import/no-unresolved -import { attachTitlebarToWindow, setupTitlebar } from 'custom-electron-titlebar/main'; -import { BrowserWindow } from 'electron'; +import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron'; import { injectCSS } from '../utils'; - -setupTitlebar(); - // Tracks menu visibility - export default (win: BrowserWindow) => { - // Css for custom scrollbar + disable drag area(was causing bugs) - injectCSS(win.webContents, path.join(__dirname, 'style.css')); + injectCSS(win.webContents, path.join(__dirname, 'titlebar.css')); win.once('ready-to-show', () => { - attachTitlebarToWindow(win); - register(win, '`', () => { win.webContents.send('toggleMenu'); }); }); + + ipcMain.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(); + + 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; + } + } + + return target; + }; + + ipcMain.handle('menu-event', (event, commandId: number) => { + const target = getMenuItemById(commandId); + if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender); + }); + + ipcMain.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), + ); + }); }; diff --git a/plugins/in-app-menu/custom-electron-titlebar.d.ts b/plugins/in-app-menu/custom-electron-titlebar.d.ts deleted file mode 100644 index f724278d65..0000000000 --- a/plugins/in-app-menu/custom-electron-titlebar.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module 'custom-electron-titlebar' { - // eslint-disable-next-line import/no-unresolved - import OriginalTitlebar from 'custom-electron-titlebar/dist/titlebar'; - // eslint-disable-next-line import/no-unresolved - import { Color as OriginalColor } from 'custom-electron-titlebar/dist/vs/base/common/color'; - - export const Color: typeof OriginalColor; - export const Titlebar: typeof OriginalTitlebar; -} diff --git a/plugins/in-app-menu/front.ts b/plugins/in-app-menu/front.ts index a09a2de4a8..a8b48146d4 100644 --- a/plugins/in-app-menu/front.ts +++ b/plugins/in-app-menu/front.ts @@ -1,11 +1,11 @@ +import path from 'node:path'; + import { ipcRenderer, Menu } from 'electron'; -// eslint-disable-next-line import/no-unresolved -import { Color, Titlebar } from 'custom-electron-titlebar'; -import config from '../../config'; -import { isEnabled } from '../../config/plugins'; +import { createPanel } from './menu/panel'; -import type { FastAverageColorResult } from 'fast-average-color'; +import { ElementFromFile } from '../utils'; +import { isEnabled } from '../../config/plugins'; type ElectronCSSStyleDeclaration = CSSStyleDeclaration & { webkitAppRegion: 'drag' | 'no-drag' }; type ElectronHTMLElement = HTMLElement & { style: ElectronCSSStyleDeclaration }; @@ -15,60 +15,60 @@ function $(selector: string) { } export default () => { - const visible = () => !!($('.cet-menubar')?.firstChild); - const bar = new Titlebar({ - icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png', - backgroundColor: Color.fromHex('#050505'), - itemBackgroundColor: Color.fromHex('#1d1d1d') , - svgColor: Color.WHITE, - menu: config.get('options.hideMenu') ? null as unknown as Menu : undefined, - }); - bar.updateTitle(' '); - document.title = 'Youtube Music'; + const titleBar = document.createElement('title-bar'); + const navBar = document.querySelector('#nav-bar-background'); - const toggleMenu = () => { - if (visible()) { - bar.updateMenu(null as unknown as Menu); - } else { - bar.refreshMenu(); - } + const logo = ElementFromFile(path.join(__dirname, '../../assets/youtube-music.svg')); + logo.classList.add('title-bar-icon'); + + titleBar.appendChild(logo); + document.body.appendChild(titleBar); + + 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(); + }); + + const menu = await ipcRenderer.invoke('get-menu') as Menu | null; + if (!menu) return; + + menu.items.forEach((menuItem) => { + const menu = document.createElement('menu-button'); + createPanel(titleBar, menu, menuItem.submenu?.items ?? []); + + menu.append(menuItem.label); + titleBar.appendChild(menu); + }); }; + updateMenu(); - $('.cet-window-icon')?.addEventListener('click', toggleMenu); - ipcRenderer.on('toggleMenu', toggleMenu); + document.title = 'Youtube Music'; ipcRenderer.on('refreshMenu', () => { - if (visible()) { - bar.refreshMenu(); - } + updateMenu(); }); - if (isEnabled('album-color-theme')) { - ipcRenderer.on('album-color-changed', (_, albumColor: FastAverageColorResult) => { - if (albumColor) { - bar.updateBackground(Color.fromHex(albumColor.hexa)); - } else { - bar.updateBackground(Color.fromHex('#050505')); - } - }); - } - if (isEnabled('picture-in-picture')) { ipcRenderer.on('pip-toggle', () => { - bar.refreshMenu(); + updateMenu(); }); } // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) document.addEventListener('apiLoaded', () => { - setNavbarMargin(); - const playPageObserver = new MutationObserver(setNavbarMargin); - const appLayout = $('ytmusic-app-layout'); - if (appLayout) { - playPageObserver.observe(appLayout, { attributeFilter: ['player-page-open_', 'playerPageOpen_'] }); - } setupSearchOpenObserver(); - setupMenuOpenObserver(); }, { once: true, passive: true }); }; @@ -84,33 +84,3 @@ function setupSearchOpenObserver() { searchOpenObserver.observe(searchBox, { attributeFilter: ['opened'] }); } } - -function setupMenuOpenObserver() { - const cetMenubar = $('.cet-menubar'); - if (cetMenubar) { - const menuOpenObserver = new MutationObserver(() => { - let isOpen = false; - for (const child of cetMenubar.children) { - if (child.classList.contains('open')) { - isOpen = true; - break; - } - } - const navBarBackground = $('#nav-bar-background'); - if (navBarBackground) { - navBarBackground.style.webkitAppRegion = isOpen ? 'no-drag' : 'drag'; - } - }); - menuOpenObserver.observe(cetMenubar, { subtree: true, attributeFilter: ['class'] }); - } -} - -function setNavbarMargin() { - const navBarBackground = $('#nav-bar-background'); - if (navBarBackground) { - navBarBackground.style.right - = $('ytmusic-app-layout')?.playerPageOpen_ - ? '0px' - : '12px'; - } -} diff --git a/plugins/in-app-menu/menu/icons.ts b/plugins/in-app-menu/menu/icons.ts new file mode 100644 index 0000000000..e88c61f6e8 --- /dev/null +++ b/plugins/in-app-menu/menu/icons.ts @@ -0,0 +1,10 @@ +const Icons = { + submenu: '', + checkbox: '', + radio: { + checked: '', + unchecked: '', + }, +}; + +export default Icons; diff --git a/plugins/in-app-menu/menu/panel.ts b/plugins/in-app-menu/menu/panel.ts new file mode 100644 index 0000000000..feffb33b43 --- /dev/null +++ b/plugins/in-app-menu/menu/panel.ts @@ -0,0 +1,125 @@ +import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron'; + +import Icons from './icons'; + +import { ElementFromHtml } from '../../utils'; + +interface PanelOptions { + placement?: 'bottom' | 'right'; + order?: number; +} + +export const createPanel = ( + parent: HTMLElement, + anchor: HTMLElement, + items: MenuItem[], + options: PanelOptions = { placement: 'bottom', order: 0 }, +) => { + const childPanels: HTMLElement[] = []; + const panel = document.createElement('menu-panel'); + panel.style.zIndex = `${options.order}`; + + const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => { + if (item.type === 'checkbox') { + if (item.checked) iconWrapper.innerHTML = Icons.checkbox; + else iconWrapper.innerHTML = ''; + } else if (item.type === 'radio') { + if (item.checked) iconWrapper.innerHTML = Icons.radio.checked; + else iconWrapper.innerHTML = Icons.radio.unchecked; + } else { + const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon; + const iconURL = nativeImageIcon?.toDataURL(); + + if (iconURL) iconWrapper.style.background = `url(${iconURL})`; + } + }; + + const radioGroups: [MenuItem, HTMLElement][] = []; + items.map((item) => { + if (item.type === 'separator') return panel.appendChild(document.createElement('menu-separator')); + + const menu = document.createElement('menu-item'); + const iconWrapper = document.createElement('menu-icon'); + + updateIconState(iconWrapper, item); + menu.appendChild(iconWrapper); + menu.append(item.label); + + menu.addEventListener('click', async () => { + await ipcRenderer.invoke('menu-event', item.commandId); + const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + + if (menuItem) { + updateIconState(iconWrapper, menuItem); + + if (menuItem.type === 'radio') { + await Promise.all( + radioGroups.map(async ([item, iconWrapper]) => { + if (item.commandId === menuItem.commandId) return; + const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null; + + if (newItem) updateIconState(iconWrapper, newItem); + }) + ); + } + } + }); + + if (item.type === 'radio') { + radioGroups.push([item, iconWrapper]); + } + + if (item.type === 'submenu') { + const subMenuIcon = document.createElement('menu-icon'); + subMenuIcon.appendChild(ElementFromHtml(Icons.submenu)); + menu.appendChild(subMenuIcon); + + const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], { + placement: 'right', + order: (options?.order ?? 0) + 1, + }); + + childPanels.push(child); + children.push(...children); + } + + panel.appendChild(menu); + }); + + /* methods */ + const isOpened = () => panel.getAttribute('open') === 'true'; + const close = () => panel.setAttribute('open', 'false'); + const open = () => { + const rect = anchor.getBoundingClientRect(); + + if (options.placement === 'bottom') { + panel.style.setProperty('--x', `${rect.x}px`); + panel.style.setProperty('--y', `${rect.y + rect.height}px`); + } else { + panel.style.setProperty('--x', `${rect.x + rect.width}px`); + panel.style.setProperty('--y', `${rect.y}px`); + } + + panel.setAttribute('open', 'true'); + }; + + anchor.addEventListener('click', () => { + if (isOpened()) close(); + else open(); + }); + + document.body.addEventListener('click', (event) => { + const path = event.composedPath(); + const isInside = path.some((it) => it === panel || it === anchor || childPanels.includes(it as HTMLElement)); + + if (!isInside) close(); + }); + + parent.appendChild(panel); + + return [ + panel, + { isOpened, close, open }, + childPanels, + ] as const; +}; diff --git a/plugins/in-app-menu/style.css b/plugins/in-app-menu/style.css deleted file mode 100644 index 231c790e3e..0000000000 --- a/plugins/in-app-menu/style.css +++ /dev/null @@ -1,118 +0,0 @@ -/* increase font size for menu and menuItems */ -.titlebar, -.menubar-menu-container .action-label { - font-size: 14px !important; -} - -/* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */ -#nav-bar-background { - opacity: 1 !important; - pointer-events: none !important; - top: 30px !important; - height: 75px !important; -} - -/* fix top gap between nav-bar and browse-page */ -#browse-page { - padding-top: 0 !important; -} - -/* https://github.com/organization/youtube-music-next/issues/4 */ -#sections { - padding-top: 30px !important; -} - -/* fix navbar hiding library items */ -ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"], -ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] { - top: 50px; - position: relative; -} - -/* remove window dragging for nav bar (conflict with titlebar drag) */ -ytmusic-nav-bar, -.tab-titleiron-icon, -ytmusic-pivot-bar-item-renderer { - -webkit-app-region: unset !important; -} - -/* move up item selection renderers */ -ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer, -ytmusic-tabs.stuck { - top: calc(var(--ytmusic-nav-bar-height) - 15px) !important; -} - -/* fix weird positioning in search screen*/ -ytmusic-header-renderer.ytmusic-search-page { - position: unset !important; -} - -/* Move navBar downwards */ -ytmusic-nav-bar[slot="nav-bar"] { - top: 17px !important; -} - -/* fix page progress bar position*/ -yt-page-navigation-progress, -#progress.yt-page-navigation-progress { - top: 30px !important; -} - -/* custom scrollbar */ -::-webkit-scrollbar { - width: 12px; - background-color: #030303; - border-radius: 100px; - -moz-border-radius: 100px; - -webkit-border-radius: 100px; -} - -/* hover effect for both scrollbar area, and scrollbar 'thumb' */ -::-webkit-scrollbar:hover { - background-color: rgba(15, 15, 15, 0.699); -} - -/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */ -::-webkit-scrollbar-thumb:vertical { - border: 2px solid rgba(0, 0, 0, 0); - - background: #3a3a3a; - background-clip: padding-box; - border-radius: 100px; - -moz-border-radius: 100px; - -webkit-border-radius: 100px; -} - -::-webkit-scrollbar-thumb:vertical:active { - background: #4d4c4c; /* some darker color when you click it */ - border-radius: 100px; - -moz-border-radius: 100px; - -webkit-border-radius: 100px; -} - -.cet-menubar-menu-container .cet-action-item { - background-color: inherit -} - -/** hideMenu toggler **/ -.cet-window-icon { - -webkit-app-region: no-drag; -} - -.cet-window-icon img { - -webkit-user-drag: none; - filter: invert(50%); -} - -/** make navbar draggable **/ -#nav-bar-background { - -webkit-app-region: drag; -} - -ytmusic-nav-bar input, -ytmusic-nav-bar span, -ytmusic-nav-bar [role="button"], -ytmusic-nav-bar yt-icon, -tp-yt-iron-dropdown { - -webkit-app-region: no-drag; -} diff --git a/plugins/in-app-menu/titlebar.css b/plugins/in-app-menu/titlebar.css new file mode 100644 index 0000000000..0c1a4dc781 --- /dev/null +++ b/plugins/in-app-menu/titlebar.css @@ -0,0 +1,157 @@ +:root { + --titlebar-background-color: #030303; + --menu-bar-height: 36px; +} + +title-bar { + -webkit-app-region: drag; + box-sizing: border-box; + + position: fixed; + top: 0; + z-index: 10000000; + + width: 100%; + height: var(--menu-bar-height, 36px); + + display: flex; + flex-flow: row; + justify-content: flex-start; + align-items: center; + gap: 4px; + + color: #f1f1f1; + font-size: 14px; + padding: 4px 12px; + background-color: var(--titlebar-background-color, #030303); + user-select: none; + + transition: opacity 200ms ease 0s, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s; +} + +menu-button { + -webkit-app-region: none; + + display: flex; + justify-content: center; + align-items: center; + align-self: stretch; + + padding: 2px 8px; + border-radius: 4px; + + cursor: pointer; +} +menu-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +menu-panel { + position: fixed; + top: var(--y, 0); + left: var(--x, 0); + + max-height: calc(100vh - var(--menu-bar-height, 36px) - 16px - var(--y, 0)); + + display: flex; + flex-flow: column; + justify-content: flex-start; + align-items: stretch; + gap: 0; + + overflow: auto; + padding: 4px; + border-radius: 8px; + pointer-events: none; + background-color: color-mix(in srgb, var(--titlebar-background-color, #030303) 50%, rgba(0, 0, 0, 0.1)); + backdrop-filter: blur(8px); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(0, 0, 0, 0.2); + + z-index: 0; + opacity: 0; + transform: scale(0.8); + transform-origin: top left; + + transition: opacity 200ms ease 0s, transform 200ms ease 0s; +} +menu-panel[open="true"] { + pointer-events: all; + opacity: 1; + transform: scale(1); +} + +menu-item { + -webkit-app-region: none; + min-height: 32px; + height: 32px; + + display: grid; + grid-template-columns: 32px 1fr minmax(32px, auto); + justify-content: flex-start; + align-items: center; + + border-radius: 4px; + cursor: pointer; +} +menu-item:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +menu-item > menu-icon { + height: 32px; + padding: 4px; + + box-sizing: border-box; +} + +menu-separator { + min-height: 1px; + height: 1px; + margin: 4px 0; + + background-color: rgba(255, 255, 255, 0.2); +} + +/* classes */ + +.title-bar-icon { + height: calc(100% - 8px); + object-fit: cover; + margin-left: -4px; +} + +/* youtube-music style */ + +ytmusic-app-layout { + margin-top: var(--menu-bar-height, 36px) !important; +} + +ytmusic-app-layout>[slot=nav-bar], #nav-bar-background.ytmusic-app-layout { + top: var(--menu-bar-height, 36px) !important; +} +#nav-bar-divider.ytmusic-app-layout { + top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important; +} +ytmusic-app[is-bauhaus-sidenav-enabled] #guide-spacer.ytmusic-app, +ytmusic-app[is-bauhaus-sidenav-enabled] #mini-guide-spacer.ytmusic-app { + margin-top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important; +} + + +html::-webkit-scrollbar { + width: 12px !important; +} + +html::-webkit-scrollbar-thumb { + border: solid 2px var(--titlebar-background-color, #030303) !important; + border-radius: 100px !important; +} + +html::-webkit-scrollbar-track { + background: var(--titlebar-background-color, #030303) !important; +} + +html::-webkit-scrollbar-track-piece:start { + background: transparent; + margin-top: var(--menu-bar-height, 36px); +}