import { useCallback, useEffect, useMemo, useState } from "react"; import { logger } from "~/client/lib/logger"; import type { FileEntry } from "../components/file-tree"; export type FetchFolderResult = { files?: FileEntry[]; directories?: Array<{ name: string; path: string }>; offset?: number; limit?: number; total?: number; hasMore?: boolean; }; type FetchFolderFn = (path: string, offset?: number) => Promise; type PathTransformFns = { strip?: (path: string) => string; add?: (path: string) => string; }; type UseFileBrowserOptions = { initialData?: FetchFolderResult; isLoading?: boolean; fetchFolder: FetchFolderFn; prefetchFolder?: (path: string) => void; pathTransform?: PathTransformFns; rootPath?: string; }; type FolderPaginationState = { currentOffset: number; limit: number; hasMore: boolean; isLoadingMore: boolean; }; 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 [folderPagination, setFolderPagination] = useState>(new Map()); const stripPath = pathTransform?.strip; const addPath = pathTransform?.add; useEffect(() => { 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)); setFolderPagination((prev) => { const next = new Map(prev); next.set(rootPath, { currentOffset: initialData.offset ?? 0, limit: initialData.limit ?? 100, hasMore: initialData.hasMore ?? false, isLoadingMore: false, }); return next; }); } } 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; }); setFolderPagination((prev) => { const next = new Map(prev); next.set(folderPath, { currentOffset: result.offset ?? 0, limit: result.limit ?? 100, hasMore: result.hasMore ?? false, isLoadingMore: false, }); 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) { logger.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 handleLoadMore = useCallback( async (folderPath: string) => { const pagination = folderPagination.get(folderPath); if (!pagination?.hasMore || pagination?.isLoadingMore) { return; } setFolderPagination((prev) => { const next = new Map(prev); next.set(folderPath, { ...pagination, isLoadingMore: true }); return next; }); try { const pathToFetch = addPath ? addPath(folderPath) : folderPath; const nextOffset = pagination.currentOffset + pagination.limit; const result = await fetchFolder(pathToFetch, nextOffset); 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; if (strippedPath !== folderPath) { next.set(strippedPath, { ...file, path: strippedPath }); } } return next; }); setFolderPagination((prev) => { const next = new Map(prev); next.set(folderPath, { currentOffset: result.offset ?? nextOffset, limit: result.limit ?? pagination.limit, hasMore: result.hasMore ?? false, isLoadingMore: false, }); return next; }); } } catch (error) { logger.error("Failed to load more files:", error); setFolderPagination((prev) => { const next = new Map(prev); next.set(folderPath, { ...pagination, isLoadingMore: false }); return next; }); } }, [folderPagination, 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], ); const getFolderPagination = useCallback( (folderPath: string) => { return folderPagination.get(folderPath) ?? { hasMore: false, isLoadingMore: false }; }, [folderPagination], ); return { fileArray, expandedFolders, loadingFolders, handleFolderExpand, handleFolderHover, handleLoadMore, getFolderPagination, isLoading: Boolean(isLoading) && fileArray.length === 0, isEmpty: fileArray.length === 0 && !isLoading, }; };