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