import { useState, useCallback, useMemo } from "react"; import { Copy, Folder, Search } from "lucide-react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import type { FileVersion, ItemDetail, MatchCandidate } from "@/api/types"; import MediaLocations from "@/components/MediaLocations"; import { useSearchItemMatchCandidates, useApplyItemMatch } from "@/hooks/queries/items"; import { useCatalogItemDetail } from "@/hooks/queries/catalogRead"; import { cn } from "@/lib/utils"; type MatchableItem = Pick< ItemDetail, "content_id" | "title" | "year" | "type" | "series_id" | "season_number" > & { library_id?: number; versions?: FileVersion[]; folder_paths?: string[]; }; interface MatchItemDialogProps { item: MatchableItem; open: boolean; onOpenChange: (open: boolean) => void; } export default function MatchItemDialog({ item, open, onOpenChange }: MatchItemDialogProps) { const [title, setTitle] = useState(item.title); const [year, setYear] = useState(item.year ? String(item.year) : ""); const [imdbId, setImdbId] = useState(""); const [tmdbId, setTmdbId] = useState(""); const [tvdbId, setTvdbId] = useState(""); const [selectedCandidate, setSelectedCandidate] = useState(null); const isSeries = item.type === "series"; const needsItemDetail = open && item.versions === undefined; const { data: enrichedItem, isLoading: enrichedItemLoading } = useCatalogItemDetail( needsItemDetail ? item.content_id : undefined, needsItemDetail ? item.library_id : undefined, ); const searchMutation = useSearchItemMatchCandidates(item.content_id); const applyMutation = useApplyItemMatch(); const candidates = searchMutation.data?.candidates ?? []; const effectiveItem = needsItemDetail ? (enrichedItem ?? item) : item; const handleSearch = useCallback(() => { setSelectedCandidate(null); searchMutation.mutate({ title: title || undefined, year: year ? parseInt(year, 10) : undefined, imdb_id: imdbId || undefined, tmdb_id: tmdbId || undefined, tvdb_id: tvdbId || undefined, }); }, [title, year, imdbId, tmdbId, tvdbId, searchMutation]); const handleApply = useCallback(() => { if (!selectedCandidate) return; applyMutation.mutate( { item, providerIds: selectedCandidate.provider_ids }, { onSuccess: () => { onOpenChange(false); }, }, ); }, [selectedCandidate, applyMutation, item, onOpenChange]); return ( Match Item
{/* Current item summary */}
{item.title} {item.year ? ({item.year}) : null} {item.type}
{(enrichedItemLoading || (isSeries ? effectiveItem.folder_paths !== undefined : effectiveItem.versions !== undefined)) && (enrichedItemLoading ? (
Loading local media…
) : isSeries ? ( ) : ( ))} {/* Search inputs */}
setTitle(e.target.value)} placeholder="Title" />
setYear(e.target.value)} placeholder="Year" type="number" />
setImdbId(e.target.value)} placeholder="tt1234567" />
setTmdbId(e.target.value)} placeholder="12345" />
setTvdbId(e.target.value)} placeholder="12345" />
{/* Candidate list */} {candidates.length > 0 && (
{candidates.map((candidate, index) => { const candidateKey = Object.entries(candidate.provider_ids) .map(([k, v]) => `${k}-${v}`) .join("_"); return ( ); })}
)} {searchMutation.isSuccess && candidates.length === 0 && (

No candidates found.

)}
{/* Apply button — pinned below scroll area */} {selectedCandidate && (
)}
); } function getFolderDisplayName(path: string): string { const trimmed = path.trim().replace(/[\\/]+$/, ""); if (!trimmed) return path; const segments = trimmed.split(/[\\/]/).filter(Boolean); return segments[segments.length - 1] || trimmed; } function computeRootPath(paths: string[]): string { if (paths.length === 0) return ""; const parentSegments = paths.map((path) => { const trimmed = path.trim().replace(/[\\/]+$/, ""); return trimmed.split(/[\\/]/).filter(Boolean).slice(0, -1); }); const first = parentSegments[0] ?? []; const minLen = Math.min(...parentSegments.map((segments) => segments.length)); let sharedLength = 0; for (let i = 0; i < minLen; i += 1) { if (parentSegments.every((segments) => segments[i] === first[i])) { sharedLength = i + 1; continue; } break; } if (sharedLength === 0) return ""; return `/${first.slice(0, sharedLength).join("/")}`; } function FolderPathsList({ paths }: { paths: string[] }) { const folderData = useMemo(() => { return { folders: paths.map((fullPath) => ({ fullPath, displayName: getFolderDisplayName(fullPath), })), rootPath: computeRootPath(paths), }; }, [paths]); if (paths.length === 0) { return (

Local media

No folder paths are available for this item.
); } return (

Local media

{folderData.rootPath ? (
Root path
{folderData.rootPath}
) : null}
{folderData.folders.map(({ fullPath, displayName }) => (
{displayName}
))}
); }