mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-22 17:06:39 -06: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