Timeline minimap and scrolling changes (#10589)

* add function to get visible timeline duration

* Don't show minimap when minimap bounds exceed timeline area

* when minimap is hidden, only scroll timeline when needed

* observe only when not showing minimap

* no need to duplicate observer

* fix out of order param

* timeline utils hook props

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins 2024-03-21 12:49:04 -05:00 committed by GitHub
parent 973275e163
commit 0ac7aaabe3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 133 additions and 26 deletions

View File

@ -11,6 +11,7 @@ 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";
import ReviewTimeline from "./ReviewTimeline"; import ReviewTimeline from "./ReviewTimeline";
import scrollIntoView from "scroll-into-view-if-needed";
export type EventReviewTimelineProps = { export type EventReviewTimelineProps = {
segmentDuration: number; segmentDuration: number;
@ -29,6 +30,7 @@ export type EventReviewTimelineProps = {
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>; setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>; setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
events: ReviewSegment[]; events: ReviewSegment[];
visibleTimestamps?: number[];
severityType: ReviewSeverity; severityType: ReviewSeverity;
timelineRef?: RefObject<HTMLDivElement>; timelineRef?: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement>; contentRef: RefObject<HTMLDivElement>;
@ -52,6 +54,7 @@ export function EventReviewTimeline({
setExportStartTime, setExportStartTime,
setExportEndTime, setExportEndTime,
events, events,
visibleTimestamps,
severityType, severityType,
timelineRef, timelineRef,
contentRef, contentRef,
@ -68,14 +71,20 @@ export function EventReviewTimeline({
const exportStartTimeRef = useRef<HTMLDivElement>(null); const exportStartTimeRef = useRef<HTMLDivElement>(null);
const exportEndRef = useRef<HTMLDivElement>(null); const exportEndRef = useRef<HTMLDivElement>(null);
const exportEndTimeRef = useRef<HTMLDivElement>(null); const exportEndTimeRef = useRef<HTMLDivElement>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef;
const timelineDuration = useMemo( const timelineDuration = useMemo(
() => timelineStart - timelineEnd, () => timelineStart - timelineEnd,
[timelineEnd, timelineStart], [timelineEnd, timelineStart],
); );
const { alignStartDateToTimeline, alignEndDateToTimeline } = const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
useTimelineUtils(segmentDuration); {
segmentDuration,
timelineDuration,
timelineRef: selectedTimelineRef,
},
);
const timelineStartAligned = useMemo( const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart), () => alignStartDateToTimeline(timelineStart),
@ -100,7 +109,7 @@ export function EventReviewTimeline({
handleMouseMove: handlebarMouseMove, handleMouseMove: handlebarMouseMove,
} = useDraggableElement({ } = useDraggableElement({
contentRef, contentRef,
timelineRef: timelineRef || internalTimelineRef, timelineRef: selectedTimelineRef,
draggableElementRef: handlebarRef, draggableElementRef: handlebarRef,
segmentDuration, segmentDuration,
showDraggableElement: showHandlebar, showDraggableElement: showHandlebar,
@ -119,7 +128,7 @@ export function EventReviewTimeline({
handleMouseMove: exportStartMouseMove, handleMouseMove: exportStartMouseMove,
} = useDraggableElement({ } = useDraggableElement({
contentRef, contentRef,
timelineRef: timelineRef || internalTimelineRef, timelineRef: selectedTimelineRef,
draggableElementRef: exportStartRef, draggableElementRef: exportStartRef,
segmentDuration, segmentDuration,
showDraggableElement: showExportHandles, showDraggableElement: showExportHandles,
@ -140,7 +149,7 @@ export function EventReviewTimeline({
handleMouseMove: exportEndMouseMove, handleMouseMove: exportEndMouseMove,
} = useDraggableElement({ } = useDraggableElement({
contentRef, contentRef,
timelineRef: timelineRef || internalTimelineRef, timelineRef: selectedTimelineRef,
draggableElementRef: exportEndRef, draggableElementRef: exportEndRef,
segmentDuration, segmentDuration,
showDraggableElement: showExportHandles, showDraggableElement: showExportHandles,
@ -213,9 +222,36 @@ export function EventReviewTimeline({
} }
}, [isDragging, onHandlebarDraggingChange]); }, [isDragging, onHandlebarDraggingChange]);
useEffect(() => {
if (
selectedTimelineRef.current &&
segments &&
visibleTimestamps &&
visibleTimestamps?.length > 0 &&
!showMinimap
) {
const alignedVisibleTimestamps = visibleTimestamps.map(
alignStartDateToTimeline,
);
const element = selectedTimelineRef.current?.querySelector(
`[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`,
);
scrollIntoView(element as HTMLDivElement, {
scrollMode: "if-needed",
behavior: "smooth",
});
}
}, [
selectedTimelineRef,
segments,
showMinimap,
alignStartDateToTimeline,
visibleTimestamps,
]);
return ( return (
<ReviewTimeline <ReviewTimeline
timelineRef={timelineRef || internalTimelineRef} timelineRef={selectedTimelineRef}
handlebarRef={handlebarRef} handlebarRef={handlebarRef}
handlebarTimeRef={handlebarTimeRef} handlebarTimeRef={handlebarTimeRef}
handlebarMouseMove={handlebarMouseMove} handlebarMouseMove={handlebarMouseMove}

View File

@ -53,8 +53,9 @@ export function EventSegment({
getEventThumbnail, getEventThumbnail,
} = useEventSegmentUtils(segmentDuration, events, severityType); } = useEventSegmentUtils(segmentDuration, events, severityType);
const { alignStartDateToTimeline, alignEndDateToTimeline } = const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
useTimelineUtils(segmentDuration); { segmentDuration },
);
const severity = useMemo( const severity = useMemo(
() => getSeverity(segmentTime, displaySeverityType), () => getSeverity(segmentTime, displaySeverityType),
@ -199,6 +200,7 @@ export function EventSegment({
return ( return (
<div <div
key={segmentKey} key={segmentKey}
data-segment-id={segmentKey}
className={segmentClasses} className={segmentClasses}
onClick={segmentClick} onClick={segmentClick}
onTouchEnd={(event) => handleTouchStart(event, segmentClick)} onTouchEnd={(event) => handleTouchStart(event, segmentClick)}

View File

@ -76,8 +76,12 @@ export function MotionReviewTimeline({
[timelineEnd, timelineStart, segmentDuration], [timelineEnd, timelineStart, segmentDuration],
); );
const { alignStartDateToTimeline, alignEndDateToTimeline } = const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
useTimelineUtils(segmentDuration); {
segmentDuration,
timelineDuration,
},
);
const timelineStartAligned = useMemo( const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,

View File

@ -42,8 +42,9 @@ export function MotionSegment({
const { getMotionSegmentValue, interpolateMotionAudioData } = const { getMotionSegmentValue, interpolateMotionAudioData } =
useMotionSegmentUtils(segmentDuration, motion_events); useMotionSegmentUtils(segmentDuration, motion_events);
const { alignStartDateToTimeline, alignEndDateToTimeline } = const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
useTimelineUtils(segmentDuration); { segmentDuration },
);
const { handleTouchStart } = useTapUtils(); const { handleTouchStart } = useTapUtils();

View File

@ -39,18 +39,22 @@ export function SummaryTimeline({
const observer = useRef<ResizeObserver | null>(null); const observer = useRef<ResizeObserver | null>(null);
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration); const reviewTimelineDuration = useMemo(
() => timelineStart - timelineEnd + 4 * segmentDuration,
[timelineEnd, timelineStart, segmentDuration],
);
const { alignStartDateToTimeline } = useTimelineUtils({
segmentDuration,
timelineDuration: reviewTimelineDuration,
timelineRef: reviewTimelineRef,
});
const timelineStartAligned = useMemo( const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
[timelineStart, alignStartDateToTimeline, segmentDuration], [timelineStart, alignStartDateToTimeline, segmentDuration],
); );
const reviewTimelineDuration = useMemo(
() => timelineStart - timelineEnd + 4 * segmentDuration,
[timelineEnd, timelineStart, segmentDuration],
);
// Generate segments for the timeline // Generate segments for the timeline
const generateSegments = useCallback(() => { const generateSegments = useCallback(() => {
const segmentCount = reviewTimelineDuration / segmentDuration; const segmentCount = reviewTimelineDuration / segmentDuration;

View File

@ -40,8 +40,13 @@ function useDraggableElement({
}: DraggableElementProps) { }: DraggableElementProps) {
const [clientYPosition, setClientYPosition] = useState<number | null>(null); const [clientYPosition, setClientYPosition] = useState<number | null>(null);
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
const { alignStartDateToTimeline, getCumulativeScrollTop } = const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils(
useTimelineUtils(segmentDuration); {
segmentDuration: segmentDuration,
timelineDuration: timelineDuration,
timelineRef,
},
);
const draggingAtTopEdge = useMemo(() => { const draggingAtTopEdge = useMemo(() => {
if (clientYPosition && timelineRef.current) { if (clientYPosition && timelineRef.current) {

View File

@ -1,6 +1,16 @@
import { useCallback } from "react"; import { useCallback } from "react";
export const useTimelineUtils = (segmentDuration: number) => { export type TimelineUtilsProps = {
segmentDuration: number;
timelineDuration?: number;
timelineRef?: React.RefObject<HTMLElement>;
};
export function useTimelineUtils({
segmentDuration,
timelineDuration,
timelineRef,
}: TimelineUtilsProps) {
const alignEndDateToTimeline = useCallback( const alignEndDateToTimeline = useCallback(
(time: number): number => { (time: number): number => {
const remainder = time % segmentDuration; const remainder = time % segmentDuration;
@ -28,9 +38,27 @@ export const useTimelineUtils = (segmentDuration: number) => {
return scrollTop; return scrollTop;
}, []); }, []);
const getVisibleTimelineDuration = useCallback(() => {
if (timelineRef?.current && timelineDuration) {
const {
scrollHeight: timelineHeight,
clientHeight: visibleTimelineHeight,
} = timelineRef.current;
const segmentHeight =
timelineHeight / (timelineDuration / segmentDuration);
const visibleTime =
(visibleTimelineHeight / segmentHeight) * segmentDuration;
return visibleTime;
}
}, [segmentDuration, timelineDuration, timelineRef]);
return { return {
alignEndDateToTimeline, alignEndDateToTimeline,
alignStartDateToTimeline, alignStartDateToTimeline,
getCumulativeScrollTop, getCumulativeScrollTop,
getVisibleTimelineDuration,
}; };
}; }

View File

@ -379,7 +379,17 @@ function DetectionReview({
// timeline interaction // timeline interaction
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration); const timelineDuration = useMemo(
() => timeRange.before - timeRange.after,
[timeRange],
);
const { alignStartDateToTimeline, getVisibleTimelineDuration } =
useTimelineUtils({
segmentDuration,
timelineDuration,
timelineRef: reviewTimelineRef,
});
const scrollLock = useScrollLockout(contentRef); const scrollLock = useScrollLockout(contentRef);
@ -448,10 +458,26 @@ function DetectionReview({
return false; return false;
} }
return contentRef.current.scrollHeight > contentRef.current.clientHeight; // don't show minimap if the view is not scrollable
if (contentRef.current.scrollHeight < contentRef.current.clientHeight) {
return false;
}
const visibleTime = getVisibleTimelineDuration();
const minimapTime = minimapBounds.end - minimapBounds.start;
if (visibleTime && minimapTime >= visibleTime * 0.75) {
return false;
}
return true;
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [contentRef.current?.scrollHeight, severity]); }, [contentRef.current?.scrollHeight, minimapBounds]);
const visibleTimestamps = useMemo(
() => minimap.map((str) => parseFloat(str)),
[minimap],
);
return ( return (
<> <>
@ -499,7 +525,7 @@ function DetectionReview({
data-segment-start={ data-segment-start={
alignStartDateToTimeline(value.start_time) - segmentDuration alignStartDateToTimeline(value.start_time) - segmentDuration
} }
className={`outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`} className={`review-item outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`}
> >
<div className="aspect-video rounded-lg overflow-hidden"> <div className="aspect-video rounded-lg overflow-hidden">
<PreviewThumbnailPlayer <PreviewThumbnailPlayer
@ -542,6 +568,7 @@ function DetectionReview({
minimapEndTime={minimapBounds.end} minimapEndTime={minimapBounds.end}
showHandlebar={previewTime != undefined} showHandlebar={previewTime != undefined}
handlebarTime={previewTime} handlebarTime={previewTime}
visibleTimestamps={visibleTimestamps}
events={reviewItems?.all ?? []} events={reviewItems?.all ?? []}
severityType={severity} severityType={severity}
contentRef={contentRef} contentRef={contentRef}