mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +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,46 +45,112 @@ 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) { | ||||||
|  |       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, |     contentRef, | ||||||
|     timelineRef, |     timelineRef, | ||||||
|       handlebarRef, |     draggableElementRef: handlebarRef, | ||||||
|       alignStartDateToTimeline, |  | ||||||
|       alignEndDateToTimeline, |  | ||||||
|     segmentDuration, |     segmentDuration, | ||||||
|       showHandlebar, |     showDraggableElement: showHandlebar, | ||||||
|       handlebarTime, |     draggableElementTime: handlebarTime, | ||||||
|       setHandlebarTime, |     setDraggableElementTime: setHandlebarTime, | ||||||
|     timelineDuration, |     timelineDuration, | ||||||
|     timelineStartAligned, |     timelineStartAligned, | ||||||
|     isDragging, |     isDragging, | ||||||
|     setIsDragging, |     setIsDragging, | ||||||
|       handlebarTimeRef, |     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
 | ||||||
| @ -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,46 +46,112 @@ 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) { | ||||||
|  |       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, |     contentRef, | ||||||
|     timelineRef, |     timelineRef, | ||||||
|       handlebarRef, |     draggableElementRef: handlebarRef, | ||||||
|       alignStartDateToTimeline, |  | ||||||
|       alignEndDateToTimeline, |  | ||||||
|     segmentDuration, |     segmentDuration, | ||||||
|       showHandlebar, |     showDraggableElement: showHandlebar, | ||||||
|       handlebarTime, |     draggableElementTime: handlebarTime, | ||||||
|       setHandlebarTime, |     setDraggableElementTime: setHandlebarTime, | ||||||
|     timelineDuration, |     timelineDuration, | ||||||
|     timelineStartAligned, |     timelineStartAligned, | ||||||
|     isDragging, |     isDragging, | ||||||
|     setIsDragging, |     setIsDragging, | ||||||
|       handlebarTimeRef, |     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
 | ||||||
| @ -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>, | ||||||
|     ) => { |     ) => { | ||||||
|  |       // prevent default only for mouse events
 | ||||||
|  |       // to avoid chrome/android issues
 | ||||||
|  |       if (e.nativeEvent instanceof MouseEvent) { | ||||||
|         e.preventDefault(); |         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