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