2025-02-10 00:13:32 +03:00
|
|
|
import React, {
|
|
|
|
|
useCallback,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
RefObject,
|
|
|
|
|
useEffect,
|
|
|
|
|
} from "react";
|
2024-03-18 23:58:54 +03:00
|
|
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
2025-02-10 00:13:32 +03:00
|
|
|
import {
|
|
|
|
|
MotionData,
|
|
|
|
|
ReviewSegment,
|
|
|
|
|
TimelineZoomDirection,
|
2025-10-29 21:04:29 +03:00
|
|
|
ZoomLevel,
|
2025-02-10 00:13:32 +03:00
|
|
|
} from "@/types/review";
|
2024-03-04 19:42:51 +03:00
|
|
|
import ReviewTimeline from "./ReviewTimeline";
|
2024-04-01 17:23:57 +03:00
|
|
|
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
|
2025-02-10 00:13:32 +03:00
|
|
|
import {
|
|
|
|
|
VirtualizedMotionSegments,
|
|
|
|
|
VirtualizedMotionSegmentsRef,
|
|
|
|
|
} from "./VirtualizedMotionSegments";
|
2025-05-23 17:55:48 +03:00
|
|
|
import { RecordingSegment } from "@/types/record";
|
2024-03-04 19:42:51 +03:00
|
|
|
|
|
|
|
|
export type MotionReviewTimelineProps = {
|
|
|
|
|
segmentDuration: number;
|
|
|
|
|
timestampSpread: number;
|
|
|
|
|
timelineStart: number;
|
|
|
|
|
timelineEnd: number;
|
|
|
|
|
showHandlebar?: boolean;
|
|
|
|
|
handlebarTime?: number;
|
|
|
|
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
2024-03-26 19:29:07 +03:00
|
|
|
onlyInitialHandlebarScroll?: boolean;
|
2024-03-23 16:33:50 +03:00
|
|
|
motionOnly?: boolean;
|
2024-03-04 19:42:51 +03:00
|
|
|
showMinimap?: boolean;
|
|
|
|
|
minimapStartTime?: number;
|
|
|
|
|
minimapEndTime?: number;
|
2024-03-18 23:58:54 +03:00
|
|
|
showExportHandles?: boolean;
|
|
|
|
|
exportStartTime?: number;
|
|
|
|
|
exportEndTime?: number;
|
|
|
|
|
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
|
|
|
|
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
2024-03-04 19:42:51 +03:00
|
|
|
events: ReviewSegment[];
|
2024-03-05 22:55:44 +03:00
|
|
|
motion_events: MotionData[];
|
2025-05-23 17:55:48 +03:00
|
|
|
noRecordingRanges?: RecordingSegment[];
|
2024-03-04 19:42:51 +03:00
|
|
|
contentRef: RefObject<HTMLDivElement>;
|
2024-03-21 05:56:15 +03:00
|
|
|
timelineRef?: RefObject<HTMLDivElement>;
|
2024-03-04 19:42:51 +03:00
|
|
|
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
2024-03-28 18:03:06 +03:00
|
|
|
dense?: boolean;
|
2025-02-10 00:13:32 +03:00
|
|
|
isZooming: boolean;
|
|
|
|
|
zoomDirection: TimelineZoomDirection;
|
2025-10-29 17:39:07 +03:00
|
|
|
alwaysShowMotionLine?: boolean;
|
2025-10-29 21:04:29 +03:00
|
|
|
onZoomChange?: (newZoomLevel: number) => void;
|
|
|
|
|
possibleZoomLevels?: ZoomLevel[];
|
|
|
|
|
currentZoomLevel?: number;
|
2024-03-04 19:42:51 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function MotionReviewTimeline({
|
|
|
|
|
segmentDuration,
|
|
|
|
|
timestampSpread,
|
|
|
|
|
timelineStart,
|
|
|
|
|
timelineEnd,
|
|
|
|
|
showHandlebar = false,
|
|
|
|
|
handlebarTime,
|
|
|
|
|
setHandlebarTime,
|
2024-03-26 19:29:07 +03:00
|
|
|
onlyInitialHandlebarScroll = false,
|
2024-03-23 16:33:50 +03:00
|
|
|
motionOnly = false,
|
2024-03-04 19:42:51 +03:00
|
|
|
showMinimap = false,
|
|
|
|
|
minimapStartTime,
|
|
|
|
|
minimapEndTime,
|
2024-03-18 23:58:54 +03:00
|
|
|
showExportHandles = false,
|
|
|
|
|
exportStartTime,
|
|
|
|
|
exportEndTime,
|
|
|
|
|
setExportStartTime,
|
|
|
|
|
setExportEndTime,
|
2024-03-04 19:42:51 +03:00
|
|
|
events,
|
|
|
|
|
motion_events,
|
2025-05-23 17:55:48 +03:00
|
|
|
noRecordingRanges,
|
2024-03-04 19:42:51 +03:00
|
|
|
contentRef,
|
2024-03-21 05:56:15 +03:00
|
|
|
timelineRef,
|
2024-03-04 19:42:51 +03:00
|
|
|
onHandlebarDraggingChange,
|
2024-03-28 18:03:06 +03:00
|
|
|
dense = false,
|
2025-02-10 00:13:32 +03:00
|
|
|
isZooming,
|
|
|
|
|
zoomDirection,
|
2025-10-29 17:39:07 +03:00
|
|
|
alwaysShowMotionLine = false,
|
2025-10-29 21:04:29 +03:00
|
|
|
onZoomChange,
|
|
|
|
|
possibleZoomLevels,
|
|
|
|
|
currentZoomLevel,
|
2024-03-04 19:42:51 +03:00
|
|
|
}: MotionReviewTimelineProps) {
|
2024-03-21 05:56:15 +03:00
|
|
|
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
2024-03-28 18:03:06 +03:00
|
|
|
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
2025-02-10 00:13:32 +03:00
|
|
|
const virtualizedSegmentsRef = useRef<VirtualizedMotionSegmentsRef>(null);
|
2024-03-18 23:58:54 +03:00
|
|
|
|
2024-03-04 19:42:51 +03:00
|
|
|
const timelineDuration = useMemo(
|
2024-03-12 18:23:54 +03:00
|
|
|
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
|
|
|
|
[timelineEnd, timelineStart, segmentDuration],
|
2024-03-04 19:42:51 +03:00
|
|
|
);
|
|
|
|
|
|
2024-03-28 18:03:06 +03:00
|
|
|
const { alignStartDateToTimeline } = useTimelineUtils({
|
|
|
|
|
segmentDuration,
|
|
|
|
|
timelineDuration,
|
|
|
|
|
});
|
2024-03-04 19:42:51 +03:00
|
|
|
|
2024-03-09 01:49:10 +03:00
|
|
|
const timelineStartAligned = useMemo(
|
2024-03-12 18:23:54 +03:00
|
|
|
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
|
|
|
|
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
2024-03-09 01:49:10 +03:00
|
|
|
);
|
|
|
|
|
|
2024-04-01 17:23:57 +03:00
|
|
|
const { getMotionSegmentValue } = useMotionSegmentUtils(
|
|
|
|
|
segmentDuration,
|
|
|
|
|
motion_events,
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-23 17:55:48 +03:00
|
|
|
const getRecordingAvailability = useCallback(
|
|
|
|
|
(time: number): boolean | undefined => {
|
2025-12-31 15:48:56 +03:00
|
|
|
if (noRecordingRanges == undefined) return undefined;
|
2025-05-23 17:55:48 +03:00
|
|
|
|
|
|
|
|
return !noRecordingRanges.some(
|
|
|
|
|
(range) => time >= range.start_time && time < range.end_time,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[noRecordingRanges],
|
|
|
|
|
);
|
|
|
|
|
|
2025-02-10 00:13:32 +03:00
|
|
|
const segmentTimes = useMemo(() => {
|
2024-04-01 17:23:57 +03:00
|
|
|
const segments = [];
|
|
|
|
|
let segmentTime = timelineStartAligned;
|
|
|
|
|
|
2025-02-10 00:13:32 +03:00
|
|
|
for (let i = 0; i < Math.ceil(timelineDuration / segmentDuration); i++) {
|
|
|
|
|
if (!motionOnly) {
|
|
|
|
|
segments.push(segmentTime);
|
|
|
|
|
} else {
|
|
|
|
|
const motionStart = segmentTime;
|
|
|
|
|
const motionEnd = motionStart + segmentDuration;
|
|
|
|
|
const overlappingReviewItems = events.some(
|
|
|
|
|
(item) =>
|
|
|
|
|
(item.start_time >= motionStart && item.start_time < motionEnd) ||
|
|
|
|
|
((item.end_time ?? timelineStart) > motionStart &&
|
|
|
|
|
(item.end_time ?? timelineStart) <= motionEnd) ||
|
|
|
|
|
(item.start_time <= motionStart &&
|
|
|
|
|
(item.end_time ?? timelineStart) >= motionEnd),
|
|
|
|
|
);
|
|
|
|
|
const firstHalfMotionValue = getMotionSegmentValue(motionStart);
|
|
|
|
|
const secondHalfMotionValue = getMotionSegmentValue(
|
|
|
|
|
motionStart + segmentDuration / 2,
|
|
|
|
|
);
|
2024-04-01 17:23:57 +03:00
|
|
|
|
2025-02-10 00:13:32 +03:00
|
|
|
const segmentMotion =
|
|
|
|
|
firstHalfMotionValue > 0 || secondHalfMotionValue > 0;
|
|
|
|
|
if (segmentMotion && !overlappingReviewItems) {
|
|
|
|
|
segments.push(segmentTime);
|
|
|
|
|
}
|
2024-04-01 17:23:57 +03:00
|
|
|
}
|
|
|
|
|
segmentTime -= segmentDuration;
|
|
|
|
|
}
|
2025-02-10 00:13:32 +03:00
|
|
|
|
2024-04-01 17:23:57 +03:00
|
|
|
return segments;
|
2024-03-04 19:42:51 +03:00
|
|
|
}, [
|
2024-03-09 01:49:10 +03:00
|
|
|
timelineStartAligned,
|
2025-02-10 00:13:32 +03:00
|
|
|
segmentDuration,
|
2024-03-04 19:42:51 +03:00
|
|
|
timelineDuration,
|
2024-03-23 16:33:50 +03:00
|
|
|
motionOnly,
|
2025-02-10 00:13:32 +03:00
|
|
|
getMotionSegmentValue,
|
|
|
|
|
events,
|
|
|
|
|
timelineStart,
|
2024-03-04 19:42:51 +03:00
|
|
|
]);
|
|
|
|
|
|
2025-02-10 00:13:32 +03:00
|
|
|
const scrollToSegment = useCallback(
|
|
|
|
|
(segmentTime: number, ifNeeded?: boolean, behavior?: ScrollBehavior) => {
|
|
|
|
|
if (virtualizedSegmentsRef.current) {
|
|
|
|
|
virtualizedSegmentsRef.current.scrollToSegment(
|
|
|
|
|
segmentTime,
|
|
|
|
|
ifNeeded,
|
|
|
|
|
behavior,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
2024-03-04 19:42:51 +03:00
|
|
|
);
|
|
|
|
|
|
2025-02-10 00:13:32 +03:00
|
|
|
// keep handlebar centered when zooming
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
scrollToSegment(
|
|
|
|
|
alignStartDateToTimeline(handlebarTime ?? timelineStart),
|
|
|
|
|
true,
|
|
|
|
|
"auto",
|
|
|
|
|
);
|
|
|
|
|
}, 0);
|
|
|
|
|
// we only want to scroll when zooming level changes
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [segmentDuration]);
|
|
|
|
|
|
2024-03-04 19:42:51 +03:00
|
|
|
return (
|
|
|
|
|
<ReviewTimeline
|
2024-03-28 18:03:06 +03:00
|
|
|
timelineRef={selectedTimelineRef}
|
|
|
|
|
contentRef={contentRef}
|
2024-03-04 19:42:51 +03:00
|
|
|
segmentDuration={segmentDuration}
|
2024-03-18 23:58:54 +03:00
|
|
|
timelineDuration={timelineDuration}
|
2024-03-28 18:03:06 +03:00
|
|
|
timelineStartAligned={timelineStartAligned}
|
2024-03-04 19:42:51 +03:00
|
|
|
showHandlebar={showHandlebar}
|
2024-03-28 18:03:06 +03:00
|
|
|
onHandlebarDraggingChange={onHandlebarDraggingChange}
|
|
|
|
|
onlyInitialHandlebarScroll={onlyInitialHandlebarScroll}
|
2024-03-18 23:58:54 +03:00
|
|
|
showExportHandles={showExportHandles}
|
2024-03-28 18:03:06 +03:00
|
|
|
handlebarTime={handlebarTime}
|
|
|
|
|
setHandlebarTime={setHandlebarTime}
|
|
|
|
|
exportStartTime={exportStartTime}
|
|
|
|
|
exportEndTime={exportEndTime}
|
|
|
|
|
setExportStartTime={setExportStartTime}
|
|
|
|
|
setExportEndTime={setExportEndTime}
|
2024-03-30 18:51:03 +03:00
|
|
|
timelineCollapsed={motionOnly}
|
2024-03-28 18:03:06 +03:00
|
|
|
dense={dense}
|
2025-02-10 00:13:32 +03:00
|
|
|
segments={segmentTimes}
|
|
|
|
|
scrollToSegment={scrollToSegment}
|
|
|
|
|
isZooming={isZooming}
|
|
|
|
|
zoomDirection={zoomDirection}
|
2025-10-29 17:39:07 +03:00
|
|
|
getRecordingAvailability={getRecordingAvailability}
|
2025-10-29 21:04:29 +03:00
|
|
|
onZoomChange={onZoomChange}
|
|
|
|
|
possibleZoomLevels={possibleZoomLevels}
|
|
|
|
|
currentZoomLevel={currentZoomLevel}
|
2024-03-04 19:42:51 +03:00
|
|
|
>
|
2025-02-10 00:13:32 +03:00
|
|
|
<VirtualizedMotionSegments
|
|
|
|
|
ref={virtualizedSegmentsRef}
|
|
|
|
|
timelineRef={selectedTimelineRef}
|
|
|
|
|
segments={segmentTimes}
|
|
|
|
|
events={events}
|
|
|
|
|
motion_events={motion_events}
|
|
|
|
|
segmentDuration={segmentDuration}
|
|
|
|
|
timestampSpread={timestampSpread}
|
|
|
|
|
showMinimap={showMinimap}
|
|
|
|
|
minimapStartTime={minimapStartTime}
|
|
|
|
|
minimapEndTime={minimapEndTime}
|
|
|
|
|
contentRef={contentRef}
|
|
|
|
|
setHandlebarTime={setHandlebarTime}
|
|
|
|
|
dense={dense}
|
|
|
|
|
motionOnly={motionOnly}
|
|
|
|
|
getMotionSegmentValue={getMotionSegmentValue}
|
2025-05-23 17:55:48 +03:00
|
|
|
getRecordingAvailability={getRecordingAvailability}
|
2025-10-29 17:39:07 +03:00
|
|
|
alwaysShowMotionLine={alwaysShowMotionLine}
|
2025-02-10 00:13:32 +03:00
|
|
|
/>
|
2024-03-04 19:42:51 +03:00
|
|
|
</ReviewTimeline>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MotionReviewTimeline;
|