mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Scrolling fixes and motion timeline changes (#10295)
* scrolling updates * only scroll by 1 segment on desktop
This commit is contained in:
		
							parent
							
								
									fb81e44283
								
							
						
					
					
						commit
						90db27e3c8
					
				@ -17,6 +17,7 @@ import {
 | 
				
			|||||||
import { HoverCardPortal } from "@radix-ui/react-hover-card";
 | 
					import { HoverCardPortal } from "@radix-ui/react-hover-card";
 | 
				
			||||||
import scrollIntoView from "scroll-into-view-if-needed";
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
import { MinimapBounds, Tick, Timestamp } from "./segment-metadata";
 | 
					import { MinimapBounds, Tick, Timestamp } from "./segment-metadata";
 | 
				
			||||||
 | 
					import useTapUtils from "@/hooks/use-tap-utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type EventSegmentProps = {
 | 
					type EventSegmentProps = {
 | 
				
			||||||
  events: ReviewSegment[];
 | 
					  events: ReviewSegment[];
 | 
				
			||||||
@ -88,6 +89,8 @@ export function EventSegment({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const apiHost = useApiHost();
 | 
					  const apiHost = useApiHost();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { handleTouchStart } = useTapUtils();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const eventThumbnail = useMemo(() => {
 | 
					  const eventThumbnail = useMemo(() => {
 | 
				
			||||||
    return getEventThumbnail(segmentTime);
 | 
					    return getEventThumbnail(segmentTime);
 | 
				
			||||||
  }, [getEventThumbnail, segmentTime]);
 | 
					  }, [getEventThumbnail, segmentTime]);
 | 
				
			||||||
@ -227,6 +230,9 @@ export function EventSegment({
 | 
				
			|||||||
                    key={`${segmentKey}_${index}_primary_data`}
 | 
					                    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]}`}
 | 
					                    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}
 | 
					                    onClick={segmentClick}
 | 
				
			||||||
 | 
					                    onTouchStart={(event) =>
 | 
				
			||||||
 | 
					                      handleTouchStart(event, segmentClick)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                  ></div>
 | 
					                  ></div>
 | 
				
			||||||
                </HoverCardTrigger>
 | 
					                </HoverCardTrigger>
 | 
				
			||||||
                <HoverCardPortal>
 | 
					                <HoverCardPortal>
 | 
				
			||||||
 | 
				
			|||||||
@ -97,7 +97,7 @@ export function MotionReviewTimeline({
 | 
				
			|||||||
          showMinimap={showMinimap}
 | 
					          showMinimap={showMinimap}
 | 
				
			||||||
          minimapStartTime={minimapStartTime}
 | 
					          minimapStartTime={minimapStartTime}
 | 
				
			||||||
          minimapEndTime={minimapEndTime}
 | 
					          minimapEndTime={minimapEndTime}
 | 
				
			||||||
          contentRef={contentRef}
 | 
					          setHandlebarTime={setHandlebarTime}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,12 @@
 | 
				
			|||||||
import { useEventUtils } from "@/hooks/use-event-utils";
 | 
					import { useEventUtils } from "@/hooks/use-event-utils";
 | 
				
			||||||
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
 | 
					import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
 | 
				
			||||||
import { MotionData, ReviewSegment } from "@/types/review";
 | 
					import { MotionData, ReviewSegment } from "@/types/review";
 | 
				
			||||||
import React, {
 | 
					import React, { useCallback, useEffect, useMemo, useRef } from "react";
 | 
				
			||||||
  RefObject,
 | 
					 | 
				
			||||||
  useCallback,
 | 
					 | 
				
			||||||
  useEffect,
 | 
					 | 
				
			||||||
  useMemo,
 | 
					 | 
				
			||||||
  useRef,
 | 
					 | 
				
			||||||
} from "react";
 | 
					 | 
				
			||||||
import scrollIntoView from "scroll-into-view-if-needed";
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
import { MinimapBounds, Tick, Timestamp } from "./segment-metadata";
 | 
					import { MinimapBounds, Tick, Timestamp } from "./segment-metadata";
 | 
				
			||||||
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
 | 
					import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
 | 
				
			||||||
import { isMobile } from "react-device-detect";
 | 
					import { isMobile } from "react-device-detect";
 | 
				
			||||||
 | 
					import useTapUtils from "@/hooks/use-tap-utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type MotionSegmentProps = {
 | 
					type MotionSegmentProps = {
 | 
				
			||||||
  events: ReviewSegment[];
 | 
					  events: ReviewSegment[];
 | 
				
			||||||
@ -22,7 +17,7 @@ type MotionSegmentProps = {
 | 
				
			|||||||
  showMinimap: boolean;
 | 
					  showMinimap: boolean;
 | 
				
			||||||
  minimapStartTime?: number;
 | 
					  minimapStartTime?: number;
 | 
				
			||||||
  minimapEndTime?: number;
 | 
					  minimapEndTime?: number;
 | 
				
			||||||
  contentRef: RefObject<HTMLDivElement>;
 | 
					  setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function MotionSegment({
 | 
					export function MotionSegment({
 | 
				
			||||||
@ -34,7 +29,7 @@ export function MotionSegment({
 | 
				
			|||||||
  showMinimap,
 | 
					  showMinimap,
 | 
				
			||||||
  minimapStartTime,
 | 
					  minimapStartTime,
 | 
				
			||||||
  minimapEndTime,
 | 
					  minimapEndTime,
 | 
				
			||||||
  contentRef,
 | 
					  setHandlebarTime,
 | 
				
			||||||
}: MotionSegmentProps) {
 | 
					}: MotionSegmentProps) {
 | 
				
			||||||
  const severityType = "all";
 | 
					  const severityType = "all";
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
@ -42,20 +37,18 @@ export function MotionSegment({
 | 
				
			|||||||
    getReviewed,
 | 
					    getReviewed,
 | 
				
			||||||
    displaySeverityType,
 | 
					    displaySeverityType,
 | 
				
			||||||
    shouldShowRoundedCorners,
 | 
					    shouldShowRoundedCorners,
 | 
				
			||||||
    getEventStart,
 | 
					 | 
				
			||||||
  } = useEventSegmentUtils(segmentDuration, events, severityType);
 | 
					  } = useEventSegmentUtils(segmentDuration, events, severityType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const { getMotionSegmentValue, interpolateMotionAudioData, getMotionStart } =
 | 
				
			||||||
    getMotionSegmentValue,
 | 
					    useMotionSegmentUtils(segmentDuration, motion_events);
 | 
				
			||||||
    getAudioSegmentValue,
 | 
					 | 
				
			||||||
    interpolateMotionAudioData,
 | 
					 | 
				
			||||||
  } = useMotionSegmentUtils(segmentDuration, motion_events);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
 | 
					  const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
 | 
				
			||||||
    events,
 | 
					    events,
 | 
				
			||||||
    segmentDuration,
 | 
					    segmentDuration,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { handleTouchStart } = useTapUtils();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const severity = useMemo(
 | 
					  const severity = useMemo(
 | 
				
			||||||
    () => getSeverity(segmentTime, displaySeverityType),
 | 
					    () => getSeverity(segmentTime, displaySeverityType),
 | 
				
			||||||
    // we know that these deps are correct
 | 
					    // we know that these deps are correct
 | 
				
			||||||
@ -74,19 +67,19 @@ export function MotionSegment({
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const startTimestamp = useMemo(() => {
 | 
					  const startTimestamp = useMemo(() => {
 | 
				
			||||||
    const eventStart = getEventStart(segmentTime);
 | 
					    const eventStart = getMotionStart(segmentTime);
 | 
				
			||||||
    if (eventStart) {
 | 
					    if (eventStart) {
 | 
				
			||||||
      return alignStartDateToTimeline(eventStart);
 | 
					      return alignStartDateToTimeline(eventStart);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // we know that these deps are correct
 | 
					    // we know that these deps are correct
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, [getEventStart, segmentTime]);
 | 
					  }, [getMotionStart, segmentTime]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]);
 | 
					  const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]);
 | 
				
			||||||
  const segmentKey = useMemo(() => segmentTime, [segmentTime]);
 | 
					  const segmentKey = useMemo(() => segmentTime, [segmentTime]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const maxSegmentWidth = useMemo(() => {
 | 
					  const maxSegmentWidth = useMemo(() => {
 | 
				
			||||||
    return isMobile ? 15 : 25;
 | 
					    return isMobile ? 30 : 50;
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const alignedMinimapStartTime = useMemo(
 | 
					  const alignedMinimapStartTime = useMemo(
 | 
				
			||||||
@ -161,32 +154,10 @@ export function MotionSegment({
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const segmentClick = useCallback(() => {
 | 
					  const segmentClick = useCallback(() => {
 | 
				
			||||||
    if (contentRef.current && startTimestamp) {
 | 
					    if (startTimestamp && setHandlebarTime) {
 | 
				
			||||||
      const element = contentRef.current.querySelector(
 | 
					      setHandlebarTime(startTimestamp);
 | 
				
			||||||
        `[data-segment-start="${startTimestamp - segmentDuration}"]`,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (element instanceof HTMLElement) {
 | 
					 | 
				
			||||||
        scrollIntoView(element, {
 | 
					 | 
				
			||||||
          scrollMode: "if-needed",
 | 
					 | 
				
			||||||
          behavior: "smooth",
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        element.classList.add(
 | 
					 | 
				
			||||||
          `outline-severity_${severityType}`,
 | 
					 | 
				
			||||||
          `shadow-severity_${severityType}`,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        element.classList.add("outline-4", "shadow-[0_0_6px_1px]");
 | 
					 | 
				
			||||||
        element.classList.remove("outline-0", "shadow-none");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Remove the classes after a short timeout
 | 
					 | 
				
			||||||
        setTimeout(() => {
 | 
					 | 
				
			||||||
          element.classList.remove("outline-4", "shadow-[0_0_6px_1px]");
 | 
					 | 
				
			||||||
          element.classList.add("outline-0", "shadow-none");
 | 
					 | 
				
			||||||
        }, 3000);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					  }, [startTimestamp, setHandlebarTime]);
 | 
				
			||||||
    // we know that these deps are correct
 | 
					 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					 | 
				
			||||||
  }, [startTimestamp]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div key={segmentKey} className={segmentClasses}>
 | 
					    <div key={segmentKey} className={segmentClasses}>
 | 
				
			||||||
@ -210,11 +181,12 @@ export function MotionSegment({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <div className="absolute left-1/2 transform -translate-x-1/2 w-[20px] md:w-[40px] h-2 z-10 cursor-pointer">
 | 
					      <div className="absolute left-1/2 transform -translate-x-1/2 w-[20px] md:w-[40px] h-2 z-10 cursor-pointer">
 | 
				
			||||||
        <div className="flex flex-row justify-center w-[20px] md:w-[40px] mb-[1px]">
 | 
					        <div className="flex flex-row justify-center w-[20px] md:w-[40px] mb-[1px]">
 | 
				
			||||||
          <div className="w-[10px] md:w-[20px] flex justify-end">
 | 
					          <div className="flex justify-center">
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              key={`${segmentKey}_motion_data_1`}
 | 
					              key={`${segmentKey}_motion_data_1`}
 | 
				
			||||||
              className={`h-[2px] rounded-full bg-motion_review`}
 | 
					              className={`h-[2px] rounded-full bg-motion_review`}
 | 
				
			||||||
              onClick={segmentClick}
 | 
					              onClick={segmentClick}
 | 
				
			||||||
 | 
					              onTouchStart={(event) => handleTouchStart(event, segmentClick)}
 | 
				
			||||||
              style={{
 | 
					              style={{
 | 
				
			||||||
                width: interpolateMotionAudioData(
 | 
					                width: interpolateMotionAudioData(
 | 
				
			||||||
                  getMotionSegmentValue(segmentTime + segmentDuration / 2),
 | 
					                  getMotionSegmentValue(segmentTime + segmentDuration / 2),
 | 
				
			||||||
@ -223,27 +195,15 @@ export function MotionSegment({
 | 
				
			|||||||
              }}
 | 
					              }}
 | 
				
			||||||
            ></div>
 | 
					            ></div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="w-[10px] md:w-[20px]">
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              key={`${segmentKey}_audio_data_1`}
 | 
					 | 
				
			||||||
              className={`h-[2px] rounded-full bg-audio_review`}
 | 
					 | 
				
			||||||
              onClick={segmentClick}
 | 
					 | 
				
			||||||
              style={{
 | 
					 | 
				
			||||||
                width: interpolateMotionAudioData(
 | 
					 | 
				
			||||||
                  getAudioSegmentValue(segmentTime + segmentDuration / 2),
 | 
					 | 
				
			||||||
                  maxSegmentWidth,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            ></div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className="flex flex-row justify-center w-[20px] md:w-[40px]">
 | 
					        <div className="flex flex-row justify-center w-[20px] md:w-[40px]">
 | 
				
			||||||
          <div className="w-[10px] md:w-[20px] flex justify-end">
 | 
					          <div className="flex justify-center">
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              key={`${segmentKey}_motion_data_2`}
 | 
					              key={`${segmentKey}_motion_data_2`}
 | 
				
			||||||
              className={`h-[2px] rounded-full bg-motion_review`}
 | 
					              className={`h-[2px] rounded-full bg-motion_review`}
 | 
				
			||||||
              onClick={segmentClick}
 | 
					              onClick={segmentClick}
 | 
				
			||||||
 | 
					              onTouchStart={(event) => handleTouchStart(event, segmentClick)}
 | 
				
			||||||
              style={{
 | 
					              style={{
 | 
				
			||||||
                width: interpolateMotionAudioData(
 | 
					                width: interpolateMotionAudioData(
 | 
				
			||||||
                  getMotionSegmentValue(segmentTime),
 | 
					                  getMotionSegmentValue(segmentTime),
 | 
				
			||||||
@ -252,19 +212,6 @@ export function MotionSegment({
 | 
				
			|||||||
              }}
 | 
					              }}
 | 
				
			||||||
            ></div>
 | 
					            ></div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="w-[10px] md:w-[20px]">
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              key={`${segmentKey}_audio_data_2`}
 | 
					 | 
				
			||||||
              className={`h-[2px] rounded-full bg-audio_review`}
 | 
					 | 
				
			||||||
              onClick={segmentClick}
 | 
					 | 
				
			||||||
              style={{
 | 
					 | 
				
			||||||
                width: interpolateMotionAudioData(
 | 
					 | 
				
			||||||
                  getAudioSegmentValue(segmentTime),
 | 
					 | 
				
			||||||
                  maxSegmentWidth,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            ></div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -44,7 +44,7 @@ export function ReviewTimeline({
 | 
				
			|||||||
      onTouchMove={handleMouseMove}
 | 
					      onTouchMove={handleMouseMove}
 | 
				
			||||||
      onMouseUp={handleMouseUp}
 | 
					      onMouseUp={handleMouseUp}
 | 
				
			||||||
      onTouchEnd={handleMouseUp}
 | 
					      onTouchEnd={handleMouseUp}
 | 
				
			||||||
      className={`relative h-full overflow-y-scroll no-scrollbar bg-secondary ${
 | 
					      className={`relative h-full overflow-y-auto no-scrollbar bg-secondary ${
 | 
				
			||||||
        isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
 | 
					        isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
 | 
				
			||||||
      }`}
 | 
					      }`}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
@ -64,7 +64,7 @@ export function ReviewTimeline({
 | 
				
			|||||||
            >
 | 
					            >
 | 
				
			||||||
              <div
 | 
					              <div
 | 
				
			||||||
                className={`bg-destructive rounded-full mx-auto ${
 | 
					                className={`bg-destructive rounded-full mx-auto ${
 | 
				
			||||||
                  segmentDuration < 60 ? "w-14 md:w-20" : "w-12 md:w-16"
 | 
					                  segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
 | 
				
			||||||
                } h-5 flex items-center justify-center`}
 | 
					                } h-5 flex items-center justify-center`}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <div
 | 
					                <div
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { useCallback, useEffect } from "react";
 | 
					import { useCallback, useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { isDesktop, isMobile } from "react-device-detect";
 | 
				
			||||||
import scrollIntoView from "scroll-into-view-if-needed";
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
import { isMobile } from "react-device-detect";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DragHandlerProps = {
 | 
					type DragHandlerProps = {
 | 
				
			||||||
  contentRef: React.RefObject<HTMLElement>;
 | 
					  contentRef: React.RefObject<HTMLElement>;
 | 
				
			||||||
@ -34,15 +34,55 @@ function useDraggableHandler({
 | 
				
			|||||||
  isDragging,
 | 
					  isDragging,
 | 
				
			||||||
  setIsDragging,
 | 
					  setIsDragging,
 | 
				
			||||||
}: DragHandlerProps) {
 | 
					}: DragHandlerProps) {
 | 
				
			||||||
 | 
					  const [clientYPosition, setClientYPosition] = useState<number | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const draggingAtTopEdge = useMemo(() => {
 | 
				
			||||||
 | 
					    if (clientYPosition && timelineRef.current) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        clientYPosition - timelineRef.current.offsetTop <
 | 
				
			||||||
 | 
					          timelineRef.current.clientHeight * 0.03 && isDragging
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [clientYPosition, timelineRef, isDragging]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const draggingAtBottomEdge = useMemo(() => {
 | 
				
			||||||
 | 
					    if (clientYPosition && timelineRef.current) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        clientYPosition >
 | 
				
			||||||
 | 
					          (timelineRef.current.clientHeight + timelineRef.current.offsetTop) *
 | 
				
			||||||
 | 
					            0.97 && isDragging
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [clientYPosition, timelineRef, isDragging]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getClientYPosition = useCallback(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					        setClientYPosition(clientY);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setClientYPosition],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleMouseDown = useCallback(
 | 
					  const handleMouseDown = useCallback(
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
      e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
 | 
					      e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
 | 
				
			||||||
    ) => {
 | 
					    ) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      e.stopPropagation();
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					      getClientYPosition(e);
 | 
				
			||||||
      setIsDragging(true);
 | 
					      setIsDragging(true);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [setIsDragging],
 | 
					    [setIsDragging, getClientYPosition],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleMouseUp = useCallback(
 | 
					  const handleMouseUp = useCallback(
 | 
				
			||||||
@ -84,7 +124,7 @@ function useDraggableHandler({
 | 
				
			|||||||
            ).toLocaleTimeString([], {
 | 
					            ).toLocaleTimeString([], {
 | 
				
			||||||
              hour: "2-digit",
 | 
					              hour: "2-digit",
 | 
				
			||||||
              minute: "2-digit",
 | 
					              minute: "2-digit",
 | 
				
			||||||
              ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
					              ...(segmentDuration < 60 && isDesktop && { second: "2-digit" }),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            if (scrollTimeline) {
 | 
					            if (scrollTimeline) {
 | 
				
			||||||
              scrollIntoView(thumb, {
 | 
					              scrollIntoView(thumb, {
 | 
				
			||||||
@ -115,20 +155,24 @@ function useDraggableHandler({
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let clientY;
 | 
					      getClientYPosition(e);
 | 
				
			||||||
      if (isMobile && e.nativeEvent instanceof TouchEvent) {
 | 
					    },
 | 
				
			||||||
        clientY = e.nativeEvent.touches[0].clientY;
 | 
					 | 
				
			||||||
      } else if (e.nativeEvent instanceof MouseEvent) {
 | 
					 | 
				
			||||||
        clientY = e.nativeEvent.clientY;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      e.preventDefault();
 | 
					    [contentRef, scrollTimeRef, timelineRef, getClientYPosition],
 | 
				
			||||||
      e.stopPropagation();
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (showHandlebar && isDragging && clientY) {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    let animationFrameId: number | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleScroll = () => {
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        timelineRef.current &&
 | 
				
			||||||
 | 
					        showHandlebar &&
 | 
				
			||||||
 | 
					        isDragging &&
 | 
				
			||||||
 | 
					        clientYPosition
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
        const {
 | 
					        const {
 | 
				
			||||||
          scrollHeight: timelineHeight,
 | 
					          scrollHeight: timelineHeight,
 | 
				
			||||||
          clientHeight: visibleTimelineHeight,
 | 
					 | 
				
			||||||
          scrollTop: scrolled,
 | 
					          scrollTop: scrolled,
 | 
				
			||||||
          offsetTop: timelineTop,
 | 
					          offsetTop: timelineTop,
 | 
				
			||||||
        } = timelineRef.current;
 | 
					        } = timelineRef.current;
 | 
				
			||||||
@ -139,10 +183,11 @@ function useDraggableHandler({
 | 
				
			|||||||
        const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
 | 
					        const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const newHandlePosition = Math.min(
 | 
					        const newHandlePosition = Math.min(
 | 
				
			||||||
          visibleTimelineHeight + parentScrollTop,
 | 
					          segmentHeight * (timelineDuration / segmentDuration) -
 | 
				
			||||||
 | 
					            segmentHeight * 2,
 | 
				
			||||||
          Math.max(
 | 
					          Math.max(
 | 
				
			||||||
            segmentHeight + scrolled,
 | 
					            segmentHeight + scrolled,
 | 
				
			||||||
            clientY - timelineTop + parentScrollTop,
 | 
					            clientYPosition - timelineTop + parentScrollTop,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -151,14 +196,24 @@ function useDraggableHandler({
 | 
				
			|||||||
          timelineStart - segmentIndex * segmentDuration,
 | 
					          timelineStart - segmentIndex * segmentDuration,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const scrollTimeline =
 | 
					        if (draggingAtTopEdge || draggingAtBottomEdge) {
 | 
				
			||||||
          clientY < visibleTimelineHeight * 0.1 ||
 | 
					          let newPosition = clientYPosition;
 | 
				
			||||||
          clientY > visibleTimelineHeight * 0.9;
 | 
					
 | 
				
			||||||
 | 
					          if (draggingAtTopEdge) {
 | 
				
			||||||
 | 
					            newPosition = scrolled - segmentHeight;
 | 
				
			||||||
 | 
					            timelineRef.current.scrollTop = newPosition;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (draggingAtBottomEdge) {
 | 
				
			||||||
 | 
					            newPosition = scrolled + segmentHeight;
 | 
				
			||||||
 | 
					            timelineRef.current.scrollTop = newPosition;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        updateHandlebarPosition(
 | 
					        updateHandlebarPosition(
 | 
				
			||||||
          newHandlePosition - segmentHeight,
 | 
					          newHandlePosition - segmentHeight,
 | 
				
			||||||
          segmentStartTime,
 | 
					          segmentStartTime,
 | 
				
			||||||
          scrollTimeline,
 | 
					          false,
 | 
				
			||||||
          false,
 | 
					          false,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -168,22 +223,41 @@ function useDraggableHandler({
 | 
				
			|||||||
              (newHandlePosition / segmentHeight) * segmentDuration,
 | 
					              (newHandlePosition / segmentHeight) * segmentDuration,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (draggingAtTopEdge || draggingAtBottomEdge) {
 | 
				
			||||||
 | 
					          animationFrameId = requestAnimationFrame(handleScroll);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const startScroll = () => {
 | 
				
			||||||
 | 
					      if (isDragging) {
 | 
				
			||||||
 | 
					        handleScroll();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stopScroll = () => {
 | 
				
			||||||
 | 
					      if (animationFrameId !== null) {
 | 
				
			||||||
 | 
					        cancelAnimationFrame(animationFrameId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    startScroll();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return stopScroll;
 | 
				
			||||||
    // we know that these deps are correct
 | 
					    // we know that these deps are correct
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
    [
 | 
					  }, [
 | 
				
			||||||
 | 
					    clientYPosition,
 | 
				
			||||||
    isDragging,
 | 
					    isDragging,
 | 
				
			||||||
      contentRef,
 | 
					 | 
				
			||||||
    segmentDuration,
 | 
					    segmentDuration,
 | 
				
			||||||
      showHandlebar,
 | 
					 | 
				
			||||||
      timelineDuration,
 | 
					 | 
				
			||||||
    timelineStart,
 | 
					    timelineStart,
 | 
				
			||||||
      updateHandlebarPosition,
 | 
					    timelineDuration,
 | 
				
			||||||
      alignStartDateToTimeline,
 | 
					    timelineRef,
 | 
				
			||||||
      getCumulativeScrollTop,
 | 
					    draggingAtTopEdge,
 | 
				
			||||||
    ],
 | 
					    draggingAtBottomEdge,
 | 
				
			||||||
  );
 | 
					    showHandlebar,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
 | 
				
			|||||||
@ -66,9 +66,25 @@ export const useMotionSegmentUtils = (
 | 
				
			|||||||
    [motion_events, getSegmentStart, getSegmentEnd],
 | 
					    [motion_events, getSegmentStart, getSegmentEnd],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getMotionStart = useCallback(
 | 
				
			||||||
 | 
					    (time: number): number => {
 | 
				
			||||||
 | 
					      const matchingEvent = motion_events.find((event) => {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          time >= getSegmentStart(event.start_time) &&
 | 
				
			||||||
 | 
					          time < getSegmentEnd(event.start_time) &&
 | 
				
			||||||
 | 
					          event.motion
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return matchingEvent?.start_time ?? 0;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [motion_events, getSegmentStart, getSegmentEnd],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    getMotionSegmentValue,
 | 
					    getMotionSegmentValue,
 | 
				
			||||||
    getAudioSegmentValue,
 | 
					    getAudioSegmentValue,
 | 
				
			||||||
    interpolateMotionAudioData,
 | 
					    interpolateMotionAudioData,
 | 
				
			||||||
 | 
					    getMotionStart,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										36
									
								
								web/src/hooks/use-tap-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/hooks/use-tap-utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { useCallback } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TapUtils {
 | 
				
			||||||
 | 
					  handleTouchStart: (
 | 
				
			||||||
 | 
					    event: React.TouchEvent<Element>,
 | 
				
			||||||
 | 
					    onClick: () => void,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useTapUtils = (): TapUtils => {
 | 
				
			||||||
 | 
					  const handleTouchStart = useCallback(
 | 
				
			||||||
 | 
					    (event: React.TouchEvent<Element>, onClick: () => void) => {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const element = event.target as Element;
 | 
				
			||||||
 | 
					      const { clientX, clientY } = event.changedTouches[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Determine if the touch is within the element's bounds
 | 
				
			||||||
 | 
					      const rect = element.getBoundingClientRect();
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        clientX >= rect.left &&
 | 
				
			||||||
 | 
					        clientX <= rect.right &&
 | 
				
			||||||
 | 
					        clientY >= rect.top &&
 | 
				
			||||||
 | 
					        clientY <= rect.bottom
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        // Call the onClick handler
 | 
				
			||||||
 | 
					        onClick();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { handleTouchStart };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default useTapUtils;
 | 
				
			||||||
@ -658,9 +658,10 @@ function MotionReview({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          ref={contentRef}
 | 
					          ref={contentRef}
 | 
				
			||||||
        className="w-full h-min m-2 grid sm:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4 overflow-auto no-scrollbar"
 | 
					          className="w-full m-2 grid sm:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4 overflow-auto no-scrollbar"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {reviewCameras.map((camera) => {
 | 
					          {reviewCameras.map((camera) => {
 | 
				
			||||||
            let grow;
 | 
					            let grow;
 | 
				
			||||||
@ -691,6 +692,7 @@ function MotionReview({
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
      <div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
 | 
					      <div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
 | 
				
			||||||
        <MotionReviewTimeline
 | 
					        <MotionReviewTimeline
 | 
				
			||||||
          segmentDuration={segmentDuration}
 | 
					          segmentDuration={segmentDuration}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user