import { openModal } from '@mantine/modals'; import isElectron from 'is-electron'; import { Fragment, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useNavigate } from 'react-router'; import packageJson from '../../../../../package.json'; import styles from './app-menu.module.css'; import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties'; import { ServerList } from '/@/renderer/features/servers/components/server-list'; import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal'; import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector'; import { openReleaseNotesModal } from '/@/renderer/release-notes-modal'; import { useAppStore, useAppStoreActions, useCommandPalette, useCurrentServer, useGeneralSettings, useSettingsStoreActions, } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { toast } from '/@/shared/components/toast/toast'; const browser = isElectron() ? window.api.browser : null; interface BaseMenuItem { id: string; type: 'conditional-group' | 'conditional-item' | 'custom' | 'divider' | 'item'; } interface ConditionalGroupItem extends BaseMenuItem { condition: boolean; items: MenuItem[]; type: 'conditional-group'; } interface ConditionalItem extends BaseMenuItem { condition: boolean; item: Omit; type: 'conditional-item'; } interface CustomItem extends BaseMenuItem { component: ReactNode; type: 'custom'; } interface DividerItem extends BaseMenuItem { type: 'divider'; } type MenuItem = ConditionalGroupItem | ConditionalItem | CustomItem | DividerItem | RegularMenuItem; interface RegularMenuItem extends BaseMenuItem { component?: 'a' | typeof Link; href?: string; icon?: keyof typeof import('/@/shared/components/icon/icon').AppIcon; iconColor?: | 'contrast' | 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn'; label: string; leftSection?: ReactNode; onClick?: () => void; rightSection?: ReactNode; target?: string; to?: string; type: 'item'; } export const AppMenu = () => { const { t } = useTranslation(); const navigate = useNavigate(); const collapsed = useAppStore((state) => state.sidebar.collapsed); const privateMode = useAppStore((state) => state.privateMode); const { setPrivateMode, setSideBar } = useAppStoreActions(); const { setSettings } = useSettingsStoreActions(); const settings = useGeneralSettings(); const currentServer = useCurrentServer(); const { open: openCommandPalette } = useCommandPalette(); const handleBrowserDevTools = () => { browser?.devtools(); }; const handleCollapseSidebar = () => { setSideBar({ collapsed: true }); }; const handleExpandSidebar = () => { setSideBar({ collapsed: false }); }; const handlePrivateModeOff = () => { setPrivateMode(false); toast.info({ message: t('form.privateMode.disabled', { postProcess: 'sentenceCase' }), title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), }); }; const handlePrivateModeOn = () => { setPrivateMode(true); toast.info({ message: t('form.privateMode.enabled', { postProcess: 'sentenceCase' }), title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), }); }; const handleManageServersModal = () => { openModal({ children: , title: t('page.manageServers.title', { postProcess: 'titleCase' }), }); }; const handleQuit = () => { browser?.quit(); }; const handleSetSideQueueLayout = (sideQueueLayout: 'horizontal' | 'vertical') => { setSettings({ general: { ...settings, sideQueueLayout, }, }); }; const serverHeaderMenuItems: MenuItem[] = currentServer ? [ { component: (
), id: 'server-selector', type: 'custom', }, { id: 'divider-server', type: 'divider', }, ] : []; const menuConfig: MenuItem[] = [ ...serverHeaderMenuItems, { icon: 'search', id: 'command-palette', label: t('page.appMenu.commandPalette', { postProcess: 'sentenceCase' }), onClick: openCommandPalette, type: 'item', }, { id: 'divider-1', type: 'divider', }, { condition: collapsed, id: 'navigation-group', items: [ { icon: 'arrowLeftS', id: 'go-back', label: t('page.appMenu.goBack', { postProcess: 'sentenceCase' }), onClick: () => navigate(-1), type: 'item', }, { icon: 'arrowRightS', id: 'go-forward', label: t('page.appMenu.goForward', { postProcess: 'sentenceCase' }), onClick: () => navigate(1), type: 'item', }, ], type: 'conditional-group', }, { condition: collapsed, id: 'sidebar-expand', item: { icon: 'panelRightOpen', id: 'expand-sidebar', label: t('page.appMenu.expandSidebar', { postProcess: 'sentenceCase' }), onClick: handleExpandSidebar, type: 'item', }, type: 'conditional-item', }, { condition: !collapsed, id: 'sidebar-collapse', item: { icon: 'panelRightClose', id: 'collapse-sidebar', label: t('page.appMenu.collapseSidebar', { postProcess: 'sentenceCase' }), onClick: handleCollapseSidebar, type: 'item', }, type: 'conditional-item', }, { id: 'divider-2', type: 'divider', }, { condition: !isServerLock(), id: 'manage-servers', item: { label: t('page.appMenu.manageServers', { postProcess: 'sentenceCase' }), leftSection: , onClick: handleManageServersModal, type: 'item', }, type: 'conditional-item', }, { id: 'divider-3', type: 'divider', }, { icon: 'settings', id: 'settings', label: t('page.appMenu.settings', { postProcess: 'sentenceCase' }), onClick: () => openSettingsModal(), type: 'item', }, { condition: privateMode, id: 'private-mode-off', item: { icon: 'lock', iconColor: 'error', label: t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' }), onClick: handlePrivateModeOff, type: 'item', }, type: 'conditional-item', }, { condition: !privateMode, id: 'private-mode-on', item: { icon: 'lockOpen', label: t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' }), onClick: handlePrivateModeOn, type: 'item', }, type: 'conditional-item', }, { id: 'divider-4', type: 'divider', }, { icon: 'brandGitHub', id: 'version', label: t('page.appMenu.version', { postProcess: 'sentenceCase', version: packageJson.version, }), onClick: () => openReleaseNotesModal( t('common.newVersion', { postProcess: 'sentenceCase', version: packageJson.version, }) as string, ), type: 'item', }, { condition: isElectron(), id: 'devtools', item: { icon: 'appWindow', id: 'open-devtools', label: t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' }), onClick: handleBrowserDevTools, type: 'item', }, type: 'conditional-item', }, { condition: isElectron(), id: 'quit', item: { icon: 'x', id: 'quit-app', label: t('page.appMenu.quit', { postProcess: 'sentenceCase' }), onClick: handleQuit, type: 'item', }, type: 'conditional-item', }, { id: 'divider-5', type: 'divider', }, { condition: settings.sideQueueType === 'sideQueue', id: 'layout-toggle-group', items: [ { component: ( handleSetSideQueueLayout('horizontal')} tooltip={{ label: t('setting.sidePlayQueueLayout', { context: 'optionHorizontal', postProcess: 'sentenceCase', }), openDelay: 0, position: 'bottom', }} variant={ settings.sideQueueLayout === 'horizontal' ? 'default' : 'transparent' } /> handleSetSideQueueLayout('vertical')} tooltip={{ label: t('setting.sidePlayQueueLayout', { context: 'optionVertical', postProcess: 'sentenceCase', }), openDelay: 0, position: 'bottom', }} variant={ settings.sideQueueLayout === 'vertical' ? 'default' : 'transparent' } /> ), id: 'layout-toggle', type: 'custom', }, ], type: 'conditional-group', }, ]; const renderMenuItem = (item: MenuItem): ReactNode => { switch (item.type) { case 'conditional-group': if (!item.condition) return null; return (
{item.items.map((subItem) => { return {renderMenuItem(subItem)}; })}
); case 'conditional-item': if (!item.condition) return null; return {renderMenuItem(item.item as MenuItem)}; case 'custom': return
{item.component}
; case 'divider': return ; case 'item': { const leftSection = item.leftSection || (item.icon && ); const props = { leftSection, ...(item.rightSection && { rightSection: item.rightSection }), ...(item.onClick && { onClick: item.onClick }), ...(item.component && { component: item.component }), ...(item.to && { to: item.to }), ...(item.href && { href: item.href }), ...(item.target && { target: item.target }), } as MenuItemProps; return ( {item.label} ); } default: return null; } }; return <>{menuConfig.map((item) => renderMenuItem(item))}; };