diff --git a/src/app/components/FileBrowser.tsx b/src/app/components/FileBrowser.tsx index 8b2c34e..90534b3 100644 --- a/src/app/components/FileBrowser.tsx +++ b/src/app/components/FileBrowser.tsx @@ -1,7 +1,19 @@ import { useEffect, useRef, useState } from 'react'; -import { Folder, File, ChevronRight, Home, RefreshCw, Upload, FolderPlus, Trash2, ArrowLeft, Loader2, FileWarning } from 'lucide-react'; import { - basename, + Folder, + File, + ChevronRight, + Home, + RefreshCw, + Upload, + FolderPlus, + ArrowLeft, + Loader2, + MoreVertical, + Check, + Trash2, +} from 'lucide-react'; +import { createFolder, deletePath, humanFileSize, @@ -14,6 +26,13 @@ import { type EntryInfo, } from '../webdav'; import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from './ui/dialog'; interface FileBrowserProps { currentPath: string; @@ -21,27 +40,19 @@ interface FileBrowserProps { onClose: () => void; } -type Mode = 'pick-file' | 'pick-folder' | 'browse'; - export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrowserProps) { - // If `currentPath` is itself a file (e.g. the IEC device is currently - // pointing at /sd/foo.d64), we want to open the file browser on the - // parent directory (/sd/) and remember that the user came from `foo.d64`. - // We resolve this asynchronously on mount; until then we show a loader - // so we never try to list a file as if it were a folder. + // Resolve the initial path: if `currentPath` is itself a file, jump + // to its parent so we never try to list a file as if it were a folder. const [path, setPath] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - const [initError, setInitError] = useState(null); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [mode, setMode] = useState('browse'); const [showNewFolder, setShowNewFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(''); + const [actionEntry, setActionEntry] = useState(null); const fileInputRef = useRef(null); - // Resolve the initial path: if `currentPath` is a file, jump to its - // parent and remember the file so it can be highlighted. + // Resolve the initial path on mount. useEffect(() => { const initial = normalizePath(currentPath || '/'); if (initial === '/') { @@ -53,25 +64,16 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow .then((info) => { if (cancelled) return; if (info && info.type === 'file') { - const parent = splitPath(info.path).parent; - setPath(parent); - setSelectedFile(info.path); + setPath(splitPath(info.path).parent); + } else if (info && info.type === 'folder') { + setPath(info.path); } else { - // Either a directory, or the path doesn't exist (in which case - // the safest fallback is to open the parent so the user can - // navigate from there). - if (info && info.type === 'folder') { - setPath(info.path); - } else { - setPath(splitPath(initial).parent); - setInitError(`"${initial}" not found; opened the parent folder.`); - } + setPath(splitPath(initial).parent); } }) - .catch((e) => { + .catch(() => { if (cancelled) return; setPath(splitPath(initial).parent); - setInitError(`Failed to resolve "${initial}": ${e?.message || e}`); }); return () => { cancelled = true; @@ -101,27 +103,29 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow }, [path]); const navigateUp = () => { - if (path === '/') return; + if (path === '/' || path === null) return; setPath(splitPath(path).parent); }; const navigateToFolder = (folderName: string) => { + if (path === null) return; setPath(joinPath(path, folderName)); }; - const selectFile = (entry: EntryInfo) => { + const selectEntry = (entry: EntryInfo) => { if (entry.type !== 'file') return; onSelect(entry.path); onClose(); }; const selectCurrentFolder = () => { + if (path === null) return; onSelect(path); onClose(); }; const refresh = () => { - void load(path); + if (path !== null) void load(path); }; const handleCreateFolder = async () => { @@ -145,10 +149,11 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow : `Delete file "${entry.name}"?`, ); if (!ok) return; + setActionEntry(null); try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); - void load(path); + if (path !== null) void load(path); } catch (e: any) { toast.error(`Failed to delete: ${e?.message || e}`); } @@ -194,32 +199,6 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow > - - - - )} - {loading && (
@@ -342,7 +298,7 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow
Failed to load directory
{error}
)} - {!loading && !error && ( + {!loading && !error && path !== null && ( <> {path !== '/' && ( - ); - })} + ))} {entries.length === 0 && (
@@ -427,26 +381,65 @@ export default function FileBrowser({ currentPath, onSelect, onClose }: FileBrow
- {mode === 'pick-folder' ? ( - - ) : mode === 'pick-file' ? ( -
- Tap a file above to select it. ({entries.filter((e) => e.type === 'file').length} files) -
- ) : ( -
- {entries.filter((e) => e.type === 'folder').length} folders ·{' '} - {entries.filter((e) => e.type === 'file').length} files -
- )} +
+ + {/* Action menu modal */} + !open && setActionEntry(null)} + > + + + {actionEntry?.name} + + {actionEntry?.type === 'folder' ? 'Folder' : 'File'} + {actionEntry?.type === 'file' && actionEntry.size > 0 + ? ` · ${humanFileSize(actionEntry.size)}` + : ''} + + +
+ {actionEntry?.type === 'file' && ( + + )} + {actionEntry?.type === 'folder' && ( + + )} + +
+
+
); }