mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Timeline minimap and scrolling changes (#10589)
* add function to get visible timeline duration * Don't show minimap when minimap bounds exceed timeline area * when minimap is hidden, only scroll timeline when needed * observe only when not showing minimap * no need to duplicate observer * fix out of order param * timeline utils hook props --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
973275e163
commit
0ac7aaabe3
@ -11,6 +11,7 @@ import EventSegment from "./EventSegment";
|
|||||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import ReviewTimeline from "./ReviewTimeline";
|
import ReviewTimeline from "./ReviewTimeline";
|
||||||
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
|
|
||||||
export type EventReviewTimelineProps = {
|
export type EventReviewTimelineProps = {
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
@ -29,6 +30,7 @@ export type EventReviewTimelineProps = {
|
|||||||
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
|
visibleTimestamps?: number[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
timelineRef?: RefObject<HTMLDivElement>;
|
timelineRef?: RefObject<HTMLDivElement>;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement>;
|
||||||
@ -52,6 +54,7 @@ export function EventReviewTimeline({
|
|||||||
setExportStartTime,
|
setExportStartTime,
|
||||||
setExportEndTime,
|
setExportEndTime,
|
||||||
events,
|
events,
|
||||||
|
visibleTimestamps,
|
||||||
severityType,
|
severityType,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
contentRef,
|
contentRef,
|
||||||
@ -68,14 +71,20 @@ export function EventReviewTimeline({
|
|||||||
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
||||||
const exportEndRef = useRef<HTMLDivElement>(null);
|
const exportEndRef = useRef<HTMLDivElement>(null);
|
||||||
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
||||||
|
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||||
|
|
||||||
const timelineDuration = useMemo(
|
const timelineDuration = useMemo(
|
||||||
() => timelineStart - timelineEnd,
|
() => timelineStart - timelineEnd,
|
||||||
[timelineEnd, timelineStart],
|
[timelineEnd, timelineStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
useTimelineUtils(segmentDuration);
|
{
|
||||||
|
segmentDuration,
|
||||||
|
timelineDuration,
|
||||||
|
timelineRef: selectedTimelineRef,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const timelineStartAligned = useMemo(
|
const timelineStartAligned = useMemo(
|
||||||
() => alignStartDateToTimeline(timelineStart),
|
() => alignStartDateToTimeline(timelineStart),
|
||||||
@ -100,7 +109,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: handlebarMouseMove,
|
handleMouseMove: handlebarMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef: timelineRef || internalTimelineRef,
|
timelineRef: selectedTimelineRef,
|
||||||
draggableElementRef: handlebarRef,
|
draggableElementRef: handlebarRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showHandlebar,
|
showDraggableElement: showHandlebar,
|
||||||
@ -119,7 +128,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: exportStartMouseMove,
|
handleMouseMove: exportStartMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef: timelineRef || internalTimelineRef,
|
timelineRef: selectedTimelineRef,
|
||||||
draggableElementRef: exportStartRef,
|
draggableElementRef: exportStartRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -140,7 +149,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: exportEndMouseMove,
|
handleMouseMove: exportEndMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef: timelineRef || internalTimelineRef,
|
timelineRef: selectedTimelineRef,
|
||||||
draggableElementRef: exportEndRef,
|
draggableElementRef: exportEndRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -213,9 +222,36 @@ export function EventReviewTimeline({
|
|||||||
}
|
}
|
||||||
}, [isDragging, onHandlebarDraggingChange]);
|
}, [isDragging, onHandlebarDraggingChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedTimelineRef.current &&
|
||||||
|
segments &&
|
||||||
|
visibleTimestamps &&
|
||||||
|
visibleTimestamps?.length > 0 &&
|
||||||
|
!showMinimap
|
||||||
|
) {
|
||||||
|
const alignedVisibleTimestamps = visibleTimestamps.map(
|
||||||
|
alignStartDateToTimeline,
|
||||||
|
);
|
||||||
|
const element = selectedTimelineRef.current?.querySelector(
|
||||||
|
`[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`,
|
||||||
|
);
|
||||||
|
scrollIntoView(element as HTMLDivElement, {
|
||||||
|
scrollMode: "if-needed",
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
selectedTimelineRef,
|
||||||
|
segments,
|
||||||
|
showMinimap,
|
||||||
|
alignStartDateToTimeline,
|
||||||
|
visibleTimestamps,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReviewTimeline
|
<ReviewTimeline
|
||||||
timelineRef={timelineRef || internalTimelineRef}
|
timelineRef={selectedTimelineRef}
|
||||||
handlebarRef={handlebarRef}
|
handlebarRef={handlebarRef}
|
||||||
handlebarTimeRef={handlebarTimeRef}
|
handlebarTimeRef={handlebarTimeRef}
|
||||||
handlebarMouseMove={handlebarMouseMove}
|
handlebarMouseMove={handlebarMouseMove}
|
||||||
|
@ -53,8 +53,9 @@ export function EventSegment({
|
|||||||
getEventThumbnail,
|
getEventThumbnail,
|
||||||
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
useTimelineUtils(segmentDuration);
|
{ segmentDuration },
|
||||||
|
);
|
||||||
|
|
||||||
const severity = useMemo(
|
const severity = useMemo(
|
||||||
() => getSeverity(segmentTime, displaySeverityType),
|
() => getSeverity(segmentTime, displaySeverityType),
|
||||||
@ -199,6 +200,7 @@ export function EventSegment({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
|
data-segment-id={segmentKey}
|
||||||
className={segmentClasses}
|
className={segmentClasses}
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||||
|
@ -76,8 +76,12 @@ export function MotionReviewTimeline({
|
|||||||
[timelineEnd, timelineStart, segmentDuration],
|
[timelineEnd, timelineStart, segmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
useTimelineUtils(segmentDuration);
|
{
|
||||||
|
segmentDuration,
|
||||||
|
timelineDuration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const timelineStartAligned = useMemo(
|
const timelineStartAligned = useMemo(
|
||||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||||
|
@ -42,8 +42,9 @@ export function MotionSegment({
|
|||||||
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
||||||
useMotionSegmentUtils(segmentDuration, motion_events);
|
useMotionSegmentUtils(segmentDuration, motion_events);
|
||||||
|
|
||||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
useTimelineUtils(segmentDuration);
|
{ segmentDuration },
|
||||||
|
);
|
||||||
|
|
||||||
const { handleTouchStart } = useTapUtils();
|
const { handleTouchStart } = useTapUtils();
|
||||||
|
|
||||||
|
@ -39,18 +39,22 @@ export function SummaryTimeline({
|
|||||||
|
|
||||||
const observer = useRef<ResizeObserver | null>(null);
|
const observer = useRef<ResizeObserver | null>(null);
|
||||||
|
|
||||||
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
const reviewTimelineDuration = useMemo(
|
||||||
|
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
||||||
|
[timelineEnd, timelineStart, segmentDuration],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { alignStartDateToTimeline } = useTimelineUtils({
|
||||||
|
segmentDuration,
|
||||||
|
timelineDuration: reviewTimelineDuration,
|
||||||
|
timelineRef: reviewTimelineRef,
|
||||||
|
});
|
||||||
|
|
||||||
const timelineStartAligned = useMemo(
|
const timelineStartAligned = useMemo(
|
||||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||||
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reviewTimelineDuration = useMemo(
|
|
||||||
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
|
||||||
[timelineEnd, timelineStart, segmentDuration],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate segments for the timeline
|
// Generate segments for the timeline
|
||||||
const generateSegments = useCallback(() => {
|
const generateSegments = useCallback(() => {
|
||||||
const segmentCount = reviewTimelineDuration / segmentDuration;
|
const segmentCount = reviewTimelineDuration / segmentDuration;
|
||||||
|
@ -40,8 +40,13 @@ function useDraggableElement({
|
|||||||
}: DraggableElementProps) {
|
}: DraggableElementProps) {
|
||||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||||
const { alignStartDateToTimeline, getCumulativeScrollTop } =
|
const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils(
|
||||||
useTimelineUtils(segmentDuration);
|
{
|
||||||
|
segmentDuration: segmentDuration,
|
||||||
|
timelineDuration: timelineDuration,
|
||||||
|
timelineRef,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const draggingAtTopEdge = useMemo(() => {
|
const draggingAtTopEdge = useMemo(() => {
|
||||||
if (clientYPosition && timelineRef.current) {
|
if (clientYPosition && timelineRef.current) {
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
export const useTimelineUtils = (segmentDuration: number) => {
|
export type TimelineUtilsProps = {
|
||||||
|
segmentDuration: number;
|
||||||
|
timelineDuration?: number;
|
||||||
|
timelineRef?: React.RefObject<HTMLElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTimelineUtils({
|
||||||
|
segmentDuration,
|
||||||
|
timelineDuration,
|
||||||
|
timelineRef,
|
||||||
|
}: TimelineUtilsProps) {
|
||||||
const alignEndDateToTimeline = useCallback(
|
const alignEndDateToTimeline = useCallback(
|
||||||
(time: number): number => {
|
(time: number): number => {
|
||||||
const remainder = time % segmentDuration;
|
const remainder = time % segmentDuration;
|
||||||
@ -28,9 +38,27 @@ export const useTimelineUtils = (segmentDuration: number) => {
|
|||||||
return scrollTop;
|
return scrollTop;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getVisibleTimelineDuration = useCallback(() => {
|
||||||
|
if (timelineRef?.current && timelineDuration) {
|
||||||
|
const {
|
||||||
|
scrollHeight: timelineHeight,
|
||||||
|
clientHeight: visibleTimelineHeight,
|
||||||
|
} = timelineRef.current;
|
||||||
|
|
||||||
|
const segmentHeight =
|
||||||
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
|
const visibleTime =
|
||||||
|
(visibleTimelineHeight / segmentHeight) * segmentDuration;
|
||||||
|
|
||||||
|
return visibleTime;
|
||||||
|
}
|
||||||
|
}, [segmentDuration, timelineDuration, timelineRef]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alignEndDateToTimeline,
|
alignEndDateToTimeline,
|
||||||
alignStartDateToTimeline,
|
alignStartDateToTimeline,
|
||||||
getCumulativeScrollTop,
|
getCumulativeScrollTop,
|
||||||
|
getVisibleTimelineDuration,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
@ -379,7 +379,17 @@ function DetectionReview({
|
|||||||
|
|
||||||
// timeline interaction
|
// timeline interaction
|
||||||
|
|
||||||
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
const timelineDuration = useMemo(
|
||||||
|
() => timeRange.before - timeRange.after,
|
||||||
|
[timeRange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { alignStartDateToTimeline, getVisibleTimelineDuration } =
|
||||||
|
useTimelineUtils({
|
||||||
|
segmentDuration,
|
||||||
|
timelineDuration,
|
||||||
|
timelineRef: reviewTimelineRef,
|
||||||
|
});
|
||||||
|
|
||||||
const scrollLock = useScrollLockout(contentRef);
|
const scrollLock = useScrollLockout(contentRef);
|
||||||
|
|
||||||
@ -448,10 +458,26 @@ function DetectionReview({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return contentRef.current.scrollHeight > contentRef.current.clientHeight;
|
// don't show minimap if the view is not scrollable
|
||||||
|
if (contentRef.current.scrollHeight < contentRef.current.clientHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleTime = getVisibleTimelineDuration();
|
||||||
|
const minimapTime = minimapBounds.end - minimapBounds.start;
|
||||||
|
if (visibleTime && minimapTime >= visibleTime * 0.75) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
// 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
|
||||||
}, [contentRef.current?.scrollHeight, severity]);
|
}, [contentRef.current?.scrollHeight, minimapBounds]);
|
||||||
|
|
||||||
|
const visibleTimestamps = useMemo(
|
||||||
|
() => minimap.map((str) => parseFloat(str)),
|
||||||
|
[minimap],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -499,7 +525,7 @@ function DetectionReview({
|
|||||||
data-segment-start={
|
data-segment-start={
|
||||||
alignStartDateToTimeline(value.start_time) - segmentDuration
|
alignStartDateToTimeline(value.start_time) - segmentDuration
|
||||||
}
|
}
|
||||||
className={`outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`}
|
className={`review-item outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`}
|
||||||
>
|
>
|
||||||
<div className="aspect-video rounded-lg overflow-hidden">
|
<div className="aspect-video rounded-lg overflow-hidden">
|
||||||
<PreviewThumbnailPlayer
|
<PreviewThumbnailPlayer
|
||||||
@ -542,6 +568,7 @@ function DetectionReview({
|
|||||||
minimapEndTime={minimapBounds.end}
|
minimapEndTime={minimapBounds.end}
|
||||||
showHandlebar={previewTime != undefined}
|
showHandlebar={previewTime != undefined}
|
||||||
handlebarTime={previewTime}
|
handlebarTime={previewTime}
|
||||||
|
visibleTimestamps={visibleTimestamps}
|
||||||
events={reviewItems?.all ?? []}
|
events={reviewItems?.all ?? []}
|
||||||
severityType={severity}
|
severityType={severity}
|
||||||
contentRef={contentRef}
|
contentRef={contentRef}
|
||||||
|
Loading…
Reference in New Issue
Block a user