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
This commit is contained in:
Josh Hawkins 2025-10-29 13:04:29 -05:00 committed by GitHub
parent 62bc2aeaab
commit 901002a0a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 284 additions and 144 deletions

View File

@ -13,6 +13,8 @@
}, },
"timeline": "Timeline", "timeline": "Timeline",
"timeline.aria": "Select timeline", "timeline.aria": "Select timeline",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"events": { "events": {
"label": "Events", "label": "Events",
"aria": "Select events", "aria": "Select events",

View File

@ -10,6 +10,7 @@ import {
ReviewSegment, ReviewSegment,
ReviewSeverity, ReviewSeverity,
TimelineZoomDirection, TimelineZoomDirection,
ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import ReviewTimeline from "./ReviewTimeline"; import ReviewTimeline from "./ReviewTimeline";
import { import {
@ -42,6 +43,9 @@ export type EventReviewTimelineProps = {
isZooming: boolean; isZooming: boolean;
zoomDirection: TimelineZoomDirection; zoomDirection: TimelineZoomDirection;
dense?: boolean; dense?: boolean;
onZoomChange?: (newZoomLevel: number) => void;
possibleZoomLevels?: ZoomLevel[];
currentZoomLevel?: number;
}; };
export function EventReviewTimeline({ export function EventReviewTimeline({
@ -69,6 +73,9 @@ export function EventReviewTimeline({
isZooming, isZooming,
zoomDirection, zoomDirection,
dense = false, dense = false,
onZoomChange,
possibleZoomLevels,
currentZoomLevel,
}: EventReviewTimelineProps) { }: EventReviewTimelineProps) {
const internalTimelineRef = useRef<HTMLDivElement>(null); const internalTimelineRef = useRef<HTMLDivElement>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef; const selectedTimelineRef = timelineRef || internalTimelineRef;
@ -157,6 +164,9 @@ export function EventReviewTimeline({
scrollToSegment={scrollToSegment} scrollToSegment={scrollToSegment}
isZooming={isZooming} isZooming={isZooming}
zoomDirection={zoomDirection} zoomDirection={zoomDirection}
onZoomChange={onZoomChange}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
> >
<VirtualizedEventSegments <VirtualizedEventSegments
ref={virtualizedSegmentsRef} ref={virtualizedSegmentsRef}

View File

@ -235,7 +235,7 @@ export function EventSegment({
<div className="flex w-[20px] flex-row justify-center md:w-[40px]"> <div className="flex w-[20px] flex-row justify-center md:w-[40px]">
<div className="flex justify-center"> <div className="flex justify-center">
<div <div
className="absolute left-1/2 z-10 ml-[2px] h-[8px] w-[8px] -translate-x-1/2 transform cursor-pointer" className="absolute left-1/2 z-10 ml-[2px] h-[8px] w-[8px] -translate-x-1/2 transform cursor-pointer md:ml-0"
data-severity={severityValue} data-severity={severityValue}
> >
<div <div

View File

@ -10,6 +10,7 @@ import {
MotionData, MotionData,
ReviewSegment, ReviewSegment,
TimelineZoomDirection, TimelineZoomDirection,
ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import ReviewTimeline from "./ReviewTimeline"; import ReviewTimeline from "./ReviewTimeline";
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils"; import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
@ -47,6 +48,9 @@ export type MotionReviewTimelineProps = {
isZooming: boolean; isZooming: boolean;
zoomDirection: TimelineZoomDirection; zoomDirection: TimelineZoomDirection;
alwaysShowMotionLine?: boolean; alwaysShowMotionLine?: boolean;
onZoomChange?: (newZoomLevel: number) => void;
possibleZoomLevels?: ZoomLevel[];
currentZoomLevel?: number;
}; };
export function MotionReviewTimeline({ export function MotionReviewTimeline({
@ -77,6 +81,9 @@ export function MotionReviewTimeline({
isZooming, isZooming,
zoomDirection, zoomDirection,
alwaysShowMotionLine = false, alwaysShowMotionLine = false,
onZoomChange,
possibleZoomLevels,
currentZoomLevel,
}: MotionReviewTimelineProps) { }: MotionReviewTimelineProps) {
const internalTimelineRef = useRef<HTMLDivElement>(null); const internalTimelineRef = useRef<HTMLDivElement>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef; const selectedTimelineRef = timelineRef || internalTimelineRef;
@ -206,6 +213,9 @@ export function MotionReviewTimeline({
isZooming={isZooming} isZooming={isZooming}
zoomDirection={zoomDirection} zoomDirection={zoomDirection}
getRecordingAvailability={getRecordingAvailability} getRecordingAvailability={getRecordingAvailability}
onZoomChange={onZoomChange}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
> >
<VirtualizedMotionSegments <VirtualizedMotionSegments
ref={virtualizedSegmentsRef} ref={virtualizedSegmentsRef}

View File

@ -2,7 +2,7 @@ import useDraggableElement from "@/hooks/use-draggable-element";
import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useTimelineUtils } from "@/hooks/use-timeline-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DraggableElement } from "@/types/draggable-element"; import { DraggableElement } from "@/types/draggable-element";
import { TimelineZoomDirection } from "@/types/review"; import { TimelineZoomDirection, ZoomLevel } from "@/types/review";
import { import {
ReactNode, ReactNode,
RefObject, RefObject,
@ -13,6 +13,11 @@ import {
useState, useState,
} from "react"; } from "react";
import { isIOS, isMobile } from "react-device-detect"; import { isIOS, isMobile } from "react-device-detect";
import { Button } from "../ui/button";
import { LuZoomIn, LuZoomOut } from "react-icons/lu";
import { useTranslation } from "react-i18next";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { TooltipPortal } from "@radix-ui/react-tooltip";
export type ReviewTimelineProps = { export type ReviewTimelineProps = {
timelineRef: RefObject<HTMLDivElement>; timelineRef: RefObject<HTMLDivElement>;
@ -37,6 +42,9 @@ export type ReviewTimelineProps = {
isZooming: boolean; isZooming: boolean;
zoomDirection: TimelineZoomDirection; zoomDirection: TimelineZoomDirection;
getRecordingAvailability?: (time: number) => boolean | undefined; getRecordingAvailability?: (time: number) => boolean | undefined;
onZoomChange?: (newZoomLevel: number) => void;
possibleZoomLevels?: ZoomLevel[];
currentZoomLevel?: number;
children: ReactNode; children: ReactNode;
}; };
@ -63,8 +71,12 @@ export function ReviewTimeline({
isZooming, isZooming,
zoomDirection, zoomDirection,
getRecordingAvailability, getRecordingAvailability,
onZoomChange,
possibleZoomLevels,
currentZoomLevel,
children, children,
}: ReviewTimelineProps) { }: ReviewTimelineProps) {
const { t } = useTranslation("views/events");
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false); const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
const [isDraggingExportStart, setIsDraggingExportStart] = useState(false); const [isDraggingExportStart, setIsDraggingExportStart] = useState(false);
const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false); const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false);
@ -78,6 +90,13 @@ export function ReviewTimeline({
const exportEndRef = useRef<HTMLDivElement>(null); const exportEndRef = useRef<HTMLDivElement>(null);
const exportEndTimeRef = useRef<HTMLDivElement>(null); const exportEndTimeRef = useRef<HTMLDivElement>(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( const isDragging = useMemo(
() => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd, () => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd,
[isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd], [isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd],
@ -348,148 +367,204 @@ export function ReviewTimeline({
}, [getRecordingAvailability, handlebarTime, segmentDuration]); }, [getRecordingAvailability, handlebarTime, segmentDuration]);
return ( return (
<div <>
ref={timelineRef} <div
className={cn( ref={timelineRef}
"no-scrollbar relative h-full select-none overflow-y-auto bg-secondary transition-all duration-500 ease-in-out", className={cn(
isZooming && zoomDirection === "in" && "animate-timeline-zoom-in", "no-scrollbar relative h-full select-none overflow-y-auto bg-secondary transition-all duration-500 ease-in-out",
isZooming && zoomDirection === "out" && "animate-timeline-zoom-out", isZooming && zoomDirection === "in" && "animate-timeline-zoom-in",
isDragging && (showHandlebar || showExportHandles) isZooming && zoomDirection === "out" && "animate-timeline-zoom-out",
? "cursor-grabbing" isDragging && (showHandlebar || showExportHandles)
: "cursor-auto", ? "cursor-grabbing"
)} : "cursor-auto",
> )}
<div ref={segmentsRef} className="relative flex flex-col"> >
<div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent"></div> <div ref={segmentsRef} className="relative flex flex-col">
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent"></div> <div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent"></div>
{children} <div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent"></div>
{children}
</div>
{children && (
<>
{showHandlebar && (
<div
className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={handlebarRef}
>
<div
className="flex touch-none select-none items-center justify-center"
onMouseDown={handleHandlebar}
onTouchStart={handleHandlebar}
>
<div
className={`relative w-full ${
isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`mx-auto rounded-full bg-destructive ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-[80px]"
: "w-20"
} h-5 ${isDraggingHandlebar && isMobile ? "fixed left-1/2 top-[18px] z-20 h-[30px] w-auto -translate-x-1/2 transform bg-destructive/80 px-3" : "static"} flex items-center justify-center`}
>
<div
ref={handlebarTimeRef}
className={`pointer-events-none text-white ${textSizeClasses("handlebar")} z-10`}
></div>
</div>
<div
className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 -translate-y-1/2 transform"}`}
></div>
</div>
</div>
{/* TODO: determine if we should keep this tooltip */}
{false && isHandlebarInNoRecordingPeriod && (
<div className="absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 rounded-md bg-destructive/80 px-4 py-1 text-center text-xs text-white shadow-lg">
No recordings
</div>
)}
</div>
)}
{showExportHandles && (
<>
<div
className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportEndRef}
>
<div
className="flex touch-none select-none items-center justify-center"
onMouseDown={handleExportEnd}
onTouchStart={handleExportEnd}
>
<div
className={`relative mt-[6.5px] w-full ${
isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`mx-auto -mt-4 bg-selected ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-[80px]"
: "w-20"
} h-5 ${isDraggingExportEnd && isMobile ? "fixed left-1/2 top-[18px] z-20 mt-0 h-[30px] w-auto -translate-x-1/2 transform rounded-full bg-selected/80 px-3" : "static rounded-tl-lg rounded-tr-lg"} flex items-center justify-center`}
>
<div
ref={exportEndTimeRef}
className={`pointer-events-none text-white ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`}
></div>
</div>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 -translate-y-1/2 transform"}`}
></div>
</div>
</div>
</div>
<div
ref={exportSectionRef}
className="absolute w-full bg-selected/50"
></div>
<div
className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportStartRef}
>
<div
className="flex touch-none select-none items-center justify-center"
onMouseDown={handleExportStart}
onTouchStart={handleExportStart}
>
<div
className={`relative -mt-[6.5px] w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 -translate-y-1/2 transform"}`}
></div>
<div
className={`mx-auto mt-4 bg-selected ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-[80px]"
: "w-20"
} h-5 ${isDraggingExportStart && isMobile ? "fixed left-1/2 top-[4px] z-20 mt-0 h-[30px] w-auto -translate-x-1/2 transform rounded-full bg-selected/80 px-3" : "static rounded-bl-lg rounded-br-lg"} flex items-center justify-center`}
>
<div
ref={exportStartTimeRef}
className={`pointer-events-none text-white ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
></div>
</div>
</div>
</div>
</div>
</>
)}
</>
)}
</div> </div>
{children && (
<> {onZoomChange && currentZoomLevelIndex !== -1 && (
{showHandlebar && ( <div
<div className={`absolute z-30 flex gap-2 ${
className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`} isMobile
role="scrollbar" ? "bottom-4 right-1 flex-col gap-3"
ref={handlebarRef} : "bottom-2 left-1/2 -translate-x-1/2"
> }`}
<div >
className="flex touch-none select-none items-center justify-center" <Tooltip>
onMouseDown={handleHandlebar} <TooltipTrigger asChild>
onTouchStart={handleHandlebar} <Button
onClick={(e) => {
const newLevel = Math.max(0, currentZoomLevelIndex - 1);
onZoomChange(newLevel);
e.currentTarget.blur();
}}
variant="outline"
disabled={currentZoomLevelIndex === 0}
className="bg-background_alt p-3 hover:bg-accent hover:text-accent-foreground active:scale-95 [@media(hover:none)]:hover:bg-background_alt"
type="button"
> >
<div <LuZoomOut className={cn("size-5 text-primary-variant")} />
className={`relative w-full ${ </Button>
isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab" </TooltipTrigger>
}`} <TooltipPortal>
> <TooltipContent>{t("zoomIn")}</TooltipContent>
<div </TooltipPortal>
className={`mx-auto rounded-full bg-destructive ${ </Tooltip>
dense <Tooltip>
? "w-12 md:w-20" <TooltipTrigger asChild>
: segmentDuration < 60 <Button
? "w-[80px]" onClick={(e) => {
: "w-20" const newLevel = Math.min(
} h-5 ${isDraggingHandlebar && isMobile ? "fixed left-1/2 top-[18px] z-20 h-[30px] w-auto -translate-x-1/2 transform bg-destructive/80 px-3" : "static"} flex items-center justify-center`} zoomLevels.length - 1,
> currentZoomLevelIndex + 1,
<div );
ref={handlebarTimeRef} onZoomChange(newLevel);
className={`pointer-events-none text-white ${textSizeClasses("handlebar")} z-10`} e.currentTarget.blur();
></div> }}
</div> variant="outline"
<div disabled={currentZoomLevelIndex === zoomLevels.length - 1}
className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 -translate-y-1/2 transform"}`} className="bg-background_alt p-3 hover:bg-accent hover:text-accent-foreground active:scale-95 [@media(hover:none)]:hover:bg-background_alt"
></div> type="button"
</div>
</div>
{/* TODO: determine if we should keep this tooltip */}
{false && isHandlebarInNoRecordingPeriod && (
<div className="absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 rounded-md bg-destructive/80 px-4 py-1 text-center text-xs text-white shadow-lg">
No recordings
</div>
)}
</div>
)}
{showExportHandles && (
<>
<div
className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportEndRef}
> >
<div <LuZoomIn className={cn("size-5 text-primary-variant")} />
className="flex touch-none select-none items-center justify-center" </Button>
onMouseDown={handleExportEnd} </TooltipTrigger>
onTouchStart={handleExportEnd} <TooltipPortal>
> <TooltipContent>{t("zoomOut")}</TooltipContent>
<div </TooltipPortal>
className={`relative mt-[6.5px] w-full ${ </Tooltip>
isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab" </div>
}`}
>
<div
className={`mx-auto -mt-4 bg-selected ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-[80px]"
: "w-20"
} h-5 ${isDraggingExportEnd && isMobile ? "fixed left-1/2 top-[18px] z-20 mt-0 h-[30px] w-auto -translate-x-1/2 transform rounded-full bg-selected/80 px-3" : "static rounded-tl-lg rounded-tr-lg"} flex items-center justify-center`}
>
<div
ref={exportEndTimeRef}
className={`pointer-events-none text-white ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`}
></div>
</div>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 -translate-y-1/2 transform"}`}
></div>
</div>
</div>
</div>
<div
ref={exportSectionRef}
className="absolute w-full bg-selected/50"
></div>
<div
className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportStartRef}
>
<div
className="flex touch-none select-none items-center justify-center"
onMouseDown={handleExportStart}
onTouchStart={handleExportStart}
>
<div
className={`relative -mt-[6.5px] w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 -translate-y-1/2 transform"}`}
></div>
<div
className={`mx-auto mt-4 bg-selected ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-[80px]"
: "w-20"
} h-5 ${isDraggingExportStart && isMobile ? "fixed left-1/2 top-[4px] z-20 mt-0 h-[30px] w-auto -translate-x-1/2 transform rounded-full bg-selected/80 px-3" : "static rounded-bl-lg rounded-br-lg"} flex items-center justify-center`}
>
<div
ref={exportStartTimeRef}
className={`pointer-events-none text-white ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
></div>
</div>
</div>
</div>
</div>
</>
)}
</>
)} )}
</div> </>
); );
} }

View File

@ -9,6 +9,7 @@ import {
ReviewData, ReviewData,
ReviewSegment, ReviewSegment,
ReviewSeverity, ReviewSeverity,
ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import CameraActivityIndicator from "@/components/indicators/CameraActivityIndicator"; import CameraActivityIndicator from "@/components/indicators/CameraActivityIndicator";
@ -180,7 +181,7 @@ function UIPlayground() {
timestampSpread: 15, timestampSpread: 15,
}); });
const possibleZoomLevels = useMemo( const possibleZoomLevels: ZoomLevel[] = useMemo(
() => [ () => [
{ segmentDuration: 60, timestampSpread: 15 }, { segmentDuration: 60, timestampSpread: 15 },
{ segmentDuration: 30, timestampSpread: 5 }, { segmentDuration: 30, timestampSpread: 5 },
@ -196,6 +197,14 @@ function UIPlayground() {
[possibleZoomLevels], [possibleZoomLevels],
); );
const currentZoomLevel = useMemo(
() =>
possibleZoomLevels.findIndex(
(level) => level.segmentDuration === zoomSettings.segmentDuration,
),
[possibleZoomLevels, zoomSettings.segmentDuration],
);
const { zoomLevel, handleZoom, isZooming, zoomDirection } = useTimelineZoom({ const { zoomLevel, handleZoom, isZooming, zoomDirection } = useTimelineZoom({
zoomSettings, zoomSettings,
zoomLevels: possibleZoomLevels, zoomLevels: possibleZoomLevels,
@ -414,6 +423,9 @@ function UIPlayground() {
dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps
isZooming={isZooming} // is the timeline actively zooming? isZooming={isZooming} // is the timeline actively zooming?
zoomDirection={zoomDirection} // is the timeline zooming in or out zoomDirection={zoomDirection} // is the timeline zooming in or out
onZoomChange={handleZoomChange}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
/> />
)} )}
{isEventsReviewTimeline && ( {isEventsReviewTimeline && (
@ -441,6 +453,9 @@ function UIPlayground() {
dense // dense will produce a smaller handlebar and only minute resolution on timestamps dense // dense will produce a smaller handlebar and only minute resolution on timestamps
isZooming={isZooming} // is the timeline actively zooming? isZooming={isZooming} // is the timeline actively zooming?
zoomDirection={zoomDirection} // is the timeline zooming in or out zoomDirection={zoomDirection} // is the timeline zooming in or out
onZoomChange={handleZoomChange}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
/> />
)} )}
</div> </div>

View File

@ -81,6 +81,11 @@ export type ConsolidatedSegmentData = {
export type TimelineZoomDirection = "in" | "out" | null; export type TimelineZoomDirection = "in" | "out" | null;
export type ZoomLevel = {
segmentDuration: number;
timestampSpread: number;
};
export enum ThreatLevel { export enum ThreatLevel {
SUSPICIOUS = 1, SUSPICIOUS = 1,
DANGER = 2, DANGER = 2,

View File

@ -19,6 +19,7 @@ import {
ReviewSeverity, ReviewSeverity,
ReviewSummary, ReviewSummary,
SegmentedReviewData, SegmentedReviewData,
ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import { getChunkedTimeRange } from "@/utils/timelineUtil"; import { getChunkedTimeRange } from "@/utils/timelineUtil";
import axios from "axios"; import axios from "axios";
@ -501,7 +502,7 @@ function DetectionReview({
timestampSpread: 15, timestampSpread: 15,
}); });
const possibleZoomLevels = useMemo( const possibleZoomLevels: ZoomLevel[] = useMemo(
() => [ () => [
{ segmentDuration: 60, timestampSpread: 15 }, { segmentDuration: 60, timestampSpread: 15 },
{ segmentDuration: 30, timestampSpread: 5 }, { segmentDuration: 30, timestampSpread: 5 },
@ -517,6 +518,14 @@ function DetectionReview({
[possibleZoomLevels], [possibleZoomLevels],
); );
const currentZoomLevel = useMemo(
() =>
possibleZoomLevels.findIndex(
(level) => level.segmentDuration === zoomSettings.segmentDuration,
),
[possibleZoomLevels, zoomSettings.segmentDuration],
);
const { isZooming, zoomDirection } = useTimelineZoom({ const { isZooming, zoomDirection } = useTimelineZoom({
zoomSettings, zoomSettings,
zoomLevels: possibleZoomLevels, zoomLevels: possibleZoomLevels,
@ -799,7 +808,7 @@ function DetectionReview({
</div> </div>
</div> </div>
<div className="flex w-[65px] flex-row md:w-[110px]"> <div className="flex w-[65px] flex-row md:w-[110px]">
<div className="no-scrollbar w-[55px] md:w-[100px]"> <div className="no-scrollbar relative w-[55px] md:w-[100px]">
{loading ? ( {loading ? (
<Skeleton className="size-full" /> <Skeleton className="size-full" />
) : ( ) : (
@ -821,6 +830,8 @@ function DetectionReview({
dense={isMobile} dense={isMobile}
isZooming={isZooming} isZooming={isZooming}
zoomDirection={zoomDirection} zoomDirection={zoomDirection}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
/> />
)} )}
</div> </div>

View File

@ -22,6 +22,7 @@ import {
ReviewFilter, ReviewFilter,
ReviewSegment, ReviewSegment,
ReviewSummary, ReviewSummary,
ZoomLevel,
} from "@/types/review"; } from "@/types/review";
import { getChunkedTimeDay } from "@/utils/timelineUtil"; import { getChunkedTimeDay } from "@/utils/timelineUtil";
import { import {
@ -884,7 +885,7 @@ function Timeline({
timestampSpread: 15, timestampSpread: 15,
}); });
const possibleZoomLevels = useMemo( const possibleZoomLevels: ZoomLevel[] = useMemo(
() => [ () => [
{ segmentDuration: 30, timestampSpread: 15 }, { segmentDuration: 30, timestampSpread: 15 },
{ segmentDuration: 15, timestampSpread: 5 }, { segmentDuration: 15, timestampSpread: 5 },
@ -900,6 +901,14 @@ function Timeline({
[possibleZoomLevels], [possibleZoomLevels],
); );
const currentZoomLevel = useMemo(
() =>
possibleZoomLevels.findIndex(
(level) => level.segmentDuration === zoomSettings.segmentDuration,
),
[possibleZoomLevels, zoomSettings.segmentDuration],
);
const { isZooming, zoomDirection } = useTimelineZoom({ const { isZooming, zoomDirection } = useTimelineZoom({
zoomSettings, zoomSettings,
zoomLevels: possibleZoomLevels, zoomLevels: possibleZoomLevels,
@ -1010,6 +1019,9 @@ function Timeline({
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)} onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
isZooming={isZooming} isZooming={isZooming}
zoomDirection={zoomDirection} zoomDirection={zoomDirection}
onZoomChange={handleZoomChange}
possibleZoomLevels={possibleZoomLevels}
currentZoomLevel={currentZoomLevel}
/> />
) : ( ) : (
<Skeleton className="size-full" /> <Skeleton className="size-full" />