import { useCallback, useMemo, useState } from "react"; import type { FileEntry } from "../components/file-tree"; type FetchFolderFn = ( path: string, ) => Promise<{ files?: FileEntry[]; directories?: Array<{ name: string; path: string }> }>; type PathTransformFns = { strip?: (path: string) => string; add?: (path: string) => string; }; type UseFileBrowserOptions = { initialData?: { files?: FileEntry[]; directories?: Array<{ name: string; path: string }> }; isLoading?: boolean; fetchFolder: FetchFolderFn; prefetchFolder?: (path: string) => void; pathTransform?: PathTransformFns; rootPath?: string; }; export const useFileBrowser = (props: UseFileBrowserOptions) => { const { initialData, isLoading, fetchFolder, prefetchFolder, pathTransform, rootPath = "/" } = props; const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [fetchedFolders, setFetchedFolders] = useState>(new Set([rootPath])); const [loadingFolders, setLoadingFolders] = useState>(new Set()); const [allFiles, setAllFiles] = useState>(new Map()); const stripPath = pathTransform?.strip; const addPath = pathTransform?.add; useMemo(() => { if (initialData?.files) { const files = initialData.files; setAllFiles((prev) => { const next = new Map(prev); for (const file of files) { const path = stripPath ? stripPath(file.path) : file.path; if (path !== rootPath) { next.set(path, { ...file, path }); } } return next; }); if (rootPath) { setFetchedFolders((prev) => new Set(prev).add(rootPath)); } } else if (initialData?.directories) { const directories = initialData.directories; setAllFiles((prev) => { const next = new Map(prev); for (const dir of directories) { next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" }); } return next; }); } }, [initialData, stripPath, rootPath]); const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]); 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 pathToFetch = addPath ? addPath(folderPath) : folderPath; const result = await fetchFolder(pathToFetch); if (result.files) { const files = result.files; setAllFiles((prev) => { const next = new Map(prev); for (const file of files) { const strippedPath = stripPath ? stripPath(file.path) : file.path; // Skip the directory itself if (strippedPath !== folderPath) { next.set(strippedPath, { ...file, path: strippedPath }); } } return next; }); } else if (result.directories) { const directories = result.directories; setAllFiles((prev) => { const next = new Map(prev); for (const dir of directories) { next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" }); } 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; }); } } }, [fetchedFolders, fetchFolder, stripPath, addPath], ); const handleFolderHover = useCallback( (folderPath: string) => { if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath) && prefetchFolder) { const pathToPrefetch = addPath ? addPath(folderPath) : folderPath; prefetchFolder(pathToPrefetch); } }, [fetchedFolders, loadingFolders, prefetchFolder, addPath], ); return { fileArray, expandedFolders, loadingFolders, handleFolderExpand, handleFolderHover, isLoading: isLoading && fileArray.length === 0, isEmpty: fileArray.length === 0 && !isLoading, }; };