diff --git a/backend/handlers/files.go b/backend/handlers/files.go index 0b6d78c..f39ce1e 100644 --- a/backend/handlers/files.go +++ b/backend/handlers/files.go @@ -1152,6 +1152,8 @@ func (h *FSHandler) StorageDashboard(c *fiber.Ctx) error { "audio": {Category: "audio"}, "documents": {Category: "documents"}, "archives": {Category: "archives"}, + "code": {Category: "code"}, + "3d_models": {Category: "3d_models"}, "other": {Category: "other"}, } diff --git a/frontend/src/components/FilePreview.tsx b/frontend/src/components/FilePreview.tsx index 4721c11..f10d785 100644 --- a/frontend/src/components/FilePreview.tsx +++ b/frontend/src/components/FilePreview.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2, Archive } from 'lucide-react'; +import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2, Archive, Folder, ChevronLeft } from 'lucide-react'; import api from '@/lib/api'; import dynamic from 'next/dynamic'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -29,9 +29,44 @@ interface FilePreviewProps { export default function FilePreview({ file, onClose, downloadToken }: FilePreviewProps) { const [content, setContent] = useState(null); const [archiveItems, setArchiveItems] = useState<{name: string, size: number, path: string}[] | null>(null); + const [archivePath, setArchivePath] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const visibleItems = useMemo(() => { + if (!archiveItems) return []; + + const itemsMap = new Map(); + + archiveItems.forEach(item => { + // Normalize path (remove leading slash if any) + const normalizedItemPath = item.path.replace(/\\/g, '/').replace(/^\/+/, ''); + if (normalizedItemPath.startsWith(archivePath)) { + const relativePath = normalizedItemPath.substring(archivePath.length); + if (relativePath === '') return; // Skip the directory itself if present + + const parts = relativePath.split('/'); + + if (parts.length === 1) { + itemsMap.set(parts[0], { name: parts[0], isDir: false, size: item.size, path: normalizedItemPath }); + } else { + const folderName = parts[0]; + if (!itemsMap.has(folderName)) { + itemsMap.set(folderName, { name: folderName, isDir: true, size: 0, path: archivePath + folderName + '/' }); + } + const folder = itemsMap.get(folderName)!; + folder.size += item.size; + } + } + }); + + return Array.from(itemsMap.values()).sort((a, b) => { + if (a.isDir && !b.isDir) return -1; + if (!a.isDir && b.isDir) return 1; + return a.name.localeCompare(b.name); + }); + }, [archiveItems, archivePath]); + useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); @@ -209,9 +244,25 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie ) : (
- Archive Contents +
+ {archivePath !== '' && ( + + )} + + {archivePath === '' ? 'Archive Contents' : archivePath} + +
- {archiveItems?.length || 0} items + {visibleItems.length} items
@@ -223,10 +274,17 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie - {archiveItems?.map((item, idx) => { - const iconConfig = getIcon(getPreviewType({ name: item.name, mime_type: '' })); + {visibleItems.map((item, idx) => { + const iconConfig = item.isDir ? : getIcon(getPreviewType({ name: item.name, mime_type: '' })); return ( - + { + if (item.isDir) setArchivePath(item.path); + }} + > {iconConfig} {item.name} @@ -237,10 +295,10 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie ); })} - {archiveItems?.length === 0 && ( + {visibleItems.length === 0 && ( - This archive is empty. + This folder is empty. )}