mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Timeline fixes and export handles (#10522)
* select an export range from timeline * height tweak
This commit is contained in:
parent
880bae1eb2
commit
d249e5b27f
@ -1,4 +1,4 @@
|
||||
import useDraggableHandler from "@/hooks/use-handle-dragging";
|
||||
import useDraggableElement from "@/hooks/use-draggable-element";
|
||||
import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
@ -8,7 +8,7 @@ import {
|
||||
RefObject,
|
||||
} from "react";
|
||||
import EventSegment from "./EventSegment";
|
||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
import ReviewTimeline from "./ReviewTimeline";
|
||||
|
||||
@ -23,6 +23,11 @@ export type EventReviewTimelineProps = {
|
||||
showMinimap?: boolean;
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
showExportHandles?: boolean;
|
||||
exportStartTime?: number;
|
||||
exportEndTime?: number;
|
||||
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
events: ReviewSegment[];
|
||||
severityType: ReviewSeverity;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
@ -40,47 +45,113 @@ export function EventReviewTimeline({
|
||||
showMinimap = false,
|
||||
minimapStartTime,
|
||||
minimapEndTime,
|
||||
showExportHandles = false,
|
||||
exportStartTime,
|
||||
exportEndTime,
|
||||
setExportStartTime,
|
||||
setExportEndTime,
|
||||
events,
|
||||
severityType,
|
||||
contentRef,
|
||||
onHandlebarDraggingChange,
|
||||
}: EventReviewTimelineProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||
const [exportStartPosition, setExportStartPosition] = useState(0);
|
||||
const [exportEndPosition, setExportEndPosition] = useState(0);
|
||||
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timelineDuration = useMemo(
|
||||
() => timelineStart - timelineEnd,
|
||||
[timelineEnd, timelineStart],
|
||||
);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||
events,
|
||||
segmentDuration,
|
||||
);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
|
||||
const timelineStartAligned = useMemo(
|
||||
() => alignStartDateToTimeline(timelineStart),
|
||||
[timelineStart, alignStartDateToTimeline],
|
||||
);
|
||||
|
||||
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
||||
useDraggableHandler({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
handlebarRef,
|
||||
alignStartDateToTimeline,
|
||||
alignEndDateToTimeline,
|
||||
segmentDuration,
|
||||
showHandlebar,
|
||||
handlebarTime,
|
||||
setHandlebarTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
handlebarTimeRef,
|
||||
});
|
||||
const paddedExportStartTime = useMemo(() => {
|
||||
if (exportStartTime) {
|
||||
return alignStartDateToTimeline(exportStartTime) + segmentDuration;
|
||||
}
|
||||
}, [exportStartTime, segmentDuration, alignStartDateToTimeline]);
|
||||
|
||||
const paddedExportEndTime = useMemo(() => {
|
||||
if (exportEndTime) {
|
||||
return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2;
|
||||
}
|
||||
}, [exportEndTime, segmentDuration, alignEndDateToTimeline]);
|
||||
|
||||
const {
|
||||
handleMouseDown: handlebarMouseDown,
|
||||
handleMouseUp: handlebarMouseUp,
|
||||
handleMouseMove: handlebarMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: handlebarRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showHandlebar,
|
||||
draggableElementTime: handlebarTime,
|
||||
setDraggableElementTime: setHandlebarTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: handlebarTimeRef,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportStartMouseDown,
|
||||
handleMouseUp: exportStartMouseUp,
|
||||
handleMouseMove: exportStartMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportStartRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportStartTime,
|
||||
draggableElementLatestTime: paddedExportEndTime,
|
||||
setDraggableElementTime: setExportStartTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: exportStartTimeRef,
|
||||
setDraggableElementPosition: setExportStartPosition,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportEndMouseDown,
|
||||
handleMouseUp: exportEndMouseUp,
|
||||
handleMouseMove: exportEndMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportEndRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportEndTime,
|
||||
draggableElementEarliestTime: paddedExportStartTime,
|
||||
setDraggableElementTime: setExportEndTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: exportEndTimeRef,
|
||||
setDraggableElementPosition: setExportEndPosition,
|
||||
});
|
||||
|
||||
// Generate segments for the timeline
|
||||
const generateSegments = useCallback(() => {
|
||||
@ -145,12 +216,26 @@ export function EventReviewTimeline({
|
||||
timelineRef={timelineRef}
|
||||
handlebarRef={handlebarRef}
|
||||
handlebarTimeRef={handlebarTimeRef}
|
||||
handleMouseMove={handleMouseMove}
|
||||
handleMouseUp={handleMouseUp}
|
||||
handleMouseDown={handleMouseDown}
|
||||
handlebarMouseMove={handlebarMouseMove}
|
||||
handlebarMouseUp={handlebarMouseUp}
|
||||
handlebarMouseDown={handlebarMouseDown}
|
||||
segmentDuration={segmentDuration}
|
||||
timelineDuration={timelineDuration}
|
||||
showHandlebar={showHandlebar}
|
||||
isDragging={isDragging}
|
||||
exportStartMouseMove={exportStartMouseMove}
|
||||
exportStartMouseUp={exportStartMouseUp}
|
||||
exportStartMouseDown={exportStartMouseDown}
|
||||
exportEndMouseMove={exportEndMouseMove}
|
||||
exportEndMouseUp={exportEndMouseUp}
|
||||
exportEndMouseDown={exportEndMouseDown}
|
||||
showExportHandles={showExportHandles}
|
||||
exportStartRef={exportStartRef}
|
||||
exportStartTimeRef={exportStartTimeRef}
|
||||
exportEndRef={exportEndRef}
|
||||
exportEndTimeRef={exportEndTimeRef}
|
||||
exportStartPosition={exportStartPosition}
|
||||
exportEndPosition={exportEndPosition}
|
||||
>
|
||||
{segments}
|
||||
</ReviewTimeline>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useApiHost } from "@/api";
|
||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
import React, {
|
||||
@ -53,10 +53,8 @@ export function EventSegment({
|
||||
getEventThumbnail,
|
||||
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||
events,
|
||||
segmentDuration,
|
||||
);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
|
||||
const severity = useMemo(
|
||||
() => getSeverity(segmentTime, displaySeverityType),
|
||||
@ -155,7 +153,7 @@ export function EventSegment({
|
||||
: ""
|
||||
} ${
|
||||
isFirstSegmentInMinimap || isLastSegmentInMinimap
|
||||
? "relative h-2 border-b-2 border-gray-500"
|
||||
? "relative h-2 border-b-2 border-neutral-600"
|
||||
: ""
|
||||
}`;
|
||||
|
||||
@ -236,7 +234,7 @@ export function EventSegment({
|
||||
key={`${segmentKey}_${index}_primary_data`}
|
||||
className={`w-full h-2 bg-gradient-to-r ${roundBottomPrimary ? "rounded-bl-full rounded-br-full" : ""} ${roundTopPrimary ? "rounded-tl-full rounded-tr-full" : ""} ${severityColors[severityValue]}`}
|
||||
onClick={segmentClick}
|
||||
onTouchStart={(event) =>
|
||||
onTouchEnd={(event) =>
|
||||
handleTouchStart(event, segmentClick)
|
||||
}
|
||||
></div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import useDraggableHandler from "@/hooks/use-handle-dragging";
|
||||
import useDraggableElement from "@/hooks/use-draggable-element";
|
||||
import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
@ -8,7 +8,7 @@ import {
|
||||
RefObject,
|
||||
} from "react";
|
||||
import MotionSegment from "./MotionSegment";
|
||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
import ReviewTimeline from "./ReviewTimeline";
|
||||
|
||||
@ -23,6 +23,11 @@ export type MotionReviewTimelineProps = {
|
||||
showMinimap?: boolean;
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
showExportHandles?: boolean;
|
||||
exportStartTime?: number;
|
||||
exportEndTime?: number;
|
||||
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
events: ReviewSegment[];
|
||||
motion_events: MotionData[];
|
||||
severityType: ReviewSeverity;
|
||||
@ -41,47 +46,113 @@ export function MotionReviewTimeline({
|
||||
showMinimap = false,
|
||||
minimapStartTime,
|
||||
minimapEndTime,
|
||||
showExportHandles = false,
|
||||
exportStartTime,
|
||||
exportEndTime,
|
||||
setExportStartTime,
|
||||
setExportEndTime,
|
||||
events,
|
||||
motion_events,
|
||||
contentRef,
|
||||
onHandlebarDraggingChange,
|
||||
}: MotionReviewTimelineProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||
const [exportStartPosition, setExportStartPosition] = useState(0);
|
||||
const [exportEndPosition, setExportEndPosition] = useState(0);
|
||||
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timelineDuration = useMemo(
|
||||
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
||||
[timelineEnd, timelineStart, segmentDuration],
|
||||
);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||
events,
|
||||
segmentDuration,
|
||||
);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
|
||||
const timelineStartAligned = useMemo(
|
||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
||||
);
|
||||
|
||||
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
||||
useDraggableHandler({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
handlebarRef,
|
||||
alignStartDateToTimeline,
|
||||
alignEndDateToTimeline,
|
||||
segmentDuration,
|
||||
showHandlebar,
|
||||
handlebarTime,
|
||||
setHandlebarTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
handlebarTimeRef,
|
||||
});
|
||||
const paddedExportStartTime = useMemo(() => {
|
||||
if (exportStartTime) {
|
||||
return alignStartDateToTimeline(exportStartTime) + segmentDuration;
|
||||
}
|
||||
}, [exportStartTime, segmentDuration, alignStartDateToTimeline]);
|
||||
|
||||
const paddedExportEndTime = useMemo(() => {
|
||||
if (exportEndTime) {
|
||||
return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2;
|
||||
}
|
||||
}, [exportEndTime, segmentDuration, alignEndDateToTimeline]);
|
||||
|
||||
const {
|
||||
handleMouseDown: handlebarMouseDown,
|
||||
handleMouseUp: handlebarMouseUp,
|
||||
handleMouseMove: handlebarMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: handlebarRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showHandlebar,
|
||||
draggableElementTime: handlebarTime,
|
||||
setDraggableElementTime: setHandlebarTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: handlebarTimeRef,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportStartMouseDown,
|
||||
handleMouseUp: exportStartMouseUp,
|
||||
handleMouseMove: exportStartMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportStartRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportStartTime,
|
||||
draggableElementLatestTime: paddedExportEndTime,
|
||||
setDraggableElementTime: setExportStartTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: exportStartTimeRef,
|
||||
setDraggableElementPosition: setExportStartPosition,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportEndMouseDown,
|
||||
handleMouseUp: exportEndMouseUp,
|
||||
handleMouseMove: exportEndMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportEndRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportEndTime,
|
||||
draggableElementEarliestTime: paddedExportStartTime,
|
||||
setDraggableElementTime: setExportEndTime,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
draggableElementTimeRef: exportEndTimeRef,
|
||||
setDraggableElementPosition: setExportEndPosition,
|
||||
});
|
||||
|
||||
// Generate segments for the timeline
|
||||
const generateSegments = useCallback(() => {
|
||||
@ -147,12 +218,26 @@ export function MotionReviewTimeline({
|
||||
timelineRef={timelineRef}
|
||||
handlebarRef={handlebarRef}
|
||||
handlebarTimeRef={handlebarTimeRef}
|
||||
handleMouseMove={handleMouseMove}
|
||||
handleMouseUp={handleMouseUp}
|
||||
handleMouseDown={handleMouseDown}
|
||||
handlebarMouseMove={handlebarMouseMove}
|
||||
handlebarMouseUp={handlebarMouseUp}
|
||||
handlebarMouseDown={handlebarMouseDown}
|
||||
segmentDuration={segmentDuration}
|
||||
timelineDuration={timelineDuration}
|
||||
showHandlebar={showHandlebar}
|
||||
isDragging={isDragging}
|
||||
exportStartMouseMove={exportStartMouseMove}
|
||||
exportStartMouseUp={exportStartMouseUp}
|
||||
exportStartMouseDown={exportStartMouseDown}
|
||||
exportEndMouseMove={exportEndMouseMove}
|
||||
exportEndMouseUp={exportEndMouseUp}
|
||||
exportEndMouseDown={exportEndMouseDown}
|
||||
showExportHandles={showExportHandles}
|
||||
exportStartRef={exportStartRef}
|
||||
exportStartTimeRef={exportStartTimeRef}
|
||||
exportEndRef={exportEndRef}
|
||||
exportEndTimeRef={exportEndTimeRef}
|
||||
exportStartPosition={exportStartPosition}
|
||||
exportEndPosition={exportEndPosition}
|
||||
>
|
||||
{segments}
|
||||
</ReviewTimeline>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||
import { MotionData, ReviewSegment } from "@/types/review";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
@ -42,10 +42,8 @@ export function MotionSegment({
|
||||
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
||||
useMotionSegmentUtils(segmentDuration, motion_events);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||
events,
|
||||
segmentDuration,
|
||||
);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
|
||||
const { handleTouchStart } = useTapUtils();
|
||||
|
||||
@ -180,7 +178,7 @@ export function MotionSegment({
|
||||
key={segmentKey}
|
||||
className={segmentClasses}
|
||||
onClick={segmentClick}
|
||||
onTouchStart={(event) => handleTouchStart(event, segmentClick)}
|
||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||
>
|
||||
<MinimapBounds
|
||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||
|
@ -1,28 +1,75 @@
|
||||
import { ReactNode, RefObject } from "react";
|
||||
import { DraggableElement } from "@/types/draggable-element";
|
||||
import {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
|
||||
export type ReviewTimelineProps = {
|
||||
timelineRef: RefObject<HTMLDivElement>;
|
||||
handlebarRef: RefObject<HTMLDivElement>;
|
||||
handlebarTimeRef: RefObject<HTMLDivElement>;
|
||||
handleMouseMove: (
|
||||
handlebarMouseMove: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
handleMouseUp: (
|
||||
handlebarMouseUp: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
handleMouseDown: (
|
||||
handlebarMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
segmentDuration: number;
|
||||
timelineDuration: number;
|
||||
showHandlebar: boolean;
|
||||
showExportHandles: boolean;
|
||||
exportStartRef: RefObject<HTMLDivElement>;
|
||||
exportStartTimeRef: RefObject<HTMLDivElement>;
|
||||
exportEndRef: RefObject<HTMLDivElement>;
|
||||
exportEndTimeRef: RefObject<HTMLDivElement>;
|
||||
exportStartMouseMove: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportStartMouseUp: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportStartMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportEndMouseMove: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportEndMouseUp: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportEndMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
isDragging: boolean;
|
||||
exportStartPosition?: number;
|
||||
exportEndPosition?: number;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@ -30,14 +77,156 @@ export function ReviewTimeline({
|
||||
timelineRef,
|
||||
handlebarRef,
|
||||
handlebarTimeRef,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
handleMouseDown,
|
||||
handlebarMouseMove,
|
||||
handlebarMouseUp,
|
||||
handlebarMouseDown,
|
||||
segmentDuration,
|
||||
timelineDuration,
|
||||
showHandlebar = false,
|
||||
showExportHandles = false,
|
||||
exportStartRef,
|
||||
exportStartTimeRef,
|
||||
exportEndRef,
|
||||
exportEndTimeRef,
|
||||
exportStartMouseMove,
|
||||
exportStartMouseUp,
|
||||
exportStartMouseDown,
|
||||
exportEndMouseMove,
|
||||
exportEndMouseUp,
|
||||
exportEndMouseDown,
|
||||
isDragging,
|
||||
exportStartPosition,
|
||||
exportEndPosition,
|
||||
children,
|
||||
}: ReviewTimelineProps) {
|
||||
const exportSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const segmentHeight = useMemo(() => {
|
||||
if (timelineRef.current) {
|
||||
const { scrollHeight: timelineHeight } =
|
||||
timelineRef.current as HTMLDivElement;
|
||||
|
||||
return timelineHeight / (timelineDuration / segmentDuration);
|
||||
}
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentDuration, timelineDuration, timelineRef, showExportHandles]);
|
||||
|
||||
const [draggableElementType, setDraggableElementType] =
|
||||
useState<DraggableElement>();
|
||||
|
||||
const handleHandlebar = useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
setDraggableElementType("handlebar");
|
||||
handlebarMouseDown(e);
|
||||
},
|
||||
[handlebarMouseDown],
|
||||
);
|
||||
|
||||
const handleExportStart = useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
setDraggableElementType("export_start");
|
||||
exportStartMouseDown(e);
|
||||
},
|
||||
[exportStartMouseDown],
|
||||
);
|
||||
|
||||
const handleExportEnd = useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
setDraggableElementType("export_end");
|
||||
exportEndMouseDown(e);
|
||||
},
|
||||
[exportEndMouseDown],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
switch (draggableElementType) {
|
||||
case "export_start":
|
||||
exportStartMouseMove(e);
|
||||
break;
|
||||
case "export_end":
|
||||
exportEndMouseMove(e);
|
||||
break;
|
||||
case "handlebar":
|
||||
handlebarMouseMove(e);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
draggableElementType,
|
||||
exportStartMouseMove,
|
||||
exportEndMouseMove,
|
||||
handlebarMouseMove,
|
||||
],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
(
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
switch (draggableElementType) {
|
||||
case "export_start":
|
||||
exportStartMouseUp(e);
|
||||
break;
|
||||
case "export_end":
|
||||
exportEndMouseUp(e);
|
||||
break;
|
||||
case "handlebar":
|
||||
handlebarMouseUp(e);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
draggableElementType,
|
||||
exportStartMouseUp,
|
||||
exportEndMouseUp,
|
||||
handlebarMouseUp,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
exportSectionRef.current &&
|
||||
segmentHeight &&
|
||||
exportStartPosition &&
|
||||
exportEndPosition
|
||||
) {
|
||||
exportSectionRef.current.style.top = `${exportEndPosition + segmentHeight}px`;
|
||||
exportSectionRef.current.style.height = `${exportStartPosition - exportEndPosition + segmentHeight / 2}px`;
|
||||
}
|
||||
}, [
|
||||
showExportHandles,
|
||||
segmentHeight,
|
||||
timelineRef,
|
||||
exportStartPosition,
|
||||
exportEndPosition,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={timelineRef}
|
||||
@ -46,7 +235,9 @@ export function ReviewTimeline({
|
||||
onMouseUp={handleMouseUp}
|
||||
onTouchEnd={handleMouseUp}
|
||||
className={`relative h-full overflow-y-auto no-scrollbar bg-secondary ${
|
||||
isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
|
||||
isDragging && (showHandlebar || showExportHandles)
|
||||
? "cursor-grabbing"
|
||||
: "cursor-auto"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col relative">
|
||||
@ -62,8 +253,8 @@ export function ReviewTimeline({
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center touch-none select-none"
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleMouseDown}
|
||||
onMouseDown={handleHandlebar}
|
||||
onTouchStart={handleHandlebar}
|
||||
>
|
||||
<div
|
||||
className={`relative w-full ${
|
||||
@ -73,20 +264,90 @@ export function ReviewTimeline({
|
||||
<div
|
||||
className={`bg-destructive rounded-full mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`}
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "handlebar" ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={handlebarTimeRef}
|
||||
className={`text-white ${isDragging && isMobile ? "text-lg" : "text-[8px] md:text-xs"} z-10`}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "handlebar" ? "text-lg" : "text-[8px] md:text-xs"} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute h-1 w-full bg-destructive ${isDragging && isMobile ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
className={`absolute h-1 w-full bg-destructive ${isDragging && isMobile && draggableElementType == "handlebar" ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showExportHandles && (
|
||||
<>
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`}
|
||||
role="scrollbar"
|
||||
ref={exportEndRef}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center touch-none select-none"
|
||||
onMouseDown={handleExportEnd}
|
||||
onTouchStart={handleExportEnd}
|
||||
>
|
||||
<div
|
||||
className={`relative mt-[6.5px] w-full ${
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-selected -mt-4 mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "export_end" ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={exportEndTimeRef}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_end" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute h-1 w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_end" ? "top-0" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={exportSectionRef}
|
||||
className="bg-selected/50 absolute w-full"
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`}
|
||||
role="scrollbar"
|
||||
ref={exportStartRef}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center touch-none select-none"
|
||||
onMouseDown={handleExportStart}
|
||||
onTouchStart={handleExportStart}
|
||||
>
|
||||
<div
|
||||
className={`relative -mt-[6.5px] w-full ${
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute h-1 w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_start" ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
<div
|
||||
className={`bg-selected mt-4 mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "export_start" ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={exportStartTimeRef}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_start" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export function MinimapBounds({
|
||||
<>
|
||||
{isFirstSegmentInMinimap && (
|
||||
<div
|
||||
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] scroll-mt-8"
|
||||
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] scroll-mt-8 pointer-events-none"
|
||||
ref={firstMinimapSegmentRef}
|
||||
>
|
||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||
@ -44,7 +44,7 @@ export function MinimapBounds({
|
||||
)}
|
||||
|
||||
{isLastSegmentInMinimap && (
|
||||
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px]">
|
||||
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] pointer-events-none">
|
||||
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
@ -61,14 +61,14 @@ export function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
|
||||
<div className="absolute">
|
||||
<div className="flex items-end content-end w-[12px] h-2">
|
||||
<div
|
||||
className={`h-0.5 ${
|
||||
className={`pointer-events-none h-0.5 ${
|
||||
timestamp.getMinutes() % timestampSpread === 0 &&
|
||||
timestamp.getSeconds() === 0
|
||||
? "w-[12px] bg-neutral-600 dark:bg-neutral-500"
|
||||
: timestamp.getMinutes() % (timestampSpread == 15 ? 5 : 1) ===
|
||||
0 && timestamp.getSeconds() === 0
|
||||
? "w-[8px] bg-neutral-500 dark:bg-neutral-600" // Minor tick mark
|
||||
: "w-[5px] bg-neutral-400 dark:bg-neutral-700"
|
||||
? "w-[8px] bg-neutral-500" // Minor tick mark
|
||||
: "w-[5px] bg-neutral-400 dark:bg-neutral-600"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
@ -88,7 +88,7 @@ export function Timestamp({
|
||||
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||
<div
|
||||
key={`${segmentKey}_timestamp`}
|
||||
className="text-[8px] text-neutral-600 dark:text-neutral-500"
|
||||
className="pointer-events-none text-[8px] text-neutral-600 dark:text-neutral-500"
|
||||
>
|
||||
{timestamp.getMinutes() % timestampSpread === 0 &&
|
||||
timestamp.getSeconds() === 0 &&
|
||||
|
@ -1,41 +1,46 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import { useTimelineUtils } from "./use-timeline-utils";
|
||||
|
||||
type DragHandlerProps = {
|
||||
type DraggableElementProps = {
|
||||
contentRef: React.RefObject<HTMLElement>;
|
||||
timelineRef: React.RefObject<HTMLDivElement>;
|
||||
handlebarRef: React.RefObject<HTMLDivElement>;
|
||||
alignStartDateToTimeline: (time: number) => number;
|
||||
alignEndDateToTimeline: (time: number) => number;
|
||||
draggableElementRef: React.RefObject<HTMLDivElement>;
|
||||
segmentDuration: number;
|
||||
showHandlebar: boolean;
|
||||
handlebarTime?: number;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
showDraggableElement: boolean;
|
||||
draggableElementTime?: number;
|
||||
draggableElementEarliestTime?: number;
|
||||
draggableElementLatestTime?: number;
|
||||
setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
timelineDuration: number;
|
||||
timelineStartAligned: number;
|
||||
isDragging: boolean;
|
||||
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>;
|
||||
};
|
||||
|
||||
function useDraggableHandler({
|
||||
function useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
handlebarRef,
|
||||
alignStartDateToTimeline,
|
||||
draggableElementRef,
|
||||
segmentDuration,
|
||||
showHandlebar,
|
||||
handlebarTime,
|
||||
setHandlebarTime,
|
||||
handlebarTimeRef,
|
||||
showDraggableElement,
|
||||
draggableElementTime,
|
||||
draggableElementEarliestTime,
|
||||
draggableElementLatestTime,
|
||||
setDraggableElementTime,
|
||||
draggableElementTimeRef,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
}: DragHandlerProps) {
|
||||
setDraggableElementPosition,
|
||||
}: DraggableElementProps) {
|
||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
||||
|
||||
const draggingAtTopEdge = useMemo(() => {
|
||||
if (clientYPosition && timelineRef.current) {
|
||||
@ -78,17 +83,32 @@ function useDraggableHandler({
|
||||
(
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
// prevent default only for mouse events
|
||||
// to avoid chrome/android issues
|
||||
if (e.nativeEvent instanceof MouseEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.stopPropagation();
|
||||
getClientYPosition(e);
|
||||
setIsDragging(true);
|
||||
|
||||
if (handlebarRef.current && clientYPosition && isDesktop) {
|
||||
const handlebarRect = handlebarRef.current.getBoundingClientRect();
|
||||
setInitialClickAdjustment(clientYPosition - handlebarRect.top);
|
||||
let clientY;
|
||||
if (isMobile && e.nativeEvent instanceof TouchEvent) {
|
||||
clientY = e.nativeEvent.touches[0].clientY;
|
||||
} else if (e.nativeEvent instanceof MouseEvent) {
|
||||
clientY = e.nativeEvent.clientY;
|
||||
}
|
||||
if (clientY && draggableElementRef.current && isDesktop) {
|
||||
const draggableElementRect =
|
||||
draggableElementRef.current.getBoundingClientRect();
|
||||
if (!isDragging) {
|
||||
setInitialClickAdjustment(clientY - draggableElementRect.top);
|
||||
}
|
||||
setClientYPosition(clientY);
|
||||
}
|
||||
},
|
||||
[setIsDragging, getClientYPosition, handlebarRef, clientYPosition],
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[setIsDragging, draggableElementRef],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
@ -114,19 +134,36 @@ function useDraggableHandler({
|
||||
return scrollTop;
|
||||
}, []);
|
||||
|
||||
const updateHandlebarPosition = useCallback(
|
||||
const timestampToPixels = useCallback(
|
||||
(time: number) => {
|
||||
const { scrollHeight: timelineHeight } =
|
||||
timelineRef.current as HTMLDivElement;
|
||||
|
||||
const segmentHeight =
|
||||
timelineHeight / (timelineDuration / segmentDuration);
|
||||
|
||||
return ((timelineStartAligned - time) / segmentDuration) * segmentHeight;
|
||||
},
|
||||
[segmentDuration, timelineRef, timelineStartAligned, timelineDuration],
|
||||
);
|
||||
|
||||
const updateDraggableElementPosition = useCallback(
|
||||
(
|
||||
newHandlePosition: number,
|
||||
newElementPosition: number,
|
||||
segmentStartTime: number,
|
||||
scrollTimeline: boolean,
|
||||
updateHandle: boolean,
|
||||
) => {
|
||||
const thumb = handlebarRef.current;
|
||||
const thumb = draggableElementRef.current;
|
||||
if (thumb) {
|
||||
requestAnimationFrame(() => {
|
||||
thumb.style.top = `${newHandlePosition}px`;
|
||||
if (handlebarTimeRef.current) {
|
||||
handlebarTimeRef.current.textContent = new Date(
|
||||
thumb.style.top = `${newElementPosition}px`;
|
||||
if (setDraggableElementPosition) {
|
||||
setDraggableElementPosition(newElementPosition);
|
||||
}
|
||||
|
||||
if (draggableElementTimeRef.current) {
|
||||
draggableElementTimeRef.current.textContent = new Date(
|
||||
segmentStartTime * 1000,
|
||||
).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
@ -143,12 +180,18 @@ function useDraggableHandler({
|
||||
}
|
||||
});
|
||||
|
||||
if (setHandlebarTime && updateHandle) {
|
||||
setHandlebarTime(segmentStartTime);
|
||||
if (setDraggableElementTime && updateHandle) {
|
||||
setDraggableElementTime(segmentStartTime);
|
||||
}
|
||||
}
|
||||
},
|
||||
[segmentDuration, handlebarTimeRef, handlebarRef, setHandlebarTime],
|
||||
[
|
||||
segmentDuration,
|
||||
draggableElementTimeRef,
|
||||
draggableElementRef,
|
||||
setDraggableElementTime,
|
||||
setDraggableElementPosition,
|
||||
],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
@ -158,7 +201,7 @@ function useDraggableHandler({
|
||||
if (
|
||||
!contentRef.current ||
|
||||
!timelineRef.current ||
|
||||
!handlebarRef.current
|
||||
!draggableElementRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -166,7 +209,7 @@ function useDraggableHandler({
|
||||
getClientYPosition(e);
|
||||
},
|
||||
|
||||
[contentRef, handlebarRef, timelineRef, getClientYPosition],
|
||||
[contentRef, draggableElementRef, timelineRef, getClientYPosition],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -175,7 +218,7 @@ function useDraggableHandler({
|
||||
const handleScroll = () => {
|
||||
if (
|
||||
timelineRef.current &&
|
||||
showHandlebar &&
|
||||
showDraggableElement &&
|
||||
isDragging &&
|
||||
clientYPosition
|
||||
) {
|
||||
@ -190,13 +233,21 @@ function useDraggableHandler({
|
||||
|
||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||
|
||||
const newHandlePosition = Math.min(
|
||||
// end of timeline
|
||||
segmentHeight * (timelineDuration / segmentDuration) -
|
||||
segmentHeight * 2,
|
||||
// bottom of timeline
|
||||
const elementEarliest = draggableElementEarliestTime
|
||||
? timestampToPixels(draggableElementEarliestTime)
|
||||
: segmentHeight * (timelineDuration / segmentDuration) -
|
||||
segmentHeight * 3;
|
||||
|
||||
// top of timeline - default 2 segments added for draggableElement visibility
|
||||
const elementLatest = draggableElementLatestTime
|
||||
? timestampToPixels(draggableElementLatestTime)
|
||||
: segmentHeight * 2 + scrolled;
|
||||
|
||||
const newElementPosition = Math.min(
|
||||
elementEarliest,
|
||||
Math.max(
|
||||
// start of timeline - 2 segments added for handlebar visibility
|
||||
segmentHeight * 2 + scrolled,
|
||||
elementLatest,
|
||||
// current Y position
|
||||
clientYPosition -
|
||||
timelineTop +
|
||||
@ -205,7 +256,7 @@ function useDraggableHandler({
|
||||
),
|
||||
);
|
||||
|
||||
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
|
||||
const segmentIndex = Math.floor(newElementPosition / segmentHeight);
|
||||
const segmentStartTime = alignStartDateToTimeline(
|
||||
timelineStartAligned - segmentIndex * segmentDuration,
|
||||
);
|
||||
@ -224,17 +275,17 @@ function useDraggableHandler({
|
||||
}
|
||||
}
|
||||
|
||||
updateHandlebarPosition(
|
||||
newHandlePosition - segmentHeight,
|
||||
updateDraggableElementPosition(
|
||||
newElementPosition - segmentHeight,
|
||||
segmentStartTime,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
if (setHandlebarTime) {
|
||||
setHandlebarTime(
|
||||
if (setDraggableElementTime) {
|
||||
setDraggableElementTime(
|
||||
timelineStartAligned -
|
||||
((newHandlePosition - segmentHeight / 2 - 2) / segmentHeight) *
|
||||
((newElementPosition - segmentHeight / 2 - 2) / segmentHeight) *
|
||||
segmentDuration,
|
||||
);
|
||||
}
|
||||
@ -264,22 +315,21 @@ function useDraggableHandler({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
clientYPosition,
|
||||
isDragging,
|
||||
segmentDuration,
|
||||
timelineStartAligned,
|
||||
timelineDuration,
|
||||
timelineRef,
|
||||
draggingAtTopEdge,
|
||||
draggingAtBottomEdge,
|
||||
showHandlebar,
|
||||
showDraggableElement,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
timelineRef.current &&
|
||||
handlebarRef.current &&
|
||||
showHandlebar &&
|
||||
handlebarTime &&
|
||||
draggableElementRef.current &&
|
||||
showDraggableElement &&
|
||||
draggableElementTime &&
|
||||
!isDragging
|
||||
) {
|
||||
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
||||
@ -290,20 +340,30 @@ function useDraggableHandler({
|
||||
|
||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||
|
||||
const newHandlePosition =
|
||||
((timelineStartAligned - handlebarTime) / segmentDuration) *
|
||||
const newElementPosition =
|
||||
((timelineStartAligned - draggableElementTime) / segmentDuration) *
|
||||
segmentHeight +
|
||||
parentScrollTop -
|
||||
scrolled -
|
||||
2; // height of handlebar horizontal line
|
||||
2; // height of draggableElement horizontal line
|
||||
|
||||
updateHandlebarPosition(newHandlePosition, handlebarTime, true, true);
|
||||
updateDraggableElementPosition(
|
||||
newElementPosition,
|
||||
draggableElementTime,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handlebarTime, showHandlebar, handlebarRef, timelineStartAligned]);
|
||||
}, [
|
||||
draggableElementTime,
|
||||
showDraggableElement,
|
||||
draggableElementRef,
|
||||
timelineStartAligned,
|
||||
]);
|
||||
|
||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||
}
|
||||
|
||||
export default useDraggableHandler;
|
||||
export default useDraggableElement;
|
@ -1,75 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
|
||||
export const useEventUtils = (
|
||||
events: ReviewSegment[],
|
||||
segmentDuration: number,
|
||||
) => {
|
||||
const isStartOfEvent = useCallback(
|
||||
(time: number): boolean => {
|
||||
return events.some((event) => {
|
||||
const segmentStart = getSegmentStart(event.start_time);
|
||||
return time >= segmentStart && time < segmentStart + segmentDuration;
|
||||
});
|
||||
},
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[events, segmentDuration],
|
||||
);
|
||||
|
||||
const isEndOfEvent = useCallback(
|
||||
(time: number): boolean => {
|
||||
return events.some((event) => {
|
||||
if (typeof event.end_time === "number") {
|
||||
const segmentEnd = getSegmentEnd(event.end_time);
|
||||
return time >= segmentEnd - segmentDuration && time < segmentEnd;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[events, segmentDuration],
|
||||
);
|
||||
|
||||
const getSegmentStart = useCallback(
|
||||
(time: number): number => {
|
||||
return Math.floor(time / segmentDuration) * segmentDuration;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
const getSegmentEnd = useCallback(
|
||||
(time: number): number => {
|
||||
return Math.ceil(time / segmentDuration) * segmentDuration;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
const alignEndDateToTimeline = useCallback(
|
||||
(time: number): number => {
|
||||
const remainder = time % segmentDuration;
|
||||
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
|
||||
return time + adjustment;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
const alignStartDateToTimeline = useCallback(
|
||||
(time: number): number => {
|
||||
const remainder = time % segmentDuration;
|
||||
const adjustment = remainder === 0 ? 0 : -remainder;
|
||||
return time + adjustment;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
return {
|
||||
isStartOfEvent,
|
||||
isEndOfEvent,
|
||||
getSegmentStart,
|
||||
getSegmentEnd,
|
||||
alignEndDateToTimeline,
|
||||
alignStartDateToTimeline,
|
||||
};
|
||||
};
|
26
web/src/hooks/use-timeline-utils.ts
Normal file
26
web/src/hooks/use-timeline-utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const useTimelineUtils = (segmentDuration: number) => {
|
||||
const alignEndDateToTimeline = useCallback(
|
||||
(time: number): number => {
|
||||
const remainder = time % segmentDuration;
|
||||
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
|
||||
return time + adjustment;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
const alignStartDateToTimeline = useCallback(
|
||||
(time: number): number => {
|
||||
const remainder = time % segmentDuration;
|
||||
const adjustment = remainder === 0 ? 0 : -remainder;
|
||||
return time + adjustment;
|
||||
},
|
||||
[segmentDuration],
|
||||
);
|
||||
|
||||
return {
|
||||
alignEndDateToTimeline,
|
||||
alignStartDateToTimeline,
|
||||
};
|
||||
};
|
@ -26,9 +26,10 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import axios from "axios";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -42,6 +43,8 @@ function Export() {
|
||||
"exports/",
|
||||
(url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data),
|
||||
);
|
||||
const location = useLocation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
// Export States
|
||||
const [camera, setCamera] = useState<string | undefined>();
|
||||
@ -142,6 +145,23 @@ function Export() {
|
||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state && location.state.start && location.state.end) {
|
||||
const startTimeString = format(
|
||||
new Date(location.state.start * 1000),
|
||||
"HH:mm:ss",
|
||||
);
|
||||
const endTimeString = format(
|
||||
new Date(location.state.end * 1000),
|
||||
"HH:mm:ss",
|
||||
);
|
||||
setStartTime(startTimeString);
|
||||
setEndTime(endTimeString);
|
||||
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
return (
|
||||
<div className="size-full p-2 overflow-hidden flex flex-col">
|
||||
<Toaster />
|
||||
@ -167,7 +187,7 @@ function Export() {
|
||||
</AlertDialog>
|
||||
|
||||
<div className="w-full h-14">
|
||||
<Create>
|
||||
<Create open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<Trigger>
|
||||
<Button variant="select">New Export</Button>
|
||||
</Trigger>
|
||||
|
@ -23,7 +23,9 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
||||
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
// Color data
|
||||
const colors = [
|
||||
@ -129,6 +131,18 @@ function UIPlayground() {
|
||||
Math.round((Date.now() / 1000 - 15 * 60) / 60) * 60,
|
||||
);
|
||||
|
||||
const [exportStartTime, setExportStartTime] = useState(
|
||||
Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60,
|
||||
);
|
||||
|
||||
const [exportEndTime, setExportEndTime] = useState(
|
||||
Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60,
|
||||
);
|
||||
|
||||
const [showExportHandles, setShowExportHandles] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useMemo(() => {
|
||||
const initialEvents = Array.from({ length: 50 }, generateRandomEvent);
|
||||
setMockEvents(initialEvents);
|
||||
@ -158,8 +172,6 @@ function UIPlayground() {
|
||||
timestampSpread: 15,
|
||||
});
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
const possibleZoomLevels = [
|
||||
{ segmentDuration: 60, timestampSpread: 15 },
|
||||
{ segmentDuration: 30, timestampSpread: 5 },
|
||||
@ -223,6 +235,26 @@ function UIPlayground() {
|
||||
<p className="text-small">
|
||||
Handlebar is dragging: {isDragging ? "yes" : "no"}
|
||||
</p>
|
||||
<p className="text-small">
|
||||
Export start timestamp: {exportStartTime} -
|
||||
{new Date(exportStartTime * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-small">
|
||||
Export end timestamp: {exportEndTime} -
|
||||
{new Date(exportEndTime * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
<div className="my-4">
|
||||
<Heading as="h4">Timeline type</Heading>
|
||||
<Select
|
||||
@ -245,17 +277,41 @@ function UIPlayground() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex p-2 justify-start items-center">
|
||||
<Switch
|
||||
id="exporthandles"
|
||||
checked={showExportHandles}
|
||||
onCheckedChange={() => {
|
||||
setShowExportHandles(!showExportHandles);
|
||||
if (showExportHandles) {
|
||||
setExportEndTime(
|
||||
Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60,
|
||||
);
|
||||
setExportStartTime(
|
||||
Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label className="ml-2" htmlFor="exporthandles">
|
||||
Show Export Handles
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/export", {
|
||||
state: { start: exportStartTime, end: exportEndTime },
|
||||
});
|
||||
}}
|
||||
disabled={!showExportHandles}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-[40px] my-4">
|
||||
<CameraActivityIndicator />
|
||||
</div>
|
||||
<div className="">
|
||||
{birdseyeConfig && (
|
||||
<BirdseyeLivePlayer
|
||||
birdseyeConfig={birdseyeConfig}
|
||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
<Button onClick={handleZoomOut} disabled={zoomLevel === 0}>
|
||||
Zoom Out
|
||||
@ -267,6 +323,14 @@ function UIPlayground() {
|
||||
Zoom In
|
||||
</Button>
|
||||
</p>
|
||||
<div className="">
|
||||
{birdseyeConfig && (
|
||||
<BirdseyeLivePlayer
|
||||
birdseyeConfig={birdseyeConfig}
|
||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Heading as="h4" className="my-5">
|
||||
Color scheme
|
||||
</Heading>
|
||||
@ -293,14 +357,6 @@ function UIPlayground() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-96 top-96 bottom-96 right-96">
|
||||
<HlsVideoPlayer
|
||||
className="size-full"
|
||||
videoRef={videoRef}
|
||||
currentSource="http://localhost:5173/vod/side_cam/start/1710345600/end/1710349200/master.m3u8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
|
||||
{!isEventsReviewTimeline && (
|
||||
<MotionReviewTimeline
|
||||
@ -315,6 +371,11 @@ function UIPlayground() {
|
||||
showMinimap // show / hide the minimap
|
||||
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
|
||||
minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
|
||||
showExportHandles={showExportHandles}
|
||||
exportStartTime={exportStartTime}
|
||||
setExportStartTime={setExportStartTime}
|
||||
exportEndTime={exportEndTime}
|
||||
setExportEndTime={setExportEndTime}
|
||||
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
||||
motion_events={mockMotionData}
|
||||
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
||||
@ -334,6 +395,11 @@ function UIPlayground() {
|
||||
showMinimap // show / hide the minimap
|
||||
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
|
||||
minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
|
||||
showExportHandles={showExportHandles}
|
||||
exportStartTime={exportStartTime}
|
||||
setExportStartTime={setExportStartTime}
|
||||
exportEndTime={exportEndTime}
|
||||
setExportEndTime={setExportEndTime}
|
||||
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
||||
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
||||
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
|
||||
|
1
web/src/types/draggable-element.ts
Normal file
1
web/src/types/draggable-element.ts
Normal file
@ -0,0 +1 @@
|
||||
export type DraggableElement = "handlebar" | "export_start" | "export_end";
|
@ -6,7 +6,7 @@ import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
|
||||
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { useScrollLockout } from "@/hooks/use-mouse-listener";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Preview } from "@/types/preview";
|
||||
@ -355,10 +355,7 @@ function DetectionReview({
|
||||
|
||||
// timeline interaction
|
||||
|
||||
const { alignStartDateToTimeline } = useEventUtils(
|
||||
reviewItems?.all ?? [],
|
||||
segmentDuration,
|
||||
);
|
||||
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
||||
|
||||
const scrollLock = useScrollLockout(contentRef);
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--selected: hsl(228, 89%, 63%);
|
||||
--selected: 228, 89%, 63%;
|
||||
--selected: 228 89% 63%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
@ -75,13 +75,13 @@
|
||||
--severity_motion_dimmed: var(--yellow-200);
|
||||
|
||||
--motion_review: hsl(44, 94%, 50%);
|
||||
--motion_review: 44, 94%, 50%;
|
||||
--motion_review: 44 94% 50%;
|
||||
|
||||
--motion_review_dimmed: hsl(44, 60%, 40%);
|
||||
--motion_review_dimmed: 44, 60%, 40%;
|
||||
--motion_review_dimmed: 44 60% 40%;
|
||||
|
||||
--audio_review: hsl(228, 94%, 67%);
|
||||
--audio_review: 228, 94%, 67%;
|
||||
--audio_review: 228 94% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -146,6 +146,6 @@
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--selected: hsl(228, 89%, 63%);
|
||||
--selected: 228, 89%, 63%;
|
||||
--selected: 228 89% 63%;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user