'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Folder, File, Search, Pin, Trash2, HardDrive, Settings, ChevronRight, Grid3X3, List, Plus, Upload, LogOut, MoreVertical, Star, Download, Pencil, Copy, Move, FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon } from 'lucide-react'; import api from '@/lib/api'; import FilePreview from './FilePreview'; import SettingsPage from './SettingsPage'; import StorageDashboard from './StorageDashboard'; import TaskManager from './TaskManager'; import ShareModal from './ShareModal'; interface FileItem { name: string; path: string; is_dir: boolean; size: number; mime_type: string; is_pinned: boolean; is_trashed: boolean; mod_time: string; checksum?: string; } interface FileExplorerProps { onLogout: () => void; } type ViewMode = 'grid' | 'list'; type SidebarSection = 'files' | 'pinned' | 'trash' | 'storage' | 'settings'; interface UploadProgress { id: string; name: string; progress: number; status: 'uploading' | 'complete' | 'error'; error?: string; } export default function FileExplorer({ onLogout }: FileExplorerProps) { const [currentPath, setCurrentPath] = useState(() => { if (typeof window !== 'undefined') { return new URLSearchParams(window.location.search).get('path') || '.'; } return '.'; }); const navigateTo = useCallback((path: string) => { setCurrentPath(path); window.history.pushState({ path }, '', `?path=${encodeURIComponent(path)}`); }, []); useEffect(() => { const handlePopState = (e: PopStateEvent) => { if (e.state && e.state.path) setCurrentPath(e.state.path); else setCurrentPath(new URLSearchParams(window.location.search).get('path') || '.'); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState('grid'); const [activeSection, setActiveSection] = useState('files'); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [selectedFiles, setSelectedFiles] = useState>(new Set()); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; file: FileItem } | null>(null); const [renaming, setRenaming] = useState(null); const [newName, setNewName] = useState(''); const [showNewFolder, setShowNewFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [dragOver, setDragOver] = useState(false); const [previewFile, setPreviewFile] = useState(null); const [showInfo, setShowInfo] = useState(false); const [sharingFile, setSharingFile] = useState(null); const [sortBy, setSortBy] = useState<'name' | 'size' | 'date' | 'type'>('name'); const [sortAsc, setSortAsc] = useState(true); const [draggedFile, setDraggedFile] = useState(null); const [uploads, setUploads] = useState([]); const [showTrashModal, setShowTrashModal] = useState(false); const [showMoveModal, setShowMoveModal] = useState(false); const [movingFiles, setMovingFiles] = useState([]); const [moveModalPath, setMoveModalPath] = useState('.'); const [moveFolders, setMoveFolders] = useState([]); const searchTimeoutRef = useRef(undefined); const fileInputRef = useRef(null); // Load files on path change const loadFiles = useCallback(async (path?: string) => { const targetPath = path ?? currentPath; setLoading(true); try { const data = await api.listDirectory(targetPath); setFiles(data.files || []); } catch { setFiles([]); } finally { setLoading(false); } }, [currentPath]); useEffect(() => { if (activeSection === 'files') { loadFiles(currentPath); } }, [currentPath, activeSection]); const loadMoveFolders = useCallback(async (path: string) => { try { const data = await api.listDirectory(path); setMoveFolders(data.files?.filter((f: FileItem) => f.is_dir).sort((a: FileItem, b: FileItem) => a.name.localeCompare(b.name)) || []); } catch { setMoveFolders([]); } }, []); useEffect(() => { if (showMoveModal) { loadMoveFolders(moveModalPath); } }, [showMoveModal, moveModalPath, loadMoveFolders]); // Keyboard shortcuts useEffect(() => { function handleKeyDown(e: KeyboardEvent) { // Don't trigger shortcuts when typing in inputs if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === 'Backspace') { e.preventDefault(); navigateUp(); } else if (e.key === 'Delete') { deleteSelected(); } else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); selectAll(); } else if (e.key === 'Escape') { setSelectedFiles(new Set()); setContextMenu(null); setRenaming(null); } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [files, selectedFiles]); // Close context menu on click outside useEffect(() => { function handleClick() { setContextMenu(null); } window.addEventListener('click', handleClick); return () => window.removeEventListener('click', handleClick); }, []); async function loadPinned() { setLoading(true); try { const data = await api.listPinned(); setFiles(data.items || []); } catch { setFiles([]); } finally { setLoading(false); } } async function loadTrash() { setLoading(true); try { const data = await api.listTrash(); setFiles(data.items?.map((item: any) => ({ ...item, is_trashed: true, })) || []); } catch { setFiles([]); } finally { setLoading(false); } } function handleSectionChange(section: SidebarSection) { setActiveSection(section); setSelectedFiles(new Set()); setSearchQuery(''); setSearchResults(null); if (section === 'files') { navigateTo('.'); loadFiles(); } else if (section === 'pinned') { loadPinned(); } else if (section === 'trash') { loadTrash(); } } function navigateUp() { if (currentPath === '.' || currentPath === '') return; const parts = currentPath.split('/'); parts.pop(); navigateTo(parts.length === 0 ? '.' : parts.join('/')); } function navigateToFolder(file: FileItem) { if (!file.is_dir) return; navigateTo(file.path); setSelectedFiles(new Set()); } function handleFileClick(file: FileItem, e: React.MouseEvent) { if (e.shiftKey) { // Multi-select with shift setSelectedFiles(prev => { const next = new Set(prev); if (next.has(file.path)) { next.delete(file.path); } else { next.add(file.path); } return next; }); } else { // Single click to open/navigate if (file.is_dir) navigateToFolder(file); else setPreviewFile(file); } } function handleContextMenu(e: React.MouseEvent, file: FileItem) { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, file }); } async function handleSearch(query: string) { setSearchQuery(query); if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); if (!query.trim()) { setSearchResults(null); return; } searchTimeoutRef.current = setTimeout(async () => { try { const data = await api.search(query); setSearchResults(data.results || []); } catch { setSearchResults([]); } }, 300); } function selectAll() { setSelectedFiles(new Set(files.map(f => f.path))); } async function deleteSelected() { if (selectedFiles.size === 0) return; for (const path of selectedFiles) { await api.deleteFile(path); } setSelectedFiles(new Set()); loadFiles(); } async function handlePin(file: FileItem) { await api.togglePin(file.path); loadFiles(); } async function handleRename(path: string) { if (!newName.trim()) return; await api.rename(path, newName); setRenaming(null); setNewName(''); loadFiles(); } async function handleCreateFolder() { if (!newFolderName.trim()) return; const folderPath = currentPath === '.' ? newFolderName : `${currentPath}/${newFolderName}`; await api.createFolder(folderPath); setShowNewFolder(false); setNewFolderName(''); loadFiles(); } async function handleRestore(file: FileItem) { await api.restoreFromTrash(file.path); loadTrash(); } async function handleEmptyTrash() { await api.emptyTrash(); loadTrash(); } async function handleMoveExecute() { if (movingFiles.length === 0) return; try { await Promise.all(movingFiles.map(src => api.move(src, moveModalPath === '.' ? '.' : moveModalPath))); setShowMoveModal(false); setMovingFiles([]); loadFiles(); setSelectedFiles(new Set()); } catch (err) { console.error(err); } } // Drag and drop upload function handleDragOver(e: React.DragEvent) { if (draggedFile) return; if (!e.dataTransfer.types.includes('Files')) return; e.preventDefault(); setDragOver(true); } function handleDragLeave() { setDragOver(false); } function handleItemDragStart(e: React.DragEvent, path: string) { e.dataTransfer.setData('text/plain', path); setDraggedFile(path); } function handleItemDragOver(e: React.DragEvent, file: FileItem) { if (!file.is_dir) return; if (draggedFile === file.path) return; e.preventDefault(); } async function handleItemDrop(e: React.DragEvent, file: FileItem) { if (!file.is_dir) return; const sourcePath = e.dataTransfer.getData('text/plain'); if (!sourcePath || sourcePath === file.path) return; e.preventDefault(); e.stopPropagation(); setDraggedFile(null); try { await api.move(sourcePath, file.path); loadFiles(); } catch (err) { console.error(err); } } async function uploadFile(file: File) { const id = `${Date.now()}-${file.name}`; setUploads(prev => [...prev, { id, name: file.name, progress: 0, status: 'uploading' }]); try { await api.upload(file, currentPath, { onProgress: (percent) => { setUploads(prev => prev.map(u => u.id === id ? { ...u, progress: percent } : u)); }, }); setUploads(prev => prev.map(u => u.id === id ? { ...u, progress: 100, status: 'complete' } : u)); setTimeout(() => { setUploads(prev => prev.filter(u => u.id !== id)); }, 3000); } catch (err: any) { setUploads(prev => prev.map(u => u.id === id ? { ...u, status: 'error', error: err.message || 'Upload failed' } : u)); } } async function handleDrop(e: React.DragEvent) { e.preventDefault(); setDragOver(false); if (draggedFile) return; const droppedFiles = Array.from(e.dataTransfer.files); await Promise.all(droppedFiles.map(file => uploadFile(file))); loadFiles(); } async function handleFileUpload(e: React.ChangeEvent) { const uploadFiles = e.target.files; if (!uploadFiles) return; await Promise.all(Array.from(uploadFiles).map(file => uploadFile(file))); e.target.value = ''; loadFiles(); } // Breadcrumb path segments const pathSegments = currentPath === '.' ? [] : currentPath.split('/'); function formatSize(bytes: number): string { if (bytes === 0) return '—'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function formatDate(dateStr: string): string { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function getFileIcon(file: FileItem, large = false) { const sizeClass = large ? "w-12 h-12" : "w-5 h-5"; if (file.is_dir) return ; const ext = file.name.split('.').pop()?.toLowerCase() || ''; const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext) || file.mime_type?.startsWith('image/'); const isVideo = ['mp4', 'mkv', 'avi', 'mov', 'webm', 'ogg'].includes(ext) || file.mime_type?.startsWith('video/'); const isMedia = isImage || isVideo; if (ext === 'pdf') { return (
PDF
); } if (isImage) { return (
); } if (isVideo) { return (
); } const fallbackIcon = (() => { let color = 'var(--color-text-tertiary)'; if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) color = '#f59e0b'; else if (['mp4', 'mkv', 'avi', 'mov', 'webm'].includes(ext)) color = '#ef4444'; else if (['mp3', 'wav', 'flac', 'ogg', 'm4a'].includes(ext)) color = '#22c55e'; else if (['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) color = '#8b5cf6'; else if (['js', 'ts', 'py', 'go', 'rs', 'md', 'txt'].includes(ext)) color = '#3b82f6'; return ; })(); if (isMedia && file.checksum && viewMode === 'grid') { return (
{ e.currentTarget.style.display = 'none'; const next = e.currentTarget.nextElementSibling as HTMLElement; if (next) next.style.display = 'block'; }} />
{fallbackIcon}
); } return fallbackIcon; } let displayFiles = searchResults !== null ? searchResults : files; displayFiles = displayFiles.filter(f => f.name !== '.uploads').sort((a, b) => { // Directories always first regardless of sort type if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; let cmp = 0; if (sortBy === 'name') cmp = a.name.localeCompare(b.name); else if (sortBy === 'size') cmp = a.size - b.size; else if (sortBy === 'date') cmp = new Date(a.mod_time).getTime() - new Date(b.mod_time).getTime(); else if (sortBy === 'type') { const extA = a.name.split('.').pop()?.toLowerCase() || ''; const extB = b.name.split('.').pop()?.toLowerCase() || ''; cmp = extA.localeCompare(extB); } return sortAsc ? cmp : -cmp; }); return (
{/* === SIDEBAR === */} {/* === MAIN CONTENT === */}
{/* Top Bar */}
{/* Breadcrumbs */}
{pathSegments.map((seg, i) => (
))}
{/* Search Bar */}
handleSearch(e.target.value)} placeholder="Search files..." className="w-full pl-9 pr-4 py-2 rounded-lg text-sm outline-none transition-all" style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)', }} /> {searchQuery && ( )}
{/* Action Buttons */}
{activeSection === 'trash' && ( )} {(activeSection === 'files' || activeSection === 'pinned' || activeSection === 'trash') && ( <>
{['name', 'size', 'date', 'type'].map(opt => ( ))}
{activeSection === 'files' && ( )} {activeSection === 'files' && ( <> )} )}
{/* Dynamic Content Area */} {activeSection === 'settings' ? ( ) : activeSection === 'storage' ? ( ) : (
{/* File Area */}
{/* Drag and drop overlay */} {dragOver && (

Drop files to upload

Files will be uploaded to the current folder

)}
{/* New Folder Input */} {showNewFolder && (
setNewFolderName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowNewFolder(false); }} placeholder="Folder name" autoFocus className="flex-1 bg-transparent text-sm outline-none border-none ring-0" style={{ color: 'var(--color-text-primary)' }} />
)}
{loading ? (
) : displayFiles.length === 0 ? (

{searchResults !== null ? 'No results found' : 'This folder is empty'}

{searchResults === null && (

Drop files here or click Upload

)}
) : viewMode === 'grid' ? ( /* Grid View */
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (

Pinned

{displayFiles.filter(f => f.is_pinned).map((file, idx) => ( handleItemDragStart(e, file.path)} onDragOver={(e: any) => handleItemDragOver(e, file)} onDrop={(e: any) => handleItemDrop(e, file)} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: idx * 0.02 }} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => handleContextMenu(e, file)} className={`group relative p-4 rounded-xl cursor-pointer transition-all duration-150 border-2 ${draggedFile === file.path ? 'opacity-50' : ''}`} style={{ backgroundColor: selectedFiles.has(file.path) ? 'var(--color-accent-subtle)' : 'var(--color-bg-elevated)', border: `1px solid ${selectedFiles.has(file.path) ? 'var(--color-accent)' : 'var(--color-border-subtle)'}`, }} >
{ const newSet = new Set(selectedFiles); if (e.target.checked) newSet.add(file.path); else newSet.delete(file.path); setSelectedFiles(newSet); }} onClick={(e) => e.stopPropagation()} className="w-4 h-4 rounded border-gray-300 cursor-pointer" />
{file.is_pinned && ( )}
{getFileIcon(file, true)}
{renaming === file.path ? ( setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(file.path); if (e.key === 'Escape') setRenaming(null); }} onBlur={() => handleRename(file.path)} autoFocus onFocus={(e) => e.target.select()} className="w-full text-xs text-center bg-transparent outline-none" style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)', }} /> ) : ( {file.name} )}
))}
)}
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (

All Files

)}
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => ( handleItemDragStart(e, file.path)} onDragOver={(e: any) => handleItemDragOver(e, file)} onDrop={(e: any) => handleItemDrop(e, file)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: idx * 0.02 }} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => handleContextMenu(e, file)} className={`group relative p-4 rounded-xl cursor-pointer transition-all duration-150 ${draggedFile === file.path ? 'opacity-50' : ''}`} style={{ backgroundColor: selectedFiles.has(file.path) ? 'var(--color-accent-subtle)' : 'var(--color-bg-elevated)', border: `1px solid ${selectedFiles.has(file.path) ? 'var(--color-accent)' : 'var(--color-border-subtle)'}`, }} >
{ const newSet = new Set(selectedFiles); if (e.target.checked) newSet.add(file.path); else newSet.delete(file.path); setSelectedFiles(newSet); }} onClick={(e) => e.stopPropagation()} className="w-4 h-4 rounded border-gray-300 cursor-pointer" />
{file.is_pinned && ( )}
{getFileIcon(file, true)}
{renaming === file.path ? ( setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(file.path); if (e.key === 'Escape') setRenaming(null); }} onBlur={() => handleRename(file.path)} autoFocus onFocus={(e) => e.target.select()} className="w-full text-xs text-center bg-transparent outline-none" style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)', }} /> ) : ( {file.name} )}
))}
) : ( /* List View */
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (

Pinned

{displayFiles.filter(f => f.is_pinned).map((file, idx) => ( handleItemDragStart(e, file.path)} onDragOver={(e: any) => handleItemDragOver(e, file)} onDrop={(e: any) => handleItemDrop(e, file)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: idx * 0.015 }} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => handleContextMenu(e, file)} className={`grid grid-cols-[1fr_100px_140px_40px] gap-4 items-center px-4 py-2.5 rounded-lg cursor-pointer transition-all duration-100 group ${draggedFile === file.path ? 'opacity-50' : ''}`} style={{ backgroundColor: selectedFiles.has(file.path) ? 'var(--color-accent-subtle)' : 'transparent' }} >
{ const newSet = new Set(selectedFiles); if (e.target.checked) newSet.add(file.path); else newSet.delete(file.path); setSelectedFiles(newSet); }} onClick={(e) => e.stopPropagation()} className="w-4 h-4 rounded border-gray-300 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" style={{ opacity: selectedFiles.has(file.path) ? 1 : undefined }} /> {getFileIcon(file)} {renaming === file.path ? ( setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(file.path); if (e.key === 'Escape') setRenaming(null); }} onBlur={() => handleRename(file.path)} autoFocus onFocus={(e) => e.target.select()} className="flex-1 text-sm bg-transparent outline-none" style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }} /> ) : ( {file.name} )} {file.is_pinned && }
{file.is_dir ? '—' : formatSize(file.size)} {formatDate(file.mod_time)}
))}
)}
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : 'Name'} Size Modified
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => ( handleItemDragStart(e, file.path)} onDragOver={(e: any) => handleItemDragOver(e, file)} onDrop={(e: any) => handleItemDrop(e, file)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: idx * 0.015 }} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => handleContextMenu(e, file)} className={`grid grid-cols-[1fr_100px_140px_40px] gap-4 items-center px-4 py-2.5 rounded-lg cursor-pointer transition-all duration-100 group ${draggedFile === file.path ? 'opacity-50' : ''}`} style={{ backgroundColor: selectedFiles.has(file.path) ? 'var(--color-accent-subtle)' : 'transparent' }} >
{ const newSet = new Set(selectedFiles); if (e.target.checked) newSet.add(file.path); else newSet.delete(file.path); setSelectedFiles(newSet); }} onClick={(e) => e.stopPropagation()} className="w-4 h-4 rounded border-gray-300 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity" style={{ opacity: selectedFiles.has(file.path) ? 1 : undefined }} /> {getFileIcon(file)} {renaming === file.path ? ( setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(file.path); if (e.key === 'Escape') setRenaming(null); }} onBlur={() => handleRename(file.path)} autoFocus onFocus={(e) => e.target.select()} className="flex-1 text-sm bg-transparent outline-none" style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }} /> ) : ( {file.name} )} {file.is_pinned && }
{file.is_dir ? '—' : formatSize(file.size)} {formatDate(file.mod_time)}
))}
)}
{/* Bulk Actions Bar */} {selectedFiles.size > 0 && (
{selectedFiles.size} selected
)}
{/* Right Info Sidebar */} {showInfo && (

File Details

{selectedFiles.size === 1 ? (() => { const file = displayFiles.find(f => f.path === Array.from(selectedFiles)[0]); if (!file) return null; return (
{getFileIcon(file)}
{file.name}
Type {file.is_dir ? 'Folder' : file.mime_type || 'Unknown'}
Size {file.is_dir ? '—' : formatSize(file.size)}
Location {file.path}
Modified {new Date(file.mod_time).toLocaleString()}
{file.checksum && (
Checksum (XXHash) {file.checksum}
)}
); })() : selectedFiles.size > 1 ? (
{selectedFiles.size} items selected
) : (
Select a file to see its details
)}
)}
)}
{/* === CONTEXT MENU === */} {contextMenu && ( e.stopPropagation()} > {activeSection === 'trash' ? ( <> } label="Restore" onClick={() => { handleRestore(contextMenu.file); setContextMenu(null); }} /> ) : ( <> } label="Download" onClick={() => { const downloadUrl = contextMenu.file.is_dir ? api.getDownloadFolderUrl(contextMenu.file.path) : api.getDownloadUrl(contextMenu.file.path); window.open(downloadUrl, '_blank'); setContextMenu(null); }} /> } label="Rename" onClick={() => { setRenaming(contextMenu.file.path); setNewName(contextMenu.file.name); setContextMenu(null); }} /> } label={contextMenu.file.is_pinned ? 'Unpin' : 'Pin'} onClick={() => { handlePin(contextMenu.file); setContextMenu(null); }} /> } label="Share" onClick={() => { setSharingFile(contextMenu.file.path); setContextMenu(null); }} /> } label="Move" onClick={() => { const filesToMove = selectedFiles.has(contextMenu.file.path) ? Array.from(selectedFiles) : [contextMenu.file.path]; setMovingFiles(filesToMove); setMoveModalPath('.'); setShowMoveModal(true); setContextMenu(null); }} />
} label="Move to Trash" danger onClick={() => { api.deleteFile(contextMenu.file.path).then(() => loadFiles()); setContextMenu(null); }} /> )} )} {/* Upload Progress Panel */} {uploads.length > 0 && (
{uploads.filter(u => u.status === 'uploading').length > 0 ? `Uploading ${uploads.filter(u => u.status === 'uploading').length} file${uploads.filter(u => u.status === 'uploading').length > 1 ? 's' : ''}` : 'Uploads complete'}
{uploads.map(upload => (
{upload.name} {upload.status === 'complete' ? '✓' : upload.status === 'error' ? '✗' : `${Math.round(upload.progress)}%`}
{upload.status === 'error' && (

{upload.error}

)}
))}
)}
setPreviewFile(null)} /> {sharingFile && ( setSharingFile(null)} /> )} {showTrashModal && (

Empty Trash?

Are you sure you want to permanently delete all items in the trash? This action cannot be undone.

)}
{showMoveModal && (

Move {movingFiles.length} item{movingFiles.length > 1 ? 's' : ''}

Home {moveModalPath !== '.' && '> ' + moveModalPath.replace(/\//g, ' > ')}
{moveFolders.length === 0 ? (
No folders here
) : ( moveFolders.map(folder => ( )) )}
)}
); } function ContextMenuItem({ icon, label, onClick, danger = false, }: { icon: React.ReactNode; label: string; onClick: () => void; danger?: boolean; }) { return ( ); }