Timeline tweaks for mobile (#10726)

* add dense prop, combine duplicate code, fix mobile bug

* put segment height in hook

* playground
This commit is contained in:
Josh Hawkins 2024-03-28 10:03:06 -05:00 committed by GitHub
parent 985b2d7b27
commit 36d5e5b45f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 258 additions and 348 deletions

View File

@ -1,12 +1,4 @@
import useDraggableElement from "@/hooks/use-draggable-element"; import { useEffect, useCallback, useMemo, useRef, RefObject } from "react";
import {
useEffect,
useCallback,
useMemo,
useRef,
useState,
RefObject,
} from "react";
import EventSegment from "./EventSegment"; import EventSegment from "./EventSegment";
import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useTimelineUtils } from "@/hooks/use-timeline-utils";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { ReviewSegment, ReviewSeverity } from "@/types/review";
@ -35,6 +27,7 @@ export type EventReviewTimelineProps = {
timelineRef?: RefObject<HTMLDivElement>; timelineRef?: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement>;
onHandlebarDraggingChange?: (isDragging: boolean) => void; onHandlebarDraggingChange?: (isDragging: boolean) => void;
dense?: boolean;
}; };
export function EventReviewTimeline({ export function EventReviewTimeline({
@ -59,18 +52,9 @@ export function EventReviewTimeline({
timelineRef, timelineRef,
contentRef, contentRef,
onHandlebarDraggingChange, onHandlebarDraggingChange,
dense = false,
}: EventReviewTimelineProps) { }: EventReviewTimelineProps) {
const [isDragging, setIsDragging] = useState(false);
const [exportStartPosition, setExportStartPosition] = useState(0);
const [exportEndPosition, setExportEndPosition] = useState(0);
const internalTimelineRef = useRef<HTMLDivElement>(null); const internalTimelineRef = 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 selectedTimelineRef = timelineRef || internalTimelineRef; const selectedTimelineRef = timelineRef || internalTimelineRef;
const timelineDuration = useMemo( const timelineDuration = useMemo(
@ -78,92 +62,17 @@ export function EventReviewTimeline({
[timelineEnd, timelineStart], [timelineEnd, timelineStart],
); );
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( const { alignStartDateToTimeline } = useTimelineUtils({
{
segmentDuration, segmentDuration,
timelineDuration, timelineDuration,
timelineRef: selectedTimelineRef, timelineRef: selectedTimelineRef,
}, });
);
const timelineStartAligned = useMemo( const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart), () => alignStartDateToTimeline(timelineStart),
[timelineStart, alignStartDateToTimeline], [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 // Generate segments for the timeline
const generateSegments = useCallback(() => { const generateSegments = useCallback(() => {
const segmentCount = Math.ceil(timelineDuration / segmentDuration); const segmentCount = Math.ceil(timelineDuration / segmentDuration);
@ -184,6 +93,7 @@ export function EventReviewTimeline({
severityType={severityType} severityType={severityType}
contentRef={contentRef} contentRef={contentRef}
setHandlebarTime={setHandlebarTime} setHandlebarTime={setHandlebarTime}
dense={dense}
/> />
); );
}); });
@ -216,12 +126,6 @@ export function EventReviewTimeline({
], ],
); );
useEffect(() => {
if (onHandlebarDraggingChange) {
onHandlebarDraggingChange(isDragging);
}
}, [isDragging, onHandlebarDraggingChange]);
useEffect(() => { useEffect(() => {
if ( if (
selectedTimelineRef.current && selectedTimelineRef.current &&
@ -254,28 +158,20 @@ export function EventReviewTimeline({
return ( return (
<ReviewTimeline <ReviewTimeline
timelineRef={selectedTimelineRef} timelineRef={selectedTimelineRef}
handlebarRef={handlebarRef} contentRef={contentRef}
handlebarTimeRef={handlebarTimeRef}
handlebarMouseMove={handlebarMouseMove}
handlebarMouseUp={handlebarMouseUp}
handlebarMouseDown={handlebarMouseDown}
segmentDuration={segmentDuration} segmentDuration={segmentDuration}
timelineDuration={timelineDuration} timelineDuration={timelineDuration}
timelineStartAligned={timelineStartAligned}
showHandlebar={showHandlebar} showHandlebar={showHandlebar}
isDragging={isDragging} onHandlebarDraggingChange={onHandlebarDraggingChange}
exportStartMouseMove={exportStartMouseMove}
exportStartMouseUp={exportStartMouseUp}
exportStartMouseDown={exportStartMouseDown}
exportEndMouseMove={exportEndMouseMove}
exportEndMouseUp={exportEndMouseUp}
exportEndMouseDown={exportEndMouseDown}
showExportHandles={showExportHandles} showExportHandles={showExportHandles}
exportStartRef={exportStartRef} handlebarTime={handlebarTime}
exportStartTimeRef={exportStartTimeRef} setHandlebarTime={setHandlebarTime}
exportEndRef={exportEndRef} exportStartTime={exportStartTime}
exportEndTimeRef={exportEndTimeRef} exportEndTime={exportEndTime}
exportStartPosition={exportStartPosition} setExportStartTime={setExportStartTime}
exportEndPosition={exportEndPosition} setExportEndTime={setExportEndTime}
dense={dense}
> >
{segments} {segments}
</ReviewTimeline> </ReviewTimeline>

View File

@ -30,6 +30,7 @@ type EventSegmentProps = {
severityType: ReviewSeverity; severityType: ReviewSeverity;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement>;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean;
}; };
export function EventSegment({ export function EventSegment({
@ -43,6 +44,7 @@ export function EventSegment({
severityType, severityType,
contentRef, contentRef,
setHandlebarTime, setHandlebarTime,
dense,
}: EventSegmentProps) { }: EventSegmentProps) {
const { const {
getSeverity, getSeverity,
@ -212,6 +214,7 @@ export function EventSegment({
alignedMinimapStartTime={alignedMinimapStartTime} alignedMinimapStartTime={alignedMinimapStartTime}
alignedMinimapEndTime={alignedMinimapEndTime} alignedMinimapEndTime={alignedMinimapEndTime}
firstMinimapSegmentRef={firstMinimapSegmentRef} firstMinimapSegmentRef={firstMinimapSegmentRef}
dense={dense}
/> />
)} )}

View File

@ -1,12 +1,4 @@
import useDraggableElement from "@/hooks/use-draggable-element"; import { useEffect, useCallback, useMemo, useRef, RefObject } from "react";
import {
useEffect,
useCallback,
useMemo,
useRef,
useState,
RefObject,
} from "react";
import MotionSegment from "./MotionSegment"; import MotionSegment from "./MotionSegment";
import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useTimelineUtils } from "@/hooks/use-timeline-utils";
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
@ -37,6 +29,7 @@ export type MotionReviewTimelineProps = {
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement>;
timelineRef?: RefObject<HTMLDivElement>; timelineRef?: RefObject<HTMLDivElement>;
onHandlebarDraggingChange?: (isDragging: boolean) => void; onHandlebarDraggingChange?: (isDragging: boolean) => void;
dense?: boolean;
}; };
export function MotionReviewTimeline({ export function MotionReviewTimeline({
@ -62,111 +55,26 @@ export function MotionReviewTimeline({
contentRef, contentRef,
timelineRef, timelineRef,
onHandlebarDraggingChange, onHandlebarDraggingChange,
dense = false,
}: MotionReviewTimelineProps) { }: MotionReviewTimelineProps) {
const [isDragging, setIsDragging] = useState(false);
const [exportStartPosition, setExportStartPosition] = useState(0);
const [exportEndPosition, setExportEndPosition] = useState(0);
const internalTimelineRef = useRef<HTMLDivElement>(null); const internalTimelineRef = useRef<HTMLDivElement>(null);
const handlebarRef = useRef<HTMLDivElement>(null); const selectedTimelineRef = timelineRef || internalTimelineRef;
const handlebarTimeRef = useRef<HTMLDivElement>(null);
const exportStartRef = useRef<HTMLDivElement>(null);
const exportStartTimeRef = useRef<HTMLDivElement>(null);
const exportEndRef = useRef<HTMLDivElement>(null);
const exportEndTimeRef = useRef<HTMLDivElement>(null);
const timelineDuration = useMemo( const timelineDuration = useMemo(
() => timelineStart - timelineEnd + 4 * segmentDuration, () => timelineStart - timelineEnd + 4 * segmentDuration,
[timelineEnd, timelineStart, segmentDuration], [timelineEnd, timelineStart, segmentDuration],
); );
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( const { alignStartDateToTimeline } = useTimelineUtils({
{
segmentDuration, segmentDuration,
timelineDuration, timelineDuration,
}, });
);
const timelineStartAligned = useMemo( const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
[timelineStart, alignStartDateToTimeline, 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 // Generate segments for the timeline
const generateSegments = useCallback(() => { const generateSegments = useCallback(() => {
const segmentCount = Math.ceil(timelineDuration / segmentDuration); const segmentCount = Math.ceil(timelineDuration / segmentDuration);
@ -187,6 +95,7 @@ export function MotionReviewTimeline({
minimapStartTime={minimapStartTime} minimapStartTime={minimapStartTime}
minimapEndTime={minimapEndTime} minimapEndTime={minimapEndTime}
setHandlebarTime={setHandlebarTime} setHandlebarTime={setHandlebarTime}
dense={dense}
/> />
); );
}); });
@ -223,14 +132,7 @@ export function MotionReviewTimeline({
], ],
); );
useEffect(() => {
if (onHandlebarDraggingChange) {
onHandlebarDraggingChange(isDragging);
}
}, [isDragging, onHandlebarDraggingChange]);
const segmentsObserver = useRef<IntersectionObserver | null>(null); const segmentsObserver = useRef<IntersectionObserver | null>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef;
useEffect(() => { useEffect(() => {
if (selectedTimelineRef.current && segments && isDesktop) { if (selectedTimelineRef.current && segments && isDesktop) {
segmentsObserver.current = new IntersectionObserver( segmentsObserver.current = new IntersectionObserver(
@ -268,29 +170,22 @@ export function MotionReviewTimeline({
return ( return (
<ReviewTimeline <ReviewTimeline
timelineRef={timelineRef || internalTimelineRef} timelineRef={selectedTimelineRef}
handlebarRef={handlebarRef} contentRef={contentRef}
handlebarTimeRef={handlebarTimeRef}
handlebarMouseMove={handlebarMouseMove}
handlebarMouseUp={handlebarMouseUp}
handlebarMouseDown={handlebarMouseDown}
segmentDuration={segmentDuration} segmentDuration={segmentDuration}
timelineDuration={timelineDuration} timelineDuration={timelineDuration}
timelineStartAligned={timelineStartAligned}
showHandlebar={showHandlebar} showHandlebar={showHandlebar}
isDragging={isDragging} onHandlebarDraggingChange={onHandlebarDraggingChange}
exportStartMouseMove={exportStartMouseMove} onlyInitialHandlebarScroll={onlyInitialHandlebarScroll}
exportStartMouseUp={exportStartMouseUp}
exportStartMouseDown={exportStartMouseDown}
exportEndMouseMove={exportEndMouseMove}
exportEndMouseUp={exportEndMouseUp}
exportEndMouseDown={exportEndMouseDown}
showExportHandles={showExportHandles} showExportHandles={showExportHandles}
exportStartRef={exportStartRef} handlebarTime={handlebarTime}
exportStartTimeRef={exportStartTimeRef} setHandlebarTime={setHandlebarTime}
exportEndRef={exportEndRef} exportStartTime={exportStartTime}
exportEndTimeRef={exportEndTimeRef} exportEndTime={exportEndTime}
exportStartPosition={exportStartPosition} setExportStartTime={setExportStartTime}
exportEndPosition={exportEndPosition} setExportEndTime={setExportEndTime}
dense={dense}
> >
{segments} {segments}
</ReviewTimeline> </ReviewTimeline>

View File

@ -19,6 +19,7 @@ type MotionSegmentProps = {
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean;
}; };
export function MotionSegment({ export function MotionSegment({
@ -32,6 +33,7 @@ export function MotionSegment({
minimapStartTime, minimapStartTime,
minimapEndTime, minimapEndTime,
setHandlebarTime, setHandlebarTime,
dense,
}: MotionSegmentProps) { }: MotionSegmentProps) {
const severityType = "all"; const severityType = "all";
const { const {
@ -203,6 +205,7 @@ export function MotionSegment({
alignedMinimapStartTime={alignedMinimapStartTime} alignedMinimapStartTime={alignedMinimapStartTime}
alignedMinimapEndTime={alignedMinimapEndTime} alignedMinimapEndTime={alignedMinimapEndTime}
firstMinimapSegmentRef={firstMinimapSegmentRef} firstMinimapSegmentRef={firstMinimapSegmentRef}
dense={dense}
/> />
)} )}

View File

@ -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 { DraggableElement } from "@/types/draggable-element";
import { import {
ReactNode, ReactNode,
@ -12,85 +14,150 @@ import { isIOS, isMobile } from "react-device-detect";
export type ReviewTimelineProps = { export type ReviewTimelineProps = {
timelineRef: RefObject<HTMLDivElement>; timelineRef: RefObject<HTMLDivElement>;
handlebarRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement>;
handlebarTimeRef: RefObject<HTMLDivElement>;
handlebarMouseMove: (e: MouseEvent | TouchEvent) => void;
handlebarMouseUp: (e: MouseEvent | TouchEvent) => void;
handlebarMouseDown: (
e:
| React.MouseEvent<HTMLDivElement, MouseEvent>
| React.TouchEvent<HTMLDivElement>,
) => void;
segmentDuration: number; segmentDuration: number;
timelineDuration: number; timelineDuration: number;
timelineStartAligned: number;
showHandlebar: boolean; showHandlebar: boolean;
showExportHandles: boolean; showExportHandles: boolean;
exportStartRef: RefObject<HTMLDivElement>; handlebarTime?: number;
exportStartTimeRef: RefObject<HTMLDivElement>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
exportEndRef: RefObject<HTMLDivElement>; onHandlebarDraggingChange?: (isDragging: boolean) => void;
exportEndTimeRef: RefObject<HTMLDivElement>; onlyInitialHandlebarScroll?: boolean;
exportStartMouseMove: (e: MouseEvent | TouchEvent) => void; exportStartTime?: number;
exportStartMouseUp: (e: MouseEvent | TouchEvent) => void; exportEndTime?: number;
exportStartMouseDown: ( setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
e: setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
| React.MouseEvent<HTMLDivElement, MouseEvent> dense: boolean;
| React.TouchEvent<HTMLDivElement>,
) => void;
exportEndMouseMove: (e: MouseEvent | TouchEvent) => void;
exportEndMouseUp: (e: MouseEvent | TouchEvent) => void;
exportEndMouseDown: (
e:
| React.MouseEvent<HTMLDivElement, MouseEvent>
| React.TouchEvent<HTMLDivElement>,
) => void;
isDragging: boolean;
exportStartPosition?: number;
exportEndPosition?: number;
children: ReactNode; children: ReactNode;
}; };
export function ReviewTimeline({ export function ReviewTimeline({
timelineRef, timelineRef,
handlebarRef, contentRef,
handlebarTimeRef,
handlebarMouseMove,
handlebarMouseUp,
handlebarMouseDown,
segmentDuration, segmentDuration,
timelineDuration, timelineDuration,
timelineStartAligned,
showHandlebar = false, showHandlebar = false,
showExportHandles = false, showExportHandles = false,
exportStartRef, handlebarTime,
exportStartTimeRef, setHandlebarTime,
exportEndRef, onHandlebarDraggingChange,
exportEndTimeRef, onlyInitialHandlebarScroll = false,
exportStartMouseMove, exportStartTime,
exportStartMouseUp, setExportStartTime,
exportStartMouseDown, exportEndTime,
exportEndMouseMove, setExportEndTime,
exportEndMouseUp, dense,
exportEndMouseDown,
isDragging,
exportStartPosition,
exportEndPosition,
children, children,
}: ReviewTimelineProps) { }: 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<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 isDragging = useMemo(
() => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd,
[isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd],
);
const exportSectionRef = useRef<HTMLDivElement>(null); 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] = const [draggableElementType, setDraggableElementType] =
useState<DraggableElement>(); useState<DraggableElement>();
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( const handleHandlebar = useCallback(
( (
e: 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(() => { useEffect(() => {
if ( if (
exportSectionRef.current && exportSectionRef.current &&
@ -218,6 +298,12 @@ export function ReviewTimeline({
}; };
}, [handleMouseMove, handleMouseUp, isDragging]); }, [handleMouseMove, handleMouseUp, isDragging]);
useEffect(() => {
if (onHandlebarDraggingChange) {
onHandlebarDraggingChange(isDraggingHandlebar);
}
}, [isDraggingHandlebar, onHandlebarDraggingChange]);
return ( return (
<div <div
ref={timelineRef} ref={timelineRef}
@ -234,7 +320,7 @@ export function ReviewTimeline({
</div> </div>
{showHandlebar && ( {showHandlebar && (
<div <div
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`} className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
role="scrollbar" role="scrollbar"
ref={handlebarRef} ref={handlebarRef}
> >
@ -245,21 +331,25 @@ export function ReviewTimeline({
> >
<div <div
className={`relative w-full ${ className={`relative w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab" isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab"
}`} }`}
> >
<div <div
className={`bg-destructive rounded-full mx-auto ${ className={`bg-destructive rounded-full mx-auto ${
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16" dense
} 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`} ? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingHandlebar && isMobile ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`}
> >
<div <div
ref={handlebarTimeRef} ref={handlebarTimeRef}
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "handlebar" ? "text-lg" : "text-[8px] md:text-xs"} z-10`} className={`text-white pointer-events-none ${textSizeClasses("handlebar")} z-10`}
></div> ></div>
</div> </div>
<div <div
className={`absolute h-[4px] w-full bg-destructive ${isDragging && isMobile && draggableElementType == "handlebar" ? "top-1" : "top-1/2 transform -translate-y-1/2"}`} className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
></div> ></div>
</div> </div>
</div> </div>
@ -268,7 +358,7 @@ export function ReviewTimeline({
{showExportHandles && ( {showExportHandles && (
<> <>
<div <div
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`} className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
role="scrollbar" role="scrollbar"
ref={exportEndRef} ref={exportEndRef}
> >
@ -279,21 +369,25 @@ export function ReviewTimeline({
> >
<div <div
className={`relative mt-[6.5px] w-full ${ className={`relative mt-[6.5px] w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab" isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab"
}`} }`}
> >
<div <div
className={`bg-selected -mt-4 mx-auto ${ className={`bg-selected -mt-4 mx-auto ${
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16" dense
} 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`} ? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportEnd && isMobile ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
> >
<div <div
ref={exportEndTimeRef} ref={exportEndTimeRef}
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_end" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`} className={`text-white pointer-events-none ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`}
></div> ></div>
</div> </div>
<div <div
className={`absolute h-[4px] w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_end" ? "top-0" : "top-1/2 transform -translate-y-1/2"}`} className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 transform -translate-y-1/2"}`}
></div> ></div>
</div> </div>
</div> </div>
@ -303,7 +397,7 @@ export function ReviewTimeline({
className="bg-selected/50 absolute w-full" className="bg-selected/50 absolute w-full"
></div> ></div>
<div <div
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`} className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
role="scrollbar" role="scrollbar"
ref={exportStartRef} ref={exportStartRef}
> >
@ -318,16 +412,20 @@ export function ReviewTimeline({
}`} }`}
> >
<div <div
className={`absolute h-[4px] w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_start" ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`} className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
></div> ></div>
<div <div
className={`bg-selected mt-4 mx-auto ${ className={`bg-selected mt-4 mx-auto ${
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16" dense
} 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`} ? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportStart && isMobile ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
> >
<div <div
ref={exportStartTimeRef} ref={exportStartTimeRef}
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_start" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`} className={`text-white pointer-events-none ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
></div> ></div>
</div> </div>
</div> </div>

View File

@ -1,11 +1,10 @@
import { isDesktop } from "react-device-detect";
type MinimapSegmentProps = { type MinimapSegmentProps = {
isFirstSegmentInMinimap: boolean; isFirstSegmentInMinimap: boolean;
isLastSegmentInMinimap: boolean; isLastSegmentInMinimap: boolean;
alignedMinimapStartTime: number; alignedMinimapStartTime: number;
alignedMinimapEndTime: number; alignedMinimapEndTime: number;
firstMinimapSegmentRef: React.MutableRefObject<HTMLDivElement | null>; firstMinimapSegmentRef: React.MutableRefObject<HTMLDivElement | null>;
dense: boolean;
}; };
type TickSegmentProps = { type TickSegmentProps = {
@ -27,6 +26,7 @@ export function MinimapBounds({
alignedMinimapStartTime, alignedMinimapStartTime,
alignedMinimapEndTime, alignedMinimapEndTime,
firstMinimapSegmentRef, firstMinimapSegmentRef,
dense,
}: MinimapSegmentProps) { }: MinimapSegmentProps) {
return ( return (
<> <>
@ -38,7 +38,7 @@ export function MinimapBounds({
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
...(isDesktop && { month: "short", day: "2-digit" }), ...(!dense && { month: "short", day: "2-digit" }),
})} })}
</div> </div>
)} )}
@ -48,7 +48,7 @@ export function MinimapBounds({
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { {new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
...(isDesktop && { month: "short", day: "2-digit" }), ...(!dense && { month: "short", day: "2-digit" }),
})} })}
</div> </div>
)} )}

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react"; 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 scrollIntoView from "scroll-into-view-if-needed";
import { useTimelineUtils } from "./use-timeline-utils"; import { useTimelineUtils } from "./use-timeline-utils";
@ -13,6 +13,7 @@ type DraggableElementProps = {
draggableElementEarliestTime?: number; draggableElementEarliestTime?: number;
draggableElementLatestTime?: number; draggableElementLatestTime?: number;
setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>; setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>;
alignSetTimeToSegment?: boolean;
initialScrollIntoViewOnly?: boolean; initialScrollIntoViewOnly?: boolean;
draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>; draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>;
timelineDuration: number; timelineDuration: number;
@ -21,6 +22,7 @@ type DraggableElementProps = {
isDragging: boolean; isDragging: boolean;
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>; setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>; setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean;
}; };
function useDraggableElement({ function useDraggableElement({
@ -33,6 +35,7 @@ function useDraggableElement({
draggableElementEarliestTime, draggableElementEarliestTime,
draggableElementLatestTime, draggableElementLatestTime,
setDraggableElementTime, setDraggableElementTime,
alignSetTimeToSegment = false,
initialScrollIntoViewOnly, initialScrollIntoViewOnly,
draggableElementTimeRef, draggableElementTimeRef,
timelineDuration, timelineDuration,
@ -41,37 +44,38 @@ function useDraggableElement({
isDragging, isDragging,
setIsDragging, setIsDragging,
setDraggableElementPosition, setDraggableElementPosition,
dense,
}: DraggableElementProps) { }: DraggableElementProps) {
const segmentHeight = 8;
const [clientYPosition, setClientYPosition] = useState<number | null>(null); const [clientYPosition, setClientYPosition] = useState<number | null>(null);
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
const [elementScrollIntoView, setElementScrollIntoView] = useState(true); const [elementScrollIntoView, setElementScrollIntoView] = useState(true);
const [scrollEdgeSize, setScrollEdgeSize] = useState<number>(); const [scrollEdgeSize, setScrollEdgeSize] = useState<number>();
const [fullTimelineHeight, setFullTimelineHeight] = useState<number>(); const [fullTimelineHeight, setFullTimelineHeight] = useState<number>();
const [segments, setSegments] = useState<HTMLDivElement[]>([]); const [segments, setSegments] = useState<HTMLDivElement[]>([]);
const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils( const { alignStartDateToTimeline, getCumulativeScrollTop, segmentHeight } =
{ useTimelineUtils({
segmentDuration: segmentDuration, segmentDuration: segmentDuration,
timelineDuration: timelineDuration, timelineDuration: timelineDuration,
timelineRef, timelineRef,
}, });
);
const draggingAtTopEdge = useMemo(() => { const draggingAtTopEdge = useMemo(() => {
if (clientYPosition && timelineRef.current && scrollEdgeSize) { if (clientYPosition && timelineRef.current && scrollEdgeSize) {
const timelineRect = timelineRef.current.getBoundingClientRect();
const timelineTopAbsolute = timelineRect.top;
return ( return (
clientYPosition - timelineRef.current.offsetTop < scrollEdgeSize && clientYPosition - timelineTopAbsolute < scrollEdgeSize && isDragging
isDragging
); );
} }
}, [clientYPosition, timelineRef, isDragging, scrollEdgeSize]); }, [clientYPosition, timelineRef, isDragging, scrollEdgeSize]);
const draggingAtBottomEdge = useMemo(() => { const draggingAtBottomEdge = useMemo(() => {
if (clientYPosition && timelineRef.current && scrollEdgeSize) { if (clientYPosition && timelineRef.current && scrollEdgeSize) {
const timelineRect = timelineRef.current.getBoundingClientRect();
const timelineTopAbsolute = timelineRect.top;
const timelineHeightAbsolute = timelineRect.height;
return ( return (
clientYPosition > timelineTopAbsolute + timelineHeightAbsolute - clientYPosition <
timelineRef.current.clientHeight +
timelineRef.current.offsetTop -
scrollEdgeSize && isDragging scrollEdgeSize && isDragging
); );
} }
@ -141,7 +145,7 @@ function useDraggableElement({
(time: number) => { (time: number) => {
return ((timelineStartAligned - time) / segmentDuration) * segmentHeight; return ((timelineStartAligned - time) / segmentDuration) * segmentHeight;
}, },
[segmentDuration, timelineStartAligned], [segmentDuration, timelineStartAligned, segmentHeight],
); );
const updateDraggableElementPosition = useCallback( const updateDraggableElementPosition = useCallback(
@ -165,7 +169,7 @@ function useDraggableElement({
).toLocaleTimeString([], { ).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
...(segmentDuration < 60 && isDesktop && { second: "2-digit" }), ...(segmentDuration < 60 && !dense && { second: "2-digit" }),
}); });
if (scrollTimeline) { if (scrollTimeline) {
scrollIntoView(thumb, { scrollIntoView(thumb, {
@ -188,6 +192,7 @@ function useDraggableElement({
draggableElementRef, draggableElementRef,
setDraggableElementTime, setDraggableElementTime,
setDraggableElementPosition, setDraggableElementPosition,
dense,
], ],
); );
@ -322,10 +327,14 @@ function useDraggableElement({
); );
if (setDraggableElementTime) { if (setDraggableElementTime) {
if (alignSetTimeToSegment) {
setDraggableElementTime(targetSegmentId);
} else {
setDraggableElementTime( setDraggableElementTime(
targetSegmentId + segmentDuration * (offset / segmentHeight), targetSegmentId + segmentDuration * (offset / segmentHeight),
); );
} }
}
if (draggingAtTopEdge || draggingAtBottomEdge) { if (draggingAtTopEdge || draggingAtBottomEdge) {
animationFrameId = requestAnimationFrame(handleScroll); animationFrameId = requestAnimationFrame(handleScroll);

View File

@ -11,6 +11,8 @@ export function useTimelineUtils({
timelineDuration, timelineDuration,
timelineRef, timelineRef,
}: TimelineUtilsProps) { }: TimelineUtilsProps) {
const segmentHeight = 8;
const alignEndDateToTimeline = useCallback( const alignEndDateToTimeline = useCallback(
(time: number): number => { (time: number): number => {
const remainder = time % segmentDuration; const remainder = time % segmentDuration;
@ -42,8 +44,6 @@ export function useTimelineUtils({
if (timelineRef?.current && timelineDuration) { if (timelineRef?.current && timelineDuration) {
const { clientHeight: visibleTimelineHeight } = timelineRef.current; const { clientHeight: visibleTimelineHeight } = timelineRef.current;
const segmentHeight = 8;
const visibleTime = const visibleTime =
(visibleTimelineHeight / segmentHeight) * segmentDuration; (visibleTimelineHeight / segmentHeight) * segmentDuration;
@ -56,5 +56,6 @@ export function useTimelineUtils({
alignStartDateToTimeline, alignStartDateToTimeline,
getCumulativeScrollTop, getCumulativeScrollTop,
getVisibleTimelineDuration, getVisibleTimelineDuration,
segmentHeight,
}; };
} }

View File

@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import SummaryTimeline from "@/components/timeline/SummaryTimeline";
import { isMobile } from "react-device-detect";
// Color data // Color data
const colors = [ const colors = [
@ -384,6 +385,7 @@ function UIPlayground() {
motion_events={mockMotionData} motion_events={mockMotionData}
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps
/> />
)} )}
{isEventsReviewTimeline && ( {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 severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
timelineRef={reviewTimelineRef} // save a ref to this timeline to connect with the summary timeline 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
/> />
)} )}
</div> </div>

View File

@ -580,6 +580,7 @@ function DetectionReview({
severityType={severity} severityType={severity}
contentRef={contentRef} contentRef={contentRef}
timelineRef={reviewTimelineRef} timelineRef={reviewTimelineRef}
dense={isMobile}
/> />
</div> </div>
<div className="w-[10px]"> <div className="w-[10px]">
@ -864,6 +865,7 @@ function MotionReview({
setScrubbing(scrubbing); setScrubbing(scrubbing);
}} }}
dense={isMobile}
/> />
</div> </div>