diff --git a/web/src/components/filter/FilterCheckBox.tsx b/web/src/components/filter/FilterCheckBox.tsx index b23a4e42c..3e8ee21cd 100644 --- a/web/src/components/filter/FilterCheckBox.tsx +++ b/web/src/components/filter/FilterCheckBox.tsx @@ -5,6 +5,7 @@ import { IconType } from "react-icons"; type FilterCheckBoxProps = { label: string; CheckIcon?: IconType; + iconClassName?: string; isChecked: boolean; onCheckedChange: (isChecked: boolean) => void; }; @@ -12,6 +13,7 @@ type FilterCheckBoxProps = { export default function FilterCheckBox({ label, CheckIcon = LuCheck, + iconClassName = "size-6", isChecked, onCheckedChange, }: FilterCheckBoxProps) { @@ -22,9 +24,9 @@ export default function FilterCheckBox({ onClick={() => onCheckedChange(!isChecked)} > {isChecked ? ( - + ) : ( -
+
)}
{label}
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 12434f67e..893fbacc6 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -280,7 +280,7 @@ type CalendarFilterButtonProps = { day?: Date; updateSelectedDay: (day?: Date) => void; }; -function CalendarFilterButton({ +export function CalendarFilterButton({ reviewSummary, day, updateSelectedDay, diff --git a/web/src/components/image/AnimatedEventThumbnail.tsx b/web/src/components/image/AnimatedEventThumbnail.tsx index b990de5b7..dad85116a 100644 --- a/web/src/components/image/AnimatedEventThumbnail.tsx +++ b/web/src/components/image/AnimatedEventThumbnail.tsx @@ -7,6 +7,7 @@ import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewSegment } from "@/types/review"; import { useNavigate } from "react-router-dom"; import { Skeleton } from "../ui/skeleton"; +import { RecordingStartingPoint } from "@/types/record"; type AnimatedEventThumbnailProps = { event: ReviewSegment; @@ -18,7 +19,13 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { const navigate = useNavigate(); const onOpenReview = useCallback(() => { - navigate("events", { state: { review: event.id } }); + navigate("events", { + state: { + camera: event.camera, + startTime: event.start_time, + severity: event.severity, + } as RecordingStartingPoint, + }); }, [navigate, event]); // image behavior diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 74652b57c..0ddecdfea 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -36,11 +36,18 @@ export default function Events() { const [reviewFilter, setReviewFilter, reviewSearchParams] = useApiFilter(); - const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { - setReviewFilter(newFilter); - // we don't want this updating - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const onUpdateFilter = useCallback( + (newFilter: ReviewFilter) => { + setReviewFilter(newFilter); + + // update recording start time if filter + // was changed on recording page + if (recording != undefined && newFilter.after != undefined) { + setRecording({ ...recording, startTime: newFilter.after }, true); + } + }, + [recording, setRecording, setReviewFilter], + ); // review paging @@ -286,10 +293,8 @@ export default function Events() { return { camera: recording.camera, - severity: recording.severity, start_time: recording.startTime, allCameras: allCameras, - cameraSegments: reviews.filter((seg) => allCameras.includes(seg.camera)), }; // previews will not update after item is selected @@ -306,9 +311,11 @@ export default function Events() { startCamera={selectedReviewData.camera} startTime={selectedReviewData.start_time} allCameras={selectedReviewData.allCameras} - severity={selectedReviewData.severity} - reviewItems={selectedReviewData.cameraSegments} + reviewItems={reviews} + reviewSummary={reviewSummary} allPreviews={allPreviews} + filter={reviewFilter} + updateFilter={onUpdateFilter} /> ); } else { diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 4f167400b..2c5b3fa61 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -1,24 +1,26 @@ +import FilterCheckBox from "@/components/filter/FilterCheckBox"; +import { CalendarFilterButton } from "@/components/filter/ReviewFilterGroup"; import PreviewPlayer, { PreviewController, } from "@/components/player/PreviewPlayer"; import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController"; import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer"; -import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { FrigateConfig } from "@/types/frigateConfig"; import { Preview } from "@/types/preview"; -import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; +import { + MotionData, + ReviewFilter, + ReviewSegment, + ReviewSummary, +} from "@/types/review"; +import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { getChunkedTimeDay } from "@/utils/timelineUtil"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; +import { FaCircle, FaVideo } from "react-icons/fa"; import { IoMdArrowRoundBack } from "react-icons/io"; import { useNavigate } from "react-router-dom"; import useSWR from "swr"; @@ -28,18 +30,22 @@ const SEGMENT_DURATION = 30; type RecordingViewProps = { startCamera: string; startTime: number; - severity: ReviewSeverity; - reviewItems: ReviewSegment[]; + reviewItems?: ReviewSegment[]; + reviewSummary?: ReviewSummary; allCameras: string[]; allPreviews?: Preview[]; + filter?: ReviewFilter; + updateFilter: (newFilter: ReviewFilter) => void; }; export function RecordingView({ startCamera, startTime, - severity, reviewItems, + reviewSummary, allCameras, allPreviews, + filter, + updateFilter, }: RecordingViewProps) { const { data: config } = useSWR("config"); const navigate = useNavigate(); @@ -54,7 +60,7 @@ export function RecordingView({ const [playbackStart, setPlaybackStart] = useState(startTime); const mainCameraReviewItems = useMemo( - () => reviewItems.filter((cam) => cam.camera == mainCamera), + () => reviewItems?.filter((cam) => cam.camera == mainCamera) ?? [], [reviewItems, mainCamera], ); @@ -157,19 +163,15 @@ export function RecordingView({ // motion timeline data - const { data: motionData } = useSWR( - severity == "significant_motion" - ? [ - "review/activity/motion", - { - before: timeRange.end, - after: timeRange.start, - scale: SEGMENT_DURATION / 2, - cameras: mainCamera, - }, - ] - : null, - ); + const { data: motionData } = useSWR([ + "review/activity/motion", + { + before: timeRange.end, + after: timeRange.start, + scale: SEGMENT_DURATION / 2, + cameras: mainCamera, + }, + ]); const mainCameraAspect = useMemo(() => { if (!config) { @@ -201,41 +203,61 @@ export function RecordingView({ return (
- - {isMobile && ( - - - - - - { - setPlaybackStart(currentTime); - setMainCamera(cam); - }} - > - {allCameras.map((cam) => ( - navigate(-1)}> + + Back + +
+ { + updateFilter({ + ...filter, + after: day == undefined ? undefined : day.getTime() / 1000, + before: + day == undefined ? undefined : getEndOfDayTimestamp(day), + }); + }} + /> + {isMobile && ( + + + + + + {allCameras.map((cam) => ( + { + setPlaybackStart(currentTime); + setMainCamera(cam); + }} + /> + ))} + + + )} +
+
- {severity != "significant_motion" ? ( - setScrubbing(scrubbing)} - /> - ) : ( - setScrubbing(scrubbing)} - /> - )} + setScrubbing(scrubbing)} + />