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 { ReviewSegment, ReviewSeverity } from "@/types/review";
import ReviewTimeline from "./ReviewTimeline";
import scrollIntoView from "scroll-into-view-if-needed";
export type EventReviewTimelineProps = {
segmentDuration: number;
@ -29,6 +30,7 @@ export type EventReviewTimelineProps = {
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
events: ReviewSegment[];
visibleTimestamps?: number[];
severityType: ReviewSeverity;
timelineRef?: RefObject<HTMLDivElement>;
contentRef: RefObject<HTMLDivElement>;
@ -52,6 +54,7 @@ export function EventReviewTimeline({
setExportStartTime,
setExportEndTime,
events,
visibleTimestamps,
severityType,
timelineRef,
contentRef,
@ -68,14 +71,20 @@ export function EventReviewTimeline({
const exportStartTimeRef = useRef<HTMLDivElement>(null);
const exportEndRef = useRef<HTMLDivElement>(null);
const exportEndTimeRef = useRef<HTMLDivElement>(null);
const selectedTimelineRef = timelineRef || internalTimelineRef;
const timelineDuration = useMemo(
() => timelineStart - timelineEnd,
[timelineEnd, timelineStart],
);
const { alignStartDateToTimeline, alignEndDateToTimeline } =
useTimelineUtils(segmentDuration);
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
{
segmentDuration,
timelineDuration,
timelineRef: selectedTimelineRef,
},
);
const timelineStartAligned = useMemo(
() => alignStartDateToTimeline(timelineStart),
@ -100,7 +109,7 @@ export function EventReviewTimeline({
handleMouseMove: handlebarMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: handlebarRef,
segmentDuration,
showDraggableElement: showHandlebar,
@ -119,7 +128,7 @@ export function EventReviewTimeline({
handleMouseMove: exportStartMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: exportStartRef,
segmentDuration,
showDraggableElement: showExportHandles,
@ -140,7 +149,7 @@ export function EventReviewTimeline({
handleMouseMove: exportEndMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: exportEndRef,
segmentDuration,
showDraggableElement: showExportHandles,
@ -213,9 +222,36 @@ export function EventReviewTimeline({
}
}, [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 (
<ReviewTimeline
timelineRef={timelineRef || internalTimelineRef}
timelineRef={selectedTimelineRef}
handlebarRef={handlebarRef}
handlebarTimeRef={handlebarTimeRef}
handlebarMouseMove={handlebarMouseMove}

View File

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

View File

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

View File

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

View File

@ -39,18 +39,22 @@ export function SummaryTimeline({
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(
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
[timelineStart, alignStartDateToTimeline, segmentDuration],
);
const reviewTimelineDuration = useMemo(
() => timelineStart - timelineEnd + 4 * segmentDuration,
[timelineEnd, timelineStart, segmentDuration],
);
// Generate segments for the timeline
const generateSegments = useCallback(() => {
const segmentCount = reviewTimelineDuration / segmentDuration;

View File

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

View File

@ -1,6 +1,16 @@
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(
(time: number): number => {
const remainder = time % segmentDuration;
@ -28,9 +38,27 @@ export const useTimelineUtils = (segmentDuration: number) => {
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 {
alignEndDateToTimeline,
alignStartDateToTimeline,
getCumulativeScrollTop,
getVisibleTimelineDuration,
};
};
}

View File

@ -379,7 +379,17 @@ function DetectionReview({
// 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);
@ -448,10 +458,26 @@ function DetectionReview({
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
// 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 (
<>
@ -499,7 +525,7 @@ function DetectionReview({
data-segment-start={
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">
<PreviewThumbnailPlayer
@ -542,6 +568,7 @@ function DetectionReview({
minimapEndTime={minimapBounds.end}
showHandlebar={previewTime != undefined}
handlebarTime={previewTime}
visibleTimestamps={visibleTimestamps}
events={reviewItems?.all ?? []}
severityType={severity}
contentRef={contentRef}