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) {