diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index b6f501d73..eec5c9a38 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -131,13 +131,16 @@ export default function MotionMaskEditPane({ axios .put(`config/set?${queryString}`, { - requires_restart: 1, + requires_restart: 0, }) .then((res) => { if (res.status === 200) { - toast.success(`${polygon.name || "Motion Mask"} has been saved.`, { - position: "top-center", - }); + toast.success( + `${polygon.name || "Motion Mask"} has been saved. Restart Frigate to apply changes.`, + { + position: "top-center", + }, + ); updateConfig(); } else { toast.error(`Failed to save config changes: ${res.statusText}`, { diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx index 885a6adfb..612896a9b 100644 --- a/web/src/components/settings/ObjectMaskEditPane.tsx +++ b/web/src/components/settings/ObjectMaskEditPane.tsx @@ -189,13 +189,16 @@ export default function ObjectMaskEditPane({ axios .put(`config/set?${queryString}`, { - requires_restart: 1, + requires_restart: 0, }) .then((res) => { if (res.status === 200) { - toast.success(`${polygon.name || "Object Mask"} has been saved.`, { - position: "top-center", - }); + toast.success( + `${polygon.name || "Object Mask"} has been saved. Restart Frigate to apply changes.`, + { + position: "top-center", + }, + ); updateConfig(); } else { toast.error(`Failed to save config changes: ${res.statusText}`, { diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index a0b436224..9f12d91da 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -197,7 +197,7 @@ export default function ZoneEditPane({ await axios.put( `config/set?cameras.${polygon.camera}.zones.${polygon.name}${renameAlertQueries}${renameDetectionQueries}`, { - requires_restart: 1, + requires_restart: 0, }, ); @@ -257,13 +257,16 @@ export default function ZoneEditPane({ axios .put( `config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${objectQueries}${alertQueries}${detectionQueries}`, - { requires_restart: 1 }, + { requires_restart: 0 }, ) .then((res) => { if (res.status === 200) { - toast.success(`Zone (${zoneName}) has been saved.`, { - position: "top-center", - }); + toast.success( + `Zone (${zoneName}) has been saved. Restart Frigate to apply changes.`, + { + position: "top-center", + }, + ); updateConfig(); } else { toast.error(`Failed to save config changes: ${res.statusText}`, { diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx index 2bce9c90a..3952946e1 100644 --- a/web/src/components/timeline/MotionSegment.tsx +++ b/web/src/components/timeline/MotionSegment.tsx @@ -7,6 +7,7 @@ import { MinimapBounds, Tick, Timestamp } from "./segment-metadata"; import { useMotionSegmentUtils } from "@/hooks/use-motion-segment-utils"; import { isDesktop, isMobile } from "react-device-detect"; import useTapUtils from "@/hooks/use-tap-utils"; +import { cn } from "@/lib/utils"; type MotionSegmentProps = { events: ReviewSegment[]; @@ -170,7 +171,16 @@ export function MotionSegment({
0 || secondHalfSegmentWidth > 0 ? "has-data" : ""} ${segmentClasses} bg-gradient-to-r ${severityColorsBg[severity[0]]}`} + className={cn( + "segment", + { + "has-data": + firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0, + }, + segmentClasses, + severity[0] && "bg-gradient-to-r", + severity[0] && severityColorsBg[severity[0]], + )} onClick={segmentClick} onTouchEnd={(event) => handleTouchStart(event, segmentClick)} > @@ -210,7 +220,14 @@ export function MotionSegment({
{ + if (containerWidth && containerHeight && containerRef.current) { + return ( + containerRef.current.offsetWidth - containerRef.current.clientWidth + ); + } + return 0; + }, [containerRef, containerHeight, containerWidth]); + + const availableWidth = useMemo( + () => (scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth), + [containerWidth, scrollBarWidth], + ); + const hasScrollbar = useMemo(() => { - return ( - containerHeight && - containerRef.current && - containerRef.current.offsetHeight < - (gridContainerRef.current?.scrollHeight ?? 0) - ); - }, [containerRef, gridContainerRef, containerHeight]); + if (containerHeight && containerRef.current) { + return ( + containerRef.current.offsetHeight < containerRef.current.scrollHeight + ); + } + }, [containerRef, containerHeight]); // fullscreen state @@ -295,13 +308,13 @@ export default function DraggableGridLayout({ // subtract container margin, 1 camera takes up at least 4 rows // account for additional margin on bottom of each row return ( - ((containerWidth ?? window.innerWidth) - 2 * marginValue) / + ((availableWidth ?? window.innerWidth) - 2 * marginValue) / 12 / aspectRatio - marginValue + marginValue / 4 ); - }, [containerWidth, marginValue]); + }, [availableWidth, marginValue]); const handleResize: ItemCallback = ( _: Layout[], @@ -312,7 +325,7 @@ export default function DraggableGridLayout({ const heightDiff = layoutItem.h - oldLayoutItem.h; const widthDiff = layoutItem.w - oldLayoutItem.w; const changeCoef = oldLayoutItem.w / oldLayoutItem.h; - if (Math.abs(heightDiff) < Math.abs(widthDiff)) { + if (Math.abs(heightDiff) < Math.abs(widthDiff) || layoutItem.w == 12) { layoutItem.h = layoutItem.w / changeCoef; placeholder.h = layoutItem.w / changeCoef; } else { @@ -545,6 +558,7 @@ const BirdseyeLivePlayerGridItem = React.forwardRef< birdseyeConfig={birdseyeConfig} liveMode={liveMode} onClick={onClick} + containerRef={ref as React.RefObject} /> {children}
@@ -603,6 +617,7 @@ const LivePlayerGridItem = React.forwardRef< cameraConfig={cameraConfig} preferredLiveMode={preferredLiveMode} onClick={onClick} + containerRef={ref as React.RefObject} /> {children}
diff --git a/web/src/views/live/LiveBirdseyeView.tsx b/web/src/views/live/LiveBirdseyeView.tsx index 13399c53e..dde52e339 100644 --- a/web/src/views/live/LiveBirdseyeView.tsx +++ b/web/src/views/live/LiveBirdseyeView.tsx @@ -42,12 +42,24 @@ export default function LiveBirdseyeView() { return config.birdseye.width / config.birdseye.height; }, [config]); + const windowAspectRatio = useMemo(() => { + return windowWidth / windowHeight; + }, [windowWidth, windowHeight]); + + const containerAspectRatio = useMemo(() => { + if (!containerRef.current) { + return windowAspectRatio; + } + + return containerRef.current.clientWidth / containerRef.current.clientHeight; + }, [windowAspectRatio, containerRef]); + const growClassName = useMemo(() => { if (isMobile) { if (isPortrait) { return "absolute left-2 right-2 top-[50%] -translate-y-[50%]"; } else { - if (cameraAspectRatio > 16 / 9) { + if (cameraAspectRatio > containerAspectRatio) { return "absolute left-0 top-[50%] -translate-y-[50%]"; } else { return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; @@ -56,7 +68,7 @@ export default function LiveBirdseyeView() { } if (fullscreen) { - if (cameraAspectRatio > 16 / 9) { + if (cameraAspectRatio > containerAspectRatio) { return "absolute inset-x-2 top-[50%] -translate-y-[50%]"; } else { return "absolute inset-y-2 left-[50%] -translate-x-[50%]"; @@ -64,7 +76,7 @@ export default function LiveBirdseyeView() { } else { return "absolute top-0 bottom-0 left-[50%] -translate-x-[50%]"; } - }, [cameraAspectRatio, fullscreen, isPortrait]); + }, [cameraAspectRatio, containerAspectRatio, fullscreen, isPortrait]); const preferredLiveMode = useMemo(() => { if (!config || !config.birdseye.restream) { @@ -78,18 +90,6 @@ export default function LiveBirdseyeView() { return "mse"; }, [config]); - const windowAspectRatio = useMemo(() => { - return windowWidth / windowHeight; - }, [windowWidth, windowHeight]); - - const containerAspectRatio = useMemo(() => { - if (!containerRef.current) { - return windowAspectRatio; - } - - return containerRef.current.clientWidth / containerRef.current.clientHeight; - }, [windowAspectRatio, containerRef]); - const aspectRatio = useMemo(() => { if (isMobile || fullscreen) { return cameraAspectRatio; diff --git a/web/src/views/settings/MotionTunerView.tsx b/web/src/views/settings/MotionTunerView.tsx index 0c36eb4a0..ad08c4f42 100644 --- a/web/src/views/settings/MotionTunerView.tsx +++ b/web/src/views/settings/MotionTunerView.tsx @@ -113,7 +113,7 @@ export default function MotionTunerView({ axios .put( `config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast}`, - { requires_restart: 1 }, + { requires_restart: 0 }, ) .then((res) => { if (res.status === 200) {