mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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:
parent
5853393396
commit
7fac91dce4
@ -31,7 +31,7 @@ function App() {
|
||||
{isMobile && <Bottombar />}
|
||||
<div
|
||||
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>
|
||||
<Routes>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
<MotionSegment
|
||||
key={segmentTime}
|
||||
events={events}
|
||||
@ -96,9 +120,11 @@ export function MotionReviewTimeline({
|
||||
minimapEndTime={minimapEndTime}
|
||||
setHandlebarTime={setHandlebarTime}
|
||||
dense={dense}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
});
|
||||
segmentTime -= segmentDuration;
|
||||
}
|
||||
return segments;
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
|
@ -30,7 +30,7 @@ export type ReviewTimelineProps = {
|
||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
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,6 +324,8 @@ 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>
|
||||
{children}
|
||||
</div>
|
||||
{children.length > 0 && (
|
||||
<>
|
||||
{showHandlebar && (
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
|
||||
@ -436,6 +441,8 @@ export function ReviewTimeline({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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<React.SetStateAction<boolean>>;
|
||||
setDraggableElementPosition?: React.Dispatch<React.SetStateAction<number>>;
|
||||
dense: boolean;
|
||||
timelineSegments: ReactNode[];
|
||||
};
|
||||
|
||||
function useDraggableElement({
|
||||
@ -45,6 +46,7 @@ function useDraggableElement({
|
||||
setIsDragging,
|
||||
setDraggableElementPosition,
|
||||
dense,
|
||||
timelineSegments,
|
||||
}: DraggableElementProps) {
|
||||
const [clientYPosition, setClientYPosition] = useState<number | null>(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) {
|
||||
|
@ -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({
|
||||
|
@ -206,12 +206,12 @@ export default function EventView({
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
||||
)}
|
||||
<ToggleGroup
|
||||
className="*:px-3 *:py-4 *:rounded-2xl"
|
||||
className="*:px-3 *:py-4 *:rounded-md"
|
||||
type="single"
|
||||
size="sm"
|
||||
value={severity}
|
||||
@ -518,7 +518,7 @@ function DetectionReview({
|
||||
)}
|
||||
|
||||
<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}
|
||||
>
|
||||
{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"
|
||||
>
|
||||
<div className="aspect-video rounded-lg overflow-hidden">
|
||||
<PreviewThumbnailPlayer
|
||||
@ -545,6 +545,9 @@ function DetectionReview({
|
||||
onClick={onSelectReview}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@ -563,7 +566,7 @@ function DetectionReview({
|
||||
)}
|
||||
</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">
|
||||
<EventReviewTimeline
|
||||
segmentDuration={segmentDuration}
|
||||
@ -809,23 +812,28 @@ function MotionReview({
|
||||
<div className="flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
|
||||
<div
|
||||
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) => {
|
||||
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 (
|
||||
<div className={`relative ${spans}`}>
|
||||
<PreviewPlayer
|
||||
key={camera.name}
|
||||
className={`${detectionType ? `outline outline-3 outline-offset-1 outline-severity_${detectionType}` : "outline-0 shadow-none"} rounded-2xl ${grow}`}
|
||||
className={`rounded-2xl ${spans} ${grow}`}
|
||||
camera={camera.name}
|
||||
timeRange={currentTimeRange}
|
||||
startTime={previewStart}
|
||||
@ -842,11 +850,15 @@ function MotionReview({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<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 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
|
||||
segmentDuration={segmentDuration}
|
||||
timestampSpread={15}
|
||||
|
@ -243,7 +243,7 @@ export function RecordingView({
|
||||
<div ref={contentRef} className="size-full pt-2 flex flex-col">
|
||||
<Toaster />
|
||||
<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 && (
|
||||
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
||||
|
Loading…
Reference in New Issue
Block a user