mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Timeline handlebar changes (#10170)
* auto scrolling handlebar with preview time * tablets can show 2 columns on the event view grid * font sizes * hide minimap when previewing
This commit is contained in:
		
							parent
							
								
									49530dc2e4
								
							
						
					
					
						commit
						a49e1bbc64
					
				@ -33,7 +33,7 @@ import { useSwipeable } from "react-swipeable";
 | 
			
		||||
type PreviewPlayerProps = {
 | 
			
		||||
  review: ReviewSegment;
 | 
			
		||||
  allPreviews?: Preview[];
 | 
			
		||||
  onTimeUpdate?: (time: number | undefined) => void;
 | 
			
		||||
  onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>;
 | 
			
		||||
  setReviewed: (reviewId: string) => void;
 | 
			
		||||
  markAboveReviewed: () => void;
 | 
			
		||||
  onClick: (reviewId: string, ctrl: boolean) => void;
 | 
			
		||||
 | 
			
		||||
@ -45,10 +45,9 @@ export function EventReviewTimeline({
 | 
			
		||||
  onHandlebarDraggingChange,
 | 
			
		||||
}: EventReviewTimelineProps) {
 | 
			
		||||
  const [isDragging, setIsDragging] = useState(false);
 | 
			
		||||
  const [currentTimeSegment, setCurrentTimeSegment] = useState<number>(0);
 | 
			
		||||
  const scrollTimeRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const timelineRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const currentTimeRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const handlebarTimeRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const observer = useRef<ResizeObserver | null>(null);
 | 
			
		||||
  const timelineDuration = useMemo(
 | 
			
		||||
    () => timelineStart - timelineEnd,
 | 
			
		||||
@ -69,12 +68,13 @@ export function EventReviewTimeline({
 | 
			
		||||
      alignEndDateToTimeline,
 | 
			
		||||
      segmentDuration,
 | 
			
		||||
      showHandlebar,
 | 
			
		||||
      handlebarTime,
 | 
			
		||||
      setHandlebarTime,
 | 
			
		||||
      timelineDuration,
 | 
			
		||||
      timelineStart,
 | 
			
		||||
      isDragging,
 | 
			
		||||
      setIsDragging,
 | 
			
		||||
      currentTimeRef,
 | 
			
		||||
      setHandlebarTime,
 | 
			
		||||
      handlebarTimeRef,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  function handleResize() {
 | 
			
		||||
@ -151,66 +151,15 @@ export function EventReviewTimeline({
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (showHandlebar) {
 | 
			
		||||
      requestAnimationFrame(() => {
 | 
			
		||||
        if (currentTimeRef.current && currentTimeSegment) {
 | 
			
		||||
          currentTimeRef.current.textContent = new Date(
 | 
			
		||||
            currentTimeSegment * 1000,
 | 
			
		||||
          ).toLocaleTimeString([], {
 | 
			
		||||
            hour: "2-digit",
 | 
			
		||||
            minute: "2-digit",
 | 
			
		||||
            ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    // we know that these deps are correct
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [currentTimeSegment, showHandlebar]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (onHandlebarDraggingChange) {
 | 
			
		||||
      onHandlebarDraggingChange(isDragging);
 | 
			
		||||
    }
 | 
			
		||||
  }, [isDragging, onHandlebarDraggingChange]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (timelineRef.current && handlebarTime && showHandlebar) {
 | 
			
		||||
      const { scrollHeight: timelineHeight } = timelineRef.current;
 | 
			
		||||
 | 
			
		||||
      // Calculate the height of an individual segment
 | 
			
		||||
      const segmentHeight =
 | 
			
		||||
        timelineHeight / (timelineDuration / segmentDuration);
 | 
			
		||||
 | 
			
		||||
      // Calculate the segment index corresponding to the target time
 | 
			
		||||
      const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime);
 | 
			
		||||
      const segmentIndex = Math.ceil(
 | 
			
		||||
        (timelineStart - alignedHandlebarTime) / segmentDuration,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Calculate the top position based on the segment index
 | 
			
		||||
      const newTopPosition = Math.max(0, segmentIndex * segmentHeight);
 | 
			
		||||
 | 
			
		||||
      // Set the top position of the handle
 | 
			
		||||
      const thumb = scrollTimeRef.current;
 | 
			
		||||
      if (thumb) {
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
          thumb.style.top = `${newTopPosition}px`;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setCurrentTimeSegment(alignedHandlebarTime);
 | 
			
		||||
    }
 | 
			
		||||
    // should only be run once
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    generateSegments();
 | 
			
		||||
    if (!currentTimeSegment && !handlebarTime) {
 | 
			
		||||
      setCurrentTimeSegment(timelineStart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: touch events for mobile
 | 
			
		||||
    document.addEventListener("mousemove", handleMouseMove);
 | 
			
		||||
    document.addEventListener("mouseup", handleMouseUp);
 | 
			
		||||
@ -220,13 +169,7 @@ export function EventReviewTimeline({
 | 
			
		||||
    };
 | 
			
		||||
    // we know that these deps are correct
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [
 | 
			
		||||
    currentTimeSegment,
 | 
			
		||||
    generateSegments,
 | 
			
		||||
    timelineStart,
 | 
			
		||||
    handleMouseUp,
 | 
			
		||||
    handleMouseMove,
 | 
			
		||||
  ]);
 | 
			
		||||
  }, [generateSegments, timelineStart, handleMouseUp, handleMouseMove]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
@ -248,12 +191,12 @@ export function EventReviewTimeline({
 | 
			
		||||
            >
 | 
			
		||||
              <div
 | 
			
		||||
                className={`bg-destructive rounded-full mx-auto ${
 | 
			
		||||
                  segmentDuration < 60 ? "w-20" : "w-16"
 | 
			
		||||
                  segmentDuration < 60 ? "w-14 md:w-20" : "w-12 md:w-16"
 | 
			
		||||
                } h-5 flex items-center justify-center`}
 | 
			
		||||
              >
 | 
			
		||||
                <div
 | 
			
		||||
                  ref={currentTimeRef}
 | 
			
		||||
                  className="text-white text-xs z-10"
 | 
			
		||||
                  ref={handlebarTimeRef}
 | 
			
		||||
                  className="text-white text-[8px] md:text-xs z-10"
 | 
			
		||||
                ></div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { useCallback, useEffect } from "react";
 | 
			
		||||
 | 
			
		||||
interface DragHandlerProps {
 | 
			
		||||
type DragHandlerProps = {
 | 
			
		||||
  contentRef: React.RefObject<HTMLElement>;
 | 
			
		||||
  timelineRef: React.RefObject<HTMLDivElement>;
 | 
			
		||||
  scrollTimeRef: React.RefObject<HTMLDivElement>;
 | 
			
		||||
@ -8,15 +8,15 @@ interface DragHandlerProps {
 | 
			
		||||
  alignEndDateToTimeline: (time: number) => number;
 | 
			
		||||
  segmentDuration: number;
 | 
			
		||||
  showHandlebar: boolean;
 | 
			
		||||
  handlebarTime?: number;
 | 
			
		||||
  setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
 | 
			
		||||
  handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>;
 | 
			
		||||
  timelineDuration: number;
 | 
			
		||||
  timelineStart: number;
 | 
			
		||||
  isDragging: boolean;
 | 
			
		||||
  setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
  currentTimeRef: React.MutableRefObject<HTMLDivElement | null>;
 | 
			
		||||
  setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: handle mobile touch events
 | 
			
		||||
function useDraggableHandler({
 | 
			
		||||
  contentRef,
 | 
			
		||||
  timelineRef,
 | 
			
		||||
@ -24,12 +24,13 @@ function useDraggableHandler({
 | 
			
		||||
  alignStartDateToTimeline,
 | 
			
		||||
  segmentDuration,
 | 
			
		||||
  showHandlebar,
 | 
			
		||||
  handlebarTime,
 | 
			
		||||
  setHandlebarTime,
 | 
			
		||||
  handlebarTimeRef,
 | 
			
		||||
  timelineDuration,
 | 
			
		||||
  timelineStart,
 | 
			
		||||
  isDragging,
 | 
			
		||||
  setIsDragging,
 | 
			
		||||
  currentTimeRef,
 | 
			
		||||
  setHandlebarTime,
 | 
			
		||||
}: DragHandlerProps) {
 | 
			
		||||
  const handleMouseDown = useCallback(
 | 
			
		||||
    (e: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
@ -51,6 +52,39 @@ function useDraggableHandler({
 | 
			
		||||
    [isDragging, setIsDragging],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const getCumulativeScrollTop = useCallback((element: HTMLElement | null) => {
 | 
			
		||||
    let scrollTop = 0;
 | 
			
		||||
    while (element) {
 | 
			
		||||
      scrollTop += element.scrollTop;
 | 
			
		||||
      element = element.parentElement;
 | 
			
		||||
    }
 | 
			
		||||
    return scrollTop;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const updateHandlebarPosition = useCallback(
 | 
			
		||||
    (newHandlePosition: number, segmentStartTime: number) => {
 | 
			
		||||
      const thumb = scrollTimeRef.current;
 | 
			
		||||
      if (thumb) {
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
          thumb.style.top = `${newHandlePosition}px`;
 | 
			
		||||
          if (handlebarTimeRef.current) {
 | 
			
		||||
            handlebarTimeRef.current.textContent = new Date(
 | 
			
		||||
              segmentStartTime * 1000,
 | 
			
		||||
            ).toLocaleTimeString([], {
 | 
			
		||||
              hour: "2-digit",
 | 
			
		||||
              minute: "2-digit",
 | 
			
		||||
              ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        if (setHandlebarTime) {
 | 
			
		||||
          setHandlebarTime(segmentStartTime);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [segmentDuration, handlebarTimeRef, scrollTimeRef, setHandlebarTime],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleMouseMove = useCallback(
 | 
			
		||||
    (e: MouseEvent) => {
 | 
			
		||||
      if (
 | 
			
		||||
@ -64,7 +98,7 @@ function useDraggableHandler({
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
      if (isDragging) {
 | 
			
		||||
      if (showHandlebar && isDragging) {
 | 
			
		||||
        const {
 | 
			
		||||
          scrollHeight: timelineHeight,
 | 
			
		||||
          clientHeight: visibleTimelineHeight,
 | 
			
		||||
@ -75,15 +109,6 @@ function useDraggableHandler({
 | 
			
		||||
        const segmentHeight =
 | 
			
		||||
          timelineHeight / (timelineDuration / segmentDuration);
 | 
			
		||||
 | 
			
		||||
        const getCumulativeScrollTop = (element: HTMLElement | null) => {
 | 
			
		||||
          let scrollTop = 0;
 | 
			
		||||
          while (element) {
 | 
			
		||||
            scrollTop += element.scrollTop;
 | 
			
		||||
            element = element.parentElement;
 | 
			
		||||
          }
 | 
			
		||||
          return scrollTop;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
 | 
			
		||||
 | 
			
		||||
        const newHandlePosition = Math.min(
 | 
			
		||||
@ -99,27 +124,10 @@ function useDraggableHandler({
 | 
			
		||||
          timelineStart - segmentIndex * segmentDuration,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (showHandlebar) {
 | 
			
		||||
          const thumb = scrollTimeRef.current;
 | 
			
		||||
          requestAnimationFrame(() => {
 | 
			
		||||
            thumb.style.top = `${newHandlePosition - segmentHeight}px`;
 | 
			
		||||
            if (currentTimeRef.current) {
 | 
			
		||||
              currentTimeRef.current.textContent = new Date(
 | 
			
		||||
                segmentStartTime * 1000,
 | 
			
		||||
              ).toLocaleTimeString([], {
 | 
			
		||||
                hour: "2-digit",
 | 
			
		||||
                minute: "2-digit",
 | 
			
		||||
                ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          if (setHandlebarTime) {
 | 
			
		||||
            setHandlebarTime(
 | 
			
		||||
              timelineStart -
 | 
			
		||||
                (newHandlePosition / segmentHeight) * segmentDuration,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        updateHandlebarPosition(
 | 
			
		||||
          newHandlePosition - segmentHeight,
 | 
			
		||||
          segmentStartTime,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    // we know that these deps are correct
 | 
			
		||||
@ -131,21 +139,43 @@ function useDraggableHandler({
 | 
			
		||||
      showHandlebar,
 | 
			
		||||
      timelineDuration,
 | 
			
		||||
      timelineStart,
 | 
			
		||||
      updateHandlebarPosition,
 | 
			
		||||
      alignStartDateToTimeline,
 | 
			
		||||
      getCumulativeScrollTop,
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // TODO: determine when we want to do this
 | 
			
		||||
    const handlebar = scrollTimeRef.current;
 | 
			
		||||
    if (handlebar && showHandlebar) {
 | 
			
		||||
      handlebar.scrollIntoView({
 | 
			
		||||
    if (
 | 
			
		||||
      timelineRef.current &&
 | 
			
		||||
      scrollTimeRef.current &&
 | 
			
		||||
      showHandlebar &&
 | 
			
		||||
      handlebarTime &&
 | 
			
		||||
      !isDragging
 | 
			
		||||
    ) {
 | 
			
		||||
      const { scrollHeight: timelineHeight, scrollTop: scrolled } =
 | 
			
		||||
        timelineRef.current;
 | 
			
		||||
 | 
			
		||||
      const segmentHeight =
 | 
			
		||||
        timelineHeight / (timelineDuration / segmentDuration);
 | 
			
		||||
 | 
			
		||||
      const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
 | 
			
		||||
 | 
			
		||||
      const newHandlePosition =
 | 
			
		||||
        ((timelineStart - handlebarTime) / segmentDuration) * segmentHeight +
 | 
			
		||||
        parentScrollTop -
 | 
			
		||||
        scrolled;
 | 
			
		||||
 | 
			
		||||
      updateHandlebarPosition(newHandlePosition - segmentHeight, handlebarTime);
 | 
			
		||||
 | 
			
		||||
      scrollTimeRef.current.scrollIntoView({
 | 
			
		||||
        behavior: "smooth",
 | 
			
		||||
        block: "center",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    // temporary until behavior is decided
 | 
			
		||||
    // we know that these deps are correct
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, []);
 | 
			
		||||
  }, [handlebarTime, showHandlebar, scrollTimeRef, timelineStart]);
 | 
			
		||||
 | 
			
		||||
  return { handleMouseDown, handleMouseUp, handleMouseMove };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -322,7 +322,7 @@ export default function EventView({
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <div
 | 
			
		||||
            className="w-full m-2 grid md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
 | 
			
		||||
            className="w-full m-2 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
 | 
			
		||||
            ref={contentRef}
 | 
			
		||||
          >
 | 
			
		||||
            {currentItems ? (
 | 
			
		||||
@ -366,7 +366,7 @@ export default function EventView({
 | 
			
		||||
            timestampSpread={15}
 | 
			
		||||
            timelineStart={timeRange.before}
 | 
			
		||||
            timelineEnd={timeRange.after}
 | 
			
		||||
            showMinimap={showMinimap}
 | 
			
		||||
            showMinimap={showMinimap && !previewTime}
 | 
			
		||||
            minimapStartTime={minimapBounds.start}
 | 
			
		||||
            minimapEndTime={minimapBounds.end}
 | 
			
		||||
            showHandlebar={previewTime != undefined}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user