import { useState, useMemo, useCallback } from "react"; import { Check, Loader2, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import type { ItemDetail, RemoteImage } from "@/api/types"; import { useItemImages, useApplyItemImage } from "@/hooks/queries/items"; import { cn } from "@/lib/utils"; type ImageTab = "poster" | "backdrop" | "logo"; const IMAGE_TABS: { key: ImageTab; label: string; aspect: string }[] = [ { key: "poster", label: "Posters", aspect: "aspect-[2/3]" }, { key: "backdrop", label: "Backdrops", aspect: "aspect-video" }, { key: "logo", label: "Logos", aspect: "aspect-video" }, ]; interface ImageSelectorTabProps { item: ItemDetail; enabled: boolean; } export default function ImageSelectorTab({ item, enabled }: ImageSelectorTabProps) { const [activeTab, setActiveTab] = useState("poster"); const [textlessOnly, setTextlessOnly] = useState(false); const [selectedImage, setSelectedImage] = useState(null); // Track which images were applied in this session (original_url per type). const [appliedImages, setAppliedImages] = useState>({}); const { data, isLoading, isError } = useItemImages(item.content_id, enabled); const applyMutation = useApplyItemImage(); const images = data?.images ?? []; const current = data?.current; const providerErrors = data?.provider_errors; const tabConfig = IMAGE_TABS.find((t) => t.key === activeTab)!; const filteredImages = useMemo(() => { let result = images.filter((img) => img.type === activeTab); if (textlessOnly && activeTab !== "logo") { result = result.filter((img) => img.language === ""); } return result; }, [images, activeTab, textlessOnly]); // The "current" image for this tab. We first check session-local applied // images (which use original_url), then fall back to the server's stored // path (which may be an S3 key or a plugin path depending on cache state). const currentOriginalUrl = appliedImages[activeTab] ?? ""; const currentStoredPath = useMemo(() => { if (!current) return ""; switch (activeTab) { case "poster": return current.poster_url ?? ""; case "backdrop": return current.backdrop_url ?? ""; case "logo": return current.logo_url ?? ""; } }, [current, activeTab]); const handleApply = useCallback(() => { if (!selectedImage) return; applyMutation.mutate( { item, request: { original_url: selectedImage.original_url, type: selectedImage.type, provider_id: selectedImage.provider_id, }, }, { onSuccess: () => { setAppliedImages((prev) => ({ ...prev, [selectedImage.type]: selectedImage.original_url, })); setSelectedImage(null); }, }, ); }, [selectedImage, applyMutation, item]); return (
{/* Info banner */}
Image changes apply immediately and are not affected by Cancel.
{/* Image type tabs */}
{IMAGE_TABS.map((tab) => ( ))} {/* Textless filter — hidden for logos */} {activeTab !== "logo" && ( )}
{/* Provider errors */} {providerErrors && Object.keys(providerErrors).length > 0 && (
Could not load from{" "} {Object.keys(providerErrors) .map((k) => k.toUpperCase()) .join(", ")}
)} {/* Image grid */} {isLoading ? (
) : isError ? (

Failed to load images.

) : filteredImages.length === 0 ? (

{textlessOnly ? "No textless images available." : "No images available."}

) : (
{filteredImages.map((img, index) => { const isSelected = selectedImage === img; const isCurrent = img.original_url === currentOriginalUrl || img.original_url === currentStoredPath; return ( ); })}
)} {/* Apply button */} {selectedImage && (
)}
); }