mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
add icons to zoom timeline
This commit is contained in:
parent
62bc2aeaab
commit
3d1bfc1fa2
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,8 @@ 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";
|
||||||
|
|
||||||
export type ReviewTimelineProps = {
|
export type ReviewTimelineProps = {
|
||||||
timelineRef: RefObject<HTMLDivElement>;
|
timelineRef: RefObject<HTMLDivElement>;
|
||||||
@ -37,6 +39,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,6 +68,9 @@ export function ReviewTimeline({
|
|||||||
isZooming,
|
isZooming,
|
||||||
zoomDirection,
|
zoomDirection,
|
||||||
getRecordingAvailability,
|
getRecordingAvailability,
|
||||||
|
onZoomChange,
|
||||||
|
possibleZoomLevels,
|
||||||
|
currentZoomLevel,
|
||||||
children,
|
children,
|
||||||
}: ReviewTimelineProps) {
|
}: ReviewTimelineProps) {
|
||||||
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
|
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
|
||||||
@ -78,6 +86,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,6 +363,7 @@ export function ReviewTimeline({
|
|||||||
}, [getRecordingAvailability, handlebarTime, segmentDuration]);
|
}, [getRecordingAvailability, handlebarTime, segmentDuration]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
ref={timelineRef}
|
ref={timelineRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -490,6 +506,45 @@ export function ReviewTimeline({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{onZoomChange && currentZoomLevelIndex !== -1 && (
|
||||||
|
<div
|
||||||
|
className={`absolute z-30 flex gap-2 ${
|
||||||
|
isMobile
|
||||||
|
? "bottom-4 right-2 flex-col gap-4"
|
||||||
|
: "bottom-2 left-1/2 -translate-x-1/2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const newLevel = Math.max(0, currentZoomLevelIndex - 1);
|
||||||
|
onZoomChange(newLevel);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
disabled={currentZoomLevelIndex === 0}
|
||||||
|
className="p-3"
|
||||||
|
title="Zoom out"
|
||||||
|
>
|
||||||
|
<LuZoomOut className={isMobile ? "size-5" : "size-4"} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const newLevel = Math.min(
|
||||||
|
zoomLevels.length - 1,
|
||||||
|
currentZoomLevelIndex + 1,
|
||||||
|
);
|
||||||
|
onZoomChange(newLevel);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
disabled={currentZoomLevelIndex === zoomLevels.length - 1}
|
||||||
|
className="p-3"
|
||||||
|
title="Zoom in"
|
||||||
|
>
|
||||||
|
<LuZoomIn className={isMobile ? "size-5" : "size-4"} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 },
|
||||||
@ -414,6 +415,8 @@ 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEventsReviewTimeline && (
|
{isEventsReviewTimeline && (
|
||||||
@ -441,6 +444,8 @@ 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 },
|
||||||
@ -799,7 +800,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 +822,8 @@ function DetectionReview({
|
|||||||
dense={isMobile}
|
dense={isMobile}
|
||||||
isZooming={isZooming}
|
isZooming={isZooming}
|
||||||
zoomDirection={zoomDirection}
|
zoomDirection={zoomDirection}
|
||||||
|
onZoomChange={handleZoomChange}
|
||||||
|
possibleZoomLevels={possibleZoomLevels}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 },
|
||||||
@ -1010,6 +1011,8 @@ function Timeline({
|
|||||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||||
isZooming={isZooming}
|
isZooming={isZooming}
|
||||||
zoomDirection={zoomDirection}
|
zoomDirection={zoomDirection}
|
||||||
|
onZoomChange={handleZoomChange}
|
||||||
|
possibleZoomLevels={possibleZoomLevels}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="size-full" />
|
<Skeleton className="size-full" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user