diff --git a/src/components/FolderPickerModal.tsx b/src/components/FolderPickerModal.tsx
index 467b9fd..9b1333b 100644
--- a/src/components/FolderPickerModal.tsx
+++ b/src/components/FolderPickerModal.tsx
@@ -1,398 +1,28 @@
-import { useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
-import { DirEntry, DirListing, FolderAddResult, useGalleryStore } from "../store";
import { Tooltip } from "./Tooltip";
-import { CheckIcon, ChevronRightIcon, CloseIcon } from "./icons";
-
-function normalizePath(path: string): string {
- return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
-}
-
-function cleanAddressInput(path: string): string {
- const trimmed = path.trim();
- if (trimmed.length >= 2) {
- const first = trimmed[0];
- const last = trimmed[trimmed.length - 1];
- if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
- return trimmed.slice(1, -1).trim();
- }
- }
- return trimmed;
-}
-
-function friendlyDirectoryError(error: unknown): string {
- const message = error instanceof Error ? error.message : String(error);
- if (/cannot find the path|os error 3|not found|no such file/i.test(message)) {
- return "Folder not found. Check the path and try again.";
- }
- return message;
-}
-
-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 (
-
- );
-}
+import { CloseIcon } from "./icons";
+import { FolderRow } from "./folderPicker/FolderRow";
+import { StagedFoldersPanel } from "./folderPicker/StagedFoldersPanel";
+import { StatusLine } from "./folderPicker/StatusLine";
+import { normalizePath } from "./folderPicker/pathUtils";
+import { useFolderPicker } from "./folderPicker/useFolderPicker";
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 [addressDraft, setAddressDraft] = useState("");
- const [addressEditing, setAddressEditing] = useState(false);
- 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 addressInputRef = 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 folderPicker = useFolderPicker();
const virtualizer = useVirtualizer({
- count: listing?.entries.length ?? 0,
- getScrollElement: () => scrollRef.current,
+ count: folderPicker.entries.length,
+ getScrollElement: () => folderPicker.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);
- setAddressDraft(nextListing.current ?? "");
- setAddressEditing(false);
- scrollRef.current?.scrollTo({ top: 0, left: 0 });
- })
- .catch((loadError) => {
- if (cancelled) return;
- setListing({ current: currentPath, parent: null, entries: [] });
- setError(friendlyDirectoryError(loadError));
- })
- .finally(() => {
- if (!cancelled) setLoading(false);
- });
- return () => {
- cancelled = true;
- };
- }, [currentPath, folderPickerOpen, listDirectories]);
-
- useEffect(() => {
- if (!folderPickerOpen) return;
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "Escape") {
- if (addressEditing) {
- setAddressDraft(listing?.current ?? "");
- setAddressEditing(false);
- return;
- }
- setFolderPickerOpen(false);
- }
- };
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [addressEditing, folderPickerOpen, listing?.current, setFolderPickerOpen]);
-
- useEffect(() => {
- if (!addressEditing) return;
- requestAnimationFrame(() => {
- addressInputRef.current?.focus();
- addressInputRef.current?.select();
- });
- }, [addressEditing]);
-
- useEffect(() => {
- if (folderPickerOpen) return;
- setCurrentPath(null);
- setAddressDraft("");
- setAddressEditing(false);
- setListing(null);
- setStagedPaths([]);
- setError(null);
- setResults(null);
- setAdding(false);
- }, [folderPickerOpen]);
-
- if (!folderPickerOpen) return null;
-
- const entries = listing?.entries ?? [];
- const addressPath = cleanAddressInput(addressEditing ? addressDraft : (listing?.current ?? ""));
- const normalizedAddressPath = addressPath ? normalizePath(addressPath) : "";
- const addressAlreadyAdded = normalizedAddressPath ? libraryPaths.has(normalizedAddressPath) : false;
- const addressAlreadyStaged = normalizedAddressPath ? stagedSet.has(normalizedAddressPath) : false;
-
- 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 stagePath = (path: string) => {
- const cleaned = cleanAddressInput(path);
- if (!cleaned) {
- setError("Enter a folder path first.");
- return;
- }
-
- const normalized = normalizePath(cleaned);
- if (libraryPaths.has(normalized)) {
- setError("That folder is already in your library.");
- return;
- }
- if (stagedSet.has(normalized)) {
- setError("That folder is already selected.");
- return;
- }
-
- setError(null);
- setResults(null);
- setStagedPaths((current) => [...current, cleaned]);
- };
-
- const navigateToAddress = () => {
- const cleaned = cleanAddressInput(addressDraft);
- setResults(null);
- setError(null);
- setCurrentPath(cleaned || null);
- };
-
- const beginAddressEdit = () => {
- setAddressDraft(listing?.current ?? "");
- setAddressEditing(true);
- };
-
- 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);
- }
- };
+ if (!folderPicker.folderPickerOpen) return null;
return (
setFolderPickerOpen(false)}
+ onClick={() => folderPicker.setFolderPickerOpen(false)}
>
setFolderPickerOpen(false)}
+ onClick={() => folderPicker.setFolderPickerOpen(false)}
>
@@ -422,36 +52,33 @@ export function FolderPickerModal() {
- {addressEditing ? (
+ {folderPicker.addressEditing ? (
@@ -490,23 +117,27 @@ export function FolderPickerModal() {
- {error ? (
+ {folderPicker.error ? (
- {error}
+ {folderPicker.error}
) : null}
-
- {loading ? (
+
+ {folderPicker.loading ? (
Loading folders...
- ) : entries.length === 0 ? (
+ ) : folderPicker.entries.length === 0 ? (
No folders found here.
) : (
{virtualizer.getVirtualItems().map((virtualItem) => {
- const entry = entries[virtualItem.index];
+ const entry = folderPicker.entries[virtualItem.index];
const normalized = normalizePath(entry.path);
return (
togglePath(entry.path)}
- onNavigate={() => setCurrentPath(entry.path)}
+ selected={folderPicker.stagedSet.has(normalized)}
+ alreadyAdded={folderPicker.libraryPaths.has(normalized)}
+ onToggle={() => folderPicker.togglePath(entry.path)}
+ onNavigate={() => folderPicker.setCurrentPath(entry.path)}
/>
);
@@ -541,33 +172,33 @@ export function FolderPickerModal() {