import { useEffect, useRef, useState } from 'react'; import type { Session } from '../types'; import * as api from '../api'; import type { FileEntry } from '../api'; import Logo from './Logo'; import { FolderGlyph, FileGlyph, CloseGlyph, UpGlyph, UploadGlyph } from './icons'; const fmtSize = (n: number) => { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; }; const join = (a: string, b: string) => (a ? `${a}/${b}` : b); const sortEntries = (es: FileEntry[]) => [...es].sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1)); const triggerDownload = (url: string, name: string) => { const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); }; // ASCII-tree rails: one cell per ancestor (vertical line if that ancestor has // more siblings below) plus the elbow cell for this row (├ normally, └ if last). // `prefix[i]` = "draw a continuation line at ancestor column i". function Rails({ prefix, isLast }: { prefix: boolean[]; isLast: boolean }) { return ( {prefix.map((cont, i) => )} ); } const padFor = (prefix: boolean[]) => ({ paddingLeft: `${(prefix.length + 1) * 1.1}em` }); // Lazily-loaded contents of one directory; recurses through FolderNode. function DirContents({ sessionId, path, prefix, onOpen }: { sessionId: string; path: string; prefix: boolean[]; onOpen: (p: string) => void; }) { const [entries, setEntries] = useState(null); const [err, setErr] = useState(false); useEffect(() => { let alive = true; api.listFiles(sessionId, path) .then((r) => { if (alive) { setEntries(r.entries); setErr(false); } }) .catch(() => { if (alive) setErr(true); }); return () => { alive = false; }; }, [sessionId, path]); if (err) return
can't read folder
; if (!entries) return
; if (entries.length === 0) return
empty
; const arr = sortEntries(entries); return ( <> {arr.map((e, i) => { const isLast = i === arr.length - 1; const p = join(path, e.name); return e.dir ? ( ) : (
triggerDownload(api.downloadUrl(sessionId, p), e.name)} title="Double-click to download" > {e.name} {fmtSize(e.size)}
); })} ); } // A folder row: single click toggles inline expand, double click opens it as the // new tree root. A short timer disambiguates the two. function FolderNode({ sessionId, path, name, prefix, isLast, onOpen }: { sessionId: string; path: string; name: string; prefix: boolean[]; isLast: boolean; onOpen: (p: string) => void; }) { const [open, setOpen] = useState(false); const timer = useRef | null>(null); const onClick = () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; setOpen((o) => !o); }, 200); }; const onDoubleClick = () => { if (timer.current) { clearTimeout(timer.current); timer.current = null; } onOpen(path); }; return ( <>
{name}
{open && } ); } export default function FilesPane({ session, focused, zoom = 100, dragId, onDragActive, onFocus, onClose, }: { session: Session; focused?: boolean; zoom?: number; dragId?: string; onDragActive?: (dragging: boolean) => void; onFocus?: () => void; onClose: () => void; }) { const [root, setRoot] = useState(''); const [rootLabel, setRootLabel] = useState('workspace'); const [busy, setBusy] = useState(false); const [dragOver, setDragOver] = useState(false); const [reloadKey, setReloadKey] = useState(0); useEffect(() => { api.listFiles(session.id, '').then((r) => setRootLabel(r.root)).catch(() => {}); }, [session.id]); const upload = async (files: FileList | File[]) => { setBusy(true); for (const f of Array.from(files)) { try { await api.uploadFile(session.id, root, f); } catch { /* skip */ } } setBusy(false); setReloadKey((k) => k + 1); }; const up = () => setRoot(root.includes('/') ? root.slice(0, root.lastIndexOf('/')) : ''); const crumbs = ['', ...root.split('/').filter(Boolean).map((_, i, arr) => arr.slice(0, i + 1).join('/'))]; return (
{/* One compact bar: logo, navigation, upload, close — no title. */}
{ e.dataTransfer.setData('text/plain', dragId); e.dataTransfer.effectAllowed = 'move'; onDragActive?.(true); } : undefined} onDragEnd={dragId ? () => onDragActive?.(false) : undefined} >
{crumbs.map((c, i) => ( {i > 0 && /} ))}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer.files.length) upload(e.dataTransfer.files); }} > {busy &&
Uploading…
}
Click a folder to expand · double-click to open it · double-click a file to download
); }