mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
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:
parent
62bc2aeaab
commit
901002a0a5
@ -13,6 +13,8 @@
|
||||
},
|
||||
"timeline": "Timeline",
|
||||
"timeline.aria": "Select timeline",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"events": {
|
||||
"label": "Events",
|
||||
"aria": "Select events",
|
||||
|
||||
@ -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<HTMLDivElement>(null);
|
||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||
@ -157,6 +164,9 @@ export function EventReviewTimeline({
|
||||
scrollToSegment={scrollToSegment}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
onZoomChange={onZoomChange}
|
||||
possibleZoomLevels={possibleZoomLevels}
|
||||
currentZoomLevel={currentZoomLevel}
|
||||
>
|
||||
<VirtualizedEventSegments
|
||||
ref={virtualizedSegmentsRef}
|
||||
|
||||
@ -235,7 +235,7 @@ export function EventSegment({
|
||||
<div className="flex w-[20px] flex-row justify-center md:w-[40px]">
|
||||
<div className="flex justify-center">
|
||||
<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}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
MotionData,
|
||||
ReviewSegment,
|
||||
TimelineZoomDirection,
|
||||
ZoomLevel,
|
||||
} from "@/types/review";
|
||||
import ReviewTimeline from "./ReviewTimeline";
|
||||
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
|
||||
@ -47,6 +48,9 @@ export type MotionReviewTimelineProps = {
|
||||
isZooming: boolean;
|
||||
zoomDirection: TimelineZoomDirection;
|
||||
alwaysShowMotionLine?: boolean;
|
||||
onZoomChange?: (newZoomLevel: number) => 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<HTMLDivElement>(null);
|
||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||
@ -206,6 +213,9 @@ export function MotionReviewTimeline({
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
getRecordingAvailability={getRecordingAvailability}
|
||||
onZoomChange={onZoomChange}
|
||||
possibleZoomLevels={possibleZoomLevels}
|
||||
currentZoomLevel={currentZoomLevel}
|
||||
>
|
||||
<VirtualizedMotionSegments
|
||||
ref={virtualizedSegmentsRef}
|
||||
|
||||
@ -2,7 +2,7 @@ import useDraggableElement from "@/hooks/use-draggable-element";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DraggableElement } from "@/types/draggable-element";
|
||||
import { TimelineZoomDirection } from "@/types/review";
|
||||
import { TimelineZoomDirection, ZoomLevel } from "@/types/review";
|
||||
import {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
@ -13,6 +13,11 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
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 = {
|
||||
timelineRef: RefObject<HTMLDivElement>;
|
||||
@ -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<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(
|
||||
() => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd,
|
||||
[isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd],
|
||||
@ -348,148 +367,204 @@ export function ReviewTimeline({
|
||||
}, [getRecordingAvailability, handlebarTime, segmentDuration]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={timelineRef}
|
||||
className={cn(
|
||||
"no-scrollbar relative h-full select-none overflow-y-auto bg-secondary transition-all duration-500 ease-in-out",
|
||||
isZooming && zoomDirection === "in" && "animate-timeline-zoom-in",
|
||||
isZooming && zoomDirection === "out" && "animate-timeline-zoom-out",
|
||||
isDragging && (showHandlebar || showExportHandles)
|
||||
? "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 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
|
||||
ref={timelineRef}
|
||||
className={cn(
|
||||
"no-scrollbar relative h-full select-none overflow-y-auto bg-secondary transition-all duration-500 ease-in-out",
|
||||
isZooming && zoomDirection === "in" && "animate-timeline-zoom-in",
|
||||
isZooming && zoomDirection === "out" && "animate-timeline-zoom-out",
|
||||
isDragging && (showHandlebar || showExportHandles)
|
||||
? "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 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>
|
||||
{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}
|
||||
|
||||
{onZoomChange && currentZoomLevelIndex !== -1 && (
|
||||
<div
|
||||
className={`absolute z-30 flex gap-2 ${
|
||||
isMobile
|
||||
? "bottom-4 right-1 flex-col gap-3"
|
||||
: "bottom-2 left-1/2 -translate-x-1/2"
|
||||
}`}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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
|
||||
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}
|
||||
<LuZoomOut className={cn("size-5 text-primary-variant")} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t("zoomIn")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
const newLevel = Math.min(
|
||||
zoomLevels.length - 1,
|
||||
currentZoomLevelIndex + 1,
|
||||
);
|
||||
onZoomChange(newLevel);
|
||||
e.currentTarget.blur();
|
||||
}}
|
||||
variant="outline"
|
||||
disabled={currentZoomLevelIndex === zoomLevels.length - 1}
|
||||
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
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<LuZoomIn className={cn("size-5 text-primary-variant")} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t("zoomOut")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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({
|
||||
</div>
|
||||
</div>
|
||||
<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 ? (
|
||||
<Skeleton className="size-full" />
|
||||
) : (
|
||||
@ -821,6 +830,8 @@ function DetectionReview({
|
||||
dense={isMobile}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
possibleZoomLevels={possibleZoomLevels}
|
||||
currentZoomLevel={currentZoomLevel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="size-full" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user