import { useMemo, useState } from "react"; import type { ReactNode } from "react"; import { CalendarClock, Clock, ExternalLink, Hash, Link2, Lock, RefreshCw, Trash2, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { Collection, UpdateCollectionRequest, UserCollectionType } from "@/api/types"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import CollectionAccessEditor from "@/components/collections/CollectionAccessEditor"; import { ImageUploadField } from "@/components/ImageUploadField"; import { useDeleteCollection, useDeleteUserCollectionImage, useUpdateCollection, } from "@/hooks/queries/collections"; import { useUserLibraries } from "@/hooks/queries/libraries"; import { useProfiles } from "@/hooks/queries/profiles"; import { useCurrentProfile } from "@/hooks/useCurrentProfile"; import { useSyncUserCollection } from "@/hooks/queries/userCollectionImports"; import { libraryEligibilityForMediaKind } from "@/lib/collectionTemplates"; import { CollectionLibraryPicker } from "@/pages/adminCollectionsShared"; import { isCollectionReadOnly } from "./userCollectionsShared"; type ImportedType = Extract; interface SourceTheme { label: string; tagline: string; /** Hex/RGB seed used for color-mix accents. Should match the brand. */ accent: string; /** Subtle gradient stops for the hero atmosphere. */ haloFrom: string; haloTo: string; initials: string; } const SOURCE_THEMES: Record = { tmdb: { label: "TMDB", tagline: "The Movie Database", accent: "#22d3ee", haloFrom: "rgba(56,189,248,0.32)", haloTo: "rgba(16,185,129,0.18)", initials: "Tm", }, trakt: { label: "Trakt", tagline: "Trakt.tv", accent: "#fb7185", haloFrom: "rgba(244,63,94,0.30)", haloTo: "rgba(251,146,60,0.18)", initials: "Tk", }, mdblist: { label: "MDBList", tagline: "mdblist.com", accent: "#fbbf24", haloFrom: "rgba(251,191,36,0.32)", haloTo: "rgba(249,115,22,0.18)", initials: "Mb", }, }; interface ImportedCollectionEditorProps { collection: Collection; onClose: () => void; } export function ImportedCollectionEditor({ collection, onClose }: ImportedCollectionEditorProps) { const importedType = collection.collection_type as ImportedType; const theme = SOURCE_THEMES[importedType]; const updateMutation = useUpdateCollection(); const deleteMutation = useDeleteCollection(); const deletePosterMutation = useDeleteUserCollectionImage(); const syncMutation = useSyncUserCollection(); const { data: profiles = [] } = useProfiles(); const { data: libraries = [] } = useUserLibraries(); const { profile } = useCurrentProfile(); const readOnly = isCollectionReadOnly(collection, profile?.id); const initialMaxItems = readSourceConfigLimit(collection); const initialSourceUrl = collection.source_url ?? ""; const initialDescription = collection.description ?? ""; const [name, setName] = useState(collection.name); const [description, setDescription] = useState(initialDescription); const [libraryIds, setLibraryIds] = useState( collection.query_definition.library_ids ?? [], ); const [isShared, setIsShared] = useState(collection.is_shared); const [allowedProfileIds, setAllowedProfileIds] = useState( collection.allowed_profile_ids ?? [], ); const [includeOnServer, setIncludeOnServer] = useState( collection.include_in_server_collections ?? false, ); const [sourceUrlInput, setSourceUrlInput] = useState(initialSourceUrl); const [maxItemsInput, setMaxItemsInput] = useState( initialMaxItems != null ? String(initialMaxItems) : "", ); const [posterFile, setPosterFile] = useState(null); const [posterSourceUrl, setPosterSourceUrl] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false); const isMDBList = importedType === "mdblist"; const parsedMaxItems = parseMaxItemsInput(maxItemsInput); const builderLibraries = useMemo( () => libraries.map((lib) => ({ id: lib.id, name: lib.name, type: lib.type })), [libraries], ); const eligibility = useMemo(() => { const scope = collection.query_definition.media_scope; if (scope === "movie") return libraryEligibilityForMediaKind("movie"); if (scope === "series") return libraryEligibilityForMediaKind("tv"); return libraryEligibilityForMediaKind("mixed"); }, [collection.query_definition.media_scope]); const isPending = updateMutation.isPending; const isSyncing = syncMutation.isPending; const trimmedPosterSource = posterSourceUrl.trim(); const posterDirty = posterFile !== null || trimmedPosterSource !== ""; const trimmedSourceUrl = sourceUrlInput.trim(); const sourceUrlDirty = isMDBList && trimmedSourceUrl !== initialSourceUrl; const maxItemsDirty = parsedMaxItems !== initialMaxItems; const descriptionDirty = description !== initialDescription; const dirtyParts = [ name.trim() !== collection.name, descriptionDirty, !arraysEqual(libraryIds, collection.query_definition.library_ids ?? []), isShared !== collection.is_shared, !arraysEqual(allowedProfileIds, collection.allowed_profile_ids ?? []), includeOnServer !== (collection.include_in_server_collections ?? false), sourceUrlDirty, maxItemsDirty, posterDirty, ]; const dirtyCount = dirtyParts.filter(Boolean).length; const dirty = dirtyCount > 0; const maxItemsInvalid = maxItemsInput.trim() !== "" && parsedMaxItems == null; const sourceUrlInvalid = sourceUrlDirty && trimmedSourceUrl === ""; const saveBlocked = maxItemsInvalid || sourceUrlInvalid; function handleSave() { if (!dirty || readOnly || saveBlocked) return; const body: UpdateCollectionRequest = { name: name.trim() || collection.name, is_shared: isShared, allowed_profile_ids: allowedProfileIds, query_definition: { ...collection.query_definition, library_ids: libraryIds, }, include_in_server_collections: includeOnServer, poster_source_url: trimmedPosterSource || undefined, }; if (descriptionDirty) { body.description = description; } if (sourceUrlDirty) { body.source_url = trimmedSourceUrl; } if (maxItemsDirty) { body.max_items = parsedMaxItems ?? 0; } updateMutation.mutate( { id: collection.id, body, poster: posterFile }, { onSuccess: () => { setPosterFile(null); setPosterSourceUrl(""); }, }, ); } function handleDiscard() { setName(collection.name); setDescription(initialDescription); setLibraryIds(collection.query_definition.library_ids ?? []); setIsShared(collection.is_shared); setAllowedProfileIds(collection.allowed_profile_ids ?? []); setIncludeOnServer(collection.include_in_server_collections ?? false); setSourceUrlInput(initialSourceUrl); setMaxItemsInput(initialMaxItems != null ? String(initialMaxItems) : ""); setPosterFile(null); setPosterSourceUrl(""); } function handleSyncNow() { if (isSyncing) return; syncMutation.mutate(collection.id); } function handleDelete() { deleteMutation.mutate(collection.id, { onSuccess: () => { onClose(); }, }); } const sourceUrl = readableSourceURL(collection); const sourcePresetLabel = sourcePresetSummary(collection, theme.label); return (
{ event.preventDefault(); handleSave(); }} >
Name setName(event.target.value)} required disabled={readOnly} className="h-11 text-[0.95rem]" />
Description