import { useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import type { ColumnDef, RowSelectionState } from "@tanstack/react-table"; import { DataTable } from "@/components/ui/data-table"; import { Alert, AlertAction, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { TabsTrigger } from "@/components/ui/tabs"; import { useDirectoryListing, useFfprobe, useJobTemplates, useRunJob, } from "../hooks/useFiles"; import { usePersistentState } from "../hooks/usePersistentState"; import { useMonitoringSettings } from "../hooks/useSettings"; import { SectionCard } from "../components/SectionCard"; import { TabbedCard } from "../components/TabbedCard"; interface DisplayRow { id: string; type: string; name: string; ext: string; size: string; modified: string; path: string; } interface FfprobeStream { index?: number; codec_type?: string; codec_name?: string; codec_long_name?: string; profile?: string; width?: number; height?: number; bit_rate?: string | number; duration?: string | number; channels?: number; sample_rate?: string | number; channel_layout?: string; pix_fmt?: string; sample_aspect_ratio?: string; display_aspect_ratio?: string; field_order?: string; level?: number | string; color_range?: string; color_space?: string; color_transfer?: string; color_primaries?: string; tags?: Record; } interface FfprobeFormat { filename?: string; format_name?: string; format_long_name?: string; duration?: string | number; size?: string | number; bit_rate?: string | number; tags?: Record; } interface FfprobeData { format?: FfprobeFormat; streams?: FfprobeStream[]; } 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 humanBytes(value: string | number | undefined): string { if (value === undefined || value === null || value === "") return "-"; const bytes = typeof value === "string" ? Number(value) : value; if (!Number.isFinite(bytes)) return "-"; return formatSize(bytes); } function humanRate(value: string | number | undefined): string { if (value === undefined || value === null || value === "") return "-"; const rate = typeof value === "string" ? Number(value) : value; if (!Number.isFinite(rate)) return "-"; const units = ["bps", "Kbps", "Mbps", "Gbps"]; let v = rate; let unitIdx = 0; while (v >= 1000 && unitIdx < units.length - 1) { v /= 1000; unitIdx++; } return `${v.toFixed(1)} ${units[unitIdx]}`; } function humanDuration(value: string | number | undefined): string { if (value === undefined || value === null || value === "") return "-"; const seconds = typeof value === "string" ? Number(value) : value; if (!Number.isFinite(seconds)) return "-"; const total = Math.max(0, Math.round(seconds)); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const secs = total % 60; if (hours > 0) return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; return `${minutes}:${String(secs).padStart(2, "0")}`; } function fieldLabel(_key: string, value: string | number | undefined): string { if (value === undefined || value === null || value === "") return "-"; return String(value); } function isVideoFile(name: string): boolean { const exts = [ ".mkv", ".mp4", ".avi", ".m4v", ".ts", ".wmv", ".mov", ".flv", ".webm", ]; return exts.some((ext) => name.toLowerCase().endsWith(ext)); } // Design §3.2: referentially-stable column defs (a new array each render would // destabilize the TanStack table instance and drop controlled selection). // Visibility-only: no sorting, no sizing/resizing (design §3.3). const fileColumns: ColumnDef[] = [ { accessorKey: "type", header: () => "Type", cell: ({ row }) => ( {row.original.type} ), }, { accessorKey: "name", header: () => "Name", cell: ({ row }) => {row.original.name}, }, { accessorKey: "ext", header: () => "Ext", cell: ({ row }) => row.original.ext, }, { accessorKey: "size", header: () => "Size", cell: ({ row }) => row.original.size, }, { accessorKey: "modified", header: () => "Modified", cell: ({ row }) => row.original.modified, }, ]; const FILE_BROWSER_STATE_KEY = "manage.files.browserState"; type FileBrowserState = { currentDir: string; pathInput: string; selectedPath: string | null; selectedJob: string; }; function defaultFileBrowserState(): FileBrowserState { return { currentDir: "/", pathInput: "/", selectedPath: null, selectedJob: "", }; } function FfprobeChip({ children, variant = "outline", }: { children: React.ReactNode; variant?: "outline" | "secondary" | "warning" | "default"; }) { return {children}; } function StreamBlock({ children }: { children: React.ReactNode }) { return
{children}
; } function FfprobeDetails({ path, data }: { path: string; data: FfprobeData }) { const format = data.format ?? {}; const streams = data.streams ?? []; const videoStreams = streams.filter( (stream) => stream.codec_type === "video", ); const audioStreams = streams.filter( (stream) => stream.codec_type === "audio", ); const subtitleStreams = streams.filter( (stream) => stream.codec_type === "subtitle", ); return (
ffprobe details
{path}
Container / format
Format:{" "} {fieldLabel("format", format.format_name)}
Long name:{" "} {fieldLabel("format_long_name", format.format_long_name)}
Duration:{" "} {humanDuration(format.duration)}
Size:{" "} {humanBytes(format.size)}
Bitrate:{" "} {humanRate(format.bit_rate)}
Filename:{" "} {fieldLabel("filename", format.filename)}
Streams
{videoStreams.length > 0 && (
Video streams
{videoStreams.map((stream, index) => { const isHdr = (stream.color_transfer ?? "") .toLowerCase() .includes("2084") || (stream.color_transfer ?? "") .toLowerCase() .includes("b67") || (stream.color_space ?? "") .toLowerCase() .includes("bt2020") || (stream.color_primaries ?? "") .toLowerCase() .includes("bt2020"); return (
#{stream.index ?? index} {stream.codec_type ?? "video"} {stream.codec_name ?? "unknown codec"} {stream.codec_long_name && ( {stream.codec_long_name} )} {stream.profile && ( {stream.profile} )} {stream.bit_rate && ( {humanRate(stream.bit_rate)} )} {stream.duration && ( {humanDuration(stream.duration)} )} {stream.width && stream.height && ( {`${stream.width}×${stream.height}`} )} {stream.pix_fmt && ( {stream.pix_fmt} )} {stream.display_aspect_ratio && ( {`DAR ${stream.display_aspect_ratio}`} )} {stream.sample_aspect_ratio && ( {`SAR ${stream.sample_aspect_ratio}`} )} {stream.level !== undefined && stream.level !== null && ( {`L${stream.level}`} )} {stream.field_order && stream.field_order !== "unknown" && ( {stream.field_order} )} {(stream.color_range || stream.color_space || stream.color_transfer || stream.color_primaries) && ( {[ stream.color_range, stream.color_space, stream.color_transfer, stream.color_primaries, ] .filter(Boolean) .join(" / ")} )}
{stream.tags?.language ? `Language: ${stream.tags.language}. ` : ""} {stream.tags?.title ? `Title: ${stream.tags.title}.` : ""}
); })}
)} {audioStreams.length > 0 && (
Audio streams
{audioStreams.map((stream, index) => (
#{stream.index ?? index} {stream.codec_type ?? "audio"} {stream.codec_name ?? "unknown codec"} {stream.channels && ( {`${stream.channels} ch`} )} {stream.sample_rate && ( {`${stream.sample_rate} Hz`} )} {stream.bit_rate && ( {humanRate(stream.bit_rate)} )} {stream.duration && ( {humanDuration(stream.duration)} )}
{stream.codec_long_name ? `${stream.codec_long_name}. ` : ""} {stream.channel_layout ? `Layout: ${stream.channel_layout}. ` : ""} {stream.tags?.language ? `Language: ${stream.tags.language}. ` : ""} {stream.tags?.title ? `Title: ${stream.tags.title}.` : ""}
))}
)} {subtitleStreams.length > 0 && (
Subtitle streams
{subtitleStreams.map((stream, index) => (
#{stream.index ?? index} {stream.codec_type ?? "subtitle"} {stream.codec_name ?? "unknown codec"} {stream.tags?.language && ( {stream.tags.language} )} {stream.tags?.title && ( {stream.tags.title} )}
))}
)} {streams.length === 0 && (
No streams found.
)}
{Object.keys(format.tags ?? {}).length > 0 && (
Tags
{Object.entries(format.tags ?? {}).map(([key, value]) => ( {`${key}: ${value}`} ))}
)}
); } function InfoAlert({ children }: { children: React.ReactNode }) { return ( {children} ); } export function FileBrowser() { const [searchParams, setSearchParams] = useSearchParams(); const [columnVisibility, setColumnVisibility] = useState< Record >({}); const { data: machines } = useMonitoringSettings(); const fileMachines = useMemo( () => (machines ?? []).filter( (machine) => machine.enabled && (machine.services.includes("files") || machine.services.includes("monitoring")), ), [machines], ); const initialRequestedPath = searchParams.get("path"); const initialMachineId = searchParams.get("machine_id") || fileMachines[0]?.id || ""; const [browserState, setBrowserState] = usePersistentState( FILE_BROWSER_STATE_KEY, () => { const requestedPath = initialRequestedPath ?? "/"; const selectedPath = requestedPath !== "/" && (isVideoFile(requestedPath) || requestedPath.includes(".")) ? requestedPath.replace(/\/+$/, "") : null; const currentDir = selectedPath ? selectedPath.replace(/\/[^/]+$/, "") || "/" : requestedPath.replace(/\/+$/, "") || "/"; return { ...defaultFileBrowserState(), currentDir, pathInput: requestedPath || currentDir, selectedPath, }; }, ); const { currentDir, pathInput, selectedPath, selectedJob } = browserState; const selectedMachineId = searchParams.get("machine_id") || initialMachineId; const navigateToSettings = useNavigate(); const updateBrowserState = (patch: Partial) => setBrowserState((current) => ({ ...current, ...patch })); const { data: listing, isLoading, error, refetch, } = useDirectoryListing(currentDir, selectedMachineId || undefined); const { data: ffprobeData, isLoading: ffprobeLoading, error: ffprobeError, } = useFfprobe( selectedPath ?? "", !!selectedPath && isVideoFile(selectedPath), selectedMachineId || undefined, ); const { data: templates } = useJobTemplates(); const runJob = useRunJob(selectedMachineId || undefined); const navigate = (path: string) => { updateBrowserState({ currentDir: path, pathInput: path, selectedPath: null, }); }; const setMachine = (machineId: string) => { setSearchParams( (current) => { const next = new URLSearchParams(current); if (machineId) next.set("machine_id", machineId); else next.delete("machine_id"); return next; }, { replace: true }, ); }; const handlePathSubmit = (e: React.KeyboardEvent) => { if (e.key === "Enter") navigate(pathInput || "/"); }; const rows: DisplayRow[] = []; if (currentDir !== "/") { const parent = currentDir.replace(/\/[^/]+\/?$/, "") || "/"; rows.push({ id: `up-${parent}`, 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() ?? "") : ""; const path = `${currentDir === "/" ? "" : currentDir}/${entry.name}`; rows.push({ id: path, type: kind, name: entry.name, ext, size: kind === "dir" ? "-" : formatSize(entry.size), modified: formatTime(entry.mtime), path, }); } } // Preserved row-click behavior (MUI DataGrid onRowClick): dir/up rows navigate; // file rows select the file for ffprobe preview (also feeds pathInput). const handleRowClick = (row: DisplayRow) => { if (row.type === "dir" || row.type === "up") { navigate(row.path); return; } updateBrowserState({ selectedPath: row.path, currentDir, pathInput: row.path, }); }; // Single-select checkbox behavior (DataTable adds a selection column under // enableRowSelection): mirrors the row-click selection for file rows. const rowSelection: RowSelectionState = selectedPath ? { [selectedPath]: true } : {}; const handleSelectionChange = ( updater: | RowSelectionState | ((prev: RowSelectionState) => RowSelectionState), ) => { const next = typeof updater === "function" ? updater(rowSelection) : updater; const selectedIds = Object.keys(next).filter((id) => next[id]); const id = selectedIds[selectedIds.length - 1]; if (!id) { updateBrowserState({ selectedPath: null }); return; } const target = rows.find((row) => row.id === id); if (target && target.type === "file") { updateBrowserState({ selectedPath: target.path, pathInput: target.path }); } else { updateBrowserState({ selectedPath: null }); } }; const selectedTemplate = templates?.find((t) => t.key === selectedJob); return (

File Browser

{fileMachines.length ? `${fileMachines.length} machine${fileMachines.length === 1 ? "" : "s"}` : "No file machines"}
0 ? selectedMachineId : ""} onChange={setMachine} tabs={fileMachines.map((machine) => ( {`${machine.name} · ${machine.mode}`} ))} > {fileMachines.length > 0 ? (
updateBrowserState({ pathInput: e.target.value }) } onKeyDown={handlePathSubmit} />
{`Current: ${currentDir} `} {selectedPath ? `| Selected: ${selectedPath} ` : ""} {listing ? `| Entries: ${listing.count}` : ""}
{error && ( {String(error)} )}
row.id} enableRowSelection rowSelection={rowSelection} onRowSelectionChange={handleSelectionChange} onRowClick={handleRowClick} enableColumnVisibilityToggle columnVisibility={columnVisibility} onColumnVisibilityChange={setColumnVisibility} emptyMessage={ isLoading ? "Loading directory..." : "This directory is empty." } />
{selectedPath ? ( isVideoFile(selectedPath) ? ( ffprobeError ? ( {String(ffprobeError)} ) : ffprobeLoading && !ffprobeData ? ( Loading ffprobe data... ) : ffprobeData ? ( ) : ( No ffprobe data available. ) ) : ( Select a video file to view ffprobe details. ) ) : ( Select a file in Browser to view ffprobe details. )} {selectedPath && templates && templates.length > 0 ? (
{selectedTemplate && (
{selectedTemplate.description}
)}
{runJob.data && (
											{`Exit: ${runJob.data.exit_status}`}
											{"\n"}
											{runJob.data.stdout}
											{runJob.data.stderr && `\nSTDERR: ${runJob.data.stderr}`}
										
)}
) : ( Select a file in Browser to run jobs. )}
) : ( No file-capable machines are configured yet. )}
); }