import { useRef, useEffect } from "preact/hooks"; import type { FileEntry } from "../types"; interface FileTableProps { entries: FileEntry[]; currentPath: string; focusIdx: number; loading: boolean; deleteConfirmIdx: number; renameIdx: number; filterQuery: string; onNavigate: (path: string) => void; onShare: (resourcePath: string) => void; onDelete: (resourcePath: string, name: string) => void; onDeleteConfirm: (idx: number) => void; onDeleteCancel: () => void; onRenameStart: (idx: number) => void; onRenameSubmit: (idx: number, newName: string) => void; onRenameCancel: () => void; onClone: (resourcePath: string) => void; onFocusRow: (idx: number) => void; } function formatSize(bytes: number): string { if (bytes === 0) return "-"; const units = ["B", "KB", "MB", "GB"]; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function formatDate(isoStr: string): string { const date = new Date(isoStr); const pad = (n: number) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } function getIconType(entry: FileEntry): string { if (entry.isDirectory) return "folder"; const ext = entry.name.split(".").pop()?.toLowerCase() || ""; const iconMap: Record = { js: "code", ts: "code", jsx: "code", tsx: "code", py: "code", rb: "code", go: "code", rs: "code", java: "code", c: "code", cpp: "code", h: "code", cs: "code", php: "code", swift: "code", kt: "code", vue: "code", svelte: "code", html: "web", htm: "web", css: "web", scss: "web", less: "web", json: "data", xml: "data", yaml: "data", yml: "data", csv: "data", sql: "data", toml: "data", md: "doc", txt: "doc", pdf: "doc", doc: "doc", docx: "doc", rtf: "doc", png: "image", jpg: "image", jpeg: "image", gif: "image", svg: "image", webp: "image", ico: "image", bmp: "image", zip: "archive", tar: "archive", gz: "archive", rar: "archive", "7z": "archive", bz2: "archive", env: "config", gitignore: "config", dockerignore: "config", editorconfig: "config", mp4: "media", mp3: "media", wav: "media", avi: "media", mkv: "media", }; return iconMap[ext] || "file"; } function FileIcon({ type }: { type: string }) { const icons: Record = { folder: , file: , code: , image: , doc: , data: , web: , archive: , config: , media: , }; return icons[type] || icons.file; } function RenameInput({ name, onSubmit, onCancel }: { name: string; onSubmit: (newName: string) => void; onCancel: () => void }) { const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); // Select filename without extension const dotIdx = name.lastIndexOf("."); if (dotIdx > 0) { inputRef.current.setSelectionRange(0, dotIdx); } else { inputRef.current.select(); } } }, [name]); return ( { if (e.key === "Enter") { e.preventDefault(); const val = (e.target as HTMLInputElement).value.trim(); if (val && val !== name) { onSubmit(val); } else { onCancel(); } } else if (e.key === "Escape") { e.preventDefault(); onCancel(); } e.stopPropagation(); }} onBlur={() => { onCancel(); }} /> ); } export function FileTable({ entries, currentPath, focusIdx, loading, deleteConfirmIdx, renameIdx, filterQuery, onNavigate, onShare, onDelete, onDeleteConfirm, onDeleteCancel, onRenameStart, onRenameSubmit, onRenameCancel, onClone, onFocusRow }: FileTableProps) { // Scroll focused row into view useEffect(() => { if (focusIdx >= 0) { const row = document.getElementById(`file-row-${focusIdx}`); if (row) row.scrollIntoView({ block: "nearest" }); } }, [focusIdx]); if (entries.length === 0 && !loading) { return (
Name Size Modified
{filterQuery ? ( <>
No matches for "{filterQuery}"
Try a different filter term
) : ( <>
No files here yet
Drop files here or click Upload to add files
)}
); } return (
= 0 ? `file-row-${focusIdx}` : undefined} > {entries.map((entry, i) => { const iconType = getIconType(entry); const href = entry.isDirectory ? `${currentPath}${encodeURIComponent(entry.name)}/` : `${currentPath}${encodeURIComponent(entry.name)}`; const resourcePath = entry.isDirectory ? `${currentPath}${encodeURIComponent(entry.name)}/` : `${currentPath}${encodeURIComponent(entry.name)}`; const downloadPath = `/__download${currentPath}${encodeURIComponent(entry.name)}${entry.isDirectory ? "/" : ""}`; const sizeStr = entry.isDirectory ? "-" : formatSize(entry.size); const dateStr = formatDate(entry.modified); const isConfirming = i === deleteConfirmIdx; const isRenaming = i === renameIdx; const cls = `entry ${entry.isDirectory ? "dir" : "file"}${i === focusIdx ? " kb-focus" : ""}${isConfirming ? " delete-confirm" : ""}${isRenaming ? " renaming" : ""}`; return ( onFocusRow(i)} > ); })}
Name Size Modified
{isConfirming ? ( Delete {entry.name}? y / n ) : isRenaming ? ( onRenameSubmit(i, newName)} onCancel={onRenameCancel} /> ) : ( { if (entry.isDirectory) { e.preventDefault(); onNavigate(href); } }} > {entry.name} )} {sizeStr} {dateStr} {(isConfirming || isRenaming) ? null : ( <> )}
); }