mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Timeline handlebar changes (#10170)
* auto scrolling handlebar with preview time * tablets can show 2 columns on the event view grid * font sizes * hide minimap when previewing
This commit is contained in:
		
							parent
							
								
									49530dc2e4
								
							
						
					
					
						commit
						a49e1bbc64
					
				| @ -33,7 +33,7 @@ import { useSwipeable } from "react-swipeable"; | |||||||
| type PreviewPlayerProps = { | type PreviewPlayerProps = { | ||||||
|   review: ReviewSegment; |   review: ReviewSegment; | ||||||
|   allPreviews?: Preview[]; |   allPreviews?: Preview[]; | ||||||
|   onTimeUpdate?: (time: number | undefined) => void; |   onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>; | ||||||
|   setReviewed: (reviewId: string) => void; |   setReviewed: (reviewId: string) => void; | ||||||
|   markAboveReviewed: () => void; |   markAboveReviewed: () => void; | ||||||
|   onClick: (reviewId: string, ctrl: boolean) => void; |   onClick: (reviewId: string, ctrl: boolean) => void; | ||||||
|  | |||||||
| @ -45,10 +45,9 @@ export function EventReviewTimeline({ | |||||||
|   onHandlebarDraggingChange, |   onHandlebarDraggingChange, | ||||||
| }: EventReviewTimelineProps) { | }: EventReviewTimelineProps) { | ||||||
|   const [isDragging, setIsDragging] = useState(false); |   const [isDragging, setIsDragging] = useState(false); | ||||||
|   const [currentTimeSegment, setCurrentTimeSegment] = useState<number>(0); |  | ||||||
|   const scrollTimeRef = useRef<HTMLDivElement>(null); |   const scrollTimeRef = useRef<HTMLDivElement>(null); | ||||||
|   const timelineRef = useRef<HTMLDivElement>(null); |   const timelineRef = useRef<HTMLDivElement>(null); | ||||||
|   const currentTimeRef = useRef<HTMLDivElement>(null); |   const handlebarTimeRef = useRef<HTMLDivElement>(null); | ||||||
|   const observer = useRef<ResizeObserver | null>(null); |   const observer = useRef<ResizeObserver | null>(null); | ||||||
|   const timelineDuration = useMemo( |   const timelineDuration = useMemo( | ||||||
|     () => timelineStart - timelineEnd, |     () => timelineStart - timelineEnd, | ||||||
| @ -69,12 +68,13 @@ export function EventReviewTimeline({ | |||||||
|       alignEndDateToTimeline, |       alignEndDateToTimeline, | ||||||
|       segmentDuration, |       segmentDuration, | ||||||
|       showHandlebar, |       showHandlebar, | ||||||
|  |       handlebarTime, | ||||||
|  |       setHandlebarTime, | ||||||
|       timelineDuration, |       timelineDuration, | ||||||
|       timelineStart, |       timelineStart, | ||||||
|       isDragging, |       isDragging, | ||||||
|       setIsDragging, |       setIsDragging, | ||||||
|       currentTimeRef, |       handlebarTimeRef, | ||||||
|       setHandlebarTime, |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|   function handleResize() { |   function handleResize() { | ||||||
| @ -151,66 +151,15 @@ export function EventReviewTimeline({ | |||||||
|     ], |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |  | ||||||
|     if (showHandlebar) { |  | ||||||
|       requestAnimationFrame(() => { |  | ||||||
|         if (currentTimeRef.current && currentTimeSegment) { |  | ||||||
|           currentTimeRef.current.textContent = new Date( |  | ||||||
|             currentTimeSegment * 1000, |  | ||||||
|           ).toLocaleTimeString([], { |  | ||||||
|             hour: "2-digit", |  | ||||||
|             minute: "2-digit", |  | ||||||
|             ...(segmentDuration < 60 && { second: "2-digit" }), |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     // we know that these deps are correct
 |  | ||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 |  | ||||||
|   }, [currentTimeSegment, showHandlebar]); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (onHandlebarDraggingChange) { |     if (onHandlebarDraggingChange) { | ||||||
|       onHandlebarDraggingChange(isDragging); |       onHandlebarDraggingChange(isDragging); | ||||||
|     } |     } | ||||||
|   }, [isDragging, onHandlebarDraggingChange]); |   }, [isDragging, onHandlebarDraggingChange]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |  | ||||||
|     if (timelineRef.current && handlebarTime && showHandlebar) { |  | ||||||
|       const { scrollHeight: timelineHeight } = timelineRef.current; |  | ||||||
| 
 |  | ||||||
|       // Calculate the height of an individual segment
 |  | ||||||
|       const segmentHeight = |  | ||||||
|         timelineHeight / (timelineDuration / segmentDuration); |  | ||||||
| 
 |  | ||||||
|       // Calculate the segment index corresponding to the target time
 |  | ||||||
|       const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime); |  | ||||||
|       const segmentIndex = Math.ceil( |  | ||||||
|         (timelineStart - alignedHandlebarTime) / segmentDuration, |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       // Calculate the top position based on the segment index
 |  | ||||||
|       const newTopPosition = Math.max(0, segmentIndex * segmentHeight); |  | ||||||
| 
 |  | ||||||
|       // Set the top position of the handle
 |  | ||||||
|       const thumb = scrollTimeRef.current; |  | ||||||
|       if (thumb) { |  | ||||||
|         requestAnimationFrame(() => { |  | ||||||
|           thumb.style.top = `${newTopPosition}px`; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       setCurrentTimeSegment(alignedHandlebarTime); |  | ||||||
|     } |  | ||||||
|     // should only be run once
 |  | ||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 |  | ||||||
|   }, []); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     generateSegments(); |     generateSegments(); | ||||||
|     if (!currentTimeSegment && !handlebarTime) { | 
 | ||||||
|       setCurrentTimeSegment(timelineStart); |  | ||||||
|     } |  | ||||||
|     // TODO: touch events for mobile
 |     // TODO: touch events for mobile
 | ||||||
|     document.addEventListener("mousemove", handleMouseMove); |     document.addEventListener("mousemove", handleMouseMove); | ||||||
|     document.addEventListener("mouseup", handleMouseUp); |     document.addEventListener("mouseup", handleMouseUp); | ||||||
| @ -220,13 +169,7 @@ export function EventReviewTimeline({ | |||||||
|     }; |     }; | ||||||
|     // 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
 | ||||||
|   }, [ |   }, [generateSegments, timelineStart, handleMouseUp, handleMouseMove]); | ||||||
|     currentTimeSegment, |  | ||||||
|     generateSegments, |  | ||||||
|     timelineStart, |  | ||||||
|     handleMouseUp, |  | ||||||
|     handleMouseMove, |  | ||||||
|   ]); |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
| @ -248,12 +191,12 @@ export function EventReviewTimeline({ | |||||||
|             > |             > | ||||||
|               <div |               <div | ||||||
|                 className={`bg-destructive rounded-full mx-auto ${ |                 className={`bg-destructive rounded-full mx-auto ${ | ||||||
|                   segmentDuration < 60 ? "w-20" : "w-16" |                   segmentDuration < 60 ? "w-14 md:w-20" : "w-12 md:w-16" | ||||||
|                 } h-5 flex items-center justify-center`}
 |                 } h-5 flex items-center justify-center`}
 | ||||||
|               > |               > | ||||||
|                 <div |                 <div | ||||||
|                   ref={currentTimeRef} |                   ref={handlebarTimeRef} | ||||||
|                   className="text-white text-xs z-10" |                   className="text-white text-[8px] md:text-xs z-10" | ||||||
|                 ></div> |                 ></div> | ||||||
|               </div> |               </div> | ||||||
|               <div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div> |               <div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { useCallback, useEffect } from "react"; | import { useCallback, useEffect } from "react"; | ||||||
| 
 | 
 | ||||||
| interface DragHandlerProps { | type DragHandlerProps = { | ||||||
|   contentRef: React.RefObject<HTMLElement>; |   contentRef: React.RefObject<HTMLElement>; | ||||||
|   timelineRef: React.RefObject<HTMLDivElement>; |   timelineRef: React.RefObject<HTMLDivElement>; | ||||||
|   scrollTimeRef: React.RefObject<HTMLDivElement>; |   scrollTimeRef: React.RefObject<HTMLDivElement>; | ||||||
| @ -8,15 +8,15 @@ interface DragHandlerProps { | |||||||
|   alignEndDateToTimeline: (time: number) => number; |   alignEndDateToTimeline: (time: number) => number; | ||||||
|   segmentDuration: number; |   segmentDuration: number; | ||||||
|   showHandlebar: boolean; |   showHandlebar: boolean; | ||||||
|  |   handlebarTime?: number; | ||||||
|  |   setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; | ||||||
|  |   handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>; | ||||||
|   timelineDuration: number; |   timelineDuration: number; | ||||||
|   timelineStart: number; |   timelineStart: number; | ||||||
|   isDragging: boolean; |   isDragging: boolean; | ||||||
|   setIsDragging: React.Dispatch<React.SetStateAction<boolean>>; |   setIsDragging: React.Dispatch<React.SetStateAction<boolean>>; | ||||||
|   currentTimeRef: React.MutableRefObject<HTMLDivElement | null>; | }; | ||||||
|   setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // TODO: handle mobile touch events
 |  | ||||||
| function useDraggableHandler({ | function useDraggableHandler({ | ||||||
|   contentRef, |   contentRef, | ||||||
|   timelineRef, |   timelineRef, | ||||||
| @ -24,12 +24,13 @@ function useDraggableHandler({ | |||||||
|   alignStartDateToTimeline, |   alignStartDateToTimeline, | ||||||
|   segmentDuration, |   segmentDuration, | ||||||
|   showHandlebar, |   showHandlebar, | ||||||
|  |   handlebarTime, | ||||||
|  |   setHandlebarTime, | ||||||
|  |   handlebarTimeRef, | ||||||
|   timelineDuration, |   timelineDuration, | ||||||
|   timelineStart, |   timelineStart, | ||||||
|   isDragging, |   isDragging, | ||||||
|   setIsDragging, |   setIsDragging, | ||||||
|   currentTimeRef, |  | ||||||
|   setHandlebarTime, |  | ||||||
| }: DragHandlerProps) { | }: DragHandlerProps) { | ||||||
|   const handleMouseDown = useCallback( |   const handleMouseDown = useCallback( | ||||||
|     (e: React.MouseEvent<HTMLDivElement>) => { |     (e: React.MouseEvent<HTMLDivElement>) => { | ||||||
| @ -51,6 +52,39 @@ function useDraggableHandler({ | |||||||
|     [isDragging, setIsDragging], |     [isDragging, setIsDragging], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  |   const getCumulativeScrollTop = useCallback((element: HTMLElement | null) => { | ||||||
|  |     let scrollTop = 0; | ||||||
|  |     while (element) { | ||||||
|  |       scrollTop += element.scrollTop; | ||||||
|  |       element = element.parentElement; | ||||||
|  |     } | ||||||
|  |     return scrollTop; | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|  |   const updateHandlebarPosition = useCallback( | ||||||
|  |     (newHandlePosition: number, segmentStartTime: number) => { | ||||||
|  |       const thumb = scrollTimeRef.current; | ||||||
|  |       if (thumb) { | ||||||
|  |         requestAnimationFrame(() => { | ||||||
|  |           thumb.style.top = `${newHandlePosition}px`; | ||||||
|  |           if (handlebarTimeRef.current) { | ||||||
|  |             handlebarTimeRef.current.textContent = new Date( | ||||||
|  |               segmentStartTime * 1000, | ||||||
|  |             ).toLocaleTimeString([], { | ||||||
|  |               hour: "2-digit", | ||||||
|  |               minute: "2-digit", | ||||||
|  |               ...(segmentDuration < 60 && { second: "2-digit" }), | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |         if (setHandlebarTime) { | ||||||
|  |           setHandlebarTime(segmentStartTime); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [segmentDuration, handlebarTimeRef, scrollTimeRef, setHandlebarTime], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   const handleMouseMove = useCallback( |   const handleMouseMove = useCallback( | ||||||
|     (e: MouseEvent) => { |     (e: MouseEvent) => { | ||||||
|       if ( |       if ( | ||||||
| @ -64,7 +98,7 @@ function useDraggableHandler({ | |||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       e.stopPropagation(); |       e.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|       if (isDragging) { |       if (showHandlebar && isDragging) { | ||||||
|         const { |         const { | ||||||
|           scrollHeight: timelineHeight, |           scrollHeight: timelineHeight, | ||||||
|           clientHeight: visibleTimelineHeight, |           clientHeight: visibleTimelineHeight, | ||||||
| @ -75,15 +109,6 @@ function useDraggableHandler({ | |||||||
|         const segmentHeight = |         const segmentHeight = | ||||||
|           timelineHeight / (timelineDuration / segmentDuration); |           timelineHeight / (timelineDuration / segmentDuration); | ||||||
| 
 | 
 | ||||||
|         const getCumulativeScrollTop = (element: HTMLElement | null) => { |  | ||||||
|           let scrollTop = 0; |  | ||||||
|           while (element) { |  | ||||||
|             scrollTop += element.scrollTop; |  | ||||||
|             element = element.parentElement; |  | ||||||
|           } |  | ||||||
|           return scrollTop; |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         const parentScrollTop = getCumulativeScrollTop(timelineRef.current); |         const parentScrollTop = getCumulativeScrollTop(timelineRef.current); | ||||||
| 
 | 
 | ||||||
|         const newHandlePosition = Math.min( |         const newHandlePosition = Math.min( | ||||||
| @ -99,28 +124,11 @@ function useDraggableHandler({ | |||||||
|           timelineStart - segmentIndex * segmentDuration, |           timelineStart - segmentIndex * segmentDuration, | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (showHandlebar) { |         updateHandlebarPosition( | ||||||
|           const thumb = scrollTimeRef.current; |           newHandlePosition - segmentHeight, | ||||||
|           requestAnimationFrame(() => { |           segmentStartTime, | ||||||
|             thumb.style.top = `${newHandlePosition - segmentHeight}px`; |  | ||||||
|             if (currentTimeRef.current) { |  | ||||||
|               currentTimeRef.current.textContent = new Date( |  | ||||||
|                 segmentStartTime * 1000, |  | ||||||
|               ).toLocaleTimeString([], { |  | ||||||
|                 hour: "2-digit", |  | ||||||
|                 minute: "2-digit", |  | ||||||
|                 ...(segmentDuration < 60 && { second: "2-digit" }), |  | ||||||
|               }); |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           if (setHandlebarTime) { |  | ||||||
|             setHandlebarTime( |  | ||||||
|               timelineStart - |  | ||||||
|                 (newHandlePosition / segmentHeight) * segmentDuration, |  | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     // 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
 | ||||||
| @ -131,21 +139,43 @@ function useDraggableHandler({ | |||||||
|       showHandlebar, |       showHandlebar, | ||||||
|       timelineDuration, |       timelineDuration, | ||||||
|       timelineStart, |       timelineStart, | ||||||
|  |       updateHandlebarPosition, | ||||||
|  |       alignStartDateToTimeline, | ||||||
|  |       getCumulativeScrollTop, | ||||||
|     ], |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     // TODO: determine when we want to do this
 |     if ( | ||||||
|     const handlebar = scrollTimeRef.current; |       timelineRef.current && | ||||||
|     if (handlebar && showHandlebar) { |       scrollTimeRef.current && | ||||||
|       handlebar.scrollIntoView({ |       showHandlebar && | ||||||
|  |       handlebarTime && | ||||||
|  |       !isDragging | ||||||
|  |     ) { | ||||||
|  |       const { scrollHeight: timelineHeight, scrollTop: scrolled } = | ||||||
|  |         timelineRef.current; | ||||||
|  | 
 | ||||||
|  |       const segmentHeight = | ||||||
|  |         timelineHeight / (timelineDuration / segmentDuration); | ||||||
|  | 
 | ||||||
|  |       const parentScrollTop = getCumulativeScrollTop(timelineRef.current); | ||||||
|  | 
 | ||||||
|  |       const newHandlePosition = | ||||||
|  |         ((timelineStart - handlebarTime) / segmentDuration) * segmentHeight + | ||||||
|  |         parentScrollTop - | ||||||
|  |         scrolled; | ||||||
|  | 
 | ||||||
|  |       updateHandlebarPosition(newHandlePosition - segmentHeight, handlebarTime); | ||||||
|  | 
 | ||||||
|  |       scrollTimeRef.current.scrollIntoView({ | ||||||
|         behavior: "smooth", |         behavior: "smooth", | ||||||
|         block: "center", |         block: "center", | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     // temporary until behavior is decided
 |     // 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, scrollTimeRef, timelineStart]); | ||||||
| 
 | 
 | ||||||
|   return { handleMouseDown, handleMouseUp, handleMouseMove }; |   return { handleMouseDown, handleMouseUp, handleMouseMove }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -322,7 +322,7 @@ export default function EventView({ | |||||||
|           )} |           )} | ||||||
| 
 | 
 | ||||||
|           <div |           <div | ||||||
|             className="w-full m-2 grid md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4" |             className="w-full m-2 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4" | ||||||
|             ref={contentRef} |             ref={contentRef} | ||||||
|           > |           > | ||||||
|             {currentItems ? ( |             {currentItems ? ( | ||||||
| @ -366,7 +366,7 @@ export default function EventView({ | |||||||
|             timestampSpread={15} |             timestampSpread={15} | ||||||
|             timelineStart={timeRange.before} |             timelineStart={timeRange.before} | ||||||
|             timelineEnd={timeRange.after} |             timelineEnd={timeRange.after} | ||||||
|             showMinimap={showMinimap} |             showMinimap={showMinimap && !previewTime} | ||||||
|             minimapStartTime={minimapBounds.start} |             minimapStartTime={minimapBounds.start} | ||||||
|             minimapEndTime={minimapBounds.end} |             minimapEndTime={minimapBounds.end} | ||||||
|             showHandlebar={previewTime != undefined} |             showHandlebar={previewTime != undefined} | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user