import { useEffect, useMemo } from "react"; import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect"; import useSWR from "swr"; import { useApiHost } from "@/api"; import { cn } from "@/lib/utils"; import { LuArrowRightCircle } from "react-icons/lu"; import { useNavigate } from "react-router-dom"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { SearchResult } from "@/types/search"; import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator"; import useImageLoaded from "@/hooks/use-image-loaded"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { useTrackedObjectUpdate } from "@/api/ws"; import { isEqual } from "lodash"; import TimeAgo from "@/components/dynamic/TimeAgo"; import SearchResultActions from "@/components/menu/SearchResultActions"; import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog"; import { FrigateConfig } from "@/types/frigateConfig"; type ExploreViewProps = { searchDetail: SearchResult | undefined; setSearchDetail: (search: SearchResult | undefined) => void; setSimilaritySearch: (search: SearchResult) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; export default function ExploreView({ searchDetail, setSearchDetail, setSimilaritySearch, onSelectSearch, }: ExploreViewProps) { // title useEffect(() => { document.title = "Explore - Frigate"; }, []); // data const { data: events, mutate, isLoading, isValidating, } = useSWR( [ "events/explore", { limit: isMobileOnly ? 5 : 10, }, ], { revalidateOnFocus: true, }, ); const eventsByLabel = useMemo(() => { if (!events) return {}; return events.reduce>((acc, event) => { const label = event.label || "Unknown"; if (!acc[label]) { acc[label] = []; } acc[label].push(event); return acc; }, {}); }, [events]); const trackedObjectUpdate = useTrackedObjectUpdate(); useEffect(() => { mutate(); // mutate / revalidate when event description updates come in // eslint-disable-next-line react-hooks/exhaustive-deps }, [trackedObjectUpdate]); // update search detail when results change useEffect(() => { if (searchDetail && events) { const updatedSearchDetail = events.find( (result) => result.id === searchDetail.id, ); if (updatedSearchDetail && !isEqual(updatedSearchDetail, searchDetail)) { setSearchDetail(updatedSearchDetail); } } }, [events, searchDetail, setSearchDetail]); if (isLoading) { return ( ); } return (
{Object.entries(eventsByLabel).map(([label, filteredEvents]) => ( ))}
); } type ThumbnailRowType = { objectType: string; searchResults?: SearchResult[]; isValidating: boolean; setSearchDetail: (search: SearchResult | undefined) => void; mutate: () => void; setSimilaritySearch: (search: SearchResult) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; function ThumbnailRow({ objectType, searchResults, isValidating, setSearchDetail, mutate, setSimilaritySearch, onSelectSearch, }: ThumbnailRowType) { const navigate = useNavigate(); const handleSearch = (label: string) => { const similaritySearchParams = new URLSearchParams({ labels: label, }).toString(); navigate(`/explore?${similaritySearchParams}`); }; return (
{objectType.replaceAll("_", " ")} {searchResults && ( ( { // @ts-expect-error we know this is correct searchResults[0].event_count }{" "} tracked objects){" "} )} {isValidating && }
{searchResults?.map((event) => (
))}
handleSearch(objectType)} >
); } type ExploreThumbnailImageProps = { event: SearchResult; setSearchDetail: (search: SearchResult | undefined) => void; mutate: () => void; setSimilaritySearch: (search: SearchResult) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; function ExploreThumbnailImage({ event, setSearchDetail, mutate, setSimilaritySearch, onSelectSearch, }: ExploreThumbnailImageProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); const handleFindSimilar = () => { if (config?.semantic_search.enabled) { setSimilaritySearch(event); } }; const handleShowObjectLifecycle = () => { onSelectSearch(event, false, "object lifecycle"); }; const handleShowSnapshot = () => { onSelectSearch(event, false, "snapshot"); }; return (
setSearchDetail(event)} onLoad={onImgLoad} alt={`${event.label} thumbnail`} /> {isDesktop && (
{event.end_time ? ( ) : (
)}
)}
); } function ExploreMoreLink({ objectType }: { objectType: string }) { const formattedType = objectType.replaceAll("_", " "); const label = formattedType.endsWith("s") ? `${formattedType}es` : `${formattedType}s`; return
Explore More {label}
; }