import { useEffect, useRef, useState } from 'react'; import { Folder, File, FileText, HardDrive, Image as ImageIcon, ChevronRight, Home, RefreshCw, Upload, FolderPlus, ArrowLeft, Loader2, MoreVertical, Check, Trash2, } from 'lucide-react'; import { createFolder, deletePath, humanFileSize, joinPath, listDirectory, normalizePath, putFileContents, splitPath, stat, type EntryInfo, } from '../webdav'; import { toast } from 'sonner'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from './ui/dialog'; const TEXT_EXTS = new Set(['txt','cfg','ini','bas','asm','seq','rel','prg','log','csv','s','lst','md','markdown','json','xml','svg','html','htm']); const IMAGE_EXTS = new Set(['png','jpg','jpeg','gif','bmp','webp']); const DISK_EXTS = new Set(['d64','d71','d81','d82','g64','g71','t64','tap','crt','nib']); function EntryIcon({ entry }: { entry: EntryInfo }) { if (entry.type === 'folder') return ; const ext = entry.name.split('.').pop()?.toLowerCase() ?? ''; if (IMAGE_EXTS.has(ext)) return ; if (DISK_EXTS.has(ext)) return ; if (TEXT_EXTS.has(ext)) return ; return ; } interface MediaBrowserProps { currentPath: string; onSelect: (path: string) => void; onClose: () => void; } export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBrowserProps) { // 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 [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showNewFolder, setShowNewFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [actionEntry, setActionEntry] = useState(null); const fileInputRef = useRef(null); // Resolve the initial path on mount. useEffect(() => { const initial = normalizePath(currentPath || '/'); if (initial === '/') { setPath('/'); return; } let cancelled = false; stat(initial) .then((info) => { if (cancelled) return; if (info && info.type === 'file') { setPath(splitPath(info.path).parent); } else if (info && info.type === 'folder') { setPath(info.path); } else { setPath(splitPath(initial).parent); } }) .catch(() => { if (cancelled) return; setPath(splitPath(initial).parent); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const load = async (p: string) => { setLoading(true); setError(null); try { const items = await listDirectory(p); setEntries(items); } catch (e: any) { const msg = (e && e.message) || 'Failed to load directory'; setError(msg); setEntries([]); } finally { setLoading(false); } }; useEffect(() => { if (path === null) return; void load(path); // eslint-disable-next-line react-hooks/exhaustive-deps }, [path]); const navigateUp = () => { if (path === '/' || path === null) return; setPath(splitPath(path).parent); }; const navigateToFolder = (folderName: string) => { if (path === null) return; setPath(joinPath(path, folderName)); }; const selectEntry = (entry: EntryInfo) => { if (entry.type !== 'file') return; onSelect(entry.path); onClose(); }; const selectCurrentFolder = () => { if (path === null) return; onSelect(path); onClose(); }; const refresh = () => { if (path !== null) void load(path); }; const handleCreateFolder = async () => { const name = newFolderName.trim(); if (!name || path === null) return; try { await createFolder(joinPath(path, name), true); setNewFolderName(''); setShowNewFolder(false); toast.success(`Created folder "${name}"`); void load(path); } catch (e: any) { toast.error(`Failed to create folder: ${e?.message || e}`); } }; const handleDelete = async (entry: EntryInfo) => { const ok = window.confirm( entry.type === 'folder' ? `Delete folder "${entry.name}" and all its contents?` : `Delete file "${entry.name}"?`, ); if (!ok) return; setActionEntry(null); try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); if (path !== null) void load(path); } catch (e: any) { toast.error(`Failed to delete: ${e?.message || e}`); } }; const handleUpload = async (file: File) => { if (path === null) return; const target = joinPath(path, file.name); try { const buf = await file.arrayBuffer(); await putFileContents(target, buf); toast.success(`Uploaded ${file.name}`); void load(path); } catch (e: any) { toast.error(`Failed to upload ${file.name}: ${e?.message || e}`); } }; const onPickFiles = (e: React.ChangeEvent) => { const files = e.target.files; if (!files) return; Array.from(files).forEach((f) => void handleUpload(f)); e.target.value = ''; }; const pathParts = (path ?? '').split('/').filter(Boolean); return (
e.stopPropagation()} >

Browse Files

{showNewFolder && (
setNewFolderName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void handleCreateFolder(); if (e.key === 'Escape') { setShowNewFolder(false); setNewFolderName(''); } }} placeholder="New folder name" className="flex-1 px-2 py-1 text-sm border border-neutral-300 rounded" autoFocus />
)}
{pathParts.map((part, index) => (
))}
{path === null && (
Resolving location…
)} {loading && (
Loading…
)} {!loading && error && (
Failed to load directory
{error}
)} {!loading && !error && path !== null && ( <> {path !== '/' && ( )} {entries.map((entry) => (
))} {entries.length === 0 && (
Empty folder
)} )}
{/* 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' && ( )}
); }