From 901002a0a5abc16d46c5207e4d3150fd2bccd7fa Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:04:29 -0500 Subject: [PATCH] Add zoom icons to timeline (#20717) * add icons to zoom timeline * fix current zoom level handling * ensure mobile buttons don't stay selected * remove icons on event review timeline * add tooltips --- web/public/locales/en/views/events.json | 2 + .../timeline/EventReviewTimeline.tsx | 10 + web/src/components/timeline/EventSegment.tsx | 2 +- .../timeline/MotionReviewTimeline.tsx | 10 + .../components/timeline/ReviewTimeline.tsx | 353 +++++++++++------- web/src/pages/UIPlayground.tsx | 17 +- web/src/types/review.ts | 5 + web/src/views/events/EventView.tsx | 15 +- web/src/views/recording/RecordingView.tsx | 14 +- 9 files changed, 284 insertions(+), 144 deletions(-) diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index c393a8bc8..333fca397 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -13,6 +13,8 @@ }, "timeline": "Timeline", "timeline.aria": "Select timeline", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", "events": { "label": "Events", "aria": "Select events", diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index 27f318edb..89e73c21d 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -10,6 +10,7 @@ import { ReviewSegment, ReviewSeverity, TimelineZoomDirection, + ZoomLevel, } from "@/types/review"; import ReviewTimeline from "./ReviewTimeline"; import { @@ -42,6 +43,9 @@ export type EventReviewTimelineProps = { isZooming: boolean; zoomDirection: TimelineZoomDirection; dense?: boolean; + onZoomChange?: (newZoomLevel: number) => void; + possibleZoomLevels?: ZoomLevel[]; + currentZoomLevel?: number; }; export function EventReviewTimeline({ @@ -69,6 +73,9 @@ export function EventReviewTimeline({ isZooming, zoomDirection, dense = false, + onZoomChange, + possibleZoomLevels, + currentZoomLevel, }: EventReviewTimelineProps) { const internalTimelineRef = useRef(null); const selectedTimelineRef = timelineRef || internalTimelineRef; @@ -157,6 +164,9 @@ export function EventReviewTimeline({ scrollToSegment={scrollToSegment} isZooming={isZooming} zoomDirection={zoomDirection} + onZoomChange={onZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} >
void; + possibleZoomLevels?: ZoomLevel[]; + currentZoomLevel?: number; }; export function MotionReviewTimeline({ @@ -77,6 +81,9 @@ export function MotionReviewTimeline({ isZooming, zoomDirection, alwaysShowMotionLine = false, + onZoomChange, + possibleZoomLevels, + currentZoomLevel, }: MotionReviewTimelineProps) { const internalTimelineRef = useRef(null); const selectedTimelineRef = timelineRef || internalTimelineRef; @@ -206,6 +213,9 @@ export function MotionReviewTimeline({ isZooming={isZooming} zoomDirection={zoomDirection} getRecordingAvailability={getRecordingAvailability} + onZoomChange={onZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} > ; @@ -37,6 +42,9 @@ export type ReviewTimelineProps = { isZooming: boolean; zoomDirection: TimelineZoomDirection; getRecordingAvailability?: (time: number) => boolean | undefined; + onZoomChange?: (newZoomLevel: number) => void; + possibleZoomLevels?: ZoomLevel[]; + currentZoomLevel?: number; children: ReactNode; }; @@ -63,8 +71,12 @@ export function ReviewTimeline({ isZooming, zoomDirection, getRecordingAvailability, + onZoomChange, + possibleZoomLevels, + currentZoomLevel, children, }: ReviewTimelineProps) { + const { t } = useTranslation("views/events"); const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false); const [isDraggingExportStart, setIsDraggingExportStart] = useState(false); const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false); @@ -78,6 +90,13 @@ export function ReviewTimeline({ const exportEndRef = useRef(null); const exportEndTimeRef = useRef(null); + // Use provided zoom levels or fallback to empty array + const zoomLevels = possibleZoomLevels ?? []; + + const currentZoomLevelIndex = + currentZoomLevel ?? + zoomLevels.findIndex((level) => level.segmentDuration === segmentDuration); + const isDragging = useMemo( () => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd, [isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd], @@ -348,148 +367,204 @@ export function ReviewTimeline({ }, [getRecordingAvailability, handlebarTime, segmentDuration]); return ( -
-
-
-
- {children} + <> +
+
+
+
+ {children} +
+ {children && ( + <> + {showHandlebar && ( +
+
+
+
+
+
+
+
+
+ {/* TODO: determine if we should keep this tooltip */} + {false && isHandlebarInNoRecordingPeriod && ( +
+ No recordings +
+ )} +
+ )} + {showExportHandles && ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + )} + + )}
- {children && ( - <> - {showHandlebar && ( -
-
+ + +
- {/* TODO: determine if we should keep this tooltip */} - {false && isHandlebarInNoRecordingPeriod && ( -
- No recordings -
- )} -
- )} - {showExportHandles && ( - <> -
+ + + + {t("zoomIn")} + + + + +
-
-
-
-
-
-
-
-
-
-
-
- - )} - + + + + + {t("zoomOut")} + + +
)} -
+ ); } diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 95599d594..f17a7d700 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -9,6 +9,7 @@ import { ReviewData, ReviewSegment, ReviewSeverity, + ZoomLevel, } from "@/types/review"; import { Button } from "@/components/ui/button"; import CameraActivityIndicator from "@/components/indicators/CameraActivityIndicator"; @@ -180,7 +181,7 @@ function UIPlayground() { timestampSpread: 15, }); - const possibleZoomLevels = useMemo( + const possibleZoomLevels: ZoomLevel[] = useMemo( () => [ { segmentDuration: 60, timestampSpread: 15 }, { segmentDuration: 30, timestampSpread: 5 }, @@ -196,6 +197,14 @@ function UIPlayground() { [possibleZoomLevels], ); + const currentZoomLevel = useMemo( + () => + possibleZoomLevels.findIndex( + (level) => level.segmentDuration === zoomSettings.segmentDuration, + ), + [possibleZoomLevels, zoomSettings.segmentDuration], + ); + const { zoomLevel, handleZoom, isZooming, zoomDirection } = useTimelineZoom({ zoomSettings, zoomLevels: possibleZoomLevels, @@ -414,6 +423,9 @@ function UIPlayground() { dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps isZooming={isZooming} // is the timeline actively zooming? zoomDirection={zoomDirection} // is the timeline zooming in or out + onZoomChange={handleZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} /> )} {isEventsReviewTimeline && ( @@ -441,6 +453,9 @@ function UIPlayground() { dense // dense will produce a smaller handlebar and only minute resolution on timestamps isZooming={isZooming} // is the timeline actively zooming? zoomDirection={zoomDirection} // is the timeline zooming in or out + onZoomChange={handleZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} /> )}
diff --git a/web/src/types/review.ts b/web/src/types/review.ts index cd1aefff5..6c9027950 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -81,6 +81,11 @@ export type ConsolidatedSegmentData = { export type TimelineZoomDirection = "in" | "out" | null; +export type ZoomLevel = { + segmentDuration: number; + timestampSpread: number; +}; + export enum ThreatLevel { SUSPICIOUS = 1, DANGER = 2, diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 2737b7be0..9b4b0bdab 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -19,6 +19,7 @@ import { ReviewSeverity, ReviewSummary, SegmentedReviewData, + ZoomLevel, } from "@/types/review"; import { getChunkedTimeRange } from "@/utils/timelineUtil"; import axios from "axios"; @@ -501,7 +502,7 @@ function DetectionReview({ timestampSpread: 15, }); - const possibleZoomLevels = useMemo( + const possibleZoomLevels: ZoomLevel[] = useMemo( () => [ { segmentDuration: 60, timestampSpread: 15 }, { segmentDuration: 30, timestampSpread: 5 }, @@ -517,6 +518,14 @@ function DetectionReview({ [possibleZoomLevels], ); + const currentZoomLevel = useMemo( + () => + possibleZoomLevels.findIndex( + (level) => level.segmentDuration === zoomSettings.segmentDuration, + ), + [possibleZoomLevels, zoomSettings.segmentDuration], + ); + const { isZooming, zoomDirection } = useTimelineZoom({ zoomSettings, zoomLevels: possibleZoomLevels, @@ -799,7 +808,7 @@ function DetectionReview({
-
+
{loading ? ( ) : ( @@ -821,6 +830,8 @@ function DetectionReview({ dense={isMobile} isZooming={isZooming} zoomDirection={zoomDirection} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} /> )}
diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index 2d3600288..00e46411e 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -22,6 +22,7 @@ import { ReviewFilter, ReviewSegment, ReviewSummary, + ZoomLevel, } from "@/types/review"; import { getChunkedTimeDay } from "@/utils/timelineUtil"; import { @@ -884,7 +885,7 @@ function Timeline({ timestampSpread: 15, }); - const possibleZoomLevels = useMemo( + const possibleZoomLevels: ZoomLevel[] = useMemo( () => [ { segmentDuration: 30, timestampSpread: 15 }, { segmentDuration: 15, timestampSpread: 5 }, @@ -900,6 +901,14 @@ function Timeline({ [possibleZoomLevels], ); + const currentZoomLevel = useMemo( + () => + possibleZoomLevels.findIndex( + (level) => level.segmentDuration === zoomSettings.segmentDuration, + ), + [possibleZoomLevels, zoomSettings.segmentDuration], + ); + const { isZooming, zoomDirection } = useTimelineZoom({ zoomSettings, zoomLevels: possibleZoomLevels, @@ -1010,6 +1019,9 @@ function Timeline({ onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)} isZooming={isZooming} zoomDirection={zoomDirection} + onZoomChange={handleZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} /> ) : (