From d249e5b27f7a56152c7cf1da21c5fc00d246c35a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:58:54 -0500 Subject: [PATCH] Timeline fixes and export handles (#10522) * select an export range from timeline * height tweak --- .../timeline/EventReviewTimeline.tsx | 139 +++++++-- web/src/components/timeline/EventSegment.tsx | 12 +- .../timeline/MotionReviewTimeline.tsx | 139 +++++++-- web/src/components/timeline/MotionSegment.tsx | 10 +- .../components/timeline/ReviewTimeline.tsx | 287 +++++++++++++++++- .../components/timeline/segment-metadata.tsx | 12 +- ...e-dragging.ts => use-draggable-element.ts} | 174 +++++++---- web/src/hooks/use-event-utils.ts | 75 ----- web/src/hooks/use-timeline-utils.ts | 26 ++ web/src/pages/Export.tsx | 24 +- web/src/pages/UIPlayground.tsx | 104 +++++-- web/src/types/draggable-element.ts | 1 + web/src/views/events/EventView.tsx | 7 +- web/themes/theme-default.css | 10 +- 14 files changed, 771 insertions(+), 249 deletions(-) rename web/src/hooks/{use-handle-dragging.ts => use-draggable-element.ts} (58%) delete mode 100644 web/src/hooks/use-event-utils.ts create mode 100644 web/src/hooks/use-timeline-utils.ts create mode 100644 web/src/types/draggable-element.ts diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index 5f6c6dc0a..47c43b69a 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -1,4 +1,4 @@ -import useDraggableHandler from "@/hooks/use-handle-dragging"; +import useDraggableElement from "@/hooks/use-draggable-element"; import { useEffect, useCallback, @@ -8,7 +8,7 @@ import { RefObject, } from "react"; import EventSegment from "./EventSegment"; -import { useEventUtils } from "@/hooks/use-event-utils"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import ReviewTimeline from "./ReviewTimeline"; @@ -23,6 +23,11 @@ export type EventReviewTimelineProps = { showMinimap?: boolean; minimapStartTime?: number; minimapEndTime?: number; + showExportHandles?: boolean; + exportStartTime?: number; + exportEndTime?: number; + setExportStartTime?: React.Dispatch>; + setExportEndTime?: React.Dispatch>; events: ReviewSegment[]; severityType: ReviewSeverity; contentRef: RefObject; @@ -40,47 +45,113 @@ export function EventReviewTimeline({ showMinimap = false, minimapStartTime, minimapEndTime, + showExportHandles = false, + exportStartTime, + exportEndTime, + setExportStartTime, + setExportEndTime, events, severityType, contentRef, onHandlebarDraggingChange, }: EventReviewTimelineProps) { const [isDragging, setIsDragging] = useState(false); - const handlebarRef = useRef(null); + const [exportStartPosition, setExportStartPosition] = useState(0); + const [exportEndPosition, setExportEndPosition] = useState(0); + const timelineRef = useRef(null); + const handlebarRef = useRef(null); const handlebarTimeRef = useRef(null); + const exportStartRef = useRef(null); + const exportStartTimeRef = useRef(null); + const exportEndRef = useRef(null); + const exportEndTimeRef = useRef(null); + const timelineDuration = useMemo( () => timelineStart - timelineEnd, [timelineEnd, timelineStart], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( - events, - segmentDuration, - ); + const { alignStartDateToTimeline, alignEndDateToTimeline } = + useTimelineUtils(segmentDuration); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart), [timelineStart, alignStartDateToTimeline], ); - const { handleMouseDown, handleMouseUp, handleMouseMove } = - useDraggableHandler({ - contentRef, - timelineRef, - handlebarRef, - alignStartDateToTimeline, - alignEndDateToTimeline, - segmentDuration, - showHandlebar, - handlebarTime, - setHandlebarTime, - timelineDuration, - timelineStartAligned, - isDragging, - setIsDragging, - handlebarTimeRef, - }); + const paddedExportStartTime = useMemo(() => { + if (exportStartTime) { + return alignStartDateToTimeline(exportStartTime) + segmentDuration; + } + }, [exportStartTime, segmentDuration, alignStartDateToTimeline]); + + const paddedExportEndTime = useMemo(() => { + if (exportEndTime) { + return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2; + } + }, [exportEndTime, segmentDuration, alignEndDateToTimeline]); + + const { + handleMouseDown: handlebarMouseDown, + handleMouseUp: handlebarMouseUp, + handleMouseMove: handlebarMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: handlebarRef, + segmentDuration, + showDraggableElement: showHandlebar, + draggableElementTime: handlebarTime, + setDraggableElementTime: setHandlebarTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: handlebarTimeRef, + }); + + const { + handleMouseDown: exportStartMouseDown, + handleMouseUp: exportStartMouseUp, + handleMouseMove: exportStartMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportStartRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportStartTime, + draggableElementLatestTime: paddedExportEndTime, + setDraggableElementTime: setExportStartTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: exportStartTimeRef, + setDraggableElementPosition: setExportStartPosition, + }); + + const { + handleMouseDown: exportEndMouseDown, + handleMouseUp: exportEndMouseUp, + handleMouseMove: exportEndMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportEndRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportEndTime, + draggableElementEarliestTime: paddedExportStartTime, + setDraggableElementTime: setExportEndTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: exportEndTimeRef, + setDraggableElementPosition: setExportEndPosition, + }); // Generate segments for the timeline const generateSegments = useCallback(() => { @@ -145,12 +216,26 @@ export function EventReviewTimeline({ timelineRef={timelineRef} handlebarRef={handlebarRef} handlebarTimeRef={handlebarTimeRef} - handleMouseMove={handleMouseMove} - handleMouseUp={handleMouseUp} - handleMouseDown={handleMouseDown} + handlebarMouseMove={handlebarMouseMove} + handlebarMouseUp={handlebarMouseUp} + handlebarMouseDown={handlebarMouseDown} segmentDuration={segmentDuration} + timelineDuration={timelineDuration} showHandlebar={showHandlebar} isDragging={isDragging} + exportStartMouseMove={exportStartMouseMove} + exportStartMouseUp={exportStartMouseUp} + exportStartMouseDown={exportStartMouseDown} + exportEndMouseMove={exportEndMouseMove} + exportEndMouseUp={exportEndMouseUp} + exportEndMouseDown={exportEndMouseDown} + showExportHandles={showExportHandles} + exportStartRef={exportStartRef} + exportStartTimeRef={exportStartTimeRef} + exportEndRef={exportEndRef} + exportEndTimeRef={exportEndTimeRef} + exportStartPosition={exportStartPosition} + exportEndPosition={exportEndPosition} > {segments} diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index ed86ee66c..08abd377f 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,5 +1,5 @@ import { useApiHost } from "@/api"; -import { useEventUtils } from "@/hooks/use-event-utils"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import React, { @@ -53,10 +53,8 @@ export function EventSegment({ getEventThumbnail, } = useEventSegmentUtils(segmentDuration, events, severityType); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( - events, - segmentDuration, - ); + const { alignStartDateToTimeline, alignEndDateToTimeline } = + useTimelineUtils(segmentDuration); const severity = useMemo( () => getSeverity(segmentTime, displaySeverityType), @@ -155,7 +153,7 @@ export function EventSegment({ : "" } ${ isFirstSegmentInMinimap || isLastSegmentInMinimap - ? "relative h-2 border-b-2 border-gray-500" + ? "relative h-2 border-b-2 border-neutral-600" : "" }`; @@ -236,7 +234,7 @@ export function EventSegment({ key={`${segmentKey}_${index}_primary_data`} className={`w-full h-2 bg-gradient-to-r ${roundBottomPrimary ? "rounded-bl-full rounded-br-full" : ""} ${roundTopPrimary ? "rounded-tl-full rounded-tr-full" : ""} ${severityColors[severityValue]}`} onClick={segmentClick} - onTouchStart={(event) => + onTouchEnd={(event) => handleTouchStart(event, segmentClick) } > diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx index 7ef4b1d73..9a1a7b428 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -1,4 +1,4 @@ -import useDraggableHandler from "@/hooks/use-handle-dragging"; +import useDraggableElement from "@/hooks/use-draggable-element"; import { useEffect, useCallback, @@ -8,7 +8,7 @@ import { RefObject, } from "react"; import MotionSegment from "./MotionSegment"; -import { useEventUtils } from "@/hooks/use-event-utils"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; import ReviewTimeline from "./ReviewTimeline"; @@ -23,6 +23,11 @@ export type MotionReviewTimelineProps = { showMinimap?: boolean; minimapStartTime?: number; minimapEndTime?: number; + showExportHandles?: boolean; + exportStartTime?: number; + exportEndTime?: number; + setExportStartTime?: React.Dispatch>; + setExportEndTime?: React.Dispatch>; events: ReviewSegment[]; motion_events: MotionData[]; severityType: ReviewSeverity; @@ -41,47 +46,113 @@ export function MotionReviewTimeline({ showMinimap = false, minimapStartTime, minimapEndTime, + showExportHandles = false, + exportStartTime, + exportEndTime, + setExportStartTime, + setExportEndTime, events, motion_events, contentRef, onHandlebarDraggingChange, }: MotionReviewTimelineProps) { const [isDragging, setIsDragging] = useState(false); - const handlebarRef = useRef(null); + const [exportStartPosition, setExportStartPosition] = useState(0); + const [exportEndPosition, setExportEndPosition] = useState(0); + const timelineRef = useRef(null); + const handlebarRef = useRef(null); const handlebarTimeRef = useRef(null); + const exportStartRef = useRef(null); + const exportStartTimeRef = useRef(null); + const exportEndRef = useRef(null); + const exportEndTimeRef = useRef(null); + const timelineDuration = useMemo( () => timelineStart - timelineEnd + 4 * segmentDuration, [timelineEnd, timelineStart, segmentDuration], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( - events, - segmentDuration, - ); + const { alignStartDateToTimeline, alignEndDateToTimeline } = + useTimelineUtils(segmentDuration); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, [timelineStart, alignStartDateToTimeline, segmentDuration], ); - const { handleMouseDown, handleMouseUp, handleMouseMove } = - useDraggableHandler({ - contentRef, - timelineRef, - handlebarRef, - alignStartDateToTimeline, - alignEndDateToTimeline, - segmentDuration, - showHandlebar, - handlebarTime, - setHandlebarTime, - timelineDuration, - timelineStartAligned, - isDragging, - setIsDragging, - handlebarTimeRef, - }); + const paddedExportStartTime = useMemo(() => { + if (exportStartTime) { + return alignStartDateToTimeline(exportStartTime) + segmentDuration; + } + }, [exportStartTime, segmentDuration, alignStartDateToTimeline]); + + const paddedExportEndTime = useMemo(() => { + if (exportEndTime) { + return alignEndDateToTimeline(exportEndTime) - segmentDuration * 2; + } + }, [exportEndTime, segmentDuration, alignEndDateToTimeline]); + + const { + handleMouseDown: handlebarMouseDown, + handleMouseUp: handlebarMouseUp, + handleMouseMove: handlebarMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: handlebarRef, + segmentDuration, + showDraggableElement: showHandlebar, + draggableElementTime: handlebarTime, + setDraggableElementTime: setHandlebarTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: handlebarTimeRef, + }); + + const { + handleMouseDown: exportStartMouseDown, + handleMouseUp: exportStartMouseUp, + handleMouseMove: exportStartMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportStartRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportStartTime, + draggableElementLatestTime: paddedExportEndTime, + setDraggableElementTime: setExportStartTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: exportStartTimeRef, + setDraggableElementPosition: setExportStartPosition, + }); + + const { + handleMouseDown: exportEndMouseDown, + handleMouseUp: exportEndMouseUp, + handleMouseMove: exportEndMouseMove, + } = useDraggableElement({ + contentRef, + timelineRef, + draggableElementRef: exportEndRef, + segmentDuration, + showDraggableElement: showExportHandles, + draggableElementTime: exportEndTime, + draggableElementEarliestTime: paddedExportStartTime, + setDraggableElementTime: setExportEndTime, + timelineDuration, + timelineStartAligned, + isDragging, + setIsDragging, + draggableElementTimeRef: exportEndTimeRef, + setDraggableElementPosition: setExportEndPosition, + }); // Generate segments for the timeline const generateSegments = useCallback(() => { @@ -147,12 +218,26 @@ export function MotionReviewTimeline({ timelineRef={timelineRef} handlebarRef={handlebarRef} handlebarTimeRef={handlebarTimeRef} - handleMouseMove={handleMouseMove} - handleMouseUp={handleMouseUp} - handleMouseDown={handleMouseDown} + handlebarMouseMove={handlebarMouseMove} + handlebarMouseUp={handlebarMouseUp} + handlebarMouseDown={handlebarMouseDown} segmentDuration={segmentDuration} + timelineDuration={timelineDuration} showHandlebar={showHandlebar} isDragging={isDragging} + exportStartMouseMove={exportStartMouseMove} + exportStartMouseUp={exportStartMouseUp} + exportStartMouseDown={exportStartMouseDown} + exportEndMouseMove={exportEndMouseMove} + exportEndMouseUp={exportEndMouseUp} + exportEndMouseDown={exportEndMouseDown} + showExportHandles={showExportHandles} + exportStartRef={exportStartRef} + exportStartTimeRef={exportStartTimeRef} + exportEndRef={exportEndRef} + exportEndTimeRef={exportEndTimeRef} + exportStartPosition={exportStartPosition} + exportEndPosition={exportEndPosition} > {segments} diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx index b06d655fb..fb9a92e85 100644 --- a/web/src/components/timeline/MotionSegment.tsx +++ b/web/src/components/timeline/MotionSegment.tsx @@ -1,4 +1,4 @@ -import { useEventUtils } from "@/hooks/use-event-utils"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; import { MotionData, ReviewSegment } from "@/types/review"; import React, { useCallback, useEffect, useMemo, useRef } from "react"; @@ -42,10 +42,8 @@ export function MotionSegment({ const { getMotionSegmentValue, interpolateMotionAudioData } = useMotionSegmentUtils(segmentDuration, motion_events); - const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( - events, - segmentDuration, - ); + const { alignStartDateToTimeline, alignEndDateToTimeline } = + useTimelineUtils(segmentDuration); const { handleTouchStart } = useTapUtils(); @@ -180,7 +178,7 @@ export function MotionSegment({ key={segmentKey} className={segmentClasses} onClick={segmentClick} - onTouchStart={(event) => handleTouchStart(event, segmentClick)} + onTouchEnd={(event) => handleTouchStart(event, segmentClick)} > ; handlebarRef: RefObject; handlebarTimeRef: RefObject; - handleMouseMove: ( + handlebarMouseMove: ( e: | React.MouseEvent | React.TouchEvent, ) => void; - handleMouseUp: ( + handlebarMouseUp: ( e: | React.MouseEvent | React.TouchEvent, ) => void; - handleMouseDown: ( + handlebarMouseDown: ( e: | React.MouseEvent | React.TouchEvent, ) => void; segmentDuration: number; + timelineDuration: number; showHandlebar: boolean; + showExportHandles: boolean; + exportStartRef: RefObject; + exportStartTimeRef: RefObject; + exportEndRef: RefObject; + exportEndTimeRef: RefObject; + exportStartMouseMove: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + exportStartMouseUp: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + exportStartMouseDown: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + exportEndMouseMove: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + exportEndMouseUp: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; + exportEndMouseDown: ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => void; isDragging: boolean; + exportStartPosition?: number; + exportEndPosition?: number; children: ReactNode; }; @@ -30,14 +77,156 @@ export function ReviewTimeline({ timelineRef, handlebarRef, handlebarTimeRef, - handleMouseMove, - handleMouseUp, - handleMouseDown, + handlebarMouseMove, + handlebarMouseUp, + handlebarMouseDown, segmentDuration, + timelineDuration, showHandlebar = false, + showExportHandles = false, + exportStartRef, + exportStartTimeRef, + exportEndRef, + exportEndTimeRef, + exportStartMouseMove, + exportStartMouseUp, + exportStartMouseDown, + exportEndMouseMove, + exportEndMouseUp, + exportEndMouseDown, isDragging, + exportStartPosition, + exportEndPosition, children, }: ReviewTimelineProps) { + const exportSectionRef = useRef(null); + + const segmentHeight = useMemo(() => { + if (timelineRef.current) { + const { scrollHeight: timelineHeight } = + timelineRef.current as HTMLDivElement; + + return timelineHeight / (timelineDuration / segmentDuration); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segmentDuration, timelineDuration, timelineRef, showExportHandles]); + + const [draggableElementType, setDraggableElementType] = + useState(); + + const handleHandlebar = useCallback( + ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => { + setDraggableElementType("handlebar"); + handlebarMouseDown(e); + }, + [handlebarMouseDown], + ); + + const handleExportStart = useCallback( + ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => { + setDraggableElementType("export_start"); + exportStartMouseDown(e); + }, + [exportStartMouseDown], + ); + + const handleExportEnd = useCallback( + ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => { + setDraggableElementType("export_end"); + exportEndMouseDown(e); + }, + [exportEndMouseDown], + ); + + const handleMouseMove = useCallback( + ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => { + switch (draggableElementType) { + case "export_start": + exportStartMouseMove(e); + break; + case "export_end": + exportEndMouseMove(e); + break; + case "handlebar": + handlebarMouseMove(e); + break; + + default: + break; + } + }, + [ + draggableElementType, + exportStartMouseMove, + exportEndMouseMove, + handlebarMouseMove, + ], + ); + + const handleMouseUp = useCallback( + ( + e: + | React.MouseEvent + | React.TouchEvent, + ) => { + switch (draggableElementType) { + case "export_start": + exportStartMouseUp(e); + break; + case "export_end": + exportEndMouseUp(e); + break; + case "handlebar": + handlebarMouseUp(e); + break; + + default: + break; + } + }, + [ + draggableElementType, + exportStartMouseUp, + exportEndMouseUp, + handlebarMouseUp, + ], + ); + + useEffect(() => { + if ( + exportSectionRef.current && + segmentHeight && + exportStartPosition && + exportEndPosition + ) { + exportSectionRef.current.style.top = `${exportEndPosition + segmentHeight}px`; + exportSectionRef.current.style.height = `${exportStartPosition - exportEndPosition + segmentHeight / 2}px`; + } + }, [ + showExportHandles, + segmentHeight, + timelineRef, + exportStartPosition, + exportEndPosition, + ]); + return (
@@ -62,8 +253,8 @@ export function ReviewTimeline({ >
)} + {showExportHandles && ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + )} ); } diff --git a/web/src/components/timeline/segment-metadata.tsx b/web/src/components/timeline/segment-metadata.tsx index 7aaf3525b..4c85f1971 100644 --- a/web/src/components/timeline/segment-metadata.tsx +++ b/web/src/components/timeline/segment-metadata.tsx @@ -32,7 +32,7 @@ export function MinimapBounds({ <> {isFirstSegmentInMinimap && (
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { @@ -44,7 +44,7 @@ export function MinimapBounds({ )} {isLastSegmentInMinimap && ( -
+
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", @@ -61,14 +61,14 @@ export function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
@@ -88,7 +88,7 @@ export function Timestamp({ {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
{timestamp.getMinutes() % timestampSpread === 0 && timestamp.getSeconds() === 0 && diff --git a/web/src/hooks/use-handle-dragging.ts b/web/src/hooks/use-draggable-element.ts similarity index 58% rename from web/src/hooks/use-handle-dragging.ts rename to web/src/hooks/use-draggable-element.ts index 293802996..dfb7826bf 100644 --- a/web/src/hooks/use-handle-dragging.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -1,41 +1,46 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; import scrollIntoView from "scroll-into-view-if-needed"; +import { useTimelineUtils } from "./use-timeline-utils"; -type DragHandlerProps = { +type DraggableElementProps = { contentRef: React.RefObject; timelineRef: React.RefObject; - handlebarRef: React.RefObject; - alignStartDateToTimeline: (time: number) => number; - alignEndDateToTimeline: (time: number) => number; + draggableElementRef: React.RefObject; segmentDuration: number; - showHandlebar: boolean; - handlebarTime?: number; - setHandlebarTime?: React.Dispatch>; - handlebarTimeRef: React.MutableRefObject; + showDraggableElement: boolean; + draggableElementTime?: number; + draggableElementEarliestTime?: number; + draggableElementLatestTime?: number; + setDraggableElementTime?: React.Dispatch>; + draggableElementTimeRef: React.MutableRefObject; timelineDuration: number; timelineStartAligned: number; isDragging: boolean; setIsDragging: React.Dispatch>; + setDraggableElementPosition?: React.Dispatch>; }; -function useDraggableHandler({ +function useDraggableElement({ contentRef, timelineRef, - handlebarRef, - alignStartDateToTimeline, + draggableElementRef, segmentDuration, - showHandlebar, - handlebarTime, - setHandlebarTime, - handlebarTimeRef, + showDraggableElement, + draggableElementTime, + draggableElementEarliestTime, + draggableElementLatestTime, + setDraggableElementTime, + draggableElementTimeRef, timelineDuration, timelineStartAligned, isDragging, setIsDragging, -}: DragHandlerProps) { + setDraggableElementPosition, +}: DraggableElementProps) { const [clientYPosition, setClientYPosition] = useState(null); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); + const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration); const draggingAtTopEdge = useMemo(() => { if (clientYPosition && timelineRef.current) { @@ -78,17 +83,32 @@ function useDraggableHandler({ ( e: React.MouseEvent | React.TouchEvent, ) => { - e.preventDefault(); + // prevent default only for mouse events + // to avoid chrome/android issues + if (e.nativeEvent instanceof MouseEvent) { + e.preventDefault(); + } e.stopPropagation(); - getClientYPosition(e); setIsDragging(true); - if (handlebarRef.current && clientYPosition && isDesktop) { - const handlebarRect = handlebarRef.current.getBoundingClientRect(); - setInitialClickAdjustment(clientYPosition - handlebarRect.top); + let clientY; + if (isMobile && e.nativeEvent instanceof TouchEvent) { + clientY = e.nativeEvent.touches[0].clientY; + } else if (e.nativeEvent instanceof MouseEvent) { + clientY = e.nativeEvent.clientY; + } + if (clientY && draggableElementRef.current && isDesktop) { + const draggableElementRect = + draggableElementRef.current.getBoundingClientRect(); + if (!isDragging) { + setInitialClickAdjustment(clientY - draggableElementRect.top); + } + setClientYPosition(clientY); } }, - [setIsDragging, getClientYPosition, handlebarRef, clientYPosition], + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + [setIsDragging, draggableElementRef], ); const handleMouseUp = useCallback( @@ -114,19 +134,36 @@ function useDraggableHandler({ return scrollTop; }, []); - const updateHandlebarPosition = useCallback( + const timestampToPixels = useCallback( + (time: number) => { + const { scrollHeight: timelineHeight } = + timelineRef.current as HTMLDivElement; + + const segmentHeight = + timelineHeight / (timelineDuration / segmentDuration); + + return ((timelineStartAligned - time) / segmentDuration) * segmentHeight; + }, + [segmentDuration, timelineRef, timelineStartAligned, timelineDuration], + ); + + const updateDraggableElementPosition = useCallback( ( - newHandlePosition: number, + newElementPosition: number, segmentStartTime: number, scrollTimeline: boolean, updateHandle: boolean, ) => { - const thumb = handlebarRef.current; + const thumb = draggableElementRef.current; if (thumb) { requestAnimationFrame(() => { - thumb.style.top = `${newHandlePosition}px`; - if (handlebarTimeRef.current) { - handlebarTimeRef.current.textContent = new Date( + thumb.style.top = `${newElementPosition}px`; + if (setDraggableElementPosition) { + setDraggableElementPosition(newElementPosition); + } + + if (draggableElementTimeRef.current) { + draggableElementTimeRef.current.textContent = new Date( segmentStartTime * 1000, ).toLocaleTimeString([], { hour: "2-digit", @@ -143,12 +180,18 @@ function useDraggableHandler({ } }); - if (setHandlebarTime && updateHandle) { - setHandlebarTime(segmentStartTime); + if (setDraggableElementTime && updateHandle) { + setDraggableElementTime(segmentStartTime); } } }, - [segmentDuration, handlebarTimeRef, handlebarRef, setHandlebarTime], + [ + segmentDuration, + draggableElementTimeRef, + draggableElementRef, + setDraggableElementTime, + setDraggableElementPosition, + ], ); const handleMouseMove = useCallback( @@ -158,7 +201,7 @@ function useDraggableHandler({ if ( !contentRef.current || !timelineRef.current || - !handlebarRef.current + !draggableElementRef.current ) { return; } @@ -166,7 +209,7 @@ function useDraggableHandler({ getClientYPosition(e); }, - [contentRef, handlebarRef, timelineRef, getClientYPosition], + [contentRef, draggableElementRef, timelineRef, getClientYPosition], ); useEffect(() => { @@ -175,7 +218,7 @@ function useDraggableHandler({ const handleScroll = () => { if ( timelineRef.current && - showHandlebar && + showDraggableElement && isDragging && clientYPosition ) { @@ -190,13 +233,21 @@ function useDraggableHandler({ const parentScrollTop = getCumulativeScrollTop(timelineRef.current); - const newHandlePosition = Math.min( - // end of timeline - segmentHeight * (timelineDuration / segmentDuration) - - segmentHeight * 2, + // bottom of timeline + const elementEarliest = draggableElementEarliestTime + ? timestampToPixels(draggableElementEarliestTime) + : segmentHeight * (timelineDuration / segmentDuration) - + segmentHeight * 3; + + // top of timeline - default 2 segments added for draggableElement visibility + const elementLatest = draggableElementLatestTime + ? timestampToPixels(draggableElementLatestTime) + : segmentHeight * 2 + scrolled; + + const newElementPosition = Math.min( + elementEarliest, Math.max( - // start of timeline - 2 segments added for handlebar visibility - segmentHeight * 2 + scrolled, + elementLatest, // current Y position clientYPosition - timelineTop + @@ -205,7 +256,7 @@ function useDraggableHandler({ ), ); - const segmentIndex = Math.floor(newHandlePosition / segmentHeight); + const segmentIndex = Math.floor(newElementPosition / segmentHeight); const segmentStartTime = alignStartDateToTimeline( timelineStartAligned - segmentIndex * segmentDuration, ); @@ -224,17 +275,17 @@ function useDraggableHandler({ } } - updateHandlebarPosition( - newHandlePosition - segmentHeight, + updateDraggableElementPosition( + newElementPosition - segmentHeight, segmentStartTime, false, false, ); - if (setHandlebarTime) { - setHandlebarTime( + if (setDraggableElementTime) { + setDraggableElementTime( timelineStartAligned - - ((newHandlePosition - segmentHeight / 2 - 2) / segmentHeight) * + ((newElementPosition - segmentHeight / 2 - 2) / segmentHeight) * segmentDuration, ); } @@ -264,22 +315,21 @@ function useDraggableHandler({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ clientYPosition, - isDragging, segmentDuration, timelineStartAligned, timelineDuration, timelineRef, draggingAtTopEdge, draggingAtBottomEdge, - showHandlebar, + showDraggableElement, ]); useEffect(() => { if ( timelineRef.current && - handlebarRef.current && - showHandlebar && - handlebarTime && + draggableElementRef.current && + showDraggableElement && + draggableElementTime && !isDragging ) { const { scrollHeight: timelineHeight, scrollTop: scrolled } = @@ -290,20 +340,30 @@ function useDraggableHandler({ const parentScrollTop = getCumulativeScrollTop(timelineRef.current); - const newHandlePosition = - ((timelineStartAligned - handlebarTime) / segmentDuration) * + const newElementPosition = + ((timelineStartAligned - draggableElementTime) / segmentDuration) * segmentHeight + parentScrollTop - scrolled - - 2; // height of handlebar horizontal line + 2; // height of draggableElement horizontal line - updateHandlebarPosition(newHandlePosition, handlebarTime, true, true); + updateDraggableElementPosition( + newElementPosition, + draggableElementTime, + true, + true, + ); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handlebarTime, showHandlebar, handlebarRef, timelineStartAligned]); + }, [ + draggableElementTime, + showDraggableElement, + draggableElementRef, + timelineStartAligned, + ]); return { handleMouseDown, handleMouseUp, handleMouseMove }; } -export default useDraggableHandler; +export default useDraggableElement; diff --git a/web/src/hooks/use-event-utils.ts b/web/src/hooks/use-event-utils.ts deleted file mode 100644 index e4d9d3d73..000000000 --- a/web/src/hooks/use-event-utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useCallback } from "react"; -import { ReviewSegment } from "@/types/review"; - -export const useEventUtils = ( - events: ReviewSegment[], - segmentDuration: number, -) => { - const isStartOfEvent = useCallback( - (time: number): boolean => { - return events.some((event) => { - const segmentStart = getSegmentStart(event.start_time); - return time >= segmentStart && time < segmentStart + segmentDuration; - }); - }, - // we know that these deps are correct - // eslint-disable-next-line react-hooks/exhaustive-deps - [events, segmentDuration], - ); - - const isEndOfEvent = useCallback( - (time: number): boolean => { - return events.some((event) => { - if (typeof event.end_time === "number") { - const segmentEnd = getSegmentEnd(event.end_time); - return time >= segmentEnd - segmentDuration && time < segmentEnd; - } - return false; - }); - }, - // we know that these deps are correct - // eslint-disable-next-line react-hooks/exhaustive-deps - [events, segmentDuration], - ); - - const getSegmentStart = useCallback( - (time: number): number => { - return Math.floor(time / segmentDuration) * segmentDuration; - }, - [segmentDuration], - ); - - const getSegmentEnd = useCallback( - (time: number): number => { - return Math.ceil(time / segmentDuration) * segmentDuration; - }, - [segmentDuration], - ); - - const alignEndDateToTimeline = useCallback( - (time: number): number => { - const remainder = time % segmentDuration; - const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; - return time + adjustment; - }, - [segmentDuration], - ); - - const alignStartDateToTimeline = useCallback( - (time: number): number => { - const remainder = time % segmentDuration; - const adjustment = remainder === 0 ? 0 : -remainder; - return time + adjustment; - }, - [segmentDuration], - ); - - return { - isStartOfEvent, - isEndOfEvent, - getSegmentStart, - getSegmentEnd, - alignEndDateToTimeline, - alignStartDateToTimeline, - }; -}; diff --git a/web/src/hooks/use-timeline-utils.ts b/web/src/hooks/use-timeline-utils.ts new file mode 100644 index 000000000..e03541259 --- /dev/null +++ b/web/src/hooks/use-timeline-utils.ts @@ -0,0 +1,26 @@ +import { useCallback } from "react"; + +export const useTimelineUtils = (segmentDuration: number) => { + const alignEndDateToTimeline = useCallback( + (time: number): number => { + const remainder = time % segmentDuration; + const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; + return time + adjustment; + }, + [segmentDuration], + ); + + const alignStartDateToTimeline = useCallback( + (time: number): number => { + const remainder = time % segmentDuration; + const adjustment = remainder === 0 ? 0 : -remainder; + return time + adjustment; + }, + [segmentDuration], + ); + + return { + alignEndDateToTimeline, + alignStartDateToTimeline, + }; +}; diff --git a/web/src/pages/Export.tsx b/web/src/pages/Export.tsx index 206f3911b..82b982cb7 100644 --- a/web/src/pages/Export.tsx +++ b/web/src/pages/Export.tsx @@ -26,9 +26,10 @@ import { Toaster } from "@/components/ui/sonner"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { format } from "date-fns"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { DateRange } from "react-day-picker"; import { isDesktop } from "react-device-detect"; +import { useLocation } from "react-router-dom"; import { toast } from "sonner"; import useSWR from "swr"; @@ -42,6 +43,8 @@ function Export() { "exports/", (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data), ); + const location = useLocation(); + const [dialogOpen, setDialogOpen] = useState(false); // Export States const [camera, setCamera] = useState(); @@ -142,6 +145,23 @@ function Export() { const Trigger = isDesktop ? DialogTrigger : DrawerTrigger; const Content = isDesktop ? DialogContent : DrawerContent; + useEffect(() => { + if (location.state && location.state.start && location.state.end) { + const startTimeString = format( + new Date(location.state.start * 1000), + "HH:mm:ss", + ); + const endTimeString = format( + new Date(location.state.end * 1000), + "HH:mm:ss", + ); + setStartTime(startTimeString); + setEndTime(endTimeString); + + setDialogOpen(true); + } + }, [location.state]); + return (
@@ -167,7 +187,7 @@ function Export() {
- + diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 395378219..8f1faa3fb 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -23,7 +23,9 @@ import { SelectValue, } from "@/components/ui/select"; import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; -import HlsVideoPlayer from "@/components/player/HlsVideoPlayer"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { useNavigate } from "react-router-dom"; // Color data const colors = [ @@ -129,6 +131,18 @@ function UIPlayground() { Math.round((Date.now() / 1000 - 15 * 60) / 60) * 60, ); + const [exportStartTime, setExportStartTime] = useState( + Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60, + ); + + const [exportEndTime, setExportEndTime] = useState( + Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60, + ); + + const [showExportHandles, setShowExportHandles] = useState(false); + + const navigate = useNavigate(); + useMemo(() => { const initialEvents = Array.from({ length: 50 }, generateRandomEvent); setMockEvents(initialEvents); @@ -158,8 +172,6 @@ function UIPlayground() { timestampSpread: 15, }); - const videoRef = useRef(null); - const possibleZoomLevels = [ { segmentDuration: 60, timestampSpread: 15 }, { segmentDuration: 30, timestampSpread: 5 }, @@ -223,6 +235,26 @@ function UIPlayground() {

Handlebar is dragging: {isDragging ? "yes" : "no"}

+

+ Export start timestamp: {exportStartTime} -  + {new Date(exportStartTime * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + month: "short", + day: "2-digit", + second: "2-digit", + })} +

+

+ Export end timestamp: {exportEndTime} -  + {new Date(exportEndTime * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + month: "short", + day: "2-digit", + second: "2-digit", + })} +

Timeline type
+
+ { + setShowExportHandles(!showExportHandles); + if (showExportHandles) { + setExportEndTime( + Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60, + ); + setExportStartTime( + Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60, + ); + } + }} + /> + +
+
+ +
-
- {birdseyeConfig && ( - - )} -

+
+ {birdseyeConfig && ( + + )} +
Color scheme @@ -293,14 +357,6 @@ function UIPlayground() {
-
- -
-
{!isEventsReviewTimeline && (