import { Backdrop, Button, Checkbox, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, List, ListItem, ListItemButton, ListItemIcon, ListItemText, 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 { 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 { useI18n } from '../hooks/useI18n' import { ffetch } from '../lib/httpClient' import { DeleteRequest, DirectoryEntry } from '../types' import { roundMiB } from '../utils' export default function Downloaded() { const serverAddr = useRecoilValue(serverURL) const navigate = useNavigate() const { i18n } = useI18n() const [openDialog, setOpenDialog] = useState(false) const files$ = useMemo(() => new Subject(), []) const selected$ = useMemo(() => new BehaviorSubject([]), []) const [isPending, startTransition] = useTransition() const fetcher = () => ffetch( `${serverAddr}/archive/downloaded`, (d) => files$.next(d), () => navigate('/login'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subdir: '', }) } ) 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('/') fetch(`${serverAddr}/archive/downloaded`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subdir: relpath }) }) .then(res => res.json()) .then(data => { files$.next(sub ? [{ name: '..', isDirectory: true, path: upperLevel, }, ...data] : data ) }) } 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 deleteSelected = () => { Promise.all(selectable .filter(entry => entry.selected) .map(entry => fetch(`${serverAddr}/archive/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: entry.path, shaSum: entry.shaSum, } as DeleteRequest) })) ).then(fetcher) } useEffect(() => { fetcher() }, [serverAddr]) const onFileClick = (path: string) => startTransition(() => { const encoded = btoa( String.fromCodePoint(...new TextEncoder().encode(path)) ) window.open(`${serverAddr}/archive/d/${encoded}`) }) const onFolderClick = (path: string) => startTransition(() => { fetcherSubfolder(path) }) return ( theme.zIndex.drawer + 1 }} open={!(files$.observed) || isPending} > {i18n.t('archiveTitle')} {selectable.length === 0 && 'No files found'} {selectable.map((file, idx) => ( {!file.isDirectory && {roundMiB(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}
  • ))}
) }