mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	More intelligent timeline scrolling (#10209)
* more intelligent timeline scrolling * keep as div
This commit is contained in:
		
							parent
							
								
									8645545ef4
								
							
						
					
					
						commit
						312dc95156
					
				
							
								
								
									
										14
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -52,6 +52,7 @@
 | 
				
			|||||||
        "react-transition-group": "^4.4.5",
 | 
					        "react-transition-group": "^4.4.5",
 | 
				
			||||||
        "react-use-websocket": "^4.7.0",
 | 
					        "react-use-websocket": "^4.7.0",
 | 
				
			||||||
        "recoil": "^0.7.7",
 | 
					        "recoil": "^0.7.7",
 | 
				
			||||||
 | 
					        "scroll-into-view-if-needed": "^3.1.0",
 | 
				
			||||||
        "sonner": "^1.4.0",
 | 
					        "sonner": "^1.4.0",
 | 
				
			||||||
        "sort-by": "^1.2.0",
 | 
					        "sort-by": "^1.2.0",
 | 
				
			||||||
        "strftime": "^0.10.2",
 | 
					        "strftime": "^0.10.2",
 | 
				
			||||||
@ -3761,6 +3762,11 @@
 | 
				
			|||||||
        "node": ">= 6"
 | 
					        "node": ">= 6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/compute-scroll-into-view": {
 | 
				
			||||||
 | 
					      "version": "3.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/concat-map": {
 | 
					    "node_modules/concat-map": {
 | 
				
			||||||
      "version": "0.0.1",
 | 
					      "version": "0.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 | 
				
			||||||
@ -7213,6 +7219,14 @@
 | 
				
			|||||||
        "loose-envify": "^1.1.0"
 | 
					        "loose-envify": "^1.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/scroll-into-view-if-needed": {
 | 
				
			||||||
 | 
					      "version": "3.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "compute-scroll-into-view": "^3.0.2"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/semver": {
 | 
					    "node_modules/semver": {
 | 
				
			||||||
      "version": "7.5.4",
 | 
					      "version": "7.5.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -57,6 +57,7 @@
 | 
				
			|||||||
    "react-transition-group": "^4.4.5",
 | 
					    "react-transition-group": "^4.4.5",
 | 
				
			||||||
    "react-use-websocket": "^4.7.0",
 | 
					    "react-use-websocket": "^4.7.0",
 | 
				
			||||||
    "recoil": "^0.7.7",
 | 
					    "recoil": "^0.7.7",
 | 
				
			||||||
 | 
					    "scroll-into-view-if-needed": "^3.1.0",
 | 
				
			||||||
    "sonner": "^1.4.0",
 | 
					    "sonner": "^1.4.0",
 | 
				
			||||||
    "sort-by": "^1.2.0",
 | 
					    "sort-by": "^1.2.0",
 | 
				
			||||||
    "strftime": "^0.10.2",
 | 
					    "strftime": "^0.10.2",
 | 
				
			||||||
 | 
				
			|||||||
@ -71,7 +71,7 @@ export default function ReviewFilterGroup({
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="mr-2">
 | 
					    <div>
 | 
				
			||||||
      <CamerasFilterButton
 | 
					      <CamerasFilterButton
 | 
				
			||||||
        allCameras={filterValues.cameras}
 | 
					        allCameras={filterValues.cameras}
 | 
				
			||||||
        selectedCameras={filter?.cameras}
 | 
					        selectedCameras={filter?.cameras}
 | 
				
			||||||
@ -246,7 +246,7 @@ function GeneralFilterButton({
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Popover>
 | 
					    <Popover>
 | 
				
			||||||
      <PopoverTrigger asChild>
 | 
					      <PopoverTrigger asChild>
 | 
				
			||||||
        <Button size="sm" className="mx-1" variant="secondary">
 | 
					        <Button size="sm" className="ml-1" variant="secondary">
 | 
				
			||||||
          <FaFilter className="md:mr-[10px] text-muted-foreground" />
 | 
					          <FaFilter className="md:mr-[10px] text-muted-foreground" />
 | 
				
			||||||
          <div className="hidden md:block">Filter</div>
 | 
					          <div className="hidden md:block">Filter</div>
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ import {
 | 
				
			|||||||
  HoverCardTrigger,
 | 
					  HoverCardTrigger,
 | 
				
			||||||
} from "../ui/hover-card";
 | 
					} from "../ui/hover-card";
 | 
				
			||||||
import { HoverCardPortal } from "@radix-ui/react-hover-card";
 | 
					import { HoverCardPortal } from "@radix-ui/react-hover-card";
 | 
				
			||||||
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type EventSegmentProps = {
 | 
					type EventSegmentProps = {
 | 
				
			||||||
  events: ReviewSegment[];
 | 
					  events: ReviewSegment[];
 | 
				
			||||||
@ -225,20 +226,14 @@ export function EventSegment({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const firstMinimapSegmentRef = useRef<HTMLDivElement>(null);
 | 
					  const firstMinimapSegmentRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let debounceTimer: ReturnType<typeof setTimeout>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function debounceScrollIntoView(element: HTMLElement) {
 | 
					 | 
				
			||||||
    clearTimeout(debounceTimer);
 | 
					 | 
				
			||||||
    debounceTimer = setTimeout(() => {
 | 
					 | 
				
			||||||
      element.scrollIntoView({ behavior: "smooth", block: "center" });
 | 
					 | 
				
			||||||
    }, 100);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    // Check if the first segment is out of view
 | 
					    // Check if the first segment is out of view
 | 
				
			||||||
    const firstSegment = firstMinimapSegmentRef.current;
 | 
					    const firstSegment = firstMinimapSegmentRef.current;
 | 
				
			||||||
    if (firstSegment && showMinimap && isFirstSegmentInMinimap) {
 | 
					    if (firstSegment && showMinimap && isFirstSegmentInMinimap) {
 | 
				
			||||||
      debounceScrollIntoView(firstSegment);
 | 
					      scrollIntoView(firstSegment, {
 | 
				
			||||||
 | 
					        scrollMode: "if-needed",
 | 
				
			||||||
 | 
					        behavior: "smooth",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // 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
 | 
				
			||||||
@ -276,7 +271,10 @@ export function EventSegment({
 | 
				
			|||||||
        `[data-segment-start="${startTimestamp - segmentDuration}"]`,
 | 
					        `[data-segment-start="${startTimestamp - segmentDuration}"]`,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      if (element instanceof HTMLElement) {
 | 
					      if (element instanceof HTMLElement) {
 | 
				
			||||||
        debounceScrollIntoView(element);
 | 
					        scrollIntoView(element, {
 | 
				
			||||||
 | 
					          scrollMode: "if-needed",
 | 
				
			||||||
 | 
					          behavior: "smooth",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
        element.classList.add(
 | 
					        element.classList.add(
 | 
				
			||||||
          `outline-severity_${severityType}`,
 | 
					          `outline-severity_${severityType}`,
 | 
				
			||||||
          `shadow-severity_${severityType}`,
 | 
					          `shadow-severity_${severityType}`,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { useCallback, useEffect } from "react";
 | 
					import { useCallback, useEffect } from "react";
 | 
				
			||||||
 | 
					import scrollIntoView from "scroll-into-view-if-needed";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DragHandlerProps = {
 | 
					type DragHandlerProps = {
 | 
				
			||||||
  contentRef: React.RefObject<HTMLElement>;
 | 
					  contentRef: React.RefObject<HTMLElement>;
 | 
				
			||||||
@ -75,6 +76,10 @@ function useDraggableHandler({
 | 
				
			|||||||
              minute: "2-digit",
 | 
					              minute: "2-digit",
 | 
				
			||||||
              ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
					              ...(segmentDuration < 60 && { second: "2-digit" }),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					            scrollIntoView(thumb, {
 | 
				
			||||||
 | 
					              scrollMode: "if-needed",
 | 
				
			||||||
 | 
					              behavior: "smooth",
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        if (setHandlebarTime) {
 | 
					        if (setHandlebarTime) {
 | 
				
			||||||
@ -167,11 +172,6 @@ function useDraggableHandler({
 | 
				
			|||||||
        scrolled;
 | 
					        scrolled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      updateHandlebarPosition(newHandlePosition - segmentHeight, handlebarTime);
 | 
					      updateHandlebarPosition(newHandlePosition - segmentHeight, handlebarTime);
 | 
				
			||||||
 | 
					 | 
				
			||||||
      scrollTimeRef.current.scrollIntoView({
 | 
					 | 
				
			||||||
        behavior: "smooth",
 | 
					 | 
				
			||||||
        block: "center",
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // 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
 | 
				
			||||||
 | 
				
			|||||||
@ -247,12 +247,12 @@ export default function EventView({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="flex flex-col size-full">
 | 
					    <div className="flex flex-col size-full">
 | 
				
			||||||
      <div className="h-10 relative flex justify-between items-center mb-2">
 | 
					      <div className="h-10 relative flex justify-between items-center m-2">
 | 
				
			||||||
        {isMobile && (
 | 
					        {isMobile && (
 | 
				
			||||||
          <Logo className="absolute inset-y-0 inset-x-1/2 -translate-x-1/2 h-8" />
 | 
					          <Logo className="absolute inset-y-0 inset-x-1/2 -translate-x-1/2 h-8" />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        <ToggleGroup
 | 
					        <ToggleGroup
 | 
				
			||||||
          className="*:px-3 *:py4 *:rounded-2xl"
 | 
					          className="*:px-3 *:py-4 *:rounded-2xl"
 | 
				
			||||||
          type="single"
 | 
					          type="single"
 | 
				
			||||||
          defaultValue="alert"
 | 
					          defaultValue="alert"
 | 
				
			||||||
          size="sm"
 | 
					          size="sm"
 | 
				
			||||||
@ -315,7 +315,7 @@ export default function EventView({
 | 
				
			|||||||
          {!isValidating && currentItems == null && (
 | 
					          {!isValidating && currentItems == null && (
 | 
				
			||||||
            <div className="size-full flex flex-col justify-center items-center">
 | 
					            <div className="size-full flex flex-col justify-center items-center">
 | 
				
			||||||
              <LuFolderCheck className="size-16" />
 | 
					              <LuFolderCheck className="size-16" />
 | 
				
			||||||
              There are no {severity} items to review
 | 
					              There are no {severity.replace(/_/g, " ")} items to review
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -120,7 +120,7 @@ export default function LiveDashboardView({
 | 
				
			|||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        className={`mt-4 grid ${layout == "grid" ? "grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : ""} gap-2 md:gap-4  *:rounded-2xl *:bg-black`}
 | 
					        className={`my-4 grid ${layout == "grid" ? "grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : ""} gap-2 md:gap-4  *:rounded-2xl *:bg-black`}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {cameras.map((camera) => {
 | 
					        {cameras.map((camera) => {
 | 
				
			||||||
          let grow;
 | 
					          let grow;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user