mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-12 18:17:36 +03:00
fix scrolling and use custom hook for interaction
This commit is contained in:
parent
fcb28cf1c2
commit
ddd029b7d9
@ -3,6 +3,8 @@ import { ObjectLifecycleSequence } from "@/types/timeline";
|
|||||||
import { LifecycleIcon } from "@/components/overlay/detail/ObjectLifecycle";
|
import { LifecycleIcon } from "@/components/overlay/detail/ObjectLifecycle";
|
||||||
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
||||||
import { useActivityStream } from "@/contexts/ActivityStreamContext";
|
import { useActivityStream } from "@/contexts/ActivityStreamContext";
|
||||||
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
|
import useUserInteraction from "@/hooks/use-user-interaction";
|
||||||
|
|
||||||
type ActivityStreamProps = {
|
type ActivityStreamProps = {
|
||||||
timelineData: ObjectLifecycleSequence[];
|
timelineData: ObjectLifecycleSequence[];
|
||||||
@ -20,6 +22,11 @@ export default function ActivityStream({
|
|||||||
|
|
||||||
const effectiveTime = currentTime + annotationOffset;
|
const effectiveTime = currentTime + annotationOffset;
|
||||||
|
|
||||||
|
// Track user interaction and adjust scrolling behavior
|
||||||
|
const { userInteracting, setProgrammaticScroll } = useUserInteraction({
|
||||||
|
elementRef: scrollRef,
|
||||||
|
});
|
||||||
|
|
||||||
// group activities by timestamp (within 1 second resolution window)
|
// group activities by timestamp (within 1 second resolution window)
|
||||||
const groupedActivities = useMemo(() => {
|
const groupedActivities = useMemo(() => {
|
||||||
const groups: { [key: number]: ObjectLifecycleSequence[] } = {};
|
const groups: { [key: number]: ObjectLifecycleSequence[] } = {};
|
||||||
@ -63,7 +70,7 @@ export default function ActivityStream({
|
|||||||
|
|
||||||
// Auto-scroll to current time
|
// Auto-scroll to current time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollRef.current) return;
|
if (!scrollRef.current || userInteracting) return;
|
||||||
|
|
||||||
// Find the last group where effectiveTimestamp <= currentTime + annotationOffset
|
// Find the last group where effectiveTimestamp <= currentTime + annotationOffset
|
||||||
let currentGroupIndex = -1;
|
let currentGroupIndex = -1;
|
||||||
@ -75,17 +82,24 @@ export default function ActivityStream({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentGroupIndex !== -1) {
|
if (currentGroupIndex !== -1) {
|
||||||
const element = scrollRef.current.children[
|
const element = scrollRef.current.querySelector(
|
||||||
currentGroupIndex
|
`[data-timestamp="${filteredGroups[currentGroupIndex].timestamp}"]`,
|
||||||
] as HTMLElement;
|
) as HTMLElement;
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({
|
setProgrammaticScroll();
|
||||||
|
scrollIntoView(element, {
|
||||||
|
scrollMode: "if-needed",
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "center",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [filteredGroups, effectiveTime, annotationOffset]);
|
}, [
|
||||||
|
filteredGroups,
|
||||||
|
effectiveTime,
|
||||||
|
annotationOffset,
|
||||||
|
userInteracting,
|
||||||
|
setProgrammaticScroll,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -127,6 +141,7 @@ function ActivityGroup({ group, isCurrent, onSeek }: ActivityGroupProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-timestamp={group.timestamp}
|
||||||
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-primary/20 bg-primary/10"
|
? "border-primary/20 bg-primary/10"
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTimelineUtils } from "./use-timeline-utils";
|
import { useTimelineUtils } from "./use-timeline-utils";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
import { useDateLocale } from "./use-date-locale";
|
import { useDateLocale } from "./use-date-locale";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useUserInteraction from "./use-user-interaction";
|
||||||
|
|
||||||
type DraggableElementProps = {
|
type DraggableElementProps = {
|
||||||
contentRef: React.RefObject<HTMLElement>;
|
contentRef: React.RefObject<HTMLElement>;
|
||||||
@ -71,9 +72,9 @@ function useDraggableElement({
|
|||||||
|
|
||||||
// track user interaction and adjust scrolling behavior
|
// track user interaction and adjust scrolling behavior
|
||||||
|
|
||||||
const [userInteracting, setUserInteracting] = useState(false);
|
const { userInteracting } = useUserInteraction({
|
||||||
const interactionTimeout = useRef<NodeJS.Timeout>();
|
elementRef: timelineRef,
|
||||||
const isProgrammaticScroll = useRef(false);
|
});
|
||||||
|
|
||||||
const draggingAtTopEdge = useMemo(() => {
|
const draggingAtTopEdge = useMemo(() => {
|
||||||
if (clientYPosition && timelineRef.current && scrollEdgeSize) {
|
if (clientYPosition && timelineRef.current && scrollEdgeSize) {
|
||||||
@ -507,47 +508,6 @@ function useDraggableElement({
|
|||||||
}
|
}
|
||||||
}, [timelineRef, segmentsRef, segments]);
|
}, [timelineRef, segmentsRef, segments]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleUserInteraction = () => {
|
|
||||||
if (!isProgrammaticScroll.current) {
|
|
||||||
setUserInteracting(true);
|
|
||||||
|
|
||||||
if (interactionTimeout.current) {
|
|
||||||
clearTimeout(interactionTimeout.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
interactionTimeout.current = setTimeout(() => {
|
|
||||||
setUserInteracting(false);
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
isProgrammaticScroll.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const timelineElement = timelineRef.current;
|
|
||||||
|
|
||||||
if (timelineElement) {
|
|
||||||
timelineElement.addEventListener("scroll", handleUserInteraction);
|
|
||||||
timelineElement.addEventListener("mousedown", handleUserInteraction);
|
|
||||||
timelineElement.addEventListener("mouseup", handleUserInteraction);
|
|
||||||
timelineElement.addEventListener("touchstart", handleUserInteraction);
|
|
||||||
timelineElement.addEventListener("touchmove", handleUserInteraction);
|
|
||||||
timelineElement.addEventListener("touchend", handleUserInteraction);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
timelineElement.removeEventListener("scroll", handleUserInteraction);
|
|
||||||
timelineElement.removeEventListener("mousedown", handleUserInteraction);
|
|
||||||
timelineElement.removeEventListener("mouseup", handleUserInteraction);
|
|
||||||
timelineElement.removeEventListener(
|
|
||||||
"touchstart",
|
|
||||||
handleUserInteraction,
|
|
||||||
);
|
|
||||||
timelineElement.removeEventListener("touchmove", handleUserInteraction);
|
|
||||||
timelineElement.removeEventListener("touchend", handleUserInteraction);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [timelineRef]);
|
|
||||||
|
|
||||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
web/src/hooks/use-user-interaction.ts
Normal file
57
web/src/hooks/use-user-interaction.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type UseUserInteractionProps = {
|
||||||
|
elementRef: React.RefObject<HTMLElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
||||||
|
const [userInteracting, setUserInteracting] = useState(false);
|
||||||
|
const interactionTimeout = useRef<NodeJS.Timeout>();
|
||||||
|
const isProgrammaticScroll = useRef(false);
|
||||||
|
|
||||||
|
const setProgrammaticScroll = useCallback(() => {
|
||||||
|
isProgrammaticScroll.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUserInteraction = () => {
|
||||||
|
if (!isProgrammaticScroll.current) {
|
||||||
|
setUserInteracting(true);
|
||||||
|
|
||||||
|
if (interactionTimeout.current) {
|
||||||
|
clearTimeout(interactionTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
interactionTimeout.current = setTimeout(() => {
|
||||||
|
setUserInteracting(false);
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
isProgrammaticScroll.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = elementRef.current;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener("scroll", handleUserInteraction);
|
||||||
|
element.addEventListener("mousedown", handleUserInteraction);
|
||||||
|
element.addEventListener("mouseup", handleUserInteraction);
|
||||||
|
element.addEventListener("touchstart", handleUserInteraction);
|
||||||
|
element.addEventListener("touchmove", handleUserInteraction);
|
||||||
|
element.addEventListener("touchend", handleUserInteraction);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener("scroll", handleUserInteraction);
|
||||||
|
element.removeEventListener("mousedown", handleUserInteraction);
|
||||||
|
element.removeEventListener("mouseup", handleUserInteraction);
|
||||||
|
element.removeEventListener("touchstart", handleUserInteraction);
|
||||||
|
element.removeEventListener("touchmove", handleUserInteraction);
|
||||||
|
element.removeEventListener("touchend", handleUserInteraction);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [elementRef]);
|
||||||
|
|
||||||
|
return { userInteracting, setProgrammaticScroll };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUserInteraction;
|
||||||
Loading…
Reference in New Issue
Block a user