diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index bd8c88190..f5829f777 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -126,7 +126,7 @@ export default function PreviewThumbnailPlayer({ playerRef={playerRef} review={review} relevantPreview={relevantPreview} - isVisible={visible} + isVisible={isMobile ? visible : true} isMobile={isMobile} setProgress={setProgress} setReviewed={setReviewed} diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index ace4c631f..3fe65dd5a 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,4 +1,5 @@ import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; +import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -12,7 +13,7 @@ import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import axios from "axios"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LuCalendar, LuFilter, LuVideo } from "react-icons/lu"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; @@ -23,6 +24,7 @@ const API_LIMIT = 100; export default function Events() { const { data: config } = useSWR("config"); const [severity, setSeverity] = useState("alert"); + const contentRef = useRef(null); // review paging @@ -63,12 +65,15 @@ export default function Events() { } = useSWRInfinite(getKey, reviewSegmentFetcher); 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); @@ -83,7 +88,12 @@ export default function Events() { }); }); - return { alert: alerts, detection: detections, significant_motion: motion }; + return { + all: all, + alert: alerts, + detection: detections, + significant_motion: motion, + }; }, [reviewPages]); const isDone = useMemo( @@ -91,18 +101,20 @@ export default function Events() { [reviewPages] ); - const observer = useRef(); + // review interaction + + const pagingObserver = useRef(); const lastReviewRef = useCallback( (node: HTMLElement | null) => { if (isValidating) return; - if (observer.current) observer.current.disconnect(); + if (pagingObserver.current) pagingObserver.current.disconnect(); try { - observer.current = new IntersectionObserver((entries) => { + pagingObserver.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !isDone) { setSize(size + 1); } }); - if (node) observer.current.observe(node); + if (node) pagingObserver.current.observe(node); } catch (e) { // no op } @@ -110,6 +122,68 @@ export default function Events() { [isValidating, isDone] ); + const [minimap, setMinimap] = useState([]); + const minimapObserver = useRef(); + useEffect(() => { + if (!contentRef.current) { + return; + } + + 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 } + ); + + 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.current] + ); + const minimapBounds = useMemo(() => { + const data = { + start: Math.floor(Date.now() / 1000) - 35 * 60, + end: Math.floor(Date.now() / 1000) - 21 * 60, + }; + const list = minimap.sort(); + + if (list.length > 0) { + data.end = parseFloat(list.at(-1)!!); + data.start = parseFloat(list[0]); + } + + return data; + }, [minimap]); + // review status const setReviewed = useCallback( @@ -156,8 +230,8 @@ export default function Events() { } return ( - <> -
+
+
-
+
{reviewItems[severity]?.map((value, segIdx) => { const lastRow = segIdx == reviewItems[severity].length - 1; const relevantPreview = Object.values(allPreviews || []).find( @@ -219,11 +296,12 @@ export default function Events() { ); return ( -
-
+
+
- +
+ +
+
); } diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 68c44ab14..6163c00ca 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -117,6 +117,7 @@ function UIPlayground() { useMemo(() => { const initialEvents = Array.from({ length: 50 }, generateRandomEvent); setMockEvents(initialEvents); + console.log(initialEvents); }, []); return ( diff --git a/web/src/types/review.ts b/web/src/types/review.ts index 2f55ddace..bb094f240 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -11,10 +11,11 @@ export interface ReviewSegment { export type ReviewSeverity = "alert" | "detection" | "significant_motion"; -type ReviewData = { +export type ReviewData = { audio: string[]; detections: string[]; objects: string[]; + sub_labels?: [string, number][]; significant_motion_areas: number[]; zones: string[]; };