From 36d5e5b45fbdbb18ec5bdbbfcd51e2e4f64d782e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:03:06 -0500 Subject: [PATCH] Timeline tweaks for mobile (#10726) * add dense prop, combine duplicate code, fix mobile bug * put segment height in hook * playground --- .../timeline/EventReviewTimeline.tsx | 142 ++-------- web/src/components/timeline/EventSegment.tsx | 3 + .../timeline/MotionReviewTimeline.tsx | 147 ++-------- web/src/components/timeline/MotionSegment.tsx | 3 + .../components/timeline/ReviewTimeline.tsx | 250 ++++++++++++------ .../components/timeline/segment-metadata.tsx | 8 +- web/src/hooks/use-draggable-element.ts | 43 +-- web/src/hooks/use-timeline-utils.ts | 5 +- web/src/pages/UIPlayground.tsx | 3 + web/src/views/events/EventView.tsx | 2 + 10 files changed, 258 insertions(+), 348 deletions(-) diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index bccf43cd2..fe85f167a 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -1,12 +1,4 @@ -import useDraggableElement from "@/hooks/use-draggable-element"; -import { - useEffect, - useCallback, - useMemo, - useRef, - useState, - RefObject, -} from "react"; +import { useEffect, useCallback, useMemo, useRef, RefObject } from "react"; import EventSegment from "./EventSegment"; import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; @@ -35,6 +27,7 @@ export type EventReviewTimelineProps = { timelineRef?: RefObject; contentRef: RefObject; onHandlebarDraggingChange?: (isDragging: boolean) => void; + dense?: boolean; }; export function EventReviewTimeline({ @@ -59,18 +52,9 @@ export function EventReviewTimeline({ timelineRef, contentRef, onHandlebarDraggingChange, + dense = false, }: EventReviewTimelineProps) { - const [isDragging, setIsDragging] = useState(false); - const [exportStartPosition, setExportStartPosition] = useState(0); - const [exportEndPosition, setExportEndPosition] = useState(0); - const internalTimelineRef = useRef(null); - const handlebarRef = useRef(null); - const handlebarTimeRef = useRef(null); - const exportStartRef = useRef(null); - const exportStartTimeRef = useRef(null); - const exportEndRef = useRef(null); - const exportEndTimeRef = useRef(null); const selectedTimelineRef = timelineRef || internalTimelineRef; const timelineDuration = useMemo( @@ -78,92 +62,17 @@ export function EventReviewTimeline({ [timelineEnd, timelineStart], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( - { - segmentDuration, - timelineDuration, - timelineRef: selectedTimelineRef, - }, - ); + const { alignStartDateToTimeline } = useTimelineUtils({ + segmentDuration, + timelineDuration, + timelineRef: selectedTimelineRef, + }); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart), [timelineStart, alignStartDateToTimeline], ); - 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: selectedTimelineRef, - draggableElementRef: handlebarRef, - segmentDuration, - showDraggableElement: showHandlebar, - draggableElementTime: handlebarTime, - setDraggableElementTime: setHandlebarTime, - timelineDuration, - timelineStartAligned, - isDragging, - setIsDragging, - draggableElementTimeRef: handlebarTimeRef, - }); - - const { - handleMouseDown: exportStartMouseDown, - handleMouseUp: exportStartMouseUp, - handleMouseMove: exportStartMouseMove, - } = useDraggableElement({ - contentRef, - timelineRef: selectedTimelineRef, - 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: selectedTimelineRef, - draggableElementRef: exportEndRef, - segmentDuration, - showDraggableElement: showExportHandles, - draggableElementTime: exportEndTime, - draggableElementEarliestTime: paddedExportStartTime, - setDraggableElementTime: setExportEndTime, - timelineDuration, - timelineStartAligned, - isDragging, - setIsDragging, - draggableElementTimeRef: exportEndTimeRef, - setDraggableElementPosition: setExportEndPosition, - }); - // Generate segments for the timeline const generateSegments = useCallback(() => { const segmentCount = Math.ceil(timelineDuration / segmentDuration); @@ -184,6 +93,7 @@ export function EventReviewTimeline({ severityType={severityType} contentRef={contentRef} setHandlebarTime={setHandlebarTime} + dense={dense} /> ); }); @@ -216,12 +126,6 @@ export function EventReviewTimeline({ ], ); - useEffect(() => { - if (onHandlebarDraggingChange) { - onHandlebarDraggingChange(isDragging); - } - }, [isDragging, onHandlebarDraggingChange]); - useEffect(() => { if ( selectedTimelineRef.current && @@ -254,28 +158,20 @@ export function EventReviewTimeline({ return ( {segments} diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index ab8f30157..08a78a30b 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -30,6 +30,7 @@ type EventSegmentProps = { severityType: ReviewSeverity; contentRef: RefObject; setHandlebarTime?: React.Dispatch>; + dense: boolean; }; export function EventSegment({ @@ -43,6 +44,7 @@ export function EventSegment({ severityType, contentRef, setHandlebarTime, + dense, }: EventSegmentProps) { const { getSeverity, @@ -212,6 +214,7 @@ export function EventSegment({ alignedMinimapStartTime={alignedMinimapStartTime} alignedMinimapEndTime={alignedMinimapEndTime} firstMinimapSegmentRef={firstMinimapSegmentRef} + dense={dense} /> )} diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx index fa94355ca..8f4e4b51d 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -1,12 +1,4 @@ -import useDraggableElement from "@/hooks/use-draggable-element"; -import { - useEffect, - useCallback, - useMemo, - useRef, - useState, - RefObject, -} from "react"; +import { useEffect, useCallback, useMemo, useRef, RefObject } from "react"; import MotionSegment from "./MotionSegment"; import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; @@ -37,6 +29,7 @@ export type MotionReviewTimelineProps = { contentRef: RefObject; timelineRef?: RefObject; onHandlebarDraggingChange?: (isDragging: boolean) => void; + dense?: boolean; }; export function MotionReviewTimeline({ @@ -62,111 +55,26 @@ export function MotionReviewTimeline({ contentRef, timelineRef, onHandlebarDraggingChange, + dense = false, }: MotionReviewTimelineProps) { - const [isDragging, setIsDragging] = useState(false); - const [exportStartPosition, setExportStartPosition] = useState(0); - const [exportEndPosition, setExportEndPosition] = useState(0); - const internalTimelineRef = useRef(null); - const handlebarRef = useRef(null); - const handlebarTimeRef = useRef(null); - const exportStartRef = useRef(null); - const exportStartTimeRef = useRef(null); - const exportEndRef = useRef(null); - const exportEndTimeRef = useRef(null); + const selectedTimelineRef = timelineRef || internalTimelineRef; const timelineDuration = useMemo( () => timelineStart - timelineEnd + 4 * segmentDuration, [timelineEnd, timelineStart, segmentDuration], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( - { - segmentDuration, - timelineDuration, - }, - ); + const { alignStartDateToTimeline } = useTimelineUtils({ + segmentDuration, + timelineDuration, + }); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, [timelineStart, alignStartDateToTimeline, segmentDuration], ); - 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: timelineRef || internalTimelineRef, - draggableElementRef: handlebarRef, - segmentDuration, - showDraggableElement: showHandlebar, - draggableElementTime: handlebarTime, - setDraggableElementTime: setHandlebarTime, - initialScrollIntoViewOnly: onlyInitialHandlebarScroll, - timelineDuration, - timelineCollapsed: motionOnly, - timelineStartAligned, - isDragging, - setIsDragging, - draggableElementTimeRef: handlebarTimeRef, - }); - - const { - handleMouseDown: exportStartMouseDown, - handleMouseUp: exportStartMouseUp, - handleMouseMove: exportStartMouseMove, - } = useDraggableElement({ - contentRef, - timelineRef: timelineRef || internalTimelineRef, - 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: timelineRef || internalTimelineRef, - draggableElementRef: exportEndRef, - segmentDuration, - showDraggableElement: showExportHandles, - draggableElementTime: exportEndTime, - draggableElementEarliestTime: paddedExportStartTime, - setDraggableElementTime: setExportEndTime, - timelineDuration, - timelineStartAligned, - isDragging, - setIsDragging, - draggableElementTimeRef: exportEndTimeRef, - setDraggableElementPosition: setExportEndPosition, - }); - // Generate segments for the timeline const generateSegments = useCallback(() => { const segmentCount = Math.ceil(timelineDuration / segmentDuration); @@ -187,6 +95,7 @@ export function MotionReviewTimeline({ minimapStartTime={minimapStartTime} minimapEndTime={minimapEndTime} setHandlebarTime={setHandlebarTime} + dense={dense} /> ); }); @@ -223,14 +132,7 @@ export function MotionReviewTimeline({ ], ); - useEffect(() => { - if (onHandlebarDraggingChange) { - onHandlebarDraggingChange(isDragging); - } - }, [isDragging, onHandlebarDraggingChange]); - const segmentsObserver = useRef(null); - const selectedTimelineRef = timelineRef || internalTimelineRef; useEffect(() => { if (selectedTimelineRef.current && segments && isDesktop) { segmentsObserver.current = new IntersectionObserver( @@ -268,29 +170,22 @@ export function MotionReviewTimeline({ return ( {segments} diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx index c7b30b741..718d443b5 100644 --- a/web/src/components/timeline/MotionSegment.tsx +++ b/web/src/components/timeline/MotionSegment.tsx @@ -19,6 +19,7 @@ type MotionSegmentProps = { minimapStartTime?: number; minimapEndTime?: number; setHandlebarTime?: React.Dispatch>; + dense: boolean; }; export function MotionSegment({ @@ -32,6 +33,7 @@ export function MotionSegment({ minimapStartTime, minimapEndTime, setHandlebarTime, + dense, }: MotionSegmentProps) { const severityType = "all"; const { @@ -203,6 +205,7 @@ export function MotionSegment({ alignedMinimapStartTime={alignedMinimapStartTime} alignedMinimapEndTime={alignedMinimapEndTime} firstMinimapSegmentRef={firstMinimapSegmentRef} + dense={dense} /> )} diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx index 35dcd6c8c..49568c82e 100644 --- a/web/src/components/timeline/ReviewTimeline.tsx +++ b/web/src/components/timeline/ReviewTimeline.tsx @@ -1,3 +1,5 @@ +import useDraggableElement from "@/hooks/use-draggable-element"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { DraggableElement } from "@/types/draggable-element"; import { ReactNode, @@ -12,85 +14,150 @@ import { isIOS, isMobile } from "react-device-detect"; export type ReviewTimelineProps = { timelineRef: RefObject; - handlebarRef: RefObject; - handlebarTimeRef: RefObject; - handlebarMouseMove: (e: MouseEvent | TouchEvent) => void; - handlebarMouseUp: (e: MouseEvent | TouchEvent) => void; - handlebarMouseDown: ( - e: - | React.MouseEvent - | React.TouchEvent, - ) => void; + contentRef: RefObject; segmentDuration: number; timelineDuration: number; + timelineStartAligned: number; showHandlebar: boolean; showExportHandles: boolean; - exportStartRef: RefObject; - exportStartTimeRef: RefObject; - exportEndRef: RefObject; - exportEndTimeRef: RefObject; - exportStartMouseMove: (e: MouseEvent | TouchEvent) => void; - exportStartMouseUp: (e: MouseEvent | TouchEvent) => void; - exportStartMouseDown: ( - e: - | React.MouseEvent - | React.TouchEvent, - ) => void; - exportEndMouseMove: (e: MouseEvent | TouchEvent) => void; - exportEndMouseUp: (e: MouseEvent | TouchEvent) => void; - exportEndMouseDown: ( - e: - | React.MouseEvent - | React.TouchEvent, - ) => void; - isDragging: boolean; - exportStartPosition?: number; - exportEndPosition?: number; + handlebarTime?: number; + setHandlebarTime?: React.Dispatch>; + onHandlebarDraggingChange?: (isDragging: boolean) => void; + onlyInitialHandlebarScroll?: boolean; + exportStartTime?: number; + exportEndTime?: number; + setExportStartTime?: React.Dispatch>; + setExportEndTime?: React.Dispatch>; + dense: boolean; children: ReactNode; }; export function ReviewTimeline({ timelineRef, - handlebarRef, - handlebarTimeRef, - handlebarMouseMove, - handlebarMouseUp, - handlebarMouseDown, + contentRef, segmentDuration, timelineDuration, + timelineStartAligned, showHandlebar = false, showExportHandles = false, - exportStartRef, - exportStartTimeRef, - exportEndRef, - exportEndTimeRef, - exportStartMouseMove, - exportStartMouseUp, - exportStartMouseDown, - exportEndMouseMove, - exportEndMouseUp, - exportEndMouseDown, - isDragging, - exportStartPosition, - exportEndPosition, + handlebarTime, + setHandlebarTime, + onHandlebarDraggingChange, + onlyInitialHandlebarScroll = false, + exportStartTime, + setExportStartTime, + exportEndTime, + setExportEndTime, + dense, children, }: ReviewTimelineProps) { + const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false); + const [isDraggingExportStart, setIsDraggingExportStart] = useState(false); + const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false); + const [exportStartPosition, setExportStartPosition] = useState(0); + const [exportEndPosition, setExportEndPosition] = useState(0); + const handlebarRef = useRef(null); + const handlebarTimeRef = useRef(null); + const exportStartRef = useRef(null); + const exportStartTimeRef = useRef(null); + const exportEndRef = useRef(null); + const exportEndTimeRef = useRef(null); + + const isDragging = useMemo( + () => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd, + [isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd], + ); const exportSectionRef = useRef(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(); + const { alignStartDateToTimeline, alignEndDateToTimeline, segmentHeight } = + useTimelineUtils({ + segmentDuration, + timelineDuration, + timelineRef, + }); + + const paddedExportStartTime = useMemo(() => { + if (exportStartTime) { + return alignStartDateToTimeline(exportStartTime) + segmentDuration; + } + }, [exportStartTime, segmentDuration, alignStartDateToTimeline]); + + const paddedExportEndTime = useMemo(() => { + if (exportEndTime) { + return alignEndDateToTimeline(exportEndTime); + } + }, [exportEndTime, alignEndDateToTimeline]); + + const { + handleMouseDown: handlebarMouseDown, + handleMouseUp: handlebarMouseUp, + handleMouseMove: handlebarMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: handlebarRef, + segmentDuration, + showDraggableElement: showHandlebar, + draggableElementTime: handlebarTime, + setDraggableElementTime: setHandlebarTime, + initialScrollIntoViewOnly: onlyInitialHandlebarScroll, + timelineDuration, + timelineStartAligned, + isDragging: isDraggingHandlebar, + setIsDragging: setIsDraggingHandlebar, + draggableElementTimeRef: handlebarTimeRef, + dense, + }); + + const { + handleMouseDown: exportStartMouseDown, + handleMouseUp: exportStartMouseUp, + handleMouseMove: exportStartMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportStartRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportStartTime, + draggableElementLatestTime: paddedExportEndTime, + setDraggableElementTime: setExportStartTime, + alignSetTimeToSegment: true, + timelineDuration, + timelineStartAligned, + isDragging: isDraggingExportStart, + setIsDragging: setIsDraggingExportStart, + draggableElementTimeRef: exportStartTimeRef, + setDraggableElementPosition: setExportStartPosition, + dense, + }); + + const { + handleMouseDown: exportEndMouseDown, + handleMouseUp: exportEndMouseUp, + handleMouseMove: exportEndMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportEndRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportEndTime, + draggableElementEarliestTime: paddedExportStartTime, + setDraggableElementTime: setExportEndTime, + alignSetTimeToSegment: true, + timelineDuration, + timelineStartAligned, + isDragging: isDraggingExportEnd, + setIsDragging: setIsDraggingExportEnd, + draggableElementTimeRef: exportEndTimeRef, + setDraggableElementPosition: setExportEndPosition, + dense, + }); + const handleHandlebar = useCallback( ( e: @@ -177,6 +244,19 @@ export function ReviewTimeline({ ], ); + const textSizeClasses = useCallback( + (draggableElement: DraggableElement) => { + if (isDragging && isMobile && draggableElementType === draggableElement) { + return "text-lg"; + } else if (dense) { + return "text-[8px] md:text-xs"; + } else { + return "text-xs"; + } + }, + [dense, isDragging, draggableElementType], + ); + useEffect(() => { if ( exportSectionRef.current && @@ -218,6 +298,12 @@ export function ReviewTimeline({ }; }, [handleMouseMove, handleMouseUp, isDragging]); + useEffect(() => { + if (onHandlebarDraggingChange) { + onHandlebarDraggingChange(isDraggingHandlebar); + } + }, [isDraggingHandlebar, onHandlebarDraggingChange]); + return (
{showHandlebar && (
@@ -245,21 +331,25 @@ export function ReviewTimeline({ >
@@ -268,7 +358,7 @@ export function ReviewTimeline({ {showExportHandles && ( <>
@@ -279,21 +369,25 @@ export function ReviewTimeline({ >
@@ -303,7 +397,7 @@ export function ReviewTimeline({ className="bg-selected/50 absolute w-full" >
@@ -318,16 +412,20 @@ export function ReviewTimeline({ }`} >
diff --git a/web/src/components/timeline/segment-metadata.tsx b/web/src/components/timeline/segment-metadata.tsx index 349f56276..33564b507 100644 --- a/web/src/components/timeline/segment-metadata.tsx +++ b/web/src/components/timeline/segment-metadata.tsx @@ -1,11 +1,10 @@ -import { isDesktop } from "react-device-detect"; - type MinimapSegmentProps = { isFirstSegmentInMinimap: boolean; isLastSegmentInMinimap: boolean; alignedMinimapStartTime: number; alignedMinimapEndTime: number; firstMinimapSegmentRef: React.MutableRefObject; + dense: boolean; }; type TickSegmentProps = { @@ -27,6 +26,7 @@ export function MinimapBounds({ alignedMinimapStartTime, alignedMinimapEndTime, firstMinimapSegmentRef, + dense, }: MinimapSegmentProps) { return ( <> @@ -38,7 +38,7 @@ export function MinimapBounds({ {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", - ...(isDesktop && { month: "short", day: "2-digit" }), + ...(!dense && { month: "short", day: "2-digit" }), })} )} @@ -48,7 +48,7 @@ export function MinimapBounds({ {new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", - ...(isDesktop && { month: "short", day: "2-digit" }), + ...(!dense && { month: "short", day: "2-digit" }), })} )} diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index 24b143e93..15b8773b2 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { isDesktop, isMobile } from "react-device-detect"; +import { isMobile } from "react-device-detect"; import scrollIntoView from "scroll-into-view-if-needed"; import { useTimelineUtils } from "./use-timeline-utils"; @@ -13,6 +13,7 @@ type DraggableElementProps = { draggableElementEarliestTime?: number; draggableElementLatestTime?: number; setDraggableElementTime?: React.Dispatch>; + alignSetTimeToSegment?: boolean; initialScrollIntoViewOnly?: boolean; draggableElementTimeRef: React.MutableRefObject; timelineDuration: number; @@ -21,6 +22,7 @@ type DraggableElementProps = { isDragging: boolean; setIsDragging: React.Dispatch>; setDraggableElementPosition?: React.Dispatch>; + dense: boolean; }; function useDraggableElement({ @@ -33,6 +35,7 @@ function useDraggableElement({ draggableElementEarliestTime, draggableElementLatestTime, setDraggableElementTime, + alignSetTimeToSegment = false, initialScrollIntoViewOnly, draggableElementTimeRef, timelineDuration, @@ -41,38 +44,39 @@ function useDraggableElement({ isDragging, setIsDragging, setDraggableElementPosition, + dense, }: DraggableElementProps) { - const segmentHeight = 8; const [clientYPosition, setClientYPosition] = useState(null); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); const [elementScrollIntoView, setElementScrollIntoView] = useState(true); const [scrollEdgeSize, setScrollEdgeSize] = useState(); const [fullTimelineHeight, setFullTimelineHeight] = useState(); const [segments, setSegments] = useState([]); - const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils( - { + const { alignStartDateToTimeline, getCumulativeScrollTop, segmentHeight } = + useTimelineUtils({ segmentDuration: segmentDuration, timelineDuration: timelineDuration, timelineRef, - }, - ); + }); const draggingAtTopEdge = useMemo(() => { if (clientYPosition && timelineRef.current && scrollEdgeSize) { + const timelineRect = timelineRef.current.getBoundingClientRect(); + const timelineTopAbsolute = timelineRect.top; return ( - clientYPosition - timelineRef.current.offsetTop < scrollEdgeSize && - isDragging + clientYPosition - timelineTopAbsolute < scrollEdgeSize && isDragging ); } }, [clientYPosition, timelineRef, isDragging, scrollEdgeSize]); const draggingAtBottomEdge = useMemo(() => { if (clientYPosition && timelineRef.current && scrollEdgeSize) { + const timelineRect = timelineRef.current.getBoundingClientRect(); + const timelineTopAbsolute = timelineRect.top; + const timelineHeightAbsolute = timelineRect.height; return ( - clientYPosition > - timelineRef.current.clientHeight + - timelineRef.current.offsetTop - - scrollEdgeSize && isDragging + timelineTopAbsolute + timelineHeightAbsolute - clientYPosition < + scrollEdgeSize && isDragging ); } }, [clientYPosition, timelineRef, isDragging, scrollEdgeSize]); @@ -141,7 +145,7 @@ function useDraggableElement({ (time: number) => { return ((timelineStartAligned - time) / segmentDuration) * segmentHeight; }, - [segmentDuration, timelineStartAligned], + [segmentDuration, timelineStartAligned, segmentHeight], ); const updateDraggableElementPosition = useCallback( @@ -165,7 +169,7 @@ function useDraggableElement({ ).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", - ...(segmentDuration < 60 && isDesktop && { second: "2-digit" }), + ...(segmentDuration < 60 && !dense && { second: "2-digit" }), }); if (scrollTimeline) { scrollIntoView(thumb, { @@ -188,6 +192,7 @@ function useDraggableElement({ draggableElementRef, setDraggableElementTime, setDraggableElementPosition, + dense, ], ); @@ -322,9 +327,13 @@ function useDraggableElement({ ); if (setDraggableElementTime) { - setDraggableElementTime( - targetSegmentId + segmentDuration * (offset / segmentHeight), - ); + if (alignSetTimeToSegment) { + setDraggableElementTime(targetSegmentId); + } else { + setDraggableElementTime( + targetSegmentId + segmentDuration * (offset / segmentHeight), + ); + } } if (draggingAtTopEdge || draggingAtBottomEdge) { diff --git a/web/src/hooks/use-timeline-utils.ts b/web/src/hooks/use-timeline-utils.ts index 0bd35a39c..9445a5b49 100644 --- a/web/src/hooks/use-timeline-utils.ts +++ b/web/src/hooks/use-timeline-utils.ts @@ -11,6 +11,8 @@ export function useTimelineUtils({ timelineDuration, timelineRef, }: TimelineUtilsProps) { + const segmentHeight = 8; + const alignEndDateToTimeline = useCallback( (time: number): number => { const remainder = time % segmentDuration; @@ -42,8 +44,6 @@ export function useTimelineUtils({ if (timelineRef?.current && timelineDuration) { const { clientHeight: visibleTimelineHeight } = timelineRef.current; - const segmentHeight = 8; - const visibleTime = (visibleTimelineHeight / segmentHeight) * segmentDuration; @@ -56,5 +56,6 @@ export function useTimelineUtils({ alignStartDateToTimeline, getCumulativeScrollTop, getVisibleTimelineDuration, + segmentHeight, }; } diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 7107abade..50647a128 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { useNavigate } from "react-router-dom"; import SummaryTimeline from "@/components/timeline/SummaryTimeline"; +import { isMobile } from "react-device-detect"; // Color data const colors = [ @@ -384,6 +385,7 @@ function UIPlayground() { motion_events={mockMotionData} 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 + dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps /> )} {isEventsReviewTimeline && ( @@ -408,6 +410,7 @@ function UIPlayground() { 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 timelineRef={reviewTimelineRef} // save a ref to this timeline to connect with the summary timeline + dense // dense will produce a smaller handlebar and only minute resolution on timestamps /> )} diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 3dccce110..3d9ccedf5 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -580,6 +580,7 @@ function DetectionReview({ severityType={severity} contentRef={contentRef} timelineRef={reviewTimelineRef} + dense={isMobile} />
@@ -864,6 +865,7 @@ function MotionReview({ setScrubbing(scrubbing); }} + dense={isMobile} />