import { useState, useMemo } from 'react' import { useWaste } from '../store' import { BrowserAdapter } from '../adapter/browser' import type { FileEntry } from '../types' function fmt(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } type SortKey = 'name' | 'size' type SortDir = 'asc' | 'desc' interface DirEntry { kind: 'dir' name: string path: string count: number } interface FileRow { kind: 'file' name: string path: string size: number } type Row = DirEntry | FileRow function buildRows(files: FileEntry[], cwd: string, search: string, sort: SortKey, sortDir: SortDir): Row[] { // Filter to entries under cwd const prefix = cwd ? cwd + '/' : '' const inDir = files.filter(f => { const p = f.path ?? f.name return p.startsWith(prefix) }) if (search.trim()) { // Flat search across all files under cwd const q = search.toLowerCase() return inDir .filter(f => f.name.toLowerCase().includes(q)) .map(f => ({ kind: 'file' as const, name: f.name, path: f.path ?? f.name, size: f.size_bytes })) .sort((a, b) => { const cmp = sort === 'name' ? a.name.localeCompare(b.name) : a.size - b.size return sortDir === 'asc' ? cmp : -cmp }) } // Build immediate children: dirs and files at this level const dirs = new Map() // dirName → file count const fileRows: FileRow[] = [] for (const f of inDir) { const rel = (f.path ?? f.name).slice(prefix.length) const slash = rel.indexOf('/') if (slash === -1) { fileRows.push({ kind: 'file', name: f.name, path: f.path ?? f.name, size: f.size_bytes }) } else { const dirName = rel.slice(0, slash) dirs.set(dirName, (dirs.get(dirName) ?? 0) + 1) } } const dirRows: DirEntry[] = Array.from(dirs.entries()).map(([name, count]) => ({ kind: 'dir', name, path: prefix + name, count, })) // Sort each group separately, then dirs first const cmpFiles = (a: FileRow, b: FileRow) => { const cmp = sort === 'name' ? a.name.localeCompare(b.name) : a.size - b.size return sortDir === 'asc' ? cmp : -cmp } dirRows.sort((a, b) => a.name.localeCompare(b.name)) fileRows.sort(cmpFiles) return [...dirRows, ...fileRows] } export function FileBrowser() { const { activeFilePeer, fileLists, setActiveFilePeer, adapter, connectedPeers } = useWaste() const [cwd, setCwd] = useState('') const [search, setSearch] = useState('') const [sort, setSort] = useState('name') const [sortDir, setSortDir] = useState('asc') if (!activeFilePeer) return null const peer = connectedPeers.find(p => p.id === activeFilePeer) const files = fileLists[activeFilePeer] ?? null function download(path: string) { if (adapter instanceof BrowserAdapter) { adapter.requestGet(activeFilePeer!, path) } } function toggleSort(key: SortKey) { if (sort === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc') else { setSort(key); setSortDir('asc') } } function navigateUp() { const parts = cwd.split('/') parts.pop() setCwd(parts.join('/')) } const rows = useMemo(() => { if (!files) return [] return buildRows(files, cwd, search, sort, sortDir) }, [files, cwd, search, sort, sortDir]) const cwdParts = cwd ? cwd.split('/') : [] return (
Files · {peer?.alias ?? activeFilePeer.slice(0, 8)}
{files === null ? (
Loading…
) : files.length === 0 ? (
No files shared
) : ( <>
setSearch(e.target.value)} placeholder="search…" />
{/* Breadcrumb */}
{cwdParts.map((seg, i) => ( / ))}
{/* Sort bar */}
    {cwd && !search && (
  • 📁 ..
  • )} {rows.length === 0 && (
  • no results
  • )} {rows.map(row => row.kind === 'dir' ? (
  • { setCwd(row.path); setSearch('') }}> 📁 {row.name} {row.count} file{row.count !== 1 ? 's' : ''}
  • ) : (
  • 📄 {row.name} {fmt(row.size)}
  • ))}
)}
) }