UI tweaks and bugfixes (#10775)

* fix wrong segments when changing filters in motion only mode

* pixel alignment, better outlines, and more figma matching

* fix stats from crashing the ui

* separate layout from aspect classes

* check for invalid value

* avoid undefined classnames
This commit is contained in:
Josh Hawkins 2024-04-01 09:23:57 -05:00 committed by GitHub
parent 5853393396
commit 7fac91dce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 221 additions and 154 deletions

View File

@ -31,7 +31,7 @@ function App() {
{isMobile && <Bottombar />} {isMobile && <Bottombar />}
<div <div
id="pageRoot" id="pageRoot"
className={`absolute top-0 right-0 overflow-hidden ${isMobile ? "left-0 bottom-16" : "left-12 bottom-8"}`} className={`absolute top-0 right-0 overflow-hidden ${isMobile ? "left-0 bottom-16" : "left-[52px] bottom-8"}`}
> >
<Suspense> <Suspense>
<Routes> <Routes>

View File

@ -170,24 +170,20 @@ export function EventSegment({
const segmentClick = useCallback(() => { const segmentClick = useCallback(() => {
if (contentRef.current && startTimestamp) { if (contentRef.current && startTimestamp) {
const element = contentRef.current.querySelector( const element = contentRef.current.querySelector(
`[data-segment-start="${startTimestamp - segmentDuration}"]`, `[data-segment-start="${startTimestamp - segmentDuration}"] .review-item-ring`,
); );
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
scrollIntoView(element, { scrollIntoView(element, {
scrollMode: "if-needed", scrollMode: "if-needed",
behavior: "smooth", behavior: "smooth",
}); });
element.classList.add( element.classList.add(`outline-severity_${severityType}`);
`outline-severity_${severityType}`, element.classList.remove("outline-transparent");
`shadow-severity_${severityType}`,
);
element.classList.add("outline-3");
element.classList.remove("outline-0");
// Remove the classes after a short timeout // Remove the classes after a short timeout
setTimeout(() => { setTimeout(() => {
element.classList.remove("outline-3"); element.classList.remove(`outline-severity_${severityType}`);
element.classList.add("outline-0"); element.classList.add("outline-transparent");
}, 3000); }, 3000);
} }

View File

@ -4,6 +4,7 @@ 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 { isDesktop } from "react-device-detect";
import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils";
export type MotionReviewTimelineProps = { export type MotionReviewTimelineProps = {
segmentDuration: number; segmentDuration: number;
@ -75,14 +76,37 @@ export function MotionReviewTimeline({
[timelineStart, alignStartDateToTimeline, segmentDuration], [timelineStart, alignStartDateToTimeline, segmentDuration],
); );
const { getMotionSegmentValue } = useMotionSegmentUtils(
segmentDuration,
motion_events,
);
// Generate segments for the timeline // Generate segments for the timeline
const generateSegments = useCallback(() => { const generateSegments = useCallback(() => {
const segmentCount = Math.ceil(timelineDuration / segmentDuration); const segments = [];
let segmentTime = timelineStartAligned;
return Array.from({ length: segmentCount }, (_, index) => { while (segmentTime >= timelineStartAligned - timelineDuration) {
const segmentTime = timelineStartAligned - index * segmentDuration; 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(
<MotionSegment <MotionSegment
key={segmentTime} key={segmentTime}
events={events} events={events}
@ -96,9 +120,11 @@ export function MotionReviewTimeline({
minimapEndTime={minimapEndTime} minimapEndTime={minimapEndTime}
setHandlebarTime={setHandlebarTime} setHandlebarTime={setHandlebarTime}
dense={dense} dense={dense}
/> />,
); );
}); segmentTime -= segmentDuration;
}
return segments;
// 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
}, [ }, [

View File

@ -30,7 +30,7 @@ export type ReviewTimelineProps = {
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>; setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
timelineCollapsed?: boolean; timelineCollapsed?: boolean;
dense: boolean; dense: boolean;
children: ReactNode; children: ReactNode[];
}; };
export function ReviewTimeline({ export function ReviewTimeline({
@ -113,6 +113,7 @@ export function ReviewTimeline({
setIsDragging: setIsDraggingHandlebar, setIsDragging: setIsDraggingHandlebar,
draggableElementTimeRef: handlebarTimeRef, draggableElementTimeRef: handlebarTimeRef,
dense, dense,
timelineSegments: children,
}); });
const { const {
@ -136,6 +137,7 @@ export function ReviewTimeline({
draggableElementTimeRef: exportStartTimeRef, draggableElementTimeRef: exportStartTimeRef,
setDraggableElementPosition: setExportStartPosition, setDraggableElementPosition: setExportStartPosition,
dense, dense,
timelineSegments: children,
}); });
const { const {
@ -159,6 +161,7 @@ export function ReviewTimeline({
draggableElementTimeRef: exportEndTimeRef, draggableElementTimeRef: exportEndTimeRef,
setDraggableElementPosition: setExportEndPosition, setDraggableElementPosition: setExportEndPosition,
dense, dense,
timelineSegments: children,
}); });
const handleHandlebar = useCallback( const handleHandlebar = useCallback(
@ -321,119 +324,123 @@ export function ReviewTimeline({
<div className="absolute bottom-0 inset-x-0 z-20 w-full h-[30px] bg-gradient-to-t from-secondary to-transparent pointer-events-none"></div> <div className="absolute bottom-0 inset-x-0 z-20 w-full h-[30px] bg-gradient-to-t from-secondary to-transparent pointer-events-none"></div>
{children} {children}
</div> </div>
{showHandlebar && ( {children.length > 0 && (
<div
className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={handlebarRef}
>
<div
className="flex items-center justify-center touch-none select-none"
onMouseDown={handleHandlebar}
onTouchStart={handleHandlebar}
>
<div
className={`relative w-full ${
isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
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`}
>
<div
ref={handlebarTimeRef}
className={`text-white pointer-events-none ${textSizeClasses("handlebar")} z-10`}
></div>
</div>
<div
className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
></div>
</div>
</div>
</div>
)}
{showExportHandles && (
<> <>
<div {showHandlebar && (
className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportEndRef}
>
<div <div
className="flex items-center justify-center touch-none select-none" className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
onMouseDown={handleExportEnd} role="scrollbar"
onTouchStart={handleExportEnd} ref={handlebarRef}
> >
<div <div
className={`relative mt-[6.5px] w-full ${ className="flex items-center justify-center touch-none select-none"
isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab" onMouseDown={handleHandlebar}
}`} onTouchStart={handleHandlebar}
> >
<div <div
className={`bg-selected -mt-4 mx-auto ${ className={`relative w-full ${
dense isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab"
? "w-12 md:w-20" }`}
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportEnd && isMobile ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
> >
<div <div
ref={exportEndTimeRef} className={`bg-destructive rounded-full mx-auto ${
className={`text-white pointer-events-none ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`} dense
></div> ? "w-12 md:w-20"
</div> : segmentDuration < 60
<div ? "w-24"
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 transform -translate-y-1/2"}`} : "w-20"
></div> } 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`}
</div> >
</div> <div
</div> ref={handlebarTimeRef}
<div className={`text-white pointer-events-none ${textSizeClasses("handlebar")} z-10`}
ref={exportSectionRef} ></div>
className="bg-selected/50 absolute w-full" </div>
></div>
<div
className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportStartRef}
>
<div
className="flex items-center justify-center touch-none select-none"
onMouseDown={handleExportStart}
onTouchStart={handleExportStart}
>
<div
className={`relative -mt-[6.5px] w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
></div>
<div
className={`bg-selected mt-4 mx-auto ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportStart && isMobile ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
>
<div <div
ref={exportStartTimeRef} className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
className={`text-white pointer-events-none ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
></div> ></div>
</div> </div>
</div> </div>
</div> </div>
</div> )}
{showExportHandles && (
<>
<div
className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportEndRef}
>
<div
className="flex items-center justify-center touch-none select-none"
onMouseDown={handleExportEnd}
onTouchStart={handleExportEnd}
>
<div
className={`relative mt-[6.5px] w-full ${
isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`bg-selected -mt-4 mx-auto ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportEnd && isMobile ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
>
<div
ref={exportEndTimeRef}
className={`text-white pointer-events-none ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`}
></div>
</div>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 transform -translate-y-1/2"}`}
></div>
</div>
</div>
</div>
<div
ref={exportSectionRef}
className="bg-selected/50 absolute w-full"
></div>
<div
className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
role="scrollbar"
ref={exportStartRef}
>
<div
className="flex items-center justify-center touch-none select-none"
onMouseDown={handleExportStart}
onTouchStart={handleExportStart}
>
<div
className={`relative -mt-[6.5px] w-full ${
isDragging ? "cursor-grabbing" : "cursor-grab"
}`}
>
<div
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
></div>
<div
className={`bg-selected mt-4 mx-auto ${
dense
? "w-12 md:w-20"
: segmentDuration < 60
? "w-24"
: "w-20"
} h-5 ${isDraggingExportStart && isMobile ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
>
<div
ref={exportStartTimeRef}
className={`text-white pointer-events-none ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
></div>
</div>
</div>
</div>
</div>
</>
)}
</> </>
)} )}
</div> </div>

View File

@ -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 { isMobile } from "react-device-detect";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import { useTimelineUtils } from "./use-timeline-utils"; import { useTimelineUtils } from "./use-timeline-utils";
@ -23,6 +23,7 @@ type DraggableElementProps = {
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>; setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>; setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>;
dense: boolean; dense: boolean;
timelineSegments: ReactNode[];
}; };
function useDraggableElement({ function useDraggableElement({
@ -45,6 +46,7 @@ function useDraggableElement({
setIsDragging, setIsDragging,
setDraggableElementPosition, setDraggableElementPosition,
dense, dense,
timelineSegments,
}: 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);
@ -213,10 +215,10 @@ function useDraggableElement({
); );
useEffect(() => { useEffect(() => {
if (timelineRef.current) { if (timelineRef.current && timelineSegments.length) {
setSegments(Array.from(timelineRef.current.querySelectorAll(".segment"))); setSegments(Array.from(timelineRef.current.querySelectorAll(".segment")));
} }
}, [timelineRef, segmentDuration, timelineDuration, timelineCollapsed]); }, [timelineRef, timelineCollapsed, timelineSegments]);
useEffect(() => { useEffect(() => {
let animationFrameId: number | null = null; let animationFrameId: number | null = null;
@ -426,7 +428,13 @@ function useDraggableElement({
]); ]);
useEffect(() => { useEffect(() => {
if (timelineRef.current && draggableElementTime && timelineCollapsed) { if (
timelineRef.current &&
draggableElementTime &&
timelineCollapsed &&
timelineSegments &&
segments
) {
setFullTimelineHeight(timelineRef.current.scrollHeight); setFullTimelineHeight(timelineRef.current.scrollHeight);
const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime); const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime);
@ -452,14 +460,30 @@ function useDraggableElement({
if (setDraggableElementTime) { if (setDraggableElementTime) {
setDraggableElementTime(searchTime); 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 // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [timelineCollapsed]); }, [timelineCollapsed, segments]);
useEffect(() => { useEffect(() => {
if (timelineRef.current && segments) { if (timelineRef.current && segments) {

View File

@ -37,9 +37,11 @@ export default function useStats(stats: FrigateStats | undefined) {
// check camera cpu usages // check camera cpu usages
Object.entries(stats["cameras"]).forEach(([name, cam]) => { Object.entries(stats["cameras"]).forEach(([name, cam]) => {
const ffmpegAvg = parseFloat( 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) { if (!isNaN(ffmpegAvg) && ffmpegAvg >= 20.0) {
problems.push({ problems.push({

View File

@ -206,12 +206,12 @@ export default function EventView({
return ( return (
<div className="py-2 flex flex-col size-full"> <div className="py-2 flex flex-col size-full">
<div className="h-11 px-2 relative flex justify-between items-center"> <div className="h-11 mb-2 pl-3 pr-2 relative flex justify-between items-center">
{isMobile && ( {isMobile && (
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" /> <Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
)} )}
<ToggleGroup <ToggleGroup
className="*:px-3 *:py-4 *:rounded-2xl" className="*:px-3 *:py-4 *:rounded-md"
type="single" type="single"
size="sm" size="sm"
value={severity} value={severity}
@ -518,7 +518,7 @@ function DetectionReview({
)} )}
<div <div
className="w-full m-2 p-1 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4" className="w-full mx-2 px-1 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
ref={contentRef} ref={contentRef}
> >
{currentItems && {currentItems &&
@ -533,7 +533,7 @@ function DetectionReview({
data-segment-start={ data-segment-start={
alignStartDateToTimeline(value.start_time) - segmentDuration 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"
> >
<div className="aspect-video rounded-lg overflow-hidden"> <div className="aspect-video rounded-lg overflow-hidden">
<PreviewThumbnailPlayer <PreviewThumbnailPlayer
@ -545,6 +545,9 @@ function DetectionReview({
onClick={onSelectReview} onClick={onSelectReview}
/> />
</div> </div>
<div
className={`review-item-ring pointer-events-none z-10 absolute rounded-lg inset-0 size-full -outline-offset-[2.8px] outline outline-3 ${selected ? `outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-transparent duration-500"}`}
/>
</div> </div>
); );
})} })}
@ -563,7 +566,7 @@ function DetectionReview({
)} )}
</div> </div>
</div> </div>
<div className="w-[65px] md:w-[110px] mt-2 flex flex-row"> <div className="w-[65px] md:w-[110px] flex flex-row">
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar"> <div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
<EventReviewTimeline <EventReviewTimeline
segmentDuration={segmentDuration} segmentDuration={segmentDuration}
@ -809,44 +812,53 @@ function MotionReview({
<div className="flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar"> <div className="flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
<div <div
ref={contentRef} ref={contentRef}
className="w-full m-2 p-1 grid sm:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4 overflow-auto no-scrollbar" className="w-full mx-2 px-1 grid sm:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4 overflow-auto no-scrollbar"
> >
{reviewCameras.map((camera) => { {reviewCameras.map((camera) => {
let grow; let grow;
let spans;
const aspectRatio = camera.detect.width / camera.detect.height; const aspectRatio = camera.detect.width / camera.detect.height;
if (aspectRatio > 2) { if (aspectRatio > 2) {
grow = "sm:col-span-2 aspect-wide"; grow = "aspect-wide";
spans = "sm:col-span-2";
} else if (aspectRatio < 1) { } 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 { } else {
grow = "aspect-video"; grow = "aspect-video";
spans = "";
} }
const detectionType = getDetectionType(camera.name); const detectionType = getDetectionType(camera.name);
return ( return (
<PreviewPlayer <div className={`relative ${spans}`}>
key={camera.name} <PreviewPlayer
className={`${detectionType ? `outline outline-3 outline-offset-1 outline-severity_${detectionType}` : "outline-0 shadow-none"} rounded-2xl ${grow}`} key={camera.name}
camera={camera.name} className={`rounded-2xl ${spans} ${grow}`}
timeRange={currentTimeRange} camera={camera.name}
startTime={previewStart} timeRange={currentTimeRange}
cameraPreviews={relevantPreviews || []} startTime={previewStart}
isScrubbing={scrubbing} cameraPreviews={relevantPreviews || []}
onControllerReady={(controller) => { isScrubbing={scrubbing}
videoPlayersRef.current[camera.name] = controller; onControllerReady={(controller) => {
}} videoPlayersRef.current[camera.name] = controller;
onClick={() => }}
onOpenRecording({ onClick={() =>
camera: camera.name, onOpenRecording({
startTime: currentTime, camera: camera.name,
severity: "significant_motion", startTime: currentTime,
}) severity: "significant_motion",
} })
/> }
/>
<div
className={`review-item-ring pointer-events-none z-10 absolute rounded-lg inset-0 size-full -outline-offset-[2.8px] outline outline-3 ${detectionType ? `outline-severity_${detectionType} shadow-severity_${detectionType}` : "outline-transparent duration-500"}`}
/>
</div>
); );
})} })}
</div> </div>
</div> </div>
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar"> <div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
<MotionReviewTimeline <MotionReviewTimeline
segmentDuration={segmentDuration} segmentDuration={segmentDuration}
timestampSpread={15} timestampSpread={15}

View File

@ -243,7 +243,7 @@ export function RecordingView({
<div ref={contentRef} className="size-full pt-2 flex flex-col"> <div ref={contentRef} className="size-full pt-2 flex flex-col">
<Toaster /> <Toaster />
<div <div
className={`w-full h-11 px-2 relative flex items-center justify-between`} className={`w-full h-11 mb-2 px-2 relative flex items-center justify-between`}
> >
{isMobile && ( {isMobile && (
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" /> <Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />