mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Timeline performance improvements (#12070)
* Use intersection observer for timeline segments * only render when visible
This commit is contained in:
parent
e9cdef9f25
commit
5c33cdba4e
@ -8,6 +8,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
@ -195,9 +196,48 @@ export function EventSegment({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [startTimestamp]);
|
}, [startTimestamp]);
|
||||||
|
|
||||||
|
const [segmentRendered, setSegmentRendered] = useState(false);
|
||||||
|
const segmentObserverRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
const segmentRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const segmentObserver = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && !segmentRendered) {
|
||||||
|
setSegmentRendered(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (segmentRef.current) {
|
||||||
|
segmentObserver.observe(segmentRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentObserverRef.current = segmentObserver;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (segmentObserverRef.current) {
|
||||||
|
segmentObserverRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [segmentRendered]);
|
||||||
|
|
||||||
|
if (!segmentRendered) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
|
ref={segmentRef}
|
||||||
|
data-segment-id={segmentKey}
|
||||||
|
className={`segment ${segmentClasses}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={segmentKey}
|
||||||
|
ref={segmentRef}
|
||||||
data-segment-id={segmentKey}
|
data-segment-id={segmentKey}
|
||||||
className={`segment ${segmentClasses}`}
|
className={`segment ${segmentClasses}`}
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useCallback, useMemo, useRef, RefObject } from "react";
|
import { useCallback, useMemo, useRef, RefObject } from "react";
|
||||||
import MotionSegment from "./MotionSegment";
|
import MotionSegment from "./MotionSegment";
|
||||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import ReviewTimeline from "./ReviewTimeline";
|
import ReviewTimeline from "./ReviewTimeline";
|
||||||
import { isDesktop } from "react-device-detect";
|
|
||||||
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
|
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
|
||||||
|
|
||||||
export type MotionReviewTimelineProps = {
|
export type MotionReviewTimelineProps = {
|
||||||
@ -165,42 +164,6 @@ export function MotionReviewTimeline({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const segmentsObserver = useRef<IntersectionObserver | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedTimelineRef.current && segments && isDesktop) {
|
|
||||||
segmentsObserver.current = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const segmentId = entry.target.getAttribute("data-segment-id");
|
|
||||||
|
|
||||||
const segmentElements =
|
|
||||||
internalTimelineRef.current?.querySelectorAll(
|
|
||||||
`[data-segment-id="${segmentId}"] .motion-segment`,
|
|
||||||
);
|
|
||||||
segmentElements?.forEach((segmentElement) => {
|
|
||||||
segmentElement.classList.remove("hidden");
|
|
||||||
segmentElement.classList.add("animate-in");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get all segment divs and observe each one
|
|
||||||
const segmentDivs =
|
|
||||||
selectedTimelineRef.current.querySelectorAll(".segment.has-data");
|
|
||||||
segmentDivs.forEach((segmentDiv) => {
|
|
||||||
segmentsObserver.current?.observe(segmentDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
segmentsObserver.current?.disconnect();
|
|
||||||
};
|
|
||||||
}, [selectedTimelineRef, segments]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReviewTimeline
|
<ReviewTimeline
|
||||||
timelineRef={selectedTimelineRef}
|
timelineRef={selectedTimelineRef}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} 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 { isDesktop, isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import useTapUtils from "@/hooks/use-tap-utils";
|
import useTapUtils from "@/hooks/use-tap-utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@ -139,11 +145,6 @@ export function MotionSegment({
|
|||||||
: ""
|
: ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const animationClassesSecondHalf = `motion-segment ${secondHalfSegmentWidth > 0 ? "hidden" : ""}
|
|
||||||
zoom-in-[0.2] ${secondHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"}`;
|
|
||||||
const animationClassesFirstHalf = `motion-segment ${firstHalfSegmentWidth > 0 ? "hidden" : ""}
|
|
||||||
zoom-in-[0.2] ${firstHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"}`;
|
|
||||||
|
|
||||||
const severityColorsBg: { [key: number]: string } = {
|
const severityColorsBg: { [key: number]: string } = {
|
||||||
1: reviewed
|
1: reviewed
|
||||||
? "from-severity_significant_motion-dimmed/10 to-severity_significant_motion/10"
|
? "from-severity_significant_motion-dimmed/10 to-severity_significant_motion/10"
|
||||||
@ -162,6 +163,44 @@ export function MotionSegment({
|
|||||||
}
|
}
|
||||||
}, [segmentTime, setHandlebarTime]);
|
}, [segmentTime, setHandlebarTime]);
|
||||||
|
|
||||||
|
const [segmentRendered, setSegmentRendered] = useState(false);
|
||||||
|
const segmentObserverRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
const segmentRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const segmentObserver = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && !segmentRendered) {
|
||||||
|
setSegmentRendered(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (segmentRef.current) {
|
||||||
|
segmentObserver.observe(segmentRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentObserverRef.current = segmentObserver;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (segmentObserverRef.current) {
|
||||||
|
segmentObserverRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [segmentRendered]);
|
||||||
|
|
||||||
|
if (!segmentRendered) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={segmentKey}
|
||||||
|
ref={segmentRef}
|
||||||
|
data-segment-id={segmentKey}
|
||||||
|
className={`segment ${segmentClasses}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(((firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0) &&
|
{(((firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0) &&
|
||||||
@ -171,6 +210,7 @@ export function MotionSegment({
|
|||||||
<div
|
<div
|
||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
data-segment-id={segmentKey}
|
data-segment-id={segmentKey}
|
||||||
|
ref={segmentRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"segment",
|
"segment",
|
||||||
{
|
{
|
||||||
@ -221,7 +261,6 @@ export function MotionSegment({
|
|||||||
key={`${segmentKey}_motion_data_1`}
|
key={`${segmentKey}_motion_data_1`}
|
||||||
data-motion-value={secondHalfSegmentWidth}
|
data-motion-value={secondHalfSegmentWidth}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDesktop && animationClassesSecondHalf,
|
|
||||||
"h-[2px]",
|
"h-[2px]",
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
secondHalfSegmentWidth
|
secondHalfSegmentWidth
|
||||||
@ -241,7 +280,6 @@ export function MotionSegment({
|
|||||||
key={`${segmentKey}_motion_data_2`}
|
key={`${segmentKey}_motion_data_2`}
|
||||||
data-motion-value={firstHalfSegmentWidth}
|
data-motion-value={firstHalfSegmentWidth}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDesktop && animationClassesFirstHalf,
|
|
||||||
"h-[2px]",
|
"h-[2px]",
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
firstHalfSegmentWidth
|
firstHalfSegmentWidth
|
||||||
|
Loading…
Reference in New Issue
Block a user