Shared file bug fixes, redesigned pin system, new info button
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
This commit is contained in:
parent
71ba029fbc
commit
2bcfca983c
7 changed files with 511 additions and 276 deletions
|
|
@ -15,6 +15,8 @@ import SettingsPage from './SettingsPage';
|
|||
import StorageDashboard from './StorageDashboard';
|
||||
import TaskManager from './TaskManager';
|
||||
import ShareModal from './ShareModal';
|
||||
import FileInfoModal from './FileInfoModal';
|
||||
import SharedFilesPage from './SharedFilesPage';
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
|
|
@ -34,7 +36,7 @@ interface FileExplorerProps {
|
|||
}
|
||||
|
||||
type ViewMode = 'grid' | 'list';
|
||||
type SidebarSection = 'files' | 'pinned' | 'trash' | 'storage' | 'settings';
|
||||
type SidebarSection = 'files' | 'trash' | 'storage' | 'settings' | 'shared';
|
||||
|
||||
interface UploadProgress {
|
||||
id: string;
|
||||
|
|
@ -80,7 +82,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<FileItem | null>(null);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const [infoFile, setInfoFile] = useState<FileItem | null>(null);
|
||||
const [sharingFile, setSharingFile] = useState<string | null>(null);
|
||||
const [sortBy, setSortBy] = useState<'name' | 'size' | 'date' | 'type'>('name');
|
||||
const [sortAsc, setSortAsc] = useState(true);
|
||||
|
|
@ -92,6 +94,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
const [moveModalPath, setMoveModalPath] = useState('.');
|
||||
const [moveFolders, setMoveFolders] = useState<FileItem[]>([]);
|
||||
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
||||
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
|
@ -105,6 +109,11 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
if (s) {
|
||||
if (s.grid_size) setGridSize(s.grid_size);
|
||||
if (s.trash_auto_purge_days) setTrashAutoPurgeDays(parseInt(s.trash_auto_purge_days, 10));
|
||||
if (s.pinned_order) {
|
||||
try {
|
||||
setPinnedOrder(JSON.parse(s.pinned_order));
|
||||
} catch(e) {}
|
||||
}
|
||||
const fetchedTheme = s.theme || 'dark';
|
||||
setTheme(fetchedTheme);
|
||||
if (fetchedTheme === 'light') {
|
||||
|
|
@ -249,13 +258,55 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
if (section === 'files') {
|
||||
navigateTo('.');
|
||||
loadFiles();
|
||||
} else if (section === 'pinned') {
|
||||
loadPinned();
|
||||
} else if (section === 'trash') {
|
||||
loadTrash();
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to keep pinned list loaded for the sidebar
|
||||
const [pinnedFiles, setPinnedFiles] = useState<FileItem[]>([]);
|
||||
useEffect(() => {
|
||||
if (pinnedExpanded) {
|
||||
api.listPinned().then(data => {
|
||||
// Sort by pinnedOrder
|
||||
const items = data.items || [];
|
||||
items.sort((a: FileItem, b: FileItem) => {
|
||||
const idxA = pinnedOrder.indexOf(a.path);
|
||||
const idxB = pinnedOrder.indexOf(b.path);
|
||||
if (idxA === -1 && idxB === -1) return a.name.localeCompare(b.name);
|
||||
if (idxA === -1) return 1;
|
||||
if (idxB === -1) return -1;
|
||||
return idxA - idxB;
|
||||
});
|
||||
setPinnedFiles(items);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}, [pinnedExpanded, pinnedOrder]);
|
||||
|
||||
async function handlePinnedDragEnd(sourcePath: string, destPath: string) {
|
||||
const newOrder = [...pinnedOrder];
|
||||
// Ensure all current pinned files are in order array
|
||||
const currentPaths = pinnedFiles.map(f => f.path);
|
||||
for (const p of currentPaths) {
|
||||
if (!newOrder.includes(p)) newOrder.push(p);
|
||||
}
|
||||
|
||||
const srcIdx = newOrder.indexOf(sourcePath);
|
||||
const destIdx = newOrder.indexOf(destPath);
|
||||
if (srcIdx > -1 && destIdx > -1) {
|
||||
newOrder.splice(srcIdx, 1);
|
||||
newOrder.splice(destIdx, 0, sourcePath);
|
||||
setPinnedOrder(newOrder);
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
const s = data?.settings || data || {};
|
||||
await api.updateSettings({ ...s, pinned_order: JSON.stringify(newOrder) });
|
||||
} catch (err) {
|
||||
console.error('Failed to save pinned order', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
if (currentPath === '.' || currentPath === '') return;
|
||||
const parts = currentPath.split('/');
|
||||
|
|
@ -328,6 +379,20 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
async function handlePin(file: FileItem) {
|
||||
await api.togglePin(file.path);
|
||||
loadFiles();
|
||||
if (pinnedExpanded) {
|
||||
api.listPinned().then(data => {
|
||||
const items = data.items || [];
|
||||
items.sort((a: FileItem, b: FileItem) => {
|
||||
const idxA = pinnedOrder.indexOf(a.path);
|
||||
const idxB = pinnedOrder.indexOf(b.path);
|
||||
if (idxA === -1 && idxB === -1) return a.name.localeCompare(b.name);
|
||||
if (idxA === -1) return 1;
|
||||
if (idxB === -1) return -1;
|
||||
return idxA - idxB;
|
||||
});
|
||||
setPinnedFiles(items);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRename(path: string) {
|
||||
|
|
@ -559,10 +624,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
});
|
||||
|
||||
const groupedFiles = useMemo(() => {
|
||||
const baseFiles = displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned);
|
||||
const baseFiles = displayFiles;
|
||||
if (activeSection !== 'trash' || trashAutoPurgeDays <= 0) {
|
||||
const defaultName = activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : '';
|
||||
return [{ name: defaultName, files: baseFiles }];
|
||||
return [{ name: '', files: baseFiles }];
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
|
@ -638,10 +702,81 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-2 space-y-1">
|
||||
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
|
||||
{[
|
||||
{ id: 'files' as SidebarSection, icon: Folder, label: 'All Files' },
|
||||
{ id: 'pinned' as SidebarSection, icon: Pin, label: 'Pinned' },
|
||||
].map(({ id, icon: Icon, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
id={`nav-${id}`}
|
||||
onClick={() => handleSectionChange(id)}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
|
||||
style={{
|
||||
backgroundColor: activeSection === id ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
color: activeSection === id ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
<Icon className="w-4.5 h-4.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Pinned Collapsible Section */}
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => setPinnedExpanded(!pinnedExpanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:bg-white/5"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Pin className="w-4.5 h-4.5" />
|
||||
Pinned
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 transition-transform duration-200 ${pinnedExpanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{pinnedExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden flex flex-col gap-0.5 mt-1"
|
||||
>
|
||||
{pinnedFiles.length === 0 ? (
|
||||
<div className="px-10 py-2 text-xs" style={{ color: 'var(--color-text-tertiary)' }}>No pinned items</div>
|
||||
) : (
|
||||
pinnedFiles.map((file) => (
|
||||
<div
|
||||
key={file.path}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('text/plain', file.path);
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const source = e.dataTransfer.getData('text/plain');
|
||||
if (source && source !== file.path) {
|
||||
handlePinnedDragEnd(source, file.path);
|
||||
}
|
||||
}}
|
||||
className="group flex items-center justify-between px-10 py-2 text-sm font-medium cursor-pointer rounded-lg transition-colors hover:bg-white/5"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
onClick={() => navigateTo(file.path)}
|
||||
>
|
||||
<span className="truncate flex-1">{file.name}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{[
|
||||
{ id: 'shared' as SidebarSection, icon: LinkIcon, label: 'Shared Files' },
|
||||
{ id: 'trash' as SidebarSection, icon: Trash2, label: 'Trash' },
|
||||
{ id: 'storage' as SidebarSection, icon: HardDrive, label: 'Storage' },
|
||||
].map(({ id, icon: Icon, label }) => (
|
||||
|
|
@ -659,7 +794,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
{label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
</nav>
|
||||
|
||||
{/* Settings Footer */}
|
||||
|
|
@ -760,7 +894,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</button>
|
||||
)}
|
||||
|
||||
{(activeSection === 'files' || activeSection === 'pinned' || activeSection === 'trash') && (
|
||||
{(activeSection === 'files' || activeSection === 'trash' || activeSection === 'shared') && (
|
||||
<>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
|
|
@ -771,19 +905,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
{theme === 'dark' ? <Sun className="w-4.5 h-4.5" /> : <Moon className="w-4.5 h-4.5" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="info-toggle"
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
className="p-2 rounded-lg transition-colors"
|
||||
style={{
|
||||
backgroundColor: showInfo ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
color: showInfo ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
||||
}}
|
||||
title="Toggle info panel"
|
||||
>
|
||||
<Info className="w-4.5 h-4.5" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-5 mx-1" style={{ backgroundColor: 'var(--color-border-subtle)' }} />
|
||||
|
||||
<div className="relative group">
|
||||
|
|
@ -856,6 +977,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
<SettingsPage onLogout={onLogout} />
|
||||
) : activeSection === 'storage' ? (
|
||||
<StorageDashboard />
|
||||
) : activeSection === 'shared' ? (
|
||||
<SharedFilesPage />
|
||||
) : (
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* File Area */}
|
||||
|
|
@ -943,87 +1066,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
) : viewMode === 'grid' ? (
|
||||
/* Grid View */
|
||||
<div className="space-y-8">
|
||||
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>Pinned</h3>
|
||||
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
|
||||
{displayFiles.filter(f => f.is_pinned).map((file, idx) => (
|
||||
<motion.div
|
||||
key={file.path}
|
||||
draggable
|
||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||
onDragLeave={(e: any) => handleItemDragLeave(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) || dragOverFolder === file.path ? 'var(--color-accent-subtle)' : 'var(--color-bg-elevated)',
|
||||
border: `1px solid ${selectedFiles.has(file.path) || dragOverFolder === file.path ? 'var(--color-accent)' : 'var(--color-border-subtle)'}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-3 left-3 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
style={{ opacity: selectedFiles.has(file.path) ? 1 : undefined }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFiles.has(file.path)}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
{file.is_pinned && (
|
||||
<Pin className="absolute top-2 right-2 w-3 h-3" style={{ color: 'var(--color-warning)' }} />
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-full h-32 flex items-center justify-center">
|
||||
{getFileIcon(file, true)}
|
||||
</div>
|
||||
{renaming === file.path ? (
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => 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)',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="text-xs font-medium text-center w-full truncate"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{groupedFiles.map((group, groupIdx) => (
|
||||
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
|
||||
<div key={group.name} className={groupIdx > 0 ? 'mt-8' : ''}>
|
||||
{group.name && (
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
|
||||
)}
|
||||
|
|
@ -1106,72 +1151,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
) : (
|
||||
/* List View */
|
||||
<div className="space-y-8">
|
||||
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider mb-2 px-4" style={{ color: 'var(--color-text-tertiary)' }}>Pinned</h3>
|
||||
<div className="space-y-0.5">
|
||||
{displayFiles.filter(f => f.is_pinned).map((file, idx) => (
|
||||
<motion.div
|
||||
key={file.path}
|
||||
draggable
|
||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||
onDragLeave={(e: any) => handleItemDragLeave(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) || dragOverFolder === file.path ? 'var(--color-accent-subtle)' : 'transparent' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFiles.has(file.path)}
|
||||
onChange={(e) => {
|
||||
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 ? (
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => 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)' }}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm truncate" style={{ color: 'var(--color-text-primary)' }}>{file.name}</span>
|
||||
)}
|
||||
{file.is_pinned && <Pin className="w-3 h-3 flex-shrink-0" style={{ color: 'var(--color-warning)' }} />}
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>{file.is_dir ? '—' : formatSize(file.size)}</span>
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>{formatDate(file.mod_time)}</span>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleContextMenu(e, file); }} className="p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<MoreVertical className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{groupedFiles.map((group, groupIdx) => (
|
||||
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
|
||||
<div key={group.name} className={groupIdx > 0 ? 'mt-8' : ''}>
|
||||
{group.name && (
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider mb-2 px-4" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
|
||||
)}
|
||||
|
|
@ -1317,98 +1299,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Right Info Sidebar */}
|
||||
<AnimatePresence>
|
||||
{showInfo && (
|
||||
<motion.aside
|
||||
initial={{ x: 320, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 320, opacity: 0 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="w-80 border-l flex-shrink-0 flex flex-col overflow-y-auto bg-opacity-50 backdrop-blur-md"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-sidebar-bg)',
|
||||
borderColor: 'var(--color-border-subtle)',
|
||||
}}
|
||||
>
|
||||
<div className="p-5 flex flex-col gap-6">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
File Details
|
||||
</h3>
|
||||
|
||||
{selectedFiles.size === 1 ? (() => {
|
||||
const file = displayFiles.find(f => f.path === Array.from(selectedFiles)[0]);
|
||||
if (!file) return null;
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-20 h-20 flex items-center justify-center">
|
||||
{getFileIcon(file)}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-center break-all" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>Type</span>
|
||||
<span className="text-sm" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{file.is_dir ? 'Folder' : file.mime_type || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>Size</span>
|
||||
<span className="text-sm" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{file.is_dir ? '—' : formatSize(file.size)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>Location</span>
|
||||
<span className="text-sm break-all" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{file.path}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>Modified</span>
|
||||
<span className="text-sm" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{new Date(file.mod_time).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{file.checksum && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>Checksum (XXHash)</span>
|
||||
<span className="text-xs font-mono break-all" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{file.checksum}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})() : selectedFiles.size > 1 ? (
|
||||
<div className="flex flex-col items-center justify-center h-40 text-center gap-2">
|
||||
<Check className="w-8 h-8" style={{ color: 'var(--color-accent)' }} />
|
||||
<span className="text-sm" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{selectedFiles.size} items selected
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-40 text-center gap-2">
|
||||
<Info className="w-8 h-8" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
<span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Select a file to see its details
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
|
@ -1441,6 +1331,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenuItem
|
||||
icon={<Info className="w-4 h-4" />}
|
||||
label="Info"
|
||||
onClick={() => {
|
||||
setInfoFile(contextMenu.file);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
label="Download"
|
||||
|
|
@ -1587,6 +1485,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{infoFile && (
|
||||
<FileInfoModal filePath={infoFile.path} onClose={() => setInfoFile(null)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showTrashModal && (
|
||||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
|
|
|
|||
Reference in a new issue