mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Summary timeline (#10569)
* implement summary timeline * implement summary timeline * merge dev * conditionally attach listeners only when dragging * set up listeners with a ref
This commit is contained in:
parent
c8fd23caa1
commit
f113acee33
@ -43,7 +43,7 @@ export default function NewReviewData({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex justify-center items-center md:mr-[100px]">
|
<div className="flex justify-center items-center md:mr-[115px] pointer-events-auto">
|
||||||
<Button
|
<Button
|
||||||
className={`${
|
className={`${
|
||||||
hasUpdate
|
hasUpdate
|
||||||
|
@ -30,6 +30,7 @@ export type EventReviewTimelineProps = {
|
|||||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
|
timelineRef?: RefObject<HTMLDivElement>;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement>;
|
||||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||||
};
|
};
|
||||||
@ -52,6 +53,7 @@ export function EventReviewTimeline({
|
|||||||
setExportEndTime,
|
setExportEndTime,
|
||||||
events,
|
events,
|
||||||
severityType,
|
severityType,
|
||||||
|
timelineRef,
|
||||||
contentRef,
|
contentRef,
|
||||||
onHandlebarDraggingChange,
|
onHandlebarDraggingChange,
|
||||||
}: EventReviewTimelineProps) {
|
}: EventReviewTimelineProps) {
|
||||||
@ -59,7 +61,7 @@ export function EventReviewTimeline({
|
|||||||
const [exportStartPosition, setExportStartPosition] = useState(0);
|
const [exportStartPosition, setExportStartPosition] = useState(0);
|
||||||
const [exportEndPosition, setExportEndPosition] = useState(0);
|
const [exportEndPosition, setExportEndPosition] = useState(0);
|
||||||
|
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||||
const exportStartRef = useRef<HTMLDivElement>(null);
|
const exportStartRef = useRef<HTMLDivElement>(null);
|
||||||
@ -98,7 +100,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: handlebarMouseMove,
|
handleMouseMove: handlebarMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: handlebarRef,
|
draggableElementRef: handlebarRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showHandlebar,
|
showDraggableElement: showHandlebar,
|
||||||
@ -117,7 +119,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: exportStartMouseMove,
|
handleMouseMove: exportStartMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: exportStartRef,
|
draggableElementRef: exportStartRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -138,7 +140,7 @@ export function EventReviewTimeline({
|
|||||||
handleMouseMove: exportEndMouseMove,
|
handleMouseMove: exportEndMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: exportEndRef,
|
draggableElementRef: exportEndRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -213,7 +215,7 @@ export function EventReviewTimeline({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReviewTimeline
|
<ReviewTimeline
|
||||||
timelineRef={timelineRef}
|
timelineRef={timelineRef || internalTimelineRef}
|
||||||
handlebarRef={handlebarRef}
|
handlebarRef={handlebarRef}
|
||||||
handlebarTimeRef={handlebarTimeRef}
|
handlebarTimeRef={handlebarTimeRef}
|
||||||
handlebarMouseMove={handlebarMouseMove}
|
handlebarMouseMove={handlebarMouseMove}
|
||||||
|
@ -68,12 +68,7 @@ export function EventSegment({
|
|||||||
[getReviewed, segmentTime],
|
[getReviewed, segmentTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { roundTopPrimary, roundBottomPrimary } = useMemo(
|
||||||
roundTopPrimary,
|
|
||||||
roundBottomPrimary,
|
|
||||||
roundTopSecondary,
|
|
||||||
roundBottomSecondary,
|
|
||||||
} = useMemo(
|
|
||||||
() => shouldShowRoundedCorners(segmentTime),
|
() => shouldShowRoundedCorners(segmentTime),
|
||||||
[shouldShowRoundedCorners, segmentTime],
|
[shouldShowRoundedCorners, segmentTime],
|
||||||
);
|
);
|
||||||
@ -253,20 +248,6 @@ export function EventSegment({
|
|||||||
</div>
|
</div>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{severityValue !== displaySeverityType && (
|
|
||||||
<div className="absolute right-0 h-2 z-10">
|
|
||||||
<div
|
|
||||||
key={`${segmentKey}_${index}_secondary_data`}
|
|
||||||
className={`
|
|
||||||
w-1 h-2 bg-gradient-to-r
|
|
||||||
${roundBottomSecondary ? "rounded-bl-full rounded-br-full" : ""}
|
|
||||||
${roundTopSecondary ? "rounded-tl-full rounded-tr-full" : ""}
|
|
||||||
${severityColors[severityValue]}
|
|
||||||
`}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,7 @@ export type MotionReviewTimelineProps = {
|
|||||||
motion_events: MotionData[];
|
motion_events: MotionData[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement>;
|
||||||
|
timelineRef?: RefObject<HTMLDivElement>;
|
||||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,13 +55,14 @@ export function MotionReviewTimeline({
|
|||||||
events,
|
events,
|
||||||
motion_events,
|
motion_events,
|
||||||
contentRef,
|
contentRef,
|
||||||
|
timelineRef,
|
||||||
onHandlebarDraggingChange,
|
onHandlebarDraggingChange,
|
||||||
}: MotionReviewTimelineProps) {
|
}: MotionReviewTimelineProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [exportStartPosition, setExportStartPosition] = useState(0);
|
const [exportStartPosition, setExportStartPosition] = useState(0);
|
||||||
const [exportEndPosition, setExportEndPosition] = useState(0);
|
const [exportEndPosition, setExportEndPosition] = useState(0);
|
||||||
|
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||||
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||||
const exportStartRef = useRef<HTMLDivElement>(null);
|
const exportStartRef = useRef<HTMLDivElement>(null);
|
||||||
@ -99,7 +101,7 @@ export function MotionReviewTimeline({
|
|||||||
handleMouseMove: handlebarMouseMove,
|
handleMouseMove: handlebarMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: handlebarRef,
|
draggableElementRef: handlebarRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showHandlebar,
|
showDraggableElement: showHandlebar,
|
||||||
@ -118,7 +120,7 @@ export function MotionReviewTimeline({
|
|||||||
handleMouseMove: exportStartMouseMove,
|
handleMouseMove: exportStartMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: exportStartRef,
|
draggableElementRef: exportStartRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -139,7 +141,7 @@ export function MotionReviewTimeline({
|
|||||||
handleMouseMove: exportEndMouseMove,
|
handleMouseMove: exportEndMouseMove,
|
||||||
} = useDraggableElement({
|
} = useDraggableElement({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef: timelineRef || internalTimelineRef,
|
||||||
draggableElementRef: exportEndRef,
|
draggableElementRef: exportEndRef,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showDraggableElement: showExportHandles,
|
showDraggableElement: showExportHandles,
|
||||||
@ -213,9 +215,46 @@ export function MotionReviewTimeline({
|
|||||||
}
|
}
|
||||||
}, [isDragging, onHandlebarDraggingChange]);
|
}, [isDragging, onHandlebarDraggingChange]);
|
||||||
|
|
||||||
|
const segmentsObserver = useRef<IntersectionObserver | null>(null);
|
||||||
|
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTimelineRef.current && segments) {
|
||||||
|
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={timelineRef}
|
timelineRef={timelineRef || internalTimelineRef}
|
||||||
handlebarRef={handlebarRef}
|
handlebarRef={handlebarRef}
|
||||||
handlebarTimeRef={handlebarTimeRef}
|
handlebarTimeRef={handlebarTimeRef}
|
||||||
handlebarMouseMove={handlebarMouseMove}
|
handlebarMouseMove={handlebarMouseMove}
|
||||||
|
@ -176,7 +176,8 @@ export function MotionSegment({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
className={segmentClasses}
|
data-segment-id={segmentKey}
|
||||||
|
className={`segment ${firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`}
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||||
>
|
>
|
||||||
@ -203,7 +204,7 @@ export function MotionSegment({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_motion_data_1`}
|
key={`${segmentKey}_motion_data_1`}
|
||||||
className={`h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
|
className={`motion-segment ${secondHalfSegmentWidth > 1 ? "hidden" : ""} zoom-in-[0.2] ${secondHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
|
||||||
style={{
|
style={{
|
||||||
width: secondHalfSegmentWidth,
|
width: secondHalfSegmentWidth,
|
||||||
}}
|
}}
|
||||||
@ -215,7 +216,7 @@ export function MotionSegment({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_motion_data_2`}
|
key={`${segmentKey}_motion_data_2`}
|
||||||
className={`h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
|
className={`motion-segment ${firstHalfSegmentWidth > 1 ? "hidden" : ""} zoom-in-[0.2] ${firstHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
|
||||||
style={{
|
style={{
|
||||||
width: firstHalfSegmentWidth,
|
width: firstHalfSegmentWidth,
|
||||||
}}
|
}}
|
||||||
|
@ -234,7 +234,7 @@ export function ReviewTimeline({
|
|||||||
onTouchMove={handleMouseMove}
|
onTouchMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onTouchEnd={handleMouseUp}
|
onTouchEnd={handleMouseUp}
|
||||||
className={`relative h-full overflow-y-auto no-scrollbar bg-secondary ${
|
className={`relative h-full overflow-y-auto no-scrollbar select-none bg-secondary ${
|
||||||
isDragging && (showHandlebar || showExportHandles)
|
isDragging && (showHandlebar || showExportHandles)
|
||||||
? "cursor-grabbing"
|
? "cursor-grabbing"
|
||||||
: "cursor-auto"
|
: "cursor-auto"
|
||||||
|
67
web/src/components/timeline/SummarySegment.tsx
Normal file
67
web/src/components/timeline/SummarySegment.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
|
||||||
|
import { ReviewSegment } from "@/types/review";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
// import useTapUtils from "@/hooks/use-tap-utils";
|
||||||
|
|
||||||
|
type SummarySegmentProps = {
|
||||||
|
events: ReviewSegment[];
|
||||||
|
segmentTime: number;
|
||||||
|
segmentDuration: number;
|
||||||
|
segmentHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SummarySegment({
|
||||||
|
events,
|
||||||
|
segmentTime,
|
||||||
|
segmentDuration,
|
||||||
|
segmentHeight,
|
||||||
|
}: SummarySegmentProps) {
|
||||||
|
const severityType = "all";
|
||||||
|
const { getSeverity, getReviewed, displaySeverityType } =
|
||||||
|
useEventSegmentUtils(segmentDuration, events, severityType);
|
||||||
|
|
||||||
|
const severity = useMemo(
|
||||||
|
() => getSeverity(segmentTime, displaySeverityType),
|
||||||
|
[getSeverity, segmentTime, displaySeverityType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reviewed = useMemo(
|
||||||
|
() => getReviewed(segmentTime),
|
||||||
|
[getReviewed, segmentTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const segmentKey = useMemo(() => segmentTime, [segmentTime]);
|
||||||
|
|
||||||
|
const severityColors: { [key: number]: string } = {
|
||||||
|
1: reviewed ? "bg-severity_motion/50" : "bg-severity_motion",
|
||||||
|
2: reviewed ? "bg-severity_detection/50" : "bg-severity_detection",
|
||||||
|
3: reviewed ? "bg-severity_alert/50" : "bg-severity_alert",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={segmentKey}
|
||||||
|
className="relative w-full"
|
||||||
|
style={{ height: segmentHeight }}
|
||||||
|
>
|
||||||
|
{severity.map((severityValue: number, index: number) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<div
|
||||||
|
className="flex justify-end cursor-pointer"
|
||||||
|
style={{ height: segmentHeight }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
key={`${segmentKey}_${index}_secondary_data`}
|
||||||
|
style={{ height: segmentHeight }}
|
||||||
|
className={`w-[10px] ${severityColors[severityValue]}`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SummarySegment;
|
317
web/src/components/timeline/SummaryTimeline.tsx
Normal file
317
web/src/components/timeline/SummaryTimeline.tsx
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import {
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { SummarySegment } from "./SummarySegment";
|
||||||
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
|
import { ReviewSegment } from "@/types/review";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
|
||||||
|
export type SummaryTimelineProps = {
|
||||||
|
reviewTimelineRef: RefObject<HTMLDivElement>;
|
||||||
|
timelineStart: number;
|
||||||
|
timelineEnd: number;
|
||||||
|
segmentDuration: number;
|
||||||
|
events: ReviewSegment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SummaryTimeline({
|
||||||
|
reviewTimelineRef,
|
||||||
|
timelineStart,
|
||||||
|
timelineEnd,
|
||||||
|
segmentDuration,
|
||||||
|
events,
|
||||||
|
}: SummaryTimelineProps) {
|
||||||
|
const summaryTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
const visibleSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [segmentHeight, setSegmentHeight] = useState(0);
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [scrollStartPosition, setScrollStartPosition] = useState<number>(0);
|
||||||
|
const [initialReviewTimelineScrollTop, setInitialReviewTimelineScrollTop] =
|
||||||
|
useState<number>(0);
|
||||||
|
|
||||||
|
const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (segmentHeight) {
|
||||||
|
return Array.from({ length: segmentCount }, (_, index) => {
|
||||||
|
const segmentTime = timelineStartAligned - index * segmentDuration;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SummarySegment
|
||||||
|
key={segmentTime}
|
||||||
|
events={events}
|
||||||
|
segmentDuration={segmentDuration}
|
||||||
|
segmentTime={segmentTime}
|
||||||
|
segmentHeight={segmentHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
segmentDuration,
|
||||||
|
timelineStartAligned,
|
||||||
|
events,
|
||||||
|
reviewTimelineDuration,
|
||||||
|
segmentHeight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const segments = useMemo(
|
||||||
|
() => generateSegments(),
|
||||||
|
// we know that these deps are correct
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[
|
||||||
|
segmentDuration,
|
||||||
|
segmentHeight,
|
||||||
|
timelineStartAligned,
|
||||||
|
events,
|
||||||
|
reviewTimelineDuration,
|
||||||
|
segmentHeight,
|
||||||
|
generateSegments,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reviewTimelineRef.current && summaryTimelineRef.current) {
|
||||||
|
const content = reviewTimelineRef.current;
|
||||||
|
const summary = summaryTimelineRef.current;
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const {
|
||||||
|
clientHeight: reviewTimelineVisibleHeight,
|
||||||
|
scrollHeight: reviewTimelineFullHeight,
|
||||||
|
scrollTop: scrolled,
|
||||||
|
} = content;
|
||||||
|
const { clientHeight: summaryTimelineVisibleHeight } = summary;
|
||||||
|
|
||||||
|
if (visibleSectionRef.current) {
|
||||||
|
visibleSectionRef.current.style.top = `${summaryTimelineVisibleHeight * (scrolled / reviewTimelineFullHeight)}px`;
|
||||||
|
visibleSectionRef.current.style.height = `${reviewTimelineVisibleHeight * (reviewTimelineVisibleHeight / reviewTimelineFullHeight)}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
content.addEventListener("scroll", handleScroll);
|
||||||
|
return () => {
|
||||||
|
content.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [reviewTimelineRef, summaryTimelineRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (summaryTimelineRef.current) {
|
||||||
|
const { clientHeight: summaryTimelineVisibleHeight } =
|
||||||
|
summaryTimelineRef.current;
|
||||||
|
|
||||||
|
setSegmentHeight(
|
||||||
|
summaryTimelineVisibleHeight /
|
||||||
|
(reviewTimelineDuration / segmentDuration),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [reviewTimelineDuration, summaryTimelineRef, segmentDuration]);
|
||||||
|
|
||||||
|
const timelineClick = useCallback(
|
||||||
|
(
|
||||||
|
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
// prevent default only for mouse events
|
||||||
|
// to avoid chrome/android issues
|
||||||
|
if (e.nativeEvent instanceof MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let clientY;
|
||||||
|
if (isMobile && e.nativeEvent instanceof TouchEvent) {
|
||||||
|
clientY = e.nativeEvent.touches[0].clientY;
|
||||||
|
} else if (e.nativeEvent instanceof MouseEvent) {
|
||||||
|
clientY = e.nativeEvent.clientY;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
clientY &&
|
||||||
|
reviewTimelineRef.current &&
|
||||||
|
summaryTimelineRef.current &&
|
||||||
|
visibleSectionRef.current
|
||||||
|
) {
|
||||||
|
const { clientHeight: summaryTimelineVisibleHeight } =
|
||||||
|
summaryTimelineRef.current;
|
||||||
|
|
||||||
|
const rect = summaryTimelineRef.current.getBoundingClientRect();
|
||||||
|
const summaryTimelineTop = rect.top;
|
||||||
|
|
||||||
|
const { scrollHeight: reviewTimelineHeight } =
|
||||||
|
reviewTimelineRef.current;
|
||||||
|
|
||||||
|
const { clientHeight: visibleSectionHeight } =
|
||||||
|
visibleSectionRef.current;
|
||||||
|
|
||||||
|
const visibleSectionOffset = -(visibleSectionHeight / 2);
|
||||||
|
|
||||||
|
const clickPercentage =
|
||||||
|
(clientY - summaryTimelineTop + visibleSectionOffset) /
|
||||||
|
summaryTimelineVisibleHeight;
|
||||||
|
|
||||||
|
reviewTimelineRef.current.scrollTo({
|
||||||
|
top: Math.floor(reviewTimelineHeight * clickPercentage),
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[reviewTimelineRef, summaryTimelineRef, visibleSectionRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(
|
||||||
|
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
// prevent default only for mouse events
|
||||||
|
// to avoid chrome/android issues
|
||||||
|
if (e.nativeEvent instanceof MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
|
||||||
|
let clientY;
|
||||||
|
if (isMobile && e.nativeEvent instanceof TouchEvent) {
|
||||||
|
clientY = e.nativeEvent.touches[0].clientY;
|
||||||
|
} else if (e.nativeEvent instanceof MouseEvent) {
|
||||||
|
clientY = e.nativeEvent.clientY;
|
||||||
|
}
|
||||||
|
if (clientY && summaryTimelineRef.current && reviewTimelineRef.current) {
|
||||||
|
setScrollStartPosition(clientY);
|
||||||
|
setInitialReviewTimelineScrollTop(reviewTimelineRef.current.scrollTop);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setIsDragging, summaryTimelineRef, reviewTimelineRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(
|
||||||
|
(e: MouseEvent | TouchEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isDragging) {
|
||||||
|
setIsDragging(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging, setIsDragging],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e: MouseEvent | TouchEvent) => {
|
||||||
|
if (
|
||||||
|
summaryTimelineRef.current &&
|
||||||
|
reviewTimelineRef.current &&
|
||||||
|
visibleSectionRef.current
|
||||||
|
) {
|
||||||
|
// prevent default only for mouse events
|
||||||
|
// to avoid chrome/android issues
|
||||||
|
if (e instanceof MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
let clientY;
|
||||||
|
if (isMobile && e instanceof TouchEvent) {
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else if (e instanceof MouseEvent) {
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
if (isDragging && clientY) {
|
||||||
|
const { clientHeight: summaryTimelineVisibleHeight } =
|
||||||
|
summaryTimelineRef.current;
|
||||||
|
|
||||||
|
const {
|
||||||
|
scrollHeight: reviewTimelineHeight,
|
||||||
|
clientHeight: reviewTimelineVisibleHeight,
|
||||||
|
} = reviewTimelineRef.current;
|
||||||
|
|
||||||
|
const { clientHeight: visibleSectionHeight } =
|
||||||
|
visibleSectionRef.current;
|
||||||
|
|
||||||
|
const deltaY =
|
||||||
|
(clientY - scrollStartPosition) *
|
||||||
|
(summaryTimelineVisibleHeight / visibleSectionHeight);
|
||||||
|
|
||||||
|
const newScrollTop = Math.min(
|
||||||
|
initialReviewTimelineScrollTop + deltaY,
|
||||||
|
reviewTimelineHeight - reviewTimelineVisibleHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
reviewTimelineRef.current.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
initialReviewTimelineScrollTop,
|
||||||
|
isDragging,
|
||||||
|
reviewTimelineRef,
|
||||||
|
scrollStartPosition,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentRef = useRef<Document | null>(document);
|
||||||
|
useEffect(() => {
|
||||||
|
const documentInstance = documentRef.current;
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
documentInstance?.addEventListener("mousemove", handleMouseMove);
|
||||||
|
documentInstance?.addEventListener("touchmove", handleMouseMove);
|
||||||
|
documentInstance?.addEventListener("mouseup", handleMouseUp);
|
||||||
|
documentInstance?.addEventListener("touchend", handleMouseUp);
|
||||||
|
} else {
|
||||||
|
documentInstance?.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
documentInstance?.removeEventListener("touchmove", handleMouseMove);
|
||||||
|
documentInstance?.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
documentInstance?.removeEventListener("touchend", handleMouseUp);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
documentInstance?.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
documentInstance?.removeEventListener("touchmove", handleMouseMove);
|
||||||
|
documentInstance?.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
documentInstance?.removeEventListener("touchend", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [handleMouseMove, handleMouseUp, isDragging]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative h-full overflow-hidden no-scrollbar select-none bg-secondary border-l-[1px] border-neutral-700`}
|
||||||
|
role="scrollbar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={summaryTimelineRef}
|
||||||
|
className="h-full flex flex-col relative z-10"
|
||||||
|
onClick={timelineClick}
|
||||||
|
onTouchEnd={timelineClick}
|
||||||
|
>
|
||||||
|
{segments}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={visibleSectionRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleMouseDown}
|
||||||
|
className={`bg-primary-foreground/30 z-20 absolute w-full touch-none ${
|
||||||
|
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SummaryTimeline;
|
@ -32,7 +32,7 @@ export function MinimapBounds({
|
|||||||
<>
|
<>
|
||||||
{isFirstSegmentInMinimap && (
|
{isFirstSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] scroll-mt-8 pointer-events-none"
|
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] scroll-mt-8 pointer-events-none select-none"
|
||||||
ref={firstMinimapSegmentRef}
|
ref={firstMinimapSegmentRef}
|
||||||
>
|
>
|
||||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||||
@ -44,7 +44,7 @@ export function MinimapBounds({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLastSegmentInMinimap && (
|
{isLastSegmentInMinimap && (
|
||||||
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] pointer-events-none">
|
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary-foreground font-medium z-20 text-center text-[10px] pointer-events-none select-none">
|
||||||
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -61,7 +61,7 @@ export function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
|
|||||||
<div className="absolute">
|
<div className="absolute">
|
||||||
<div className="flex items-end content-end w-[12px] h-2">
|
<div className="flex items-end content-end w-[12px] h-2">
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-none h-0.5 ${
|
className={`pointer-events-none select-none h-0.5 ${
|
||||||
timestamp.getMinutes() % timestampSpread === 0 &&
|
timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
timestamp.getSeconds() === 0
|
timestamp.getSeconds() === 0
|
||||||
? "w-[12px] bg-neutral-600 dark:bg-neutral-500"
|
? "w-[12px] bg-neutral-600 dark:bg-neutral-500"
|
||||||
@ -88,7 +88,7 @@ export function Timestamp({
|
|||||||
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_timestamp`}
|
key={`${segmentKey}_timestamp`}
|
||||||
className="pointer-events-none text-[8px] text-neutral-600 dark:text-neutral-500"
|
className="pointer-events-none select-none text-[8px] text-neutral-600 dark:text-neutral-500"
|
||||||
>
|
>
|
||||||
{timestamp.getMinutes() % timestampSpread === 0 &&
|
{timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
timestamp.getSeconds() === 0 &&
|
timestamp.getSeconds() === 0 &&
|
||||||
|
@ -40,7 +40,8 @@ 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 } = useTimelineUtils(segmentDuration);
|
const { alignStartDateToTimeline, getCumulativeScrollTop } =
|
||||||
|
useTimelineUtils(segmentDuration);
|
||||||
|
|
||||||
const draggingAtTopEdge = useMemo(() => {
|
const draggingAtTopEdge = useMemo(() => {
|
||||||
if (clientYPosition && timelineRef.current) {
|
if (clientYPosition && timelineRef.current) {
|
||||||
@ -125,15 +126,6 @@ function useDraggableElement({
|
|||||||
[isDragging, setIsDragging],
|
[isDragging, setIsDragging],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCumulativeScrollTop = useCallback((element: HTMLElement | null) => {
|
|
||||||
let scrollTop = 0;
|
|
||||||
while (element) {
|
|
||||||
scrollTop += element.scrollTop;
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
return scrollTop;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const timestampToPixels = useCallback(
|
const timestampToPixels = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
const { scrollHeight: timelineHeight } =
|
const { scrollHeight: timelineHeight } =
|
||||||
|
@ -58,11 +58,12 @@ export const useEventSegmentUtils = (
|
|||||||
);
|
);
|
||||||
const highestSeverityValue = Math.max(...severityValues);
|
const highestSeverityValue = Math.max(...severityValues);
|
||||||
|
|
||||||
if (
|
if (severityValues.includes(displaySeverityType)) {
|
||||||
severityValues.includes(displaySeverityType) &&
|
const otherSeverityValues = severityValues.filter(
|
||||||
displaySeverityType !== highestSeverityValue
|
(severity) => severity !== displaySeverityType,
|
||||||
) {
|
);
|
||||||
return [displaySeverityType, highestSeverityValue];
|
const highestOtherSeverityValue = Math.max(...otherSeverityValues);
|
||||||
|
return [displaySeverityType, highestOtherSeverityValue];
|
||||||
} else {
|
} else {
|
||||||
return [highestSeverityValue];
|
return [highestSeverityValue];
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,18 @@ export const useTimelineUtils = (segmentDuration: number) => {
|
|||||||
[segmentDuration],
|
[segmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getCumulativeScrollTop = useCallback((element: HTMLElement | null) => {
|
||||||
|
let scrollTop = 0;
|
||||||
|
while (element) {
|
||||||
|
scrollTop += element.scrollTop;
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
return scrollTop;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alignEndDateToTimeline,
|
alignEndDateToTimeline,
|
||||||
alignStartDateToTimeline,
|
alignStartDateToTimeline,
|
||||||
|
getCumulativeScrollTop,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,7 @@ import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
||||||
|
|
||||||
// Color data
|
// Color data
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -125,6 +126,7 @@ const generateRandomEvent = (): ReviewSegment => {
|
|||||||
function UIPlayground() {
|
function UIPlayground() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const reviewTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
const [mockEvents, setMockEvents] = useState<ReviewSegment[]>([]);
|
const [mockEvents, setMockEvents] = useState<ReviewSegment[]>([]);
|
||||||
const [mockMotionData, setMockMotionData] = useState<MotionData[]>([]);
|
const [mockMotionData, setMockMotionData] = useState<MotionData[]>([]);
|
||||||
const [handlebarTime, setHandlebarTime] = useState(
|
const [handlebarTime, setHandlebarTime] = useState(
|
||||||
@ -144,7 +146,7 @@ function UIPlayground() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
const initialEvents = Array.from({ length: 50 }, generateRandomEvent);
|
const initialEvents = Array.from({ length: 10 }, generateRandomEvent);
|
||||||
setMockEvents(initialEvents);
|
setMockEvents(initialEvents);
|
||||||
setMockMotionData(generateRandomMotionAudioData());
|
setMockMotionData(generateRandomMotionAudioData());
|
||||||
}, []);
|
}, []);
|
||||||
@ -403,9 +405,21 @@ function UIPlayground() {
|
|||||||
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
||||||
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
||||||
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
|
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
|
||||||
|
timelineRef={reviewTimelineRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isEventsReviewTimeline && (
|
||||||
|
<div className="w-[10px]">
|
||||||
|
<SummaryTimeline
|
||||||
|
reviewTimelineRef={reviewTimelineRef}
|
||||||
|
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
|
||||||
|
timelineEnd={Math.floor(Date.now() / 1000) - 6 * 60 * 60} // end of timeline - the later time
|
||||||
|
segmentDuration={zoomSettings.segmentDuration}
|
||||||
|
events={mockEvents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -36,6 +36,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import PreviewPlayer, {
|
import PreviewPlayer, {
|
||||||
PreviewController,
|
PreviewController,
|
||||||
} from "@/components/player/PreviewPlayer";
|
} from "@/components/player/PreviewPlayer";
|
||||||
|
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
|
|
||||||
type EventViewProps = {
|
type EventViewProps = {
|
||||||
@ -329,6 +330,8 @@ function DetectionReview({
|
|||||||
onSelectReview,
|
onSelectReview,
|
||||||
pullLatestData,
|
pullLatestData,
|
||||||
}: DetectionReviewProps) {
|
}: DetectionReviewProps) {
|
||||||
|
const reviewTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const segmentDuration = 60;
|
const segmentDuration = 60;
|
||||||
|
|
||||||
// review data
|
// review data
|
||||||
@ -458,7 +461,7 @@ function DetectionReview({
|
|||||||
>
|
>
|
||||||
{filter?.before == undefined && (
|
{filter?.before == undefined && (
|
||||||
<NewReviewData
|
<NewReviewData
|
||||||
className="absolute w-full z-30"
|
className="absolute w-full z-30 pointer-events-none"
|
||||||
contentRef={contentRef}
|
contentRef={contentRef}
|
||||||
severity={severity}
|
severity={severity}
|
||||||
hasUpdate={hasUpdate}
|
hasUpdate={hasUpdate}
|
||||||
@ -527,21 +530,33 @@ function DetectionReview({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
<div className="w-[65px] md:w-[110px] mt-2 flex flex-row">
|
||||||
<EventReviewTimeline
|
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
|
||||||
segmentDuration={segmentDuration}
|
<EventReviewTimeline
|
||||||
timestampSpread={15}
|
segmentDuration={segmentDuration}
|
||||||
timelineStart={timeRange.before}
|
timestampSpread={15}
|
||||||
timelineEnd={timeRange.after}
|
timelineStart={timeRange.before}
|
||||||
showMinimap={showMinimap && !previewTime}
|
timelineEnd={timeRange.after}
|
||||||
minimapStartTime={minimapBounds.start}
|
showMinimap={showMinimap && !previewTime}
|
||||||
minimapEndTime={minimapBounds.end}
|
minimapStartTime={minimapBounds.start}
|
||||||
showHandlebar={previewTime != undefined}
|
minimapEndTime={minimapBounds.end}
|
||||||
handlebarTime={previewTime}
|
showHandlebar={previewTime != undefined}
|
||||||
events={reviewItems?.all ?? []}
|
handlebarTime={previewTime}
|
||||||
severityType={severity}
|
events={reviewItems?.all ?? []}
|
||||||
contentRef={contentRef}
|
severityType={severity}
|
||||||
/>
|
contentRef={contentRef}
|
||||||
|
timelineRef={reviewTimelineRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-[10px]">
|
||||||
|
<SummaryTimeline
|
||||||
|
reviewTimelineRef={reviewTimelineRef}
|
||||||
|
timelineStart={timeRange.before}
|
||||||
|
timelineEnd={timeRange.after}
|
||||||
|
segmentDuration={segmentDuration}
|
||||||
|
events={reviewItems?.all ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -22,16 +22,16 @@
|
|||||||
--primary: 0 0% 100%;
|
--primary: 0 0% 100%;
|
||||||
|
|
||||||
--primary-foreground: hsl(0, 0%, 0%);
|
--primary-foreground: hsl(0, 0%, 0%);
|
||||||
--primary-foreground: 0, 0%, 0%;
|
--primary-foreground: 0 0% 0%;
|
||||||
|
|
||||||
--secondary: hsl(0, 0%, 96%);
|
--secondary: hsl(0, 0%, 96%);
|
||||||
--secondary: 0, 0%, 96%;
|
--secondary: 0 0% 96%;
|
||||||
|
|
||||||
--secondary-foreground: hsl(0, 0%, 45%);
|
--secondary-foreground: hsl(0, 0%, 45%);
|
||||||
--secondary-foreground: 0, 0%, 45%;
|
--secondary-foreground: 0 0% 45%;
|
||||||
|
|
||||||
--secondary-highlight: hsl(0, 0%, 94%);
|
--secondary-highlight: hsl(0, 0%, 94%);
|
||||||
--secondary-highlight: 0, 0%, 94%;
|
--secondary-highlight: 0 0% 94%;
|
||||||
|
|
||||||
--muted: hsl(210 40% 96.1%);
|
--muted: hsl(210 40% 96.1%);
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
@ -104,28 +104,28 @@
|
|||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--primary: hsl(0, 0%, 9%);
|
--primary: hsl(0, 0%, 9%);
|
||||||
--primary: 0, 0%, 9%;
|
--primary: 0 0% 9%;
|
||||||
|
|
||||||
--primary-foreground: hsl(0, 0%, 100%);
|
--primary-foreground: hsl(0, 0%, 100%);
|
||||||
--primary-foreground: 0, 0%, 100%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: hsl(0, 0%, 15%);
|
--secondary: hsl(0, 0%, 15%);
|
||||||
--secondary: 0, 0%, 15%;
|
--secondary: 0 0% 15%;
|
||||||
|
|
||||||
--secondary-foreground: hsl(0, 0%, 83%);
|
--secondary-foreground: hsl(0, 0%, 83%);
|
||||||
--secondary-foreground: 0, 0%, 83%;
|
--secondary-foreground: 0 0% 83%;
|
||||||
|
|
||||||
--secondary-highlight: hsl(0, 0%, 25%);
|
--secondary-highlight: hsl(0, 0%, 25%);
|
||||||
--secondary-highlight: 0, 0%, 25%;
|
--secondary-highlight: 0 0% 25%;
|
||||||
|
|
||||||
--muted: hsl(0, 0%, 8%);
|
--muted: hsl(0, 0%, 8%);
|
||||||
--muted: 0, 0%, 8%;
|
--muted: 0 0% 8%;
|
||||||
|
|
||||||
--muted-foreground: hsl(0, 0%, 32%);
|
--muted-foreground: hsl(0, 0%, 32%);
|
||||||
--muted-foreground: 0, 0%, 32%;
|
--muted-foreground: 0 0% 32%;
|
||||||
|
|
||||||
--accent: hsl(0, 0%, 15%);
|
--accent: hsl(0, 0%, 15%);
|
||||||
--accent: 0, 0%, 15%;
|
--accent: 0 0% 15%;
|
||||||
|
|
||||||
--accent-foreground: hsl(210 40% 98%);
|
--accent-foreground: hsl(210 40% 98%);
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
@ -137,7 +137,7 @@
|
|||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: hsl(0, 0%, 32%);
|
--border: hsl(0, 0%, 32%);
|
||||||
--border: 0, 0%, 32%;
|
--border: 0 0% 32%;
|
||||||
|
|
||||||
--input: hsl(217.2 32.6% 17.5%);
|
--input: hsl(217.2 32.6% 17.5%);
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
Loading…
Reference in New Issue
Block a user