import { useQuery, useQueryClient } from "@tanstack/react-query"; import { FolderOpen } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { listFilesOptions } from "~/api-client/@tanstack/react-query.gen"; import { listFiles } from "~/api-client/sdk.gen"; import { FileTree } from "~/components/file-tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import type { Volume } from "~/lib/types"; type Props = { volume: Volume; }; interface FileEntry { name: string; path: string; type: "file" | "directory"; size?: number; modifiedAt?: number; } export const FilesTabContent = ({ volume }: Props) => { const queryClient = useQueryClient(); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [fetchedFolders, setFetchedFolders] = useState>(new Set(["/"])); const [loadingFolders, setLoadingFolders] = useState>(new Set()); const [allFiles, setAllFiles] = useState>(new Map()); const { data, isLoading, error } = useQuery({ ...listFilesOptions({ path: { name: volume.name } }), enabled: volume.status === "mounted", refetchInterval: 10000, }); useMemo(() => { if (data?.files) { setAllFiles((prev) => { const next = new Map(prev); for (const file of data.files) { next.set(file.path, file); } return next; }); } }, [data]); const handleFolderExpand = useCallback( async (folderPath: string) => { setExpandedFolders((prev) => { const next = new Set(prev); next.add(folderPath); return next; }); if (!fetchedFolders.has(folderPath)) { setLoadingFolders((prev) => new Set(prev).add(folderPath)); try { const result = await listFiles({ path: { name: volume.name }, query: { path: folderPath }, throwOnError: true, }); if (result.data?.files) { setAllFiles((prev) => { const next = new Map(prev); for (const file of result.data.files) { next.set(file.path, file); } return next; }); } setFetchedFolders((prev) => new Set(prev).add(folderPath)); } catch (error) { console.error("Failed to fetch folder contents:", error); } finally { setLoadingFolders((prev) => { const next = new Set(prev); next.delete(folderPath); return next; }); } } }, [volume.name, fetchedFolders], ); // Prefetch folder contents on hover const handleFolderHover = useCallback( (folderPath: string) => { if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath)) { queryClient.prefetchQuery( listFilesOptions({ path: { name: volume.name }, query: { path: folderPath }, }), ); } }, [volume.name, fetchedFolders, loadingFolders, queryClient], ); const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]); if (volume.status !== "mounted") { return (

Volume must be mounted to browse files.

Mount the volume to explore its contents.

); } return ( File Explorer Browse the files and folders in this volume. {isLoading && (

Loading files...

)} {error && (

Failed to load files: {error.message}

)} {!isLoading && !error && (
{fileArray.length === 0 ? (

This volume is empty.

Files and folders will appear here once you add them.

) : ( )}
)}
); };