import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; import { ReviewSegment } from "@/types/review"; import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { MinimapBounds, Tick, Timestamp } from "./segment-metadata"; import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils"; import { isMobile } from "react-device-detect"; import useTapUtils from "@/hooks/use-tap-utils"; import { cn } from "@/lib/utils"; type MotionSegmentProps = { events: ReviewSegment[]; segmentTime: number; segmentDuration: number; timestampSpread: number; firstHalfMotionValue: number; secondHalfMotionValue: number; hasRecording?: boolean; prevIsNoRecording?: boolean; nextIsNoRecording?: boolean; motionOnly: boolean; showMinimap: boolean; minimapStartTime?: number; minimapEndTime?: number; setHandlebarTime?: React.Dispatch>; scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void; dense: boolean; alwaysShowMotionLine?: boolean; }; export function MotionSegment({ events, segmentTime, segmentDuration, timestampSpread, firstHalfMotionValue, secondHalfMotionValue, hasRecording, prevIsNoRecording, nextIsNoRecording, motionOnly, showMinimap, minimapStartTime, minimapEndTime, setHandlebarTime, scrollToSegment, dense, alwaysShowMotionLine = false, }: MotionSegmentProps) { const severityType = "all"; const { getSeverity, getReviewed, displaySeverityType } = useEventSegmentUtils(segmentDuration, events, severityType); const { interpolateMotionAudioData } = useMotionSegmentUtils( segmentDuration, [], ); const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( { segmentDuration }, ); const { handleTouchStart } = useTapUtils(); const severity = useMemo( () => getSeverity(segmentTime, displaySeverityType), // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps [getSeverity, segmentTime], ); const reviewed = useMemo( () => getReviewed(segmentTime), [getReviewed, segmentTime], ); const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); const segmentKey = useMemo( () => `${segmentTime}_${segmentDuration}`, [segmentTime, segmentDuration], ); const maxSegmentWidth = useMemo(() => { return isMobile ? 30 : 50; }, []); const firstHalfSegmentWidth = useMemo(() => { return interpolateMotionAudioData(firstHalfMotionValue, maxSegmentWidth); }, [maxSegmentWidth, firstHalfMotionValue, interpolateMotionAudioData]); const secondHalfSegmentWidth = useMemo(() => { return interpolateMotionAudioData(secondHalfMotionValue, maxSegmentWidth); }, [maxSegmentWidth, secondHalfMotionValue, interpolateMotionAudioData]); const alignedMinimapStartTime = useMemo( () => alignStartDateToTimeline(minimapStartTime ?? 0), [minimapStartTime, alignStartDateToTimeline], ); const alignedMinimapEndTime = useMemo( () => alignEndDateToTimeline(minimapEndTime ?? 0), [minimapEndTime, alignEndDateToTimeline], ); const isInMinimapRange = useMemo(() => { return ( showMinimap && segmentTime >= alignedMinimapStartTime && segmentTime < alignedMinimapEndTime ); }, [ showMinimap, alignedMinimapStartTime, alignedMinimapEndTime, segmentTime, ]); const isFirstSegmentInMinimap = useMemo(() => { return showMinimap && segmentTime === alignedMinimapStartTime; }, [showMinimap, segmentTime, alignedMinimapStartTime]); const isLastSegmentInMinimap = useMemo(() => { return showMinimap && segmentTime === alignedMinimapEndTime; }, [showMinimap, segmentTime, alignedMinimapEndTime]); // Bottom border: current segment HAS recording, but next segment (below/earlier) has NO recording const isFirstSegmentWithoutRecording = useMemo(() => { return hasRecording === true && nextIsNoRecording === true; }, [hasRecording, nextIsNoRecording]); // Top border: current segment HAS recording, but prev segment (above/later) has NO recording const isLastSegmentWithoutRecording = useMemo(() => { return hasRecording === true && prevIsNoRecording === true; }, [hasRecording, prevIsNoRecording]); const firstMinimapSegmentRef = useRef(null); useEffect(() => { // Check if the first segment is out of view const firstSegment = firstMinimapSegmentRef.current; if (firstSegment && showMinimap && isFirstSegmentInMinimap) { scrollToSegment(alignedMinimapStartTime); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); const segmentClasses = `h-[8px] relative w-full ${ showMinimap ? isInMinimapRange ? "bg-secondary-highlight" : isLastSegmentInMinimap ? "" : "opacity-70" : "" } ${ isFirstSegmentInMinimap || isLastSegmentInMinimap ? "relative h-[8px] border-b-2 border-gray-500" : "" }`; const severityColorsBg: { [key: number]: string } = { 1: reviewed ? "from-severity_significant_motion-dimmed/10 to-severity_significant_motion/10" : "from-severity_significant_motion-dimmed/20 to-severity_significant_motion/20", 2: reviewed ? "from-severity_detection-dimmed/10 to-severity_detection/10" : "from-severity_detection-dimmed/20 to-severity_detection/20", 3: reviewed ? "from-severity_alert-dimmed/10 to-severity_alert/10" : "from-severity_alert-dimmed/20 to-severity_alert/20", }; const segmentClick = useCallback(() => { if (setHandlebarTime) { setHandlebarTime(segmentTime); } }, [segmentTime, setHandlebarTime]); return ( <> {(((firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0) && motionOnly && severity[0] < 2) || !motionOnly) && (
0 || secondHalfSegmentWidth > 0, }, segmentClasses, severity[0] && "bg-gradient-to-r", severity[0] && severityColorsBg[severity[0]], hasRecording == false && "bg-background", )} onClick={segmentClick} onTouchEnd={(event) => handleTouchStart(event, segmentClick)} > {isFirstSegmentWithoutRecording && (
)} {isLastSegmentWithoutRecording && (
)} {!motionOnly && ( <> {showMinimap && ( )} )} {(hasRecording || firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0 || alwaysShowMotionLine) && (
)}
)} ); } export default MotionSegment;