import { Backdrop, Button, Checkbox, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, MenuList, Paper, SpeedDial, SpeedDialAction, SpeedDialIcon, Typography } from '@mui/material' import DeleteForeverIcon from '@mui/icons-material/DeleteForever' import FolderIcon from '@mui/icons-material/Folder' import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile' import VideoFileIcon from '@mui/icons-material/VideoFile' import DownloadIcon from '@mui/icons-material/Download' import { matchW } from 'fp-ts/lib/TaskEither' import { pipe } from 'fp-ts/lib/function' import { useEffect, useMemo, useState, useTransition } from 'react' import { useNavigate } from 'react-router-dom' import { useRecoilValue } from 'recoil' import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs' import { serverURL } from '../atoms/settings' import { useObservable } from '../hooks/observable' import { useToast } from '../hooks/toast' import { useI18n } from '../hooks/useI18n' import { ffetch } from '../lib/httpClient' import { DirectoryEntry } from '../types' import { base64URLEncode, formatSize } from '../utils' export default function Downloaded() { const [menuPos, setMenuPos] = useState({ x: 0, y: 0 }) const [showMenu, setShowMenu] = useState(false) const [currentFile, setCurrentFile] = useState() const serverAddr = useRecoilValue(serverURL) const navigate = useNavigate() const { i18n } = useI18n() const { pushMessage } = useToast() const [openDialog, setOpenDialog] = useState(false) const files$ = useMemo(() => new Subject(), []) const selected$ = useMemo(() => new BehaviorSubject([]), []) const [isPending, startTransition] = useTransition() const fetcher = () => pipe( ffetch( `${serverAddr}/archive/downloaded`, { method: 'POST', body: JSON.stringify({ subdir: '', }) } ), matchW( (e) => { pushMessage(e, 'error') navigate('/login') }, (d) => files$.next(d ?? []), ) )() const fetcherSubfolder = (sub: string) => { const folders = sub.startsWith('/') ? sub.substring(1).split('/') : sub.split('/') const relpath = folders.length >= 2 ? folders.slice(-(folders.length - 1)).join('/') : folders.pop() const _upperLevel = folders.slice(1, -1) const upperLevel = _upperLevel.length === 2 ? ['.', ..._upperLevel].join('/') : _upperLevel.join('/') const task = ffetch(`${serverAddr}/archive/downloaded`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subdir: relpath }) }) pipe( task, matchW( (l) => pushMessage(l, 'error'), (r) => files$.next(sub ? [{ isDirectory: true, isVideo: false, modTime: '', name: '..', path: upperLevel, size: 0, }, ...r.filter(f => f.name !== '')] : r.filter(f => f.name !== '') ) ) )() } const selectable$ = useMemo(() => files$.pipe( combineLatestWith(selected$), map(([data, selected]) => data.map(x => ({ ...x, selected: selected.includes(x.name) }))), share() ), []) const selectable = useObservable(selectable$, []) const addSelected = (name: string) => { selected$.value.includes(name) ? selected$.next(selected$.value.filter(val => val !== name)) : selected$.next([...selected$.value, name]) } const deleteFile = (entry: DirectoryEntry) => pipe( ffetch(`${serverAddr}/archive/delete`, { method: 'POST', body: JSON.stringify({ path: entry.path, }) }), matchW( (l) => pushMessage(l, 'error'), (_) => fetcher() ) )() const deleteSelected = () => { Promise.all(selectable .filter(entry => entry.selected) .map(deleteFile) ).then(fetcher) } useEffect(() => { fetcher() }, [serverAddr]) const onFileClick = (path: string) => startTransition(() => { const encoded = base64URLEncode(path) window.open(`${serverAddr}/archive/v/${encoded}?token=${localStorage.getItem('token')}`) }) const downloadFile = (path: string) => startTransition(() => { const encoded = base64URLEncode(path) window.open(`${serverAddr}/archive/d/${encoded}?token=${localStorage.getItem('token')}`) }) const onFolderClick = (path: string) => startTransition(() => { fetcherSubfolder(path) }) return ( setShowMenu(false)} > { if (currentFile) { downloadFile(currentFile?.path) setCurrentFile(undefined) } }} onDelete={() => { if (currentFile) { deleteFile(currentFile) setCurrentFile(undefined) } }} /> theme.zIndex.drawer + 1 }} open={!(files$.observed) || isPending} > setShowMenu(false)} > {i18n.t('archiveTitle')} {selectable.length === 0 && 'No files found'} {selectable.map((file, idx) => ( { e.preventDefault() setCurrentFile(file) setMenuPos({ x: e.clientX, y: e.clientY }) setShowMenu(true) }} key={idx} secondaryAction={
{!file.isDirectory && {formatSize(file.size)} } {!file.isDirectory && <> addSelected(file.name)} /> }
} disablePadding > file.isDirectory ? onFolderClick(file.path) : onFileClick(file.path) }> {file.isDirectory ? : file.isVideo ? : }
))}
} > } tooltipTitle={`Delete selected`} tooltipOpen onClick={() => { if (selected$.value.length > 0) { setOpenDialog(true) } }} /> setOpenDialog(false)} > Are you sure? You're deleting:
    {selected$.value.map((entry, idx) => (
  • {entry}
  • ))}
) } const IconMenu: React.FC<{ posX: number posY: number hide: boolean onDownload: () => void onDelete: () => void }> = ({ posX, posY, hide, onDelete, onDownload }) => { return ( theme.zIndex.drawer + 1, }}> Download Delete ) }