/** * AppMenu — installs the native Tauri application menu (top menu bar). * * Mac shows a real menu bar; Windows/Linux get the in-window menu via the * window decorations. On web (Vite dev outside Tauri) the component renders an * inline HTML menu strip so the same items are still reachable for testing. * * Events: * - The Rust side doesn't dispatch routes; the menu handlers call React Router * via the provided `navigate` callback. Tauri actions (fullscreen, quit) are * invoked via the JS API. */ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { Menu, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'; const isTauri = () => typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; export function AppMenu() { const navigate = useNavigate(); const { t } = useTranslation(); useEffect(() => { if (!isTauri()) return; let installed: Menu | null = null; (async () => { try { const fileMenu = await Submenu.new({ text: t('appMenu.file'), items: [ await MenuItem.new({ id: 'new-inspection', text: t('appMenu.newInspection'), accelerator: 'CmdOrCtrl+N', action: () => navigate('/inspect'), }), await MenuItem.new({ id: 'open-file', text: t('appMenu.openFile'), accelerator: 'CmdOrCtrl+O', action: () => window.dispatchEvent(new CustomEvent('hasarui:open-file-shortcut')), }), await PredefinedMenuItem.new({ item: 'Separator' }), await MenuItem.new({ id: 'preferences', text: t('appMenu.preferences'), accelerator: 'CmdOrCtrl+,', action: () => navigate('/settings'), }), await PredefinedMenuItem.new({ item: 'Separator' }), await PredefinedMenuItem.new({ item: 'Quit', text: t('appMenu.quit') }), ], }); const editMenu = await Submenu.new({ text: t('appMenu.edit'), items: [ await PredefinedMenuItem.new({ item: 'Undo' }), await PredefinedMenuItem.new({ item: 'Redo' }), await PredefinedMenuItem.new({ item: 'Separator' }), await PredefinedMenuItem.new({ item: 'Cut' }), await PredefinedMenuItem.new({ item: 'Copy' }), await PredefinedMenuItem.new({ item: 'Paste' }), await PredefinedMenuItem.new({ item: 'SelectAll' }), ], }); const viewMenu = await Submenu.new({ text: t('appMenu.view'), items: [ await MenuItem.new({ id: 'fullscreen', text: t('appMenu.fullscreen'), accelerator: 'F11', action: async () => { const w = getCurrentWindow(); const isFs = await w.isFullscreen(); await w.setFullscreen(!isFs); }, }), await PredefinedMenuItem.new({ item: 'Minimize', text: t('appMenu.minimize') }), ], }); const helpMenu = await Submenu.new({ text: t('appMenu.help'), items: [ await MenuItem.new({ id: 'docs', text: t('appMenu.documentation'), action: async () => { const { open } = await import('@tauri-apps/plugin-shell'); await open('https://github.com/'); }, }), await MenuItem.new({ id: 'about', text: t('appMenu.about'), action: () => navigate('/settings'), }), ], }); installed = await Menu.new({ items: [fileMenu, editMenu, viewMenu, helpMenu] }); await installed.setAsAppMenu(); } catch (e) { console.warn('AppMenu install failed:', e); } })(); return () => { // Tauri menu doesn't strictly require teardown — replaced on next mount. installed = null; }; }, [navigate, t]); // Web fallback strip if (isTauri()) return null; return ; } function WebMenuStrip() { const { t } = useTranslation(); const navigate = useNavigate(); const [open, setOpen] = useState(null); function close() { setOpen(null); } return (
setOpen('file')} items={[ { label: t('appMenu.newInspection'), onClick: () => navigate('/inspect') }, { label: t('appMenu.preferences'), onClick: () => navigate('/settings'), }, ]} /> setOpen('view')} items={[]} /> setOpen('help')} items={[]} />
); } function MenuButton({ label, items, open, onOpen, }: { label: string; open: boolean; onOpen: () => void; items: { label: string; onClick: () => void }[]; }) { return (
{open && items.length > 0 && (
{items.map((it) => ( ))}
)}
); } export default AppMenu;