refactor(in-app-menu): refactor in-app-menu plugin

resolve #13
This commit is contained in:
Su-Yong
2023-10-03 03:31:25 +09:00
parent a5fe8bc589
commit 5a7774e7b1
11 changed files with 399 additions and 225 deletions

13
assets/youtube-music.svg Normal file
View File

@ -0,0 +1,13 @@
<svg xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 176 176" enable-background="new 0 0 176 176" xml:space="preserve">
<metadata>
<sfw xmlns="http://ns.adobe.com/SaveForWeb/1.0/">
<slices/>
<sliceSourceBounds bottomLeftOrigin="true" height="176" width="176" x="8" y="-184"/>
</sfw>
</metadata>
<g id="XMLID_167_">
<circle id="XMLID_791_" fill="#FF0000" cx="88" cy="88" r="88"/>
<path id="XMLID_42_" fill="#FFFFFF" d="M88,46c23.1,0,42,18.8,42,42s-18.8,42-42,42s-42-18.8-42-42S64.9,46,88,46 M88,42 c-25.4,0-46,20.6-46,46s20.6,46,46,46s46-20.6,46-46S113.4,42,88,42L88,42z"/>
<polygon id="XMLID_274_" fill="#FFFFFF" points="72,111 111,87 72,65 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 902 B

View File

@ -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()

9
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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),
);
});
};

View File

@ -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;
}

View File

@ -1,12 +1,12 @@
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 { createPanel } from './menu/panel';
import { ElementFromFile } from '../utils';
import { isEnabled } from '../../config/plugins';
import type { FastAverageColorResult } from 'fast-average-color';
type ElectronCSSStyleDeclaration = CSSStyleDeclaration & { webkitAppRegion: 'drag' | 'no-drag' };
type ElectronHTMLElement = HTMLElement & { style: ElectronCSSStyleDeclaration };
@ -15,60 +15,60 @@ function $<E extends Element = Element>(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(' ');
const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
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();
document.title = 'Youtube Music';
const toggleMenu = () => {
if (visible()) {
bar.updateMenu(null as unknown as Menu);
} else {
bar.refreshMenu();
}
};
$('.cet-window-icon')?.addEventListener('click', toggleMenu);
ipcRenderer.on('toggleMenu', toggleMenu);
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 = $<ElectronHTMLElement>('#nav-bar-background');
if (navBarBackground) {
navBarBackground.style.webkitAppRegion = isOpen ? 'no-drag' : 'drag';
}
});
menuOpenObserver.observe(cetMenubar, { subtree: true, attributeFilter: ['class'] });
}
}
function setNavbarMargin() {
const navBarBackground = $<HTMLElement>('#nav-bar-background');
if (navBarBackground) {
navBarBackground.style.right
= $<HTMLElement & { playerPageOpen_: boolean }>('ytmusic-app-layout')?.playerPageOpen_
? '0px'
: '12px';
}
}

View File

@ -0,0 +1,10 @@
const Icons = {
submenu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
checkbox: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
radio: {
checked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
unchecked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
},
};
export default Icons;

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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);
}