diff --git a/web/src/App.tsx b/web/src/App.tsx index c50c53307..743d98711 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -31,7 +31,7 @@ function App() { {isMobile && }
diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index 08a78a30b..7f8d0f72c 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -170,24 +170,20 @@ export function EventSegment({ const segmentClick = useCallback(() => { if (contentRef.current && startTimestamp) { const element = contentRef.current.querySelector( - `[data-segment-start="${startTimestamp - segmentDuration}"]`, + `[data-segment-start="${startTimestamp - segmentDuration}"] .review-item-ring`, ); if (element instanceof HTMLElement) { scrollIntoView(element, { scrollMode: "if-needed", behavior: "smooth", }); - element.classList.add( - `outline-severity_${severityType}`, - `shadow-severity_${severityType}`, - ); - element.classList.add("outline-3"); - element.classList.remove("outline-0"); + element.classList.add(`outline-severity_${severityType}`); + element.classList.remove("outline-transparent"); // Remove the classes after a short timeout setTimeout(() => { - element.classList.remove("outline-3"); - element.classList.add("outline-0"); + element.classList.remove(`outline-severity_${severityType}`); + element.classList.add("outline-transparent"); }, 3000); } diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx index 43d5f324d..deac2ea44 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -4,6 +4,7 @@ import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; import ReviewTimeline from "./ReviewTimeline"; import { isDesktop } from "react-device-detect"; +import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils"; export type MotionReviewTimelineProps = { segmentDuration: number; @@ -75,14 +76,37 @@ export function MotionReviewTimeline({ [timelineStart, alignStartDateToTimeline, segmentDuration], ); + const { getMotionSegmentValue } = useMotionSegmentUtils( + segmentDuration, + motion_events, + ); + // Generate segments for the timeline const generateSegments = useCallback(() => { - const segmentCount = Math.ceil(timelineDuration / segmentDuration); + const segments = []; + let segmentTime = timelineStartAligned; - return Array.from({ length: segmentCount }, (_, index) => { - const segmentTime = timelineStartAligned - index * segmentDuration; + while (segmentTime >= timelineStartAligned - timelineDuration) { + const motionStart = segmentTime; + const motionEnd = motionStart + segmentDuration; - return ( + const segmentMotion = + getMotionSegmentValue(motionStart) > 0 || + getMotionSegmentValue(motionStart + segmentDuration / 2) > 0; + const overlappingReviewItems = events.some( + (item) => + (item.start_time >= motionStart && item.start_time < motionEnd) || + (item.end_time > motionStart && item.end_time <= motionEnd) || + (item.start_time <= motionStart && item.end_time >= motionEnd), + ); + + if ((!segmentMotion || overlappingReviewItems) && motionOnly) { + // exclude segment if necessary when in motion only mode + segmentTime -= segmentDuration; + continue; + } + + segments.push( + />, ); - }); + segmentTime -= segmentDuration; + } + return segments; // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [ diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx index 6a686c8e5..6d5ba883f 100644 --- a/web/src/components/timeline/ReviewTimeline.tsx +++ b/web/src/components/timeline/ReviewTimeline.tsx @@ -30,7 +30,7 @@ export type ReviewTimelineProps = { setExportEndTime?: React.Dispatch>; timelineCollapsed?: boolean; dense: boolean; - children: ReactNode; + children: ReactNode[]; }; export function ReviewTimeline({ @@ -113,6 +113,7 @@ export function ReviewTimeline({ setIsDragging: setIsDraggingHandlebar, draggableElementTimeRef: handlebarTimeRef, dense, + timelineSegments: children, }); const { @@ -136,6 +137,7 @@ export function ReviewTimeline({ draggableElementTimeRef: exportStartTimeRef, setDraggableElementPosition: setExportStartPosition, dense, + timelineSegments: children, }); const { @@ -159,6 +161,7 @@ export function ReviewTimeline({ draggableElementTimeRef: exportEndTimeRef, setDraggableElementPosition: setExportEndPosition, dense, + timelineSegments: children, }); const handleHandlebar = useCallback( @@ -321,119 +324,123 @@ export function ReviewTimeline({
{children}
- {showHandlebar && ( -
-
-
-
-
-
-
-
-
-
- )} - {showExportHandles && ( + {children.length > 0 && ( <> -
+ {showHandlebar && (
-
-
-
-
-
-
-
-
-
-
-
+ className={`bg-destructive rounded-full mx-auto ${ + dense + ? "w-12 md:w-20" + : segmentDuration < 60 + ? "w-24" + : "w-20" + } h-5 ${isDraggingHandlebar && isMobile ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`} + > +
+
- + )} + {showExportHandles && ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + )} )} diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index 15b8773b2..1f957e39d 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; import scrollIntoView from "scroll-into-view-if-needed"; import { useTimelineUtils } from "./use-timeline-utils"; @@ -23,6 +23,7 @@ type DraggableElementProps = { setIsDragging: React.Dispatch>; setDraggableElementPosition?: React.Dispatch>; dense: boolean; + timelineSegments: ReactNode[]; }; function useDraggableElement({ @@ -45,6 +46,7 @@ function useDraggableElement({ setIsDragging, setDraggableElementPosition, dense, + timelineSegments, }: DraggableElementProps) { const [clientYPosition, setClientYPosition] = useState(null); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); @@ -213,10 +215,10 @@ function useDraggableElement({ ); useEffect(() => { - if (timelineRef.current) { + if (timelineRef.current && timelineSegments.length) { setSegments(Array.from(timelineRef.current.querySelectorAll(".segment"))); } - }, [timelineRef, segmentDuration, timelineDuration, timelineCollapsed]); + }, [timelineRef, timelineCollapsed, timelineSegments]); useEffect(() => { let animationFrameId: number | null = null; @@ -426,7 +428,13 @@ function useDraggableElement({ ]); useEffect(() => { - if (timelineRef.current && draggableElementTime && timelineCollapsed) { + if ( + timelineRef.current && + draggableElementTime && + timelineCollapsed && + timelineSegments && + segments + ) { setFullTimelineHeight(timelineRef.current.scrollHeight); const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime); @@ -452,14 +460,30 @@ function useDraggableElement({ if (setDraggableElementTime) { setDraggableElementTime(searchTime); } - break; + return; + } + } + } + if (!segmentElement) { + // segment still not found, just start at the beginning of the timeline or at now() + if (segments?.length) { + const searchTime = parseInt( + segments[0].getAttribute("data-segment-id") || "0", + 10, + ); + if (setDraggableElementTime) { + setDraggableElementTime(searchTime); + } + } else { + if (setDraggableElementTime) { + setDraggableElementTime(timelineStartAligned); } } } } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [timelineCollapsed]); + }, [timelineCollapsed, segments]); useEffect(() => { if (timelineRef.current && segments) { diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index b5e0407a6..57461d063 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -37,9 +37,11 @@ export default function useStats(stats: FrigateStats | undefined) { // check camera cpu usages Object.entries(stats["cameras"]).forEach(([name, cam]) => { const ffmpegAvg = parseFloat( - stats["cpu_usages"][cam["ffmpeg_pid"]].cpu_average, + stats["cpu_usages"][cam["ffmpeg_pid"]]?.cpu_average, + ); + const detectAvg = parseFloat( + stats["cpu_usages"][cam["pid"]]?.cpu_average, ); - const detectAvg = parseFloat(stats["cpu_usages"][cam["pid"]].cpu_average); if (!isNaN(ffmpegAvg) && ffmpegAvg >= 20.0) { problems.push({ diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 6d49762e3..1524deeea 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -206,12 +206,12 @@ export default function EventView({ return (
-
+
{isMobile && ( )} {currentItems && @@ -533,7 +533,7 @@ function DetectionReview({ data-segment-start={ alignStartDateToTimeline(value.start_time) - segmentDuration } - className={`review-item outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-3 outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`} + className="review-item relative rounded-lg" >
+
); })} @@ -563,7 +566,7 @@ function DetectionReview({ )}
-
+
{reviewCameras.map((camera) => { let grow; + let spans; const aspectRatio = camera.detect.width / camera.detect.height; if (aspectRatio > 2) { - grow = "sm:col-span-2 aspect-wide"; + grow = "aspect-wide"; + spans = "sm:col-span-2"; } else if (aspectRatio < 1) { - grow = "md:row-span-2 md:h-full aspect-tall"; + grow = "md:h-full aspect-tall"; + spans = "md:row-span-2"; } else { grow = "aspect-video"; + spans = ""; } const detectionType = getDetectionType(camera.name); return ( - { - videoPlayersRef.current[camera.name] = controller; - }} - onClick={() => - onOpenRecording({ - camera: camera.name, - startTime: currentTime, - severity: "significant_motion", - }) - } - /> +
+ { + videoPlayersRef.current[camera.name] = controller; + }} + onClick={() => + onOpenRecording({ + camera: camera.name, + startTime: currentTime, + severity: "significant_motion", + }) + } + /> +
+
); })}
-
+
{isMobile && (