import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { ArrowUp, Check, FolderOpen, RefreshCw } from "lucide-react"; import { fetchFilesystemBrowse, useFilesystemBrowse } from "@/hooks/queries/admin/libraries"; import { adminKeys } from "@/hooks/queries/keys"; import { useDebounce } from "@/hooks/useDebounce"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import PathAutocompleteInput from "@/components/PathAutocompleteInput"; interface FolderBrowserProps { open: boolean; onOpenChange: (open: boolean) => void; onSelect: (paths: string[]) => void; existingPaths?: string[]; } const ROOT_PATH = "/"; export default function FolderBrowser({ open, onOpenChange, onSelect, existingPaths = [], }: FolderBrowserProps) { const [currentPath, setCurrentPath] = useState(ROOT_PATH); const [draftPath, setDraftPath] = useState(ROOT_PATH); const [validationError, setValidationError] = useState(""); const [selectedPaths, setSelectedPaths] = useState>(new Set()); const queryClient = useQueryClient(); const debouncedDraftPath = useDebounce(draftPath.trim(), 200); const { data, isLoading, isFetching, error, refetch } = useFilesystemBrowse(currentPath); const resolvedPath = data?.path ?? currentPath; const alreadyAdded = existingPaths.includes(resolvedPath); const parentPath = data?.parent ?? resolvedPath; const canGoUp = parentPath !== resolvedPath; useEffect(() => { if ( debouncedDraftPath.length === 0 || !debouncedDraftPath.startsWith(ROOT_PATH) || debouncedDraftPath === currentPath ) { return; } let cancelled = false; queryClient .fetchQuery({ queryKey: adminKeys.filesystemBrowse(debouncedDraftPath), queryFn: () => fetchFilesystemBrowse(debouncedDraftPath), staleTime: 60_000, }) .then((nextFolder) => { if (cancelled) { return; } setValidationError(""); setCurrentPath((prev) => (prev === nextFolder.path ? prev : nextFolder.path)); }) .catch(() => { // Ignore transient live-browse failures while the user is still typing. }); return () => { cancelled = true; }; }, [currentPath, debouncedDraftPath, queryClient]); function navigate(nextPath: string) { setValidationError(""); setCurrentPath(nextPath); setDraftPath(nextPath); } async function handleBrowseSubmit() { const trimmed = draftPath.trim(); if (!trimmed.startsWith(ROOT_PATH)) { setValidationError("Use an absolute path that starts with /."); return; } try { setValidationError(""); const nextFolder = await queryClient.fetchQuery({ queryKey: adminKeys.filesystemBrowse(trimmed), queryFn: () => fetchFilesystemBrowse(trimmed), staleTime: 60_000, }); setCurrentPath(nextFolder.path); setDraftPath(nextFolder.path); } catch (err) { setValidationError(err instanceof Error ? err.message : "Failed to browse folder"); } } function toggleSelected(path: string) { setSelectedPaths((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); } function handleSelectCurrent() { if (alreadyAdded) { return; } onSelect([resolvedPath]); setSelectedPaths(new Set()); onOpenChange(false); } function handleAddSelected() { const paths = Array.from(selectedPaths).filter((p) => !existingPaths.includes(p)); if (paths.length === 0) return; onSelect(paths); setSelectedPaths(new Set()); onOpenChange(false); } // Count selected paths that aren't already on the library const addableSelected = Array.from(selectedPaths).filter( (p) => !existingPaths.includes(p), ).length; return ( { if (!next) setSelectedPaths(new Set()); onOpenChange(next); }} > Browse Library Folders
{ setValidationError(""); setDraftPath(value); }} placeholder="/mnt/media" onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); void handleBrowseSubmit(); } }} />
{validationError ?

{validationError}

: null} {!validationError && error instanceof Error ? (

{error.message}

) : null} {alreadyAdded ? (

This folder is already listed on the library.

) : null}

Current folder

{resolvedPath}

{isLoading ? (
{Array.from({ length: 6 }).map((_, index) => ( ))}
) : null} {!isLoading && data?.entries.length === 0 ? (

No subfolders found here.

) : null} {!isLoading && data?.entries.map((entry) => { const isSelected = selectedPaths.has(entry.path); const isExisting = existingPaths.includes(entry.path); return (
{/* Checkbox area */} {/* Navigate into folder */}
); })}
{selectedPaths.size > 0 ? `${selectedPaths.size} folder${selectedPaths.size !== 1 ? "s" : ""} selected` : "Select folders or use current"}
{addableSelected > 0 ? ( ) : ( )}
); }