Skip to content

Commit

Permalink
Timeline minimap and scrolling changes (#10589)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
hawkeye217 and NickM-27 authored Mar 21, 2024
1 parent 973275e commit 0ac7aaa
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 26 deletions.
48 changes: 42 additions & 6 deletions web/src/components/timeline/EventReviewTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>;
Expand All @@ -52,6 +54,7 @@ export function EventReviewTimeline({
setExportStartTime,
setExportEndTime,
events,
visibleTimestamps,
severityType,
timelineRef,
contentRef,
Expand All @@ -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),
Expand All @@ -100,7 +109,7 @@ export function EventReviewTimeline({
handleMouseMove: handlebarMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: handlebarRef,
segmentDuration,
showDraggableElement: showHandlebar,
Expand All @@ -119,7 +128,7 @@ export function EventReviewTimeline({
handleMouseMove: exportStartMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: exportStartRef,
segmentDuration,
showDraggableElement: showExportHandles,
Expand All @@ -140,7 +149,7 @@ export function EventReviewTimeline({
handleMouseMove: exportEndMouseMove,
} = useDraggableElement({
contentRef,
timelineRef: timelineRef || internalTimelineRef,
timelineRef: selectedTimelineRef,
draggableElementRef: exportEndRef,
segmentDuration,
showDraggableElement: showExportHandles,
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/timeline/EventSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -199,6 +200,7 @@ export function EventSegment({
return (
<div
key={segmentKey}
data-segment-id={segmentKey}
className={segmentClasses}
onClick={segmentClick}
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/timeline/MotionReviewTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions web/src/components/timeline/MotionSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
16 changes: 10 additions & 6 deletions web/src/components/timeline/SummaryTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions web/src/hooks/use-draggable-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 30 additions & 2 deletions web/src/hooks/use-timeline-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
};
};
}
35 changes: 31 additions & 4 deletions web/src/views/events/EventView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 (
<>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -542,6 +568,7 @@ function DetectionReview({
minimapEndTime={minimapBounds.end}
showHandlebar={previewTime != undefined}
handlebarTime={previewTime}
visibleTimestamps={visibleTimestamps}
events={reviewItems?.all ?? []}
severityType={severity}
contentRef={contentRef}
Expand Down

0 comments on commit 0ac7aaa

Please sign in to comment.