import Logo from "@/components/Logo"; import NewReviewData from "@/components/dynamic/NewReviewData"; import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useEventUtils } from "@/hooks/use-event-utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { Preview } from "@/types/preview"; import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; import { LuFolderCheck } from "react-icons/lu"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; type EventViewProps = { reviewPages?: ReviewSegment[][]; relevantPreviews?: Preview[]; timeRange: { before: number; after: number }; reachedEnd: boolean; isValidating: boolean; filter?: ReviewFilter; severity: ReviewSeverity; setSeverity: (severity: ReviewSeverity) => void; loadNextPage: () => void; markItemAsReviewed: (reviewId: string) => void; onSelectReview: (reviewId: string) => void; pullLatestData: () => void; updateFilter: (filter: ReviewFilter) => void; }; export default function EventView({ reviewPages, relevantPreviews, timeRange, reachedEnd, isValidating, filter, severity, setSeverity, loadNextPage, markItemAsReviewed, onSelectReview, pullLatestData, updateFilter, }: EventViewProps) { const { data: config } = useSWR("config"); const contentRef = useRef(null); const segmentDuration = 60; // review paging const reviewItems = useMemo(() => { const all: ReviewSegment[] = []; const alerts: ReviewSegment[] = []; const detections: ReviewSegment[] = []; const motion: ReviewSegment[] = []; reviewPages?.forEach((page) => { page.forEach((segment) => { all.push(segment); switch (segment.severity) { case "alert": alerts.push(segment); break; case "detection": detections.push(segment); break; default: motion.push(segment); break; } }); }); return { all: all, alert: alerts, detection: detections, significant_motion: motion, }; }, [reviewPages]); const { alignStartDateToTimeline } = useEventUtils( reviewItems.all, segmentDuration, ); const currentItems = useMemo(() => { const current = reviewItems[severity]; if (!current || current.length == 0) { return null; } return current; }, [reviewItems, severity]); const showMinimap = useMemo(() => { if (!contentRef.current) { return false; } return contentRef.current.scrollHeight > contentRef.current.clientHeight; // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [contentRef.current?.scrollHeight, severity]); // review interaction const pagingObserver = useRef(); const lastReviewRef = useCallback( (node: HTMLElement | null) => { if (isValidating) return; if (pagingObserver.current) pagingObserver.current.disconnect(); try { pagingObserver.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !reachedEnd) { loadNextPage(); } }); if (node) pagingObserver.current.observe(node); } catch (e) { // no op } }, [isValidating, reachedEnd, loadNextPage], ); const [minimap, setMinimap] = useState([]); const minimapObserver = useRef(); useEffect(() => { const visibleTimestamps = new Set(); minimapObserver.current = new IntersectionObserver( (entries) => { entries.forEach((entry) => { const start = (entry.target as HTMLElement).dataset.start; if (!start) { return; } if (entry.isIntersecting) { visibleTimestamps.add(start); } else { visibleTimestamps.delete(start); } setMinimap([...visibleTimestamps]); }); }, { root: contentRef.current, threshold: isDesktop ? 0.1 : 0.5 }, ); return () => { minimapObserver.current?.disconnect(); }; }, [contentRef]); const minimapRef = useCallback( (node: HTMLElement | null) => { if (!minimapObserver.current) { return; } try { if (node) minimapObserver.current.observe(node); } catch (e) { // no op } }, [minimapObserver], ); const minimapBounds = useMemo(() => { const data = { start: 0, end: 0, }; const list = minimap.sort(); if (list.length > 0) { data.end = parseFloat(list.at(-1) || "0"); data.start = parseFloat(list[0]); } return data; }, [minimap]); if (!config) { return ; } return (
{isMobile && ( )} setSeverity(value)} >
Alerts
Detections
Motion
{filter?.before == undefined && ( )} {!isValidating && currentItems == null && (
There are no {severity} items to review
)}
{currentItems ? ( currentItems.map((value, segIdx) => { const lastRow = segIdx == reviewItems[severity].length - 1; const relevantPreview = Object.values( relevantPreviews || [], ).find( (preview) => preview.camera == value.camera && preview.start < value.start_time && preview.end > value.end_time, ); return (
{lastRow && !reachedEnd && }
); }) ) : severity != "alert" ? (
) : null}
); }