| 1 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 2 | |
| 3 | import { createElement as h, FC } from 'react'; |
| 4 | import { List, ListItemButton, ListItemIcon, ListItemText, Box } from '@mui/material' |
| 5 | import { |
| 6 | AccountTree, Extension, History, Home, Logout, ManageAccounts, Monitor, Public, Settings, Translate, Code, |
| 7 | SvgIconComponent |
| 8 | } from '@mui/icons-material' |
| 9 | import _ from 'lodash' |
| 10 | import { Link, useLocation } from 'wouter' |
| 11 | import MonitorPage from './MonitorPage' |
| 12 | import OptionsPage from './OptionsPage'; |
| 13 | import VfsPage from './VfsPage'; |
| 14 | import AccountsPage from './AccountsPage'; |
| 15 | import HomePage from './HomePage' |
| 16 | import LogoutPage from './LogoutPage'; |
| 17 | import LangPage from './LangPage' |
| 18 | import LogsPage from './LogsPage'; |
| 19 | import PluginsPage from './PluginsPage'; |
| 20 | import { getHFS, HIDE_IN_TESTS, replaceStringToReact, WEBSITE } from '@hfs/shared' |
| 21 | import CustomHtmlPage from './CustomHtmlPage'; |
| 22 | import InternetPage from './InternetPage' |
| 23 | import { useWindowSize } from 'usehooks-ts' |
| 24 | import { hTooltip } from './mui' |
| 25 | import { PageProps } from './App' |
| 26 | import { confirmDialog } from './dialog' |
| 27 | |
| 28 | export interface MenuEntry { |
| 29 | path: `/${string}` |
| 30 | icon: SvgIconComponent |
| 31 | label?: string |
| 32 | title?: string |
| 33 | comp: FC<PageProps> |
| 34 | noPaddingOnMobile?: true |
| 35 | subRoutes?: true |
| 36 | } |
| 37 | |
| 38 | export const mainMenu: MenuEntry[] = [ |
| 39 | { path: '/', icon: Home, label: "Home", comp: HomePage }, |
| 40 | { path: '/fs', icon: AccountTree, label: "Shared files", comp: VfsPage }, |
| 41 | { path: '/accounts', icon: ManageAccounts, comp: AccountsPage }, |
| 42 | { path: '/options', icon: Settings, comp: OptionsPage }, |
| 43 | { path: '/internet', icon: Public, comp: InternetPage }, |
| 44 | { path: '/monitoring', icon: Monitor, comp: MonitorPage, noPaddingOnMobile: true }, |
| 45 | { path: '/logs', icon: History, comp: LogsPage, noPaddingOnMobile: true, subRoutes: true }, |
| 46 | { path: '/language', icon: Translate, comp: LangPage }, |
| 47 | { path: '/plugins', icon: Extension, comp: PluginsPage, noPaddingOnMobile: true, subRoutes: true }, |
| 48 | { path: '/html', icon: Code, label: "Custom HTML", comp: CustomHtmlPage }, |
| 49 | { path: '/logout', icon: Logout, comp: LogoutPage } |
| 50 | ] |
| 51 | |
| 52 | export default function Menu({ onSelect, itemTitle }: { onSelect: ()=>void, itemTitle: (idx: number) => string }) { |
| 53 | const { VERSION } = getHFS() |
| 54 | const logo = 'hfs-logo.svg' |
| 55 | const short = useWindowSize().height < 700 |
| 56 | const currentPath = useLocation()[0] |
| 57 | return h(Box, { sx: { display: 'flex', flexDirection: 'column', bgcolor: 'primary.main', minHeight: '100%' } }, |
| 58 | h(List, { |
| 59 | sx:{ |
| 60 | pr: 1, py: 0, color: 'primary.contrastText', |
| 61 | height: '100vh', boxSizing: 'border-box', // grow as screen permits, so we know the extra space for the logo |
| 62 | overflowY: 'auto', // ...and account for clipping |
| 63 | position: 'sticky', top: 0, // be independent (scrolling-wise) |
| 64 | display: 'flex', flexDirection: 'column', '&>a': { flex: '0' }, |
| 65 | } |
| 66 | }, |
| 67 | h(Box, { id: 'hfs-name', sx: { display: 'flex', px: 2, py: .5, gap: 2, alignItems: 'center' } }, |
| 68 | h(Box, { |
| 69 | sx: { color: 'primary.contrastText', fontSize: 'min(3rem, max(5vw, 4vh))', cursor: 'pointer' }, |
| 70 | async onClick() { |
| 71 | if (await confirmDialog("Open HFS website?")) |
| 72 | window.open(WEBSITE) |
| 73 | } |
| 74 | }, 'HFS'), |
| 75 | h(Box, { sx: { fontSize: 'small' }, className: HIDE_IN_TESTS }, replaceStringToReact(VERSION||'', /-/, () => h('br'))), |
| 76 | short && h('img', { src: logo, style: { height: '2.5em' } }), |
| 77 | ), |
| 78 | mainMenu.map((it, idx) => hTooltip( itemTitle(idx), getMenuLabel(it) + ' ' + itemTitle(idx), |
| 79 | h(ListItemButton, { |
| 80 | // @ts-expect-error mui createElement overload does not infer custom Link props |
| 81 | component: Link, |
| 82 | href: it.path, |
| 83 | onClick: onSelect, |
| 84 | selected: matchesMenuPath(it, currentPath), |
| 85 | sx: { '&.Mui-selected': { '&,&:hover': { bgcolor: 'primary.dark', textDecoration: 'underline' } } }, |
| 86 | children: undefined, // shut up ts |
| 87 | }, |
| 88 | it.icon && h(ListItemIcon, { sx: { color: 'primary.contrastText', minWidth: 48 } }, h(it.icon)), |
| 89 | h(ListItemText, { sx: { whiteSpace: 'nowrap' }, primary: getMenuLabel(it) }) |
| 90 | ), |
| 91 | { key: it.path, placement: 'right' } |
| 92 | )), |
| 93 | !short && h(Box, { id: 'hfs-logo', sx: { flex: 1, opacity: .7, background: `url(${logo}) no-repeat bottom`, backgroundSize: 'contain', margin: 2 } }), |
| 94 | ) |
| 95 | ) |
| 96 | } |
| 97 | |
| 98 | export function getMenuLabel(it: MenuEntry) { |
| 99 | return it && (it.label ?? _.capitalize(it.path.slice(1))) |
| 100 | } |
| 101 | |
| 102 | export function matchesMenuPath(it: MenuEntry, path: string) { |
| 103 | return path === it.path || path.startsWith(it.path + '/') |
| 104 | } |