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 { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
import ReviewTimeline from "./ReviewTimeline";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
|
||||
export type EventReviewTimelineProps = {
|
||||
segmentDuration: number;
|
||||
@ -29,6 +30,7 @@ export type EventReviewTimelineProps = {
|
||||
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
events: ReviewSegment[];
|
||||
visibleTimestamps?: number[];
|
||||
severityType: ReviewSeverity;
|
||||
timelineRef?: RefObject<HTMLDivElement>;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
@ -52,6 +54,7 @@ export function EventReviewTimeline({
|
||||
setExportStartTime,
|
||||
setExportEndTime,
|
||||
events,
|
||||
visibleTimestamps,
|
||||
severityType,
|
||||
timelineRef,
|
||||
contentRef,
|
||||
@ -68,14 +71,20 @@ export function EventReviewTimeline({
|
||||
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||
|
||||
const timelineDuration = useMemo(
|
||||
() => timelineStart - timelineEnd,
|
||||
[timelineEnd, timelineStart],
|
||||
);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||
{
|
||||
segmentDuration,
|
||||
timelineDuration,
|
||||
timelineRef: selectedTimelineRef,
|
||||
},
|
||||
);
|
||||
|
||||
const timelineStartAligned = useMemo(
|
||||
() => alignStartDateToTimeline(timelineStart),
|
||||
@ -100,7 +109,7 @@ export function EventReviewTimeline({
|
||||
handleMouseMove: handlebarMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef: timelineRef || internalTimelineRef,
|
||||
timelineRef: selectedTimelineRef,
|
||||
draggableElementRef: handlebarRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showHandlebar,
|
||||
@ -119,7 +128,7 @@ export function EventReviewTimeline({
|
||||
handleMouseMove: exportStartMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef: timelineRef || internalTimelineRef,
|
||||
timelineRef: selectedTimelineRef,
|
||||
draggableElementRef: exportStartRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
@ -140,7 +149,7 @@ export function EventReviewTimeline({
|
||||
handleMouseMove: exportEndMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef: timelineRef || internalTimelineRef,
|
||||
timelineRef: selectedTimelineRef,
|
||||
draggableElementRef: exportEndRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
@ -213,9 +222,36 @@ export function EventReviewTimeline({
|
||||
}
|
||||
}, [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 (
|
||||
<ReviewTimeline
|
||||
timelineRef={timelineRef || internalTimelineRef}
|
||||
timelineRef={selectedTimelineRef}
|
||||
handlebarRef={handlebarRef}
|
||||
handlebarTimeRef={handlebarTimeRef}
|
||||
handlebarMouseMove={handlebarMouseMove}
|
||||
|
@ -53,8 +53,9 @@ export function EventSegment({
|
||||
getEventThumbnail,
|
||||
} = useEventSegmentUtils(segmentDuration, events, severityType);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||
{ segmentDuration },
|
||||
);
|
||||
|
||||
const severity = useMemo(
|
||||
() => getSeverity(segmentTime, displaySeverityType),
|
||||
@ -199,6 +200,7 @@ export function EventSegment({
|
||||
return (
|
||||
<div
|
||||
key={segmentKey}
|
||||
data-segment-id={segmentKey}
|
||||
className={segmentClasses}
|
||||
onClick={segmentClick}
|
||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||
|
@ -76,8 +76,12 @@ export function MotionReviewTimeline({
|
||||
[timelineEnd, timelineStart, segmentDuration],
|
||||
);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||
{
|
||||
segmentDuration,
|
||||
timelineDuration,
|
||||
},
|
||||
);
|
||||
|
||||
const timelineStartAligned = useMemo(
|
||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||
|
@ -42,8 +42,9 @@ export function MotionSegment({
|
||||
const { getMotionSegmentValue, interpolateMotionAudioData } =
|
||||
useMotionSegmentUtils(segmentDuration, motion_events);
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||
{ segmentDuration },
|
||||
);
|
||||
|
||||
const { handleTouchStart } = useTapUtils();
|
||||
|
||||
|
@ -39,18 +39,22 @@ export function SummaryTimeline({
|
||||
|
||||
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(
|
||||
() => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration,
|
||||
[timelineStart, alignStartDateToTimeline, segmentDuration],
|
||||
);
|
||||
|
||||
const reviewTimelineDuration = useMemo(
|
||||
() => timelineStart - timelineEnd + 4 * segmentDuration,
|
||||
[timelineEnd, timelineStart, segmentDuration],
|
||||
);
|
||||
|
||||
// Generate segments for the timeline
|
||||
const generateSegments = useCallback(() => {
|
||||
const segmentCount = reviewTimelineDuration / segmentDuration;
|
||||
|
@ -40,8 +40,13 @@ function useDraggableElement({
|
||||
}: DraggableElementProps) {
|
||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||
const { alignStartDateToTimeline, getCumulativeScrollTop } =
|
||||
useTimelineUtils(segmentDuration);
|
||||
const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils(
|
||||
{
|
||||
segmentDuration: segmentDuration,
|
||||
timelineDuration: timelineDuration,
|
||||
timelineRef,
|
||||
},
|
||||
);
|
||||
|
||||
const draggingAtTopEdge = useMemo(() => {
|
||||
if (clientYPosition && timelineRef.current) {
|
||||
|
@ -1,6 +1,16 @@
|
||||
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(
|
||||
(time: number): number => {
|
||||
const remainder = time % segmentDuration;
|
||||
@ -28,9 +38,27 @@ export const useTimelineUtils = (segmentDuration: number) => {
|
||||
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 {
|
||||
alignEndDateToTimeline,
|
||||
alignStartDateToTimeline,
|
||||
getCumulativeScrollTop,
|
||||
getVisibleTimelineDuration,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -379,7 +379,17 @@ function DetectionReview({
|
||||
|
||||
// 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);
|
||||
|
||||
@ -448,10 +458,26 @@ function DetectionReview({
|
||||
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
|
||||
// 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 (
|
||||
<>
|
||||
@ -499,7 +525,7 @@ function DetectionReview({
|
||||
data-segment-start={
|
||||
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">
|
||||
<PreviewThumbnailPlayer
|
||||
@ -542,6 +568,7 @@ function DetectionReview({
|
||||
minimapEndTime={minimapBounds.end}
|
||||
showHandlebar={previewTime != undefined}
|
||||
handlebarTime={previewTime}
|
||||
visibleTimestamps={visibleTimestamps}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
contentRef={contentRef}
|
||||
|
Loading…
Reference in New Issue
Block a user