import { useState, useCallback, useRef } from "react"; import { AgGridReact } from "ag-grid-react"; import { useDirectoryListing, useFfprobe, useJobTemplates, useRunJob, } from "../hooks/useFiles"; interface DisplayRow { type: string; name: string; ext: string; size: string; modified: string; path: string; } function formatSize(bytes: number): string { if (bytes === 0) return "-"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unitIdx = 0; while (value >= 1000 && unitIdx < units.length - 1) { value /= 1000; unitIdx++; } return `${value.toFixed(1)} ${units[unitIdx]}`; } function formatTime(epoch: number): string { if (!epoch) return ""; return new Date(epoch * 1000).toLocaleString(); } function isVideoFile(name: string): boolean { const exts = [ ".mkv", ".mp4", ".avi", ".m4v", ".ts", ".wmv", ".mov", ".flv", ".webm", ]; return exts.some((ext) => name.toLowerCase().endsWith(ext)); } export function FileBrowser() { const [currentDir, setCurrentDir] = useState("/"); const [pathInput, setPathInput] = useState("/"); const [selectedPath, setSelectedPath] = useState(null); const { data: listing, isLoading, error } = useDirectoryListing(currentDir); const { data: ffprobeData } = useFfprobe( selectedPath ?? "", !!selectedPath && isVideoFile(selectedPath), ); const { data: templates } = useJobTemplates(); const runJob = useRunJob(); const gridRef = useRef>(null); const navigate = useCallback((path: string) => { setCurrentDir(path); setPathInput(path); setSelectedPath(null); }, []); const handlePathSubmit = (e: React.KeyboardEvent) => { if (e.key === "Enter") { navigate(pathInput || "/"); } }; // Build display rows const rows: DisplayRow[] = []; if (currentDir !== "/") { const parent = currentDir.replace(/\/[^/]+\/?$/, "") || "/"; rows.push({ type: "up", name: "..", ext: "", size: "-", modified: "", path: parent, }); } if (listing) { for (const entry of listing.entries) { const kind = entry.type === "d" ? "dir" : "file"; const ext = kind === "file" ? (entry.name.split(".").pop() ?? "") : ""; rows.push({ type: kind, name: entry.name, ext, size: kind === "dir" ? "-" : formatSize(entry.size), modified: formatTime(entry.mtime), path: `${currentDir === "/" ? "" : currentDir}/${entry.name}`, }); } } const columnDefs = [ { field: "type" as const, headerName: "Type", width: 80 }, { field: "name" as const, headerName: "Name", flex: 2 }, { field: "ext" as const, headerName: "Ext", width: 80 }, { field: "size" as const, headerName: "Size", width: 110 }, { field: "modified" as const, headerName: "Modified", width: 180 }, ]; const onRowClicked = useCallback( (event: { data?: DisplayRow }) => { const row = event.data; if (!row) return; if (row.type === "dir" || row.type === "up") { navigate(row.path); } else { setSelectedPath(row.path); } }, [navigate], ); return (
{/* Path input */}
setPathInput(e.target.value)} onKeyDown={handlePathSubmit} className="border rounded px-3 py-1 text-sm flex-1" placeholder="Remote path (press Enter to navigate)" />
{/* Status */}
Current: {currentDir} {selectedPath && ( Selected: {selectedPath} )} {listing && Entries: {listing.count}}
{error &&

Error: {String(error)}

} {/* File listing grid */}
ref={gridRef} rowData={rows} columnDefs={columnDefs} rowSelection="single" onRowClicked={onRowClicked} loading={isLoading} suppressCellFocus animateRows={false} />
{/* ffprobe preview */} {selectedPath && isVideoFile(selectedPath) && (

ffprobe preview: {selectedPath}

{ffprobeData ? (
							{JSON.stringify(ffprobeData, null, 2)}
						
) : (

Loading ffprobe data...

)}
)} {/* Jobs */} {selectedPath && templates && templates.length > 0 && (

Jobs

{templates.map((tpl) => ( ))}
{runJob.data && (
							Exit: {runJob.data.exit_status}
							{"\n"}
							{runJob.data.stdout}
							{runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`}
						
)}
)}
); }