import { isDesktop, isIOS, isMobile } from "react-device-detect"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "../../ui/sheet"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { getIconForLabel } from "@/utils/iconUtil"; import { useApiHost } from "@/api"; import { ReviewDetailPaneType, ReviewSegment } from "@/types/review"; import { Event } from "@/types/event"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog"; import ObjectLifecycle from "./ObjectLifecycle"; import Chip from "@/components/indicators/Chip"; import { FaDownload, FaImages, FaShareAlt } from "react-icons/fa"; import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon"; import { FaArrowsRotate } from "react-icons/fa6"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { baseUrl } from "@/api/baseUrl"; import { shareOrCopy } from "@/utils/browserUtil"; import { MobilePage, MobilePageContent, MobilePageDescription, MobilePageHeader, MobilePageTitle, } from "@/components/mobile/MobilePage"; import { useOverlayState } from "@/hooks/use-overlay-state"; import { DownloadVideoButton } from "@/components/button/DownloadVideoButton"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { LuSearch } from "react-icons/lu"; type ReviewDetailDialogProps = { review?: ReviewSegment; setReview: (review: ReviewSegment | undefined) => void; }; export default function ReviewDetailDialog({ review, setReview, }: ReviewDetailDialogProps) { const { data: config } = useSWR("config", { revalidateOnFocus: false, }); const navigate = useNavigate(); // upload const [upload, setUpload] = useState(); // data const { data: events } = useSWR( review ? ["event_ids", { ids: review.data.detections.join(",") }] : null, ); const hasMismatch = useMemo(() => { if (!review || !events) { return false; } return events.length != review?.data.detections.length; }, [review, events]); const formattedDate = useFormattedTimestamp( review?.start_time ?? 0, config?.ui.time_format == "24hour" ? "%b %-d %Y, %H:%M" : "%b %-d %Y, %I:%M %p", config?.ui.timezone, ); // content const [selectedEvent, setSelectedEvent] = useState(); const [pane, setPane] = useState("overview"); // dialog and mobile page const [isOpen, setIsOpen] = useOverlayState( "reviewPane", review != undefined, ); const handleOpenChange = useCallback( (open: boolean) => { setIsOpen(open); if (!open) { // short timeout to allow the mobile page animation // to complete before updating the state setTimeout(() => { setReview(undefined); setSelectedEvent(undefined); setPane("overview"); }, 300); } }, [setReview, setIsOpen], ); useEffect(() => { setIsOpen(review != undefined); // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [review]); const Overlay = isDesktop ? Sheet : MobilePage; const Content = isDesktop ? SheetContent : MobilePageContent; const Header = isDesktop ? SheetHeader : MobilePageHeader; const Title = isDesktop ? SheetTitle : MobilePageTitle; const Description = isDesktop ? SheetDescription : MobilePageDescription; if (!review) { return; } return ( <> setUpload(undefined)} onEventUploaded={() => { if (upload) { upload.plus_id = "new_upload"; } }} /> {pane == "overview" && (
Review Item Details Review item details
Share this review item Download
)} {pane == "overview" && (
Camera
{review.camera.replaceAll("_", " ")}
Timestamp
{formattedDate}
Objects
{events?.map((event) => { return (
{getIconForLabel( event.label, "size-3 text-primary", )} {event.sub_label ?? event.label} ( {Math.round(event.data.top_score * 100)}%)
{ navigate(`/explore?event_id=${event.id}`); }} >
View in Explore
); })}
{review.data.zones.length > 0 && (
Zones
{review.data.zones.map((zone) => { return (
{zone.replaceAll("_", " ")}
); })}
)}
{hasMismatch && (
Some objects that were detected are not included in this list because the object does not have a snapshot
)}
{events?.map((event) => ( ))}
)} {pane == "details" && selectedEvent && (
)}
); } type EventItemProps = { event: Event; setPane: React.Dispatch>; setSelectedEvent: React.Dispatch>; setUpload?: React.Dispatch>; }; function EventItem({ event, setPane, setSelectedEvent, setUpload, }: EventItemProps) { const { data: config } = useSWR("config", { revalidateOnFocus: false, }); const apiHost = useApiHost(); const imgRef = useRef(null); const [hovered, setHovered] = useState(isMobile); const navigate = useNavigate(); return ( <>
setHovered(true) : undefined} onMouseLeave={isDesktop ? () => setHovered(false) : undefined} key={event.id} > {event.has_snapshot && ( <>
)} {hovered && (
Download {event.has_snapshot && event.plus_id == undefined && event.data.type == "object" && config?.plus.enabled && ( { setUpload?.(event); }} > Submit to Frigate+ )} {event.has_clip && ( { setPane("details"); setSelectedEvent(event); }} > View Object Lifecycle )} {event.has_snapshot && config?.semantic_search.enabled && ( { navigate( `/explore?search_type=similarity&event_id=${event.id}`, ); }} > Find Similar )}
)}
); }