import { type Menu, type MenuItem } from 'electron'; import { createEffect, createResource, createSignal, Index, Match, onCleanup, onMount, Show, Switch, } from 'solid-js'; import { css } from 'solid-styled-components'; import { TransitionGroup } from 'solid-transition-group'; import { MenuButton } from './MenuButton'; import { Panel } from './Panel'; import { PanelItem } from './PanelItem'; import { IconButton } from './IconButton'; import { WindowController } from './WindowController'; import { cacheNoArgs } from '@/providers/decorators'; import type { RendererContext } from '@/types/contexts'; import type { InAppMenuConfig } from '../constants'; const titleStyle = cacheNoArgs( () => css` -webkit-app-region: drag; box-sizing: border-box; position: fixed; top: 0; z-index: 10000000; width: 100%; height: var(--menu-bar-height, 32px); display: flex; flex-flow: row; justify-content: flex-start; align-items: center; gap: 4px; color: #f1f1f1; font-size: 12px; padding: 4px 4px 4px var(--offset-left, 4px); background-color: var(--titlebar-background-color, #030303); user-select: none; transition: opacity 200ms ease 0s, transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s; &[data-macos='true'] { padding: 4px 4px 4px 74px; } ytmusic-app:has(ytmusic-player[player-ui-state='FULLSCREEN']) ~ &:not([data-show='true']) { transform: translateY(calc(-1 * var(--menu-bar-height, 32px))); } `, ); const separatorStyle = cacheNoArgs( () => css` min-height: 1px; height: 1px; margin: 4px 0; background-color: rgba(255, 255, 255, 0.2); `, ); const animationStyle = cacheNoArgs(() => ({ enter: css` opacity: 0; transform: translateX(-50%) scale(0.8); `, enterActive: css` transition: opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1), transform 0.1s cubic-bezier(0.33, 1, 0.68, 1); `, exitTo: css` opacity: 0; transform: translateX(-50%) scale(0.8); `, exitActive: css` transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0), transform 0.1s cubic-bezier(0.32, 0, 0.67, 0); `, move: css` transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1); `, fakeTarget: css` position: absolute; opacity: 0; `, fake: css` transition: all 0.00000000001s; `, })); export type PanelRendererProps = { items: Electron.Menu['items']; level?: number[]; onClick?: (commandId: number, radioGroup?: MenuItem[]) => void; }; const PanelRenderer = (props: PanelRendererProps) => { const radioGroup = () => props.items.filter((it) => it.type === 'radio'); return ( {(subItem) => ( props.onClick?.(subItem().commandId)} /> props.onClick?.(subItem().commandId)} /> props.onClick?.(subItem().commandId, radioGroup()) } />
)}
); }; export type TitleBarProps = { ipc: RendererContext['ipc']; isMacOS?: boolean; enableController?: boolean; initialCollapsed?: boolean; }; export const TitleBar = (props: TitleBarProps) => { const [collapsed, setCollapsed] = createSignal(props.initialCollapsed); const [ignoreTransition, setIgnoreTransition] = createSignal(false); const [openTarget, setOpenTarget] = createSignal(null); const [menu, setMenu] = createSignal(null); const [mouseY, setMouseY] = createSignal(0); const [data, { refetch }] = createResource( async () => (await props.ipc.invoke('get-menu')) as Promise, ); const [isMaximized, { refetch: refetchMaximize }] = createResource( async () => (await props.ipc.invoke('window-is-maximized')) as Promise, ); const handleToggleMaximize = async () => { if (isMaximized()) { await props.ipc.invoke('window-unmaximize'); } else { await props.ipc.invoke('window-maximize'); } await refetchMaximize(); }; const handleMinimize = async () => { await props.ipc.invoke('window-minimize'); }; const handleClose = async () => { await props.ipc.invoke('window-close'); }; const refreshMenuItem = async (originalMenu: Menu, commandId: number) => { const menuItem = (await window.ipcRenderer.invoke( 'get-menu-by-id', commandId, )) as MenuItem | null; const newMenu = structuredClone(originalMenu); const stack = [...(newMenu?.items ?? [])]; let now: MenuItem | undefined = stack.pop(); while (now) { const index = now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ?? -1; if (index >= 0) { if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem); else now?.submenu?.items?.splice(index, 1); } if (now?.submenu) { stack.push(...now.submenu.items); } now = stack.pop(); } return newMenu; }; const handleItemClick = async ( commandId: number, radioGroup?: MenuItem[], ) => { const menuData = menu(); if (!menuData) return; if (Array.isArray(radioGroup)) { let newMenu = menuData; for (const item of radioGroup) { newMenu = await refreshMenuItem(newMenu, item.commandId); } setMenu(newMenu); return; } setMenu(await refreshMenuItem(menuData, commandId)); }; const listener = (e: MouseEvent) => { setMouseY(e.clientY); }; onMount(() => { props.ipc.on('close-all-in-app-menu-panel', async () => { setIgnoreTransition(true); setMenu(null); await refetch(); setMenu(data() ?? null); setIgnoreTransition(false); }); props.ipc.on('refresh-in-app-menu', async () => { setIgnoreTransition(true); await refetch(); setMenu(data() ?? null); setIgnoreTransition(false); }); props.ipc.on('toggle-in-app-menu', () => { setCollapsed(!collapsed()); }); props.ipc.on('window-maximize', refetchMaximize); props.ipc.on('window-unmaximize', refetchMaximize); // close menu when the outside of the panel or sub-panel is clicked document.body.addEventListener('click', (e) => { if ( e.target instanceof HTMLElement && !( e.target.closest('nav[data-ytmd-main-panel]') || e.target.closest('ul[data-ytmd-sub-panel]') ) ) { setOpenTarget(null); } }); // tracking mouse position window.addEventListener('mousemove', listener); const ytmusicAppLayout = document.querySelector('#layout'); ytmusicAppLayout?.addEventListener('scroll', () => { const scrollValue = ytmusicAppLayout.scrollTop; if (scrollValue > 20) { ytmusicAppLayout.classList.add('content-scrolled'); } else { ytmusicAppLayout.classList.remove('content-scrolled'); } }); }); createEffect(() => { if (!menu() && data()) { setMenu(data() ?? null); } }); onCleanup(() => { window.removeEventListener('mousemove', listener); }); return ( ); };