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 {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
RefObject,
|
RefObject,
|
||||||
} from "react";
|
} from "react";
|
||||||
import EventSegment from "./EventSegment";
|
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 { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import ReviewTimeline from "./ReviewTimeline";
|
import ReviewTimeline from "./ReviewTimeline";
|
||||||
|
|
||||||
@ -23,6 +23,11 @@ export type EventReviewTimelineProps = {
|
|||||||
showMinimap?: boolean;
|
showMinimap?: boolean;
|
||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
minimapEndTime?: number;
|
minimapEndTime?: number;
|
||||||
|
showExportHandles?: boolean;
|
||||||
|
exportStartTime?: number;
|
||||||
|
exportEndTime?: number;
|
||||||
|
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement>;
|
||||||
@ -40,47 +45,113 @@ export function EventReviewTimeline({
|
|||||||
showMinimap = false,
|
showMinimap = false,
|
||||||
minimapStartTime,
|
minimapStartTime,
|
||||||
minimapEndTime,
|
minimapEndTime,
|
||||||
|
showExportHandles = false,
|
||||||
|
exportStartTime,
|
||||||
|
exportEndTime,
|
||||||
|
setExportStartTime,
|
||||||
|
setExportEndTime,
|
||||||
events,
|
events,
|
||||||
severityType,
|
severityType,
|
||||||
contentRef,
|
contentRef,
|
||||||
onHandlebarDraggingChange,
|
onHandlebarDraggingChange,
|
||||||
}: EventReviewTimelineProps) {
|
}: EventReviewTimelineProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
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 timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarTimeRef = 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(
|
const timelineDuration = useMemo(
|
||||||
() => timelineStart - timelineEnd,
|
() => timelineStart - timelineEnd,
|
||||||
[timelineEnd, timelineStart],
|
[timelineEnd, timelineStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||||
events,
|
useTimelineUtils(segmentDuration);
|
||||||
segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timelineStartAligned = useMemo(
|
const timelineStartAligned = useMemo(
|
||||||
() => alignStartDateToTimeline(timelineStart),
|
() => alignStartDateToTimeline(timelineStart),
|
||||||
[timelineStart, alignStartDateToTimeline],
|
[timelineStart, alignStartDateToTimeline],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
const paddedExportStartTime = useMemo(() => {
|
||||||
useDraggableHandler({
|
if (exportStartTime) {
|
||||||
contentRef,
|
return alignStartDateToTimeline(exportStartTime) + segmentDuration;
|
||||||
timelineRef,
|
}
|
||||||
handlebarRef,
|
}, [exportStartTime, segmentDuration, alignStartDateToTimeline]);
|
||||||
alignStartDateToTimeline,
|
|
||||||
alignEndDateToTimeline,
|
const paddedExportEndTime = useMemo(() => {
|
||||||
segmentDuration,
|
if (exportEndTime) {
|
||||||
showHandlebar,
|
return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2;
|
||||||
handlebarTime,
|
}
|
||||||
setHandlebarTime,
|
}, [exportEndTime, segmentDuration, alignEndDateToTimeline]);
|
||||||
timelineDuration,
|
|
||||||
timelineStartAligned,
|
const {
|
||||||
isDragging,
|
handleMouseDown: handlebarMouseDown,
|
||||||
setIsDragging,
|
handleMouseUp: handlebarMouseUp,
|
||||||
handlebarTimeRef,
|
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
|
// Generate segments for the timeline
|
||||||
const generateSegments = useCallback(() => {
|
const generateSegments = useCallback(() => {
|
||||||
@ -145,12 +216,26 @@ export function EventReviewTimeline({
|
|||||||
timelineRef={timelineRef}
|
timelineRef={timelineRef}
|
||||||
handlebarRef={handlebarRef}
|
handlebarRef={handlebarRef}
|
||||||
handlebarTimeRef={handlebarTimeRef}
|
handlebarTimeRef={handlebarTimeRef}
|
||||||
handleMouseMove={handleMouseMove}
|
handlebarMouseMove={handlebarMouseMove}
|
||||||
handleMouseUp={handleMouseUp}
|
handlebarMouseUp={handlebarMouseUp}
|
||||||
handleMouseDown={handleMouseDown}
|
handlebarMouseDown={handlebarMouseDown}
|
||||||
segmentDuration={segmentDuration}
|
segmentDuration={segmentDuration}
|
||||||
|
timelineDuration={timelineDuration}
|
||||||
showHandlebar={showHandlebar}
|
showHandlebar={showHandlebar}
|
||||||
isDragging={isDragging}
|
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}
|
{segments}
|
||||||
</ReviewTimeline>
|
</ReviewTimeline>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useApiHost } from "@/api";
|
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 { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import React, {
|
import React, {
|
||||||
@ -53,10 +53,8 @@ export function EventSegment({
|
|||||||
getEventThumbnail,
|
getEventThumbnail,
|
||||||
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||||
events,
|
useTimelineUtils(segmentDuration);
|
||||||
segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
const severity = useMemo(
|
const severity = useMemo(
|
||||||
() => getSeverity(segmentTime, displaySeverityType),
|
() => getSeverity(segmentTime, displaySeverityType),
|
||||||
@ -155,7 +153,7 @@ export function EventSegment({
|
|||||||
: ""
|
: ""
|
||||||
} ${
|
} ${
|
||||||
isFirstSegmentInMinimap || isLastSegmentInMinimap
|
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`}
|
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]}`}
|
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}
|
onClick={segmentClick}
|
||||||
onTouchStart={(event) =>
|
onTouchEnd={(event) =>
|
||||||
handleTouchStart(event, segmentClick)
|
handleTouchStart(event, segmentClick)
|
||||||
}
|
}
|
||||||
></div>
|
></div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import useDraggableHandler from "@/hooks/use-handle-dragging";
|
import useDraggableElement from "@/hooks/use-draggable-element";
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
RefObject,
|
RefObject,
|
||||||
} from "react";
|
} from "react";
|
||||||
import MotionSegment from "./MotionSegment";
|
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 { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import ReviewTimeline from "./ReviewTimeline";
|
import ReviewTimeline from "./ReviewTimeline";
|
||||||
|
|
||||||
@ -23,6 +23,11 @@ export type MotionReviewTimelineProps = {
|
|||||||
showMinimap?: boolean;
|
showMinimap?: boolean;
|
||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
minimapEndTime?: number;
|
minimapEndTime?: number;
|
||||||
|
showExportHandles?: boolean;
|
||||||
|
exportStartTime?: number;
|
||||||
|
exportEndTime?: number;
|
||||||
|
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
motion_events: MotionData[];
|
motion_events: MotionData[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
@ -41,47 +46,113 @@ export function MotionReviewTimeline({
|
|||||||
showMinimap = false,
|
showMinimap = false,
|
||||||
minimapStartTime,
|
minimapStartTime,
|
||||||
minimapEndTime,
|
minimapEndTime,
|
||||||
|
showExportHandles = false,
|
||||||
|
exportStartTime,
|
||||||
|
exportEndTime,
|
||||||
|
setExportStartTime,
|
||||||
|
setExportEndTime,
|
||||||
events,
|
events,
|
||||||
motion_events,
|
motion_events,
|
||||||
contentRef,
|
contentRef,
|
||||||
onHandlebarDraggingChange,
|
onHandlebarDraggingChange,
|
||||||
}: MotionReviewTimelineProps) {
|
}: MotionReviewTimelineProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
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 timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarTimeRef = 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(
|
const timelineDuration = useMemo(
|
||||||
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
||||||
[timelineEnd, timelineStart, segmentDuration],
|
[timelineEnd, timelineStart, segmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||||
events,
|
useTimelineUtils(segmentDuration);
|
||||||
segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timelineStartAligned = useMemo(
|
const timelineStartAligned = useMemo(
|
||||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||||
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
const paddedExportStartTime = useMemo(() => {
|
||||||
useDraggableHandler({
|
if (exportStartTime) {
|
||||||
contentRef,
|
return alignStartDateToTimeline(exportStartTime) + segmentDuration;
|
||||||
timelineRef,
|
}
|
||||||
handlebarRef,
|
}, [exportStartTime, segmentDuration, alignStartDateToTimeline]);
|
||||||
alignStartDateToTimeline,
|
|
||||||
alignEndDateToTimeline,
|
const paddedExportEndTime = useMemo(() => {
|
||||||
segmentDuration,
|
if (exportEndTime) {
|
||||||
showHandlebar,
|
return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2;
|
||||||
handlebarTime,
|
}
|
||||||
setHandlebarTime,
|
}, [exportEndTime, segmentDuration, alignEndDateToTimeline]);
|
||||||
timelineDuration,
|
|
||||||
timelineStartAligned,
|
const {
|
||||||
isDragging,
|
handleMouseDown: handlebarMouseDown,
|
||||||
setIsDragging,
|
handleMouseUp: handlebarMouseUp,
|
||||||
handlebarTimeRef,
|
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
|
// Generate segments for the timeline
|
||||||
const generateSegments = useCallback(() => {
|
const generateSegments = useCallback(() => {
|
||||||
@ -147,12 +218,26 @@ export function MotionReviewTimeline({
|
|||||||
timelineRef={timelineRef}
|
timelineRef={timelineRef}
|
||||||
handlebarRef={handlebarRef}
|
handlebarRef={handlebarRef}
|
||||||
handlebarTimeRef={handlebarTimeRef}
|
handlebarTimeRef={handlebarTimeRef}
|
||||||
handleMouseMove={handleMouseMove}
|
handlebarMouseMove={handlebarMouseMove}
|
||||||
handleMouseUp={handleMouseUp}
|
handlebarMouseUp={handlebarMouseUp}
|
||||||
handleMouseDown={handleMouseDown}
|
handlebarMouseDown={handlebarMouseDown}
|
||||||
segmentDuration={segmentDuration}
|
segmentDuration={segmentDuration}
|
||||||
|
timelineDuration={timelineDuration}
|
||||||
showHandlebar={showHandlebar}
|
showHandlebar={showHandlebar}
|
||||||
isDragging={isDragging}
|
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}
|
{segments}
|
||||||
</ReviewTimeline>
|
</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 { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||||
import { MotionData, ReviewSegment } from "@/types/review";
|
import { MotionData, ReviewSegment } from "@/types/review";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
@ -42,10 +42,8 @@ export function MotionSegment({
|
|||||||
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
||||||
useMotionSegmentUtils(segmentDuration, motion_events);
|
useMotionSegmentUtils(segmentDuration, motion_events);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||||
events,
|
useTimelineUtils(segmentDuration);
|
||||||
segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { handleTouchStart } = useTapUtils();
|
const { handleTouchStart } = useTapUtils();
|
||||||
|
|
||||||
@ -180,7 +178,7 @@ export function MotionSegment({
|
|||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
className={segmentClasses}
|
className={segmentClasses}
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
onTouchStart={(event) => handleTouchStart(event, segmentClick)}
|
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||||
>
|
>
|
||||||
<MinimapBounds
|
<MinimapBounds
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
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";
|
import { isIOS, isMobile } from "react-device-detect";
|
||||||
|
|
||||||
export type ReviewTimelineProps = {
|
export type ReviewTimelineProps = {
|
||||||
timelineRef: RefObject<HTMLDivElement>;
|
timelineRef: RefObject<HTMLDivElement>;
|
||||||
handlebarRef: RefObject<HTMLDivElement>;
|
handlebarRef: RefObject<HTMLDivElement>;
|
||||||
handlebarTimeRef: RefObject<HTMLDivElement>;
|
handlebarTimeRef: RefObject<HTMLDivElement>;
|
||||||
handleMouseMove: (
|
handlebarMouseMove: (
|
||||||
e:
|
e:
|
||||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
| React.TouchEvent<HTMLDivElement>,
|
| React.TouchEvent<HTMLDivElement>,
|
||||||
) => void;
|
) => void;
|
||||||
handleMouseUp: (
|
handlebarMouseUp: (
|
||||||
e:
|
e:
|
||||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
| React.TouchEvent<HTMLDivElement>,
|
| React.TouchEvent<HTMLDivElement>,
|
||||||
) => void;
|
) => void;
|
||||||
handleMouseDown: (
|
handlebarMouseDown: (
|
||||||
e:
|
e:
|
||||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
| React.TouchEvent<HTMLDivElement>,
|
| React.TouchEvent<HTMLDivElement>,
|
||||||
) => void;
|
) => void;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
|
timelineDuration: number;
|
||||||
showHandlebar: boolean;
|
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;
|
isDragging: boolean;
|
||||||
|
exportStartPosition?: number;
|
||||||
|
exportEndPosition?: number;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,14 +77,156 @@ export function ReviewTimeline({
|
|||||||
timelineRef,
|
timelineRef,
|
||||||
handlebarRef,
|
handlebarRef,
|
||||||
handlebarTimeRef,
|
handlebarTimeRef,
|
||||||
handleMouseMove,
|
handlebarMouseMove,
|
||||||
handleMouseUp,
|
handlebarMouseUp,
|
||||||
handleMouseDown,
|
handlebarMouseDown,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
|
timelineDuration,
|
||||||
showHandlebar = false,
|
showHandlebar = false,
|
||||||
|
showExportHandles = false,
|
||||||
|
exportStartRef,
|
||||||
|
exportStartTimeRef,
|
||||||
|
exportEndRef,
|
||||||
|
exportEndTimeRef,
|
||||||
|
exportStartMouseMove,
|
||||||
|
exportStartMouseUp,
|
||||||
|
exportStartMouseDown,
|
||||||
|
exportEndMouseMove,
|
||||||
|
exportEndMouseUp,
|
||||||
|
exportEndMouseDown,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
exportStartPosition,
|
||||||
|
exportEndPosition,
|
||||||
children,
|
children,
|
||||||
}: ReviewTimelineProps) {
|
}: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={timelineRef}
|
ref={timelineRef}
|
||||||
@ -46,7 +235,9 @@ export function ReviewTimeline({
|
|||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onTouchEnd={handleMouseUp}
|
onTouchEnd={handleMouseUp}
|
||||||
className={`relative h-full overflow-y-auto no-scrollbar bg-secondary ${
|
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">
|
<div className="flex flex-col relative">
|
||||||
@ -62,8 +253,8 @@ export function ReviewTimeline({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center touch-none select-none"
|
className="flex items-center justify-center touch-none select-none"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleHandlebar}
|
||||||
onTouchStart={handleMouseDown}
|
onTouchStart={handleHandlebar}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative w-full ${
|
className={`relative w-full ${
|
||||||
@ -73,20 +264,90 @@ export function ReviewTimeline({
|
|||||||
<div
|
<div
|
||||||
className={`bg-destructive rounded-full mx-auto ${
|
className={`bg-destructive rounded-full mx-auto ${
|
||||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
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
|
<div
|
||||||
ref={handlebarTimeRef}
|
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>
|
</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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export function MinimapBounds({
|
|||||||
<>
|
<>
|
||||||
{isFirstSegmentInMinimap && (
|
{isFirstSegmentInMinimap && (
|
||||||
<div
|
<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}
|
ref={firstMinimapSegmentRef}
|
||||||
>
|
>
|
||||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||||
@ -44,7 +44,7 @@ export function MinimapBounds({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLastSegmentInMinimap && (
|
{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([], {
|
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -61,14 +61,14 @@ export function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
|
|||||||
<div className="absolute">
|
<div className="absolute">
|
||||||
<div className="flex items-end content-end w-[12px] h-2">
|
<div className="flex items-end content-end w-[12px] h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-0.5 ${
|
className={`pointer-events-none h-0.5 ${
|
||||||
timestamp.getMinutes() % timestampSpread === 0 &&
|
timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
timestamp.getSeconds() === 0
|
timestamp.getSeconds() === 0
|
||||||
? "w-[12px] bg-neutral-600 dark:bg-neutral-500"
|
? "w-[12px] bg-neutral-600 dark:bg-neutral-500"
|
||||||
: timestamp.getMinutes() % (timestampSpread == 15 ? 5 : 1) ===
|
: timestamp.getMinutes() % (timestampSpread == 15 ? 5 : 1) ===
|
||||||
0 && timestamp.getSeconds() === 0
|
0 && timestamp.getSeconds() === 0
|
||||||
? "w-[8px] bg-neutral-500 dark:bg-neutral-600" // Minor tick mark
|
? "w-[8px] bg-neutral-500" // Minor tick mark
|
||||||
: "w-[5px] bg-neutral-400 dark:bg-neutral-700"
|
: "w-[5px] bg-neutral-400 dark:bg-neutral-600"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@ export function Timestamp({
|
|||||||
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_timestamp`}
|
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.getMinutes() % timestampSpread === 0 &&
|
||||||
timestamp.getSeconds() === 0 &&
|
timestamp.getSeconds() === 0 &&
|
||||||
|
@ -1,41 +1,46 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import scrollIntoView from "scroll-into-view-if-needed";
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
|
import { useTimelineUtils } from "./use-timeline-utils";
|
||||||
|
|
||||||
type DragHandlerProps = {
|
type DraggableElementProps = {
|
||||||
contentRef: React.RefObject<HTMLElement>;
|
contentRef: React.RefObject<HTMLElement>;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement>;
|
||||||
handlebarRef: React.RefObject<HTMLDivElement>;
|
draggableElementRef: React.RefObject<HTMLDivElement>;
|
||||||
alignStartDateToTimeline: (time: number) => number;
|
|
||||||
alignEndDateToTimeline: (time: number) => number;
|
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
showHandlebar: boolean;
|
showDraggableElement: boolean;
|
||||||
handlebarTime?: number;
|
draggableElementTime?: number;
|
||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
draggableElementEarliestTime?: number;
|
||||||
handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
draggableElementLatestTime?: number;
|
||||||
|
setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
timelineDuration: number;
|
timelineDuration: number;
|
||||||
timelineStartAligned: number;
|
timelineStartAligned: number;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useDraggableHandler({
|
function useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
handlebarRef,
|
draggableElementRef,
|
||||||
alignStartDateToTimeline,
|
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showHandlebar,
|
showDraggableElement,
|
||||||
handlebarTime,
|
draggableElementTime,
|
||||||
setHandlebarTime,
|
draggableElementEarliestTime,
|
||||||
handlebarTimeRef,
|
draggableElementLatestTime,
|
||||||
|
setDraggableElementTime,
|
||||||
|
draggableElementTimeRef,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
timelineStartAligned,
|
timelineStartAligned,
|
||||||
isDragging,
|
isDragging,
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
}: DragHandlerProps) {
|
setDraggableElementPosition,
|
||||||
|
}: DraggableElementProps) {
|
||||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||||
|
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
||||||
|
|
||||||
const draggingAtTopEdge = useMemo(() => {
|
const draggingAtTopEdge = useMemo(() => {
|
||||||
if (clientYPosition && timelineRef.current) {
|
if (clientYPosition && timelineRef.current) {
|
||||||
@ -78,17 +83,32 @@ function useDraggableHandler({
|
|||||||
(
|
(
|
||||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
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();
|
e.stopPropagation();
|
||||||
getClientYPosition(e);
|
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
|
|
||||||
if (handlebarRef.current && clientYPosition && isDesktop) {
|
let clientY;
|
||||||
const handlebarRect = handlebarRef.current.getBoundingClientRect();
|
if (isMobile && e.nativeEvent instanceof TouchEvent) {
|
||||||
setInitialClickAdjustment(clientYPosition - handlebarRect.top);
|
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(
|
const handleMouseUp = useCallback(
|
||||||
@ -114,19 +134,36 @@ function useDraggableHandler({
|
|||||||
return scrollTop;
|
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,
|
segmentStartTime: number,
|
||||||
scrollTimeline: boolean,
|
scrollTimeline: boolean,
|
||||||
updateHandle: boolean,
|
updateHandle: boolean,
|
||||||
) => {
|
) => {
|
||||||
const thumb = handlebarRef.current;
|
const thumb = draggableElementRef.current;
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
thumb.style.top = `${newHandlePosition}px`;
|
thumb.style.top = `${newElementPosition}px`;
|
||||||
if (handlebarTimeRef.current) {
|
if (setDraggableElementPosition) {
|
||||||
handlebarTimeRef.current.textContent = new Date(
|
setDraggableElementPosition(newElementPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggableElementTimeRef.current) {
|
||||||
|
draggableElementTimeRef.current.textContent = new Date(
|
||||||
segmentStartTime * 1000,
|
segmentStartTime * 1000,
|
||||||
).toLocaleTimeString([], {
|
).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@ -143,12 +180,18 @@ function useDraggableHandler({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setHandlebarTime && updateHandle) {
|
if (setDraggableElementTime && updateHandle) {
|
||||||
setHandlebarTime(segmentStartTime);
|
setDraggableElementTime(segmentStartTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[segmentDuration, handlebarTimeRef, handlebarRef, setHandlebarTime],
|
[
|
||||||
|
segmentDuration,
|
||||||
|
draggableElementTimeRef,
|
||||||
|
draggableElementRef,
|
||||||
|
setDraggableElementTime,
|
||||||
|
setDraggableElementPosition,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
@ -158,7 +201,7 @@ function useDraggableHandler({
|
|||||||
if (
|
if (
|
||||||
!contentRef.current ||
|
!contentRef.current ||
|
||||||
!timelineRef.current ||
|
!timelineRef.current ||
|
||||||
!handlebarRef.current
|
!draggableElementRef.current
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -166,7 +209,7 @@ function useDraggableHandler({
|
|||||||
getClientYPosition(e);
|
getClientYPosition(e);
|
||||||
},
|
},
|
||||||
|
|
||||||
[contentRef, handlebarRef, timelineRef, getClientYPosition],
|
[contentRef, draggableElementRef, timelineRef, getClientYPosition],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -175,7 +218,7 @@ function useDraggableHandler({
|
|||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (
|
if (
|
||||||
timelineRef.current &&
|
timelineRef.current &&
|
||||||
showHandlebar &&
|
showDraggableElement &&
|
||||||
isDragging &&
|
isDragging &&
|
||||||
clientYPosition
|
clientYPosition
|
||||||
) {
|
) {
|
||||||
@ -190,13 +233,21 @@ function useDraggableHandler({
|
|||||||
|
|
||||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||||
|
|
||||||
const newHandlePosition = Math.min(
|
// bottom of timeline
|
||||||
// end of timeline
|
const elementEarliest = draggableElementEarliestTime
|
||||||
segmentHeight * (timelineDuration / segmentDuration) -
|
? timestampToPixels(draggableElementEarliestTime)
|
||||||
segmentHeight * 2,
|
: 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(
|
Math.max(
|
||||||
// start of timeline - 2 segments added for handlebar visibility
|
elementLatest,
|
||||||
segmentHeight * 2 + scrolled,
|
|
||||||
// current Y position
|
// current Y position
|
||||||
clientYPosition -
|
clientYPosition -
|
||||||
timelineTop +
|
timelineTop +
|
||||||
@ -205,7 +256,7 @@ function useDraggableHandler({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
|
const segmentIndex = Math.floor(newElementPosition / segmentHeight);
|
||||||
const segmentStartTime = alignStartDateToTimeline(
|
const segmentStartTime = alignStartDateToTimeline(
|
||||||
timelineStartAligned - segmentIndex * segmentDuration,
|
timelineStartAligned - segmentIndex * segmentDuration,
|
||||||
);
|
);
|
||||||
@ -224,17 +275,17 @@ function useDraggableHandler({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHandlebarPosition(
|
updateDraggableElementPosition(
|
||||||
newHandlePosition - segmentHeight,
|
newElementPosition - segmentHeight,
|
||||||
segmentStartTime,
|
segmentStartTime,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (setHandlebarTime) {
|
if (setDraggableElementTime) {
|
||||||
setHandlebarTime(
|
setDraggableElementTime(
|
||||||
timelineStartAligned -
|
timelineStartAligned -
|
||||||
((newHandlePosition - segmentHeight / 2 - 2) / segmentHeight) *
|
((newElementPosition - segmentHeight / 2 - 2) / segmentHeight) *
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -264,22 +315,21 @@ function useDraggableHandler({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
clientYPosition,
|
clientYPosition,
|
||||||
isDragging,
|
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
timelineStartAligned,
|
timelineStartAligned,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
draggingAtTopEdge,
|
draggingAtTopEdge,
|
||||||
draggingAtBottomEdge,
|
draggingAtBottomEdge,
|
||||||
showHandlebar,
|
showDraggableElement,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
timelineRef.current &&
|
timelineRef.current &&
|
||||||
handlebarRef.current &&
|
draggableElementRef.current &&
|
||||||
showHandlebar &&
|
showDraggableElement &&
|
||||||
handlebarTime &&
|
draggableElementTime &&
|
||||||
!isDragging
|
!isDragging
|
||||||
) {
|
) {
|
||||||
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
||||||
@ -290,20 +340,30 @@ function useDraggableHandler({
|
|||||||
|
|
||||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||||
|
|
||||||
const newHandlePosition =
|
const newElementPosition =
|
||||||
((timelineStartAligned - handlebarTime) / segmentDuration) *
|
((timelineStartAligned - draggableElementTime) / segmentDuration) *
|
||||||
segmentHeight +
|
segmentHeight +
|
||||||
parentScrollTop -
|
parentScrollTop -
|
||||||
scrolled -
|
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
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handlebarTime, showHandlebar, handlebarRef, timelineStartAligned]);
|
}, [
|
||||||
|
draggableElementTime,
|
||||||
|
showDraggableElement,
|
||||||
|
draggableElementRef,
|
||||||
|
timelineStartAligned,
|
||||||
|
]);
|
||||||
|
|
||||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
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 { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -42,6 +43,8 @@ function Export() {
|
|||||||
"exports/",
|
"exports/",
|
||||||
(url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data),
|
(url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data),
|
||||||
);
|
);
|
||||||
|
const location = useLocation();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Export States
|
// Export States
|
||||||
const [camera, setCamera] = useState<string | undefined>();
|
const [camera, setCamera] = useState<string | undefined>();
|
||||||
@ -142,6 +145,23 @@ function Export() {
|
|||||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
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 (
|
return (
|
||||||
<div className="size-full p-2 overflow-hidden flex flex-col">
|
<div className="size-full p-2 overflow-hidden flex flex-col">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
@ -167,7 +187,7 @@ function Export() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<div className="w-full h-14">
|
<div className="w-full h-14">
|
||||||
<Create>
|
<Create open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<Trigger>
|
<Trigger>
|
||||||
<Button variant="select">New Export</Button>
|
<Button variant="select">New Export</Button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
|
@ -23,7 +23,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
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
|
// Color data
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -129,6 +131,18 @@ function UIPlayground() {
|
|||||||
Math.round((Date.now() / 1000 - 15 * 60) / 60) * 60,
|
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(() => {
|
useMemo(() => {
|
||||||
const initialEvents = Array.from({ length: 50 }, generateRandomEvent);
|
const initialEvents = Array.from({ length: 50 }, generateRandomEvent);
|
||||||
setMockEvents(initialEvents);
|
setMockEvents(initialEvents);
|
||||||
@ -158,8 +172,6 @@ function UIPlayground() {
|
|||||||
timestampSpread: 15,
|
timestampSpread: 15,
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
||||||
|
|
||||||
const possibleZoomLevels = [
|
const possibleZoomLevels = [
|
||||||
{ segmentDuration: 60, timestampSpread: 15 },
|
{ segmentDuration: 60, timestampSpread: 15 },
|
||||||
{ segmentDuration: 30, timestampSpread: 5 },
|
{ segmentDuration: 30, timestampSpread: 5 },
|
||||||
@ -223,6 +235,26 @@ function UIPlayground() {
|
|||||||
<p className="text-small">
|
<p className="text-small">
|
||||||
Handlebar is dragging: {isDragging ? "yes" : "no"}
|
Handlebar is dragging: {isDragging ? "yes" : "no"}
|
||||||
</p>
|
</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">
|
<div className="my-4">
|
||||||
<Heading as="h4">Timeline type</Heading>
|
<Heading as="h4">Timeline type</Heading>
|
||||||
<Select
|
<Select
|
||||||
@ -245,17 +277,41 @@ function UIPlayground() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="w-[40px] my-4">
|
||||||
<CameraActivityIndicator />
|
<CameraActivityIndicator />
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
|
||||||
{birdseyeConfig && (
|
|
||||||
<BirdseyeLivePlayer
|
|
||||||
birdseyeConfig={birdseyeConfig}
|
|
||||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
<Button onClick={handleZoomOut} disabled={zoomLevel === 0}>
|
<Button onClick={handleZoomOut} disabled={zoomLevel === 0}>
|
||||||
Zoom Out
|
Zoom Out
|
||||||
@ -267,6 +323,14 @@ function UIPlayground() {
|
|||||||
Zoom In
|
Zoom In
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
|
<div className="">
|
||||||
|
{birdseyeConfig && (
|
||||||
|
<BirdseyeLivePlayer
|
||||||
|
birdseyeConfig={birdseyeConfig}
|
||||||
|
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Heading as="h4" className="my-5">
|
<Heading as="h4" className="my-5">
|
||||||
Color scheme
|
Color scheme
|
||||||
</Heading>
|
</Heading>
|
||||||
@ -293,14 +357,6 @@ function UIPlayground() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
|
||||||
{!isEventsReviewTimeline && (
|
{!isEventsReviewTimeline && (
|
||||||
<MotionReviewTimeline
|
<MotionReviewTimeline
|
||||||
@ -315,6 +371,11 @@ function UIPlayground() {
|
|||||||
showMinimap // show / hide the minimap
|
showMinimap // show / hide the minimap
|
||||||
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
|
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)
|
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
|
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
||||||
motion_events={mockMotionData}
|
motion_events={mockMotionData}
|
||||||
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
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
|
showMinimap // show / hide the minimap
|
||||||
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
|
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)
|
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
|
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
|
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
|
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 EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
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 { useScrollLockout } from "@/hooks/use-mouse-listener";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Preview } from "@/types/preview";
|
import { Preview } from "@/types/preview";
|
||||||
@ -355,10 +355,7 @@ function DetectionReview({
|
|||||||
|
|
||||||
// timeline interaction
|
// timeline interaction
|
||||||
|
|
||||||
const { alignStartDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
||||||
reviewItems?.all ?? [],
|
|
||||||
segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollLock = useScrollLockout(contentRef);
|
const scrollLock = useScrollLockout(contentRef);
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--selected: hsl(228, 89%, 63%);
|
--selected: hsl(228, 89%, 63%);
|
||||||
--selected: 228, 89%, 63%;
|
--selected: 228 89% 63%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
@ -75,13 +75,13 @@
|
|||||||
--severity_motion_dimmed: var(--yellow-200);
|
--severity_motion_dimmed: var(--yellow-200);
|
||||||
|
|
||||||
--motion_review: hsl(44, 94%, 50%);
|
--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: hsl(44, 60%, 40%);
|
||||||
--motion_review_dimmed: 44, 60%, 40%;
|
--motion_review_dimmed: 44 60% 40%;
|
||||||
|
|
||||||
--audio_review: hsl(228, 94%, 67%);
|
--audio_review: hsl(228, 94%, 67%);
|
||||||
--audio_review: 228, 94%, 67%;
|
--audio_review: 228 94% 67%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -146,6 +146,6 @@
|
|||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
|
||||||
--selected: hsl(228, 89%, 63%);
|
--selected: hsl(228, 89%, 63%);
|
||||||
--selected: 228, 89%, 63%;
|
--selected: 228 89% 63%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user