import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; import { Dialog, DialogContent, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { IoClose } from "react-icons/io5"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import Heading from "@/components/ui/heading"; import { cn } from "@/lib/utils"; import { Event } from "@/types/event"; import { useApiHost } from "@/api"; import { isDesktop, isMobile } from "react-device-detect"; import ActivityIndicator from "../indicators/activity-indicator"; type ImagePickerProps = { selectedImageId?: string; setSelectedImageId?: (id: string) => void; camera: string; limit?: number; direct?: boolean; className?: string; }; export default function ImagePicker({ selectedImageId, setSelectedImageId, camera, limit = 100, direct = false, className, }: ImagePickerProps) { const { t } = useTranslation(["components/dialog", "views/settings"]); const [open, setOpen] = useState(false); const containerRef = useRef(null); const [searchTerm, setSearchTerm] = useState(""); const [loadedImages, setLoadedImages] = useState>(new Set()); const { data: events } = useSWR( `events?camera=${camera}&limit=${limit}`, { revalidateOnFocus: false, }, ); const apiHost = useApiHost(); const images = useMemo(() => { if (!events) return []; return events.filter( (event) => (event.label.toLowerCase().includes(searchTerm.toLowerCase()) || (event.sub_label && event.sub_label.toLowerCase().includes(searchTerm.toLowerCase())) || searchTerm === "") && event.camera === camera, ); }, [events, searchTerm, camera]); const selectedImage = useMemo( () => images.find((img) => img.id === selectedImageId), [images, selectedImageId], ); const handleImageSelect = useCallback( (id: string) => { if (setSelectedImageId) { setSelectedImageId(id); } if (!direct) { setOpen(false); } }, [setSelectedImageId, direct], ); const handleImageLoad = useCallback((imageId: string) => { setLoadedImages((prev) => new Set(prev).add(imageId)); }, []); const renderSearchInput = () => ( { setSearchTerm(e.target.value); // Clear selected image when user starts typing if (setSelectedImageId) { setSelectedImageId(""); } }} /> ); const renderImageGrid = () => (
{images.length === 0 ? (
{t("imagePicker.noImages")}
) : ( images.map((image) => (
{image.label} handleImageSelect(image.id)} onLoad={() => handleImageLoad(image.id)} loading="lazy" /> {!loadedImages.has(image.id) && (
)}
)) )}
); if (direct) { return (
{renderSearchInput()} {renderImageGrid()}
); } return (
{ setOpen(open); }} > {!selectedImageId ? ( ) : (
{selectedImage?.label handleImageLoad(selectedImageId || "")} loading="lazy" /> {selectedImageId && !loadedImages.has(selectedImageId) && (
)}
{selectedImage?.label || t("imagePicker.unknownLabel")} {selectedImage?.sub_label ? ` (${selectedImage.sub_label})` : ""}
{ if (setSelectedImageId) { setSelectedImageId(""); } }} />
)}
{t("imagePicker.selectImage")}
{t("imagePicker.selectImage")}
{t("triggers.dialog.form.content.imageDesc", { ns: "views/settings", })}
{renderSearchInput()}
{renderImageGrid()}
); }