| 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, ReactNode } from 'react' |
| 4 | import { Alert, Box, ButtonProps, List, ListItem, ListItemIcon, ListItemText } from '@mui/material' |
| 5 | import { Add, Save, Storage, Undo } from '@mui/icons-material' |
| 6 | import addFiles, { addLink, addVirtual } from './addFiles' |
| 7 | import MenuButton from './MenuButton' |
| 8 | import { osIcon } from './LogsPage' |
| 9 | import { reloadVfs } from './VfsPage' |
| 10 | import { Dict, prefix, VFS_STORED_KEYS } from './misc' |
| 11 | import { state, undoVfs, useSnapState } from './state' |
| 12 | import _ from 'lodash' |
| 13 | import { Btn, Flex, reloadBtn, useBreakpoint, useCtrlShortcutButton } from './mui' |
| 14 | import { apiCall, ApiObject, useApi } from './api' |
| 15 | import VfsPathField from './VfsPathField' |
| 16 | import { alertDialog, promptDialog } from './dialog' |
| 17 | import { formatDiskSpace } from './FilePicker' |
| 18 | import { getDiskSpaces } from '../../src/util-os' |
| 19 | import { adminApis } from '../../src/adminApis' |
| 20 | |
| 21 | export default function VfsMenuBar({ statusApi, add }: { add: ReactNode, statusApi: ApiObject }) { |
| 22 | const { vfsModified, vfsUndo } = useSnapState() |
| 23 | return h(Flex, { |
| 24 | zIndex: 2, |
| 25 | gap: 1, |
| 26 | backgroundColor: 'background.paper', |
| 27 | width: 'fit-content', |
| 28 | flexWrap: 'wrap', |
| 29 | }, |
| 30 | h(AddVfsBtn), |
| 31 | h(Btn, { |
| 32 | ref: useCtrlShortcutButton(['s']).ref, |
| 33 | icon: Save, |
| 34 | title: "Save\n(ctrl+s)", |
| 35 | disabled: !vfsModified && "No changes to save", |
| 36 | modified: vfsModified, |
| 37 | doneAnimation: true, |
| 38 | onClick: () => saveVfs().finally(statusApi.reload) |
| 39 | }), |
| 40 | h(Btn, { |
| 41 | icon: Undo, |
| 42 | title: "Undo/redo last change", |
| 43 | disabled: !vfsUndo && "No changes to undo", |
| 44 | onClick: undoVfs, |
| 45 | }), |
| 46 | reloadBtn(() => reloadVfs()), |
| 47 | h(Btn, { |
| 48 | icon: Storage, |
| 49 | title: "Disk spaces", |
| 50 | onClick: () => apiCall<Awaited<ReturnType<typeof getDiskSpaces>>>('get_disk_spaces').then(res => |
| 51 | alertDialog(h(List, { dense: true }, res.map(x => h(ListItem, { key: x.name }, |
| 52 | h(ListItemIcon, {}, h(Storage)), |
| 53 | h(ListItemText, { |
| 54 | sx: { wordBreak: 'break-word' }, |
| 55 | primary: x.name + prefix(' (', x.description, ')'), |
| 56 | secondary: formatDiskSpace(x) |
| 57 | }), |
| 58 | ))), { title: "Disk spaces" }) |
| 59 | .then(() => false), // no success-animation for IconBtn |
| 60 | alertDialog) |
| 61 | }), |
| 62 | add, |
| 63 | h(SystemIntegrationButton, statusApi.data) |
| 64 | ) |
| 65 | } |
| 66 | |
| 67 | export function AddVfsBtn(props: Partial<ButtonProps>) { |
| 68 | return h(MenuButton, { |
| 69 | variant: 'contained', |
| 70 | icon: Add, |
| 71 | title: "Add item to virtual file system", |
| 72 | ...props, |
| 73 | items: [ |
| 74 | { children: "virtual folder", onClick: addVirtual }, |
| 75 | { children: "file or folder from disk", onClick: addFiles }, |
| 76 | { children: "web-link", onClick: addLink }, |
| 77 | ] |
| 78 | }) |
| 79 | } |
| 80 | |
| 81 | function SystemIntegrationButton({ platform }: { platform: string | undefined }) { |
| 82 | const isWindows = platform === 'win32' |
| 83 | const { data: integrated, reload } = useApi<typeof adminApis.windows_integrated>(isWindows && 'windows_integrated') |
| 84 | const sm = useBreakpoint('sm') |
| 85 | return !isWindows ? null : h(Btn, { |
| 86 | icon: osIcon('Windows'), |
| 87 | variant: 'outlined', |
| 88 | doneMessage: true, |
| 89 | ...(!integrated?.is ? { |
| 90 | children: "System integration", |
| 91 | async onClick() { |
| 92 | const msg = h(Box, { sx: { width: { xs: '100%', sm: '34em' } } }, |
| 93 | h('img', { src: 'win-shell.png', style: { display: 'block', width: '100%' } }), |
| 94 | h(Alert, { severity: 'info' }, "We are going to add a command in the right-click of Windows File Manager.", |
| 95 | h(Box, {}, "It will also automatically copy the URL, ready to paste!")), |
| 96 | ) |
| 97 | const parent = await promptDialog(msg, { |
| 98 | field: { comp: VfsPathField, files: false, label: "Add to this folder", placeholder: "home", |
| 99 | autoFocus: sm }, // this dialog is tall, and mobile keyboard will disrupt user's ability to view its content |
| 100 | form: { saveOnEnter: false } |
| 101 | }) |
| 102 | return typeof parent === 'string' && apiCall('windows_integration', { parent }).then(reload) |
| 103 | } |
| 104 | } : { |
| 105 | confirm: true, |
| 106 | children: "Remove integration", |
| 107 | onClick: () => apiCall('windows_remove').then(reload), |
| 108 | }) |
| 109 | }) |
| 110 | } |
| 111 | |
| 112 | async function saveVfs() { |
| 113 | const uriRemaps: Dict<string> = {} |
| 114 | recurVfs(n => n.originalId !== n.id && (uriRemaps[n.originalId] = n.id)) |
| 115 | await apiCall('set_vfs', { |
| 116 | uri: '/', |
| 117 | uriRemaps, |
| 118 | props: (function recur(n=state.vfs) { |
| 119 | const ret = _.pick(n, VFS_STORED_KEYS) |
| 120 | ret.children = n?.children?.map(recur) as any |
| 121 | return ret |
| 122 | })() |
| 123 | }) |
| 124 | recurVfs(n => n.originalId = n.id) // reset |
| 125 | state.vfsModified = false |
| 126 | } |
| 127 | |
| 128 | function recurVfs(cb: (n: NonNullable<typeof state.vfs>) => any, node=state.vfs) { |
| 129 | if (!node) return |
| 130 | cb(node) |
| 131 | node?.children?.forEach(child => recurVfs(cb, child)) |
| 132 | return node |
| 133 | } |