diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 170124262..9ab090fdd 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -240,22 +240,37 @@ export default function Events() { // selected items - const selectedData = useMemo(() => { + const selectedReviewData = useMemo(() => { if (!config) { return undefined; } - if (!selectedReviewId) { - return undefined; - } - if (!reviewPages) { return undefined; } + if (!selectedReviewId) { + return undefined; + } + const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras); const allReviews = reviewPages.flat(); + + if (selectedReviewId.startsWith("motion")) { + const motionData = selectedReviewId.split(","); + // format is motion,camera,start_time + return { + camera: motionData[1], + severity: "significant_motion" as ReviewSeverity, + start_time: parseFloat(motionData[2]), + allCameras: allCameras, + cameraSegments: allReviews.filter((seg) => + allCameras.includes(seg.camera), + ), + }; + } + const selectedReview = allReviews.find( (item) => item.id == selectedReviewId, ); @@ -265,7 +280,9 @@ export default function Events() { } return { - selected: selectedReview, + camera: selectedReview.camera, + severity: selectedReview.severity, + start_time: selectedReview.start_time, allCameras: allCameras, cameraSegments: allReviews.filter((seg) => allCameras.includes(seg.camera), @@ -280,12 +297,14 @@ export default function Events() { return ; } - if (selectedData) { + if (selectedReviewData) { if (isMobile) { return ( ); @@ -293,11 +312,11 @@ export default function Events() { return ( ); diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index b5cb1c176..8bddf8a3c 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -286,6 +286,7 @@ export default function EventView({ relevantPreviews={relevantPreviews} timeRange={timeRange} filter={filter} + onSelectReview={onSelectReview} /> )} @@ -528,6 +529,7 @@ type MotionReviewProps = { relevantPreviews?: Preview[]; timeRange: { before: number; after: number }; filter?: ReviewFilter; + onSelectReview: (data: string) => void; }; function MotionReview({ contentRef, @@ -535,6 +537,7 @@ function MotionReview({ relevantPreviews, timeRange, filter, + onSelectReview, }: MotionReviewProps) { const segmentDuration = 30; const { data: config } = useSWR("config"); @@ -652,6 +655,9 @@ function MotionReview({ videoPlayersRef.current[camera.name] = controller; setPlayerReady(true); }} + onClick={() => + onSelectReview(`motion,${camera.name},${currentTime}`) + } /> ); })} diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 21bf884ff..30ef6617e 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -2,13 +2,17 @@ import DynamicVideoPlayer, { DynamicVideoController, } from "@/components/player/DynamicVideoPlayer"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; +import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; import { Button } from "@/components/ui/button"; import { Preview } from "@/types/preview"; -import { ReviewSegment, ReviewSeverity } from "@/types/review"; +import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; import { getChunkedTimeDay } from "@/utils/timelineUtil"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { IoMdArrowRoundBack } from "react-icons/io"; import { useNavigate } from "react-router-dom"; +import useSWR from "swr"; + +const SEGMENT_DURATION = 30; type DesktopRecordingViewProps = { startCamera: string; @@ -116,6 +120,21 @@ export function DesktopRecordingView({ [allCameras, currentTime, mainCamera], ); + // motion timeline data + + const { data: motionData } = useSWR( + severity == "significant_motion" + ? [ + "review/activity", + { + before: timeRange.end, + after: timeRange.start, + scale: SEGMENT_DURATION / 2, + }, + ] + : null, + ); + return (
); } type MobileRecordingViewProps = { - selectedReview: ReviewSegment; + startCamera: string; + startTime: number; + severity: ReviewSeverity; reviewItems: ReviewSegment[]; relevantPreviews?: Preview[]; }; export function MobileRecordingView({ - selectedReview, + startCamera, + startTime, + severity, reviewItems, relevantPreviews, }: MobileRecordingViewProps) { @@ -219,16 +259,10 @@ export function MobileRecordingView({ // timeline time - const timeRange = useMemo( - () => getChunkedTimeDay(selectedReview.start_time), - [selectedReview], - ); + const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]); const [selectedRangeIdx, setSelectedRangeIdx] = useState( timeRange.ranges.findIndex((chunk) => { - return ( - chunk.start <= selectedReview.start_time && - chunk.end >= selectedReview.start_time - ); + return chunk.start <= startTime && chunk.end >= startTime; }), ); @@ -251,7 +285,7 @@ export function MobileRecordingView({ const [scrubbing, setScrubbing] = useState(false); const [currentTime, setCurrentTime] = useState( - selectedReview?.start_time || Date.now() / 1000, + startTime || Date.now() / 1000, ); useEffect(() => { @@ -269,6 +303,21 @@ export function MobileRecordingView({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrubbing]); + // motion timeline data + + const { data: motionData } = useSWR( + severity == "significant_motion" + ? [ + "review/activity", + { + before: timeRange.end, + after: timeRange.start, + scale: SEGMENT_DURATION / 2, + }, + ] + : null, + ); + return (
);