import { useEffect, useMemo, useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { DirEntry, DirListing, FolderAddResult, useGalleryStore } from "../store"; function normalizePath(path: string): string { return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); } function folderName(path: string): string { const trimmed = path.replace(/[\\/]+$/, ""); const parts = trimmed.split(/[\\/]+/).filter(Boolean); return parts.length > 0 ? parts[parts.length - 1] : path; } function buildBreadcrumbs(path: string | null): { label: string; path: string | null }[] { if (!path) return [{ label: "This PC / Home", path: null }]; const separator = path.includes("\\") ? "\\" : "/"; const normalized = path.replace(/[\\/]+$/, ""); const windowsDrive = normalized.match(/^[A-Za-z]:/); if (windowsDrive) { const drive = windowsDrive[0] + "\\"; const rest = normalized.slice(2).split(/[\\/]+/).filter(Boolean); let current = drive; return [ { label: "This PC", path: null }, { label: drive, path: drive }, ...rest.map((part) => { current = current.endsWith("\\") ? `${current}${part}` : `${current}\\${part}`; return { label: part, path: current }; }), ]; } const parts = normalized.split(/[\\/]+/).filter(Boolean); let current = separator === "/" ? "" : ""; return [ { label: "/", path: null }, ...parts.map((part) => { current = `${current}/${part}`; return { label: part, path: current }; }), ]; } function StatusLine({ results }: { results: FolderAddResult[] | null }) { if (!results) return null; const added = results.filter((result) => result.status === "added").length; const skipped = results.filter((result) => result.status === "skipped").length; const failed = results.filter((result) => result.status === "error").length; return (

Added {added}, skipped {skipped}, failed {failed}.

); } function FolderRow({ entry, selected, alreadyAdded, onToggle, onNavigate, }: { entry: DirEntry; selected: boolean; alreadyAdded: boolean; onToggle: () => void; onNavigate: () => void; }) { return (
{alreadyAdded ? ( Added ) : null}
); } function StagedFoldersPanel({ stagedPaths, onRemove, onClear, }: { stagedPaths: string[]; onRemove: (path: string) => void; onClear: () => void; }) { return ( ); } export function FolderPickerModal() { const folderPickerOpen = useGalleryStore((state) => state.folderPickerOpen); const setFolderPickerOpen = useGalleryStore((state) => state.setFolderPickerOpen); const folders = useGalleryStore((state) => state.folders); const listDirectories = useGalleryStore((state) => state.listDirectories); const addFolders = useGalleryStore((state) => state.addFolders); const [listing, setListing] = useState(null); const [currentPath, setCurrentPath] = useState(null); const [stagedPaths, setStagedPaths] = useState([]); const [loading, setLoading] = useState(false); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); const [results, setResults] = useState(null); const scrollRef = useRef(null); const libraryPaths = useMemo(() => new Set(folders.map((folder) => normalizePath(folder.path))), [folders]); const stagedSet = useMemo(() => new Set(stagedPaths.map(normalizePath)), [stagedPaths]); const breadcrumbs = useMemo(() => buildBreadcrumbs(listing?.current ?? null), [listing?.current]); const virtualizer = useVirtualizer({ count: listing?.entries.length ?? 0, getScrollElement: () => scrollRef.current, estimateSize: () => 48, overscan: 8, }); useEffect(() => { if (!folderPickerOpen) return; let cancelled = false; setLoading(true); setError(null); void listDirectories(currentPath) .then((nextListing) => { if (cancelled) return; setListing(nextListing); scrollRef.current?.scrollTo({ top: 0, left: 0 }); }) .catch((loadError) => { if (cancelled) return; setListing({ current: currentPath, parent: null, entries: [] }); setError(loadError instanceof Error ? loadError.message : String(loadError)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [currentPath, folderPickerOpen, listDirectories]); useEffect(() => { if (!folderPickerOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setFolderPickerOpen(false); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [folderPickerOpen, setFolderPickerOpen]); useEffect(() => { if (folderPickerOpen) return; setCurrentPath(null); setListing(null); setStagedPaths([]); setError(null); setResults(null); setAdding(false); }, [folderPickerOpen]); if (!folderPickerOpen) return null; const entries = listing?.entries ?? []; const togglePath = (path: string) => { const normalized = normalizePath(path); if (libraryPaths.has(normalized)) return; setResults(null); setStagedPaths((current) => { const exists = current.some((staged) => normalizePath(staged) === normalized); return exists ? current.filter((staged) => normalizePath(staged) !== normalized) : [...current, path]; }); }; const removeStagedPath = (path: string) => { const normalized = normalizePath(path); setResults(null); setStagedPaths((current) => current.filter((staged) => normalizePath(staged) !== normalized)); }; const clearStagedPaths = () => { setResults(null); setStagedPaths([]); }; const confirmAdd = async () => { if (stagedPaths.length === 0 || adding) return; setAdding(true); setError(null); try { const addResults = await addFolders(stagedPaths); const failed = addResults.filter((result) => result.status === "error"); setResults(addResults); if (failed.length > 0) { setStagedPaths(stagedPaths.filter((_, i) => addResults[i]?.status === "error")); setError(failed.map((failure) => failure.data).join("; ")); return; } setFolderPickerOpen(false); } catch (addError) { setError(addError instanceof Error ? addError.message : String(addError)); } finally { setAdding(false); } }; return (
setFolderPickerOpen(false)} >
event.stopPropagation()} >

Add media folders

Choose folders from any location, then add them together.

{error ? (
{error}
) : null}
{loading ? (
Loading folders...
) : entries.length === 0 ? (
No folders found here.
) : (
{virtualizer.getVirtualItems().map((virtualItem) => { const entry = entries[virtualItem.index]; const normalized = normalizePath(entry.path); return (
togglePath(entry.path)} onNavigate={() => setCurrentPath(entry.path)} />
); })}
)}
); }