mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-30 20:04:54 +03:00
render masks and zones based on ws enabled state
This commit is contained in:
parent
14d9068bfa
commit
c3be785061
@ -7,6 +7,7 @@ import { Polygon, PolygonType } from "@/types/canvas";
|
|||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { snapPointToLines } from "@/utils/canvasUtil";
|
import { snapPointToLines } from "@/utils/canvasUtil";
|
||||||
|
import { usePolygonStates } from "@/hooks/use-polygon-states";
|
||||||
|
|
||||||
type PolygonCanvasProps = {
|
type PolygonCanvasProps = {
|
||||||
containerRef: RefObject<HTMLDivElement>;
|
containerRef: RefObject<HTMLDivElement>;
|
||||||
@ -40,6 +41,7 @@ export function PolygonCanvas({
|
|||||||
const imageRef = useRef<Konva.Image | null>(null);
|
const imageRef = useRef<Konva.Image | null>(null);
|
||||||
const stageRef = useRef<Konva.Stage>(null);
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
|
const getPolygonEnabled = usePolygonStates(polygons);
|
||||||
|
|
||||||
const videoElement = useMemo(() => {
|
const videoElement = useMemo(() => {
|
||||||
if (camera && width && height) {
|
if (camera && width && height) {
|
||||||
@ -321,7 +323,7 @@ export function PolygonCanvas({
|
|||||||
isActive={index === activePolygonIndex}
|
isActive={index === activePolygonIndex}
|
||||||
isHovered={index === hoveredPolygonIndex}
|
isHovered={index === hoveredPolygonIndex}
|
||||||
isFinished={polygon.isFinished}
|
isFinished={polygon.isFinished}
|
||||||
enabled={polygon.enabled}
|
enabled={getPolygonEnabled(polygon)}
|
||||||
color={polygon.color}
|
color={polygon.color}
|
||||||
handlePointDragMove={handlePointDragMove}
|
handlePointDragMove={handlePointDragMove}
|
||||||
handleGroupDragEnd={handleGroupDragEnd}
|
handleGroupDragEnd={handleGroupDragEnd}
|
||||||
@ -351,7 +353,7 @@ export function PolygonCanvas({
|
|||||||
isActive={true}
|
isActive={true}
|
||||||
isHovered={activePolygonIndex === hoveredPolygonIndex}
|
isHovered={activePolygonIndex === hoveredPolygonIndex}
|
||||||
isFinished={polygons[activePolygonIndex].isFinished}
|
isFinished={polygons[activePolygonIndex].isFinished}
|
||||||
enabled={polygons[activePolygonIndex].enabled}
|
enabled={getPolygonEnabled(polygons[activePolygonIndex])}
|
||||||
color={polygons[activePolygonIndex].color}
|
color={polygons[activePolygonIndex].color}
|
||||||
handlePointDragMove={handlePointDragMove}
|
handlePointDragMove={handlePointDragMove}
|
||||||
handleGroupDragEnd={handleGroupDragEnd}
|
handleGroupDragEnd={handleGroupDragEnd}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { buttonVariants } from "../ui/button";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useMotionMaskState, useObjectMaskState, useZoneState } from "@/api/ws";
|
||||||
|
|
||||||
type PolygonItemProps = {
|
type PolygonItemProps = {
|
||||||
polygon: Polygon;
|
polygon: Polygon;
|
||||||
@ -66,6 +67,31 @@ export default function PolygonItem({
|
|||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const { payload: motionMaskState, send: sendMotionMaskState } =
|
||||||
|
useMotionMaskState(polygon.camera, polygon.name);
|
||||||
|
const { payload: objectMaskState, send: sendObjectMaskState } =
|
||||||
|
useObjectMaskState(polygon.camera, polygon.name);
|
||||||
|
const { payload: zoneState, send: sendZoneState } = useZoneState(
|
||||||
|
polygon.camera,
|
||||||
|
polygon.name,
|
||||||
|
);
|
||||||
|
const isPolygonEnabled = useMemo(() => {
|
||||||
|
const wsState =
|
||||||
|
polygon.type === "zone"
|
||||||
|
? zoneState
|
||||||
|
: polygon.type === "motion_mask"
|
||||||
|
? motionMaskState
|
||||||
|
: objectMaskState;
|
||||||
|
const wsEnabled =
|
||||||
|
wsState === "ON" ? true : wsState === "OFF" ? false : undefined;
|
||||||
|
return wsEnabled ?? polygon.enabled ?? true;
|
||||||
|
}, [
|
||||||
|
polygon.enabled,
|
||||||
|
polygon.type,
|
||||||
|
zoneState,
|
||||||
|
motionMaskState,
|
||||||
|
objectMaskState,
|
||||||
|
]);
|
||||||
|
|
||||||
const cameraConfig = useMemo(() => {
|
const cameraConfig = useMemo(() => {
|
||||||
if (polygon?.camera && config) {
|
if (polygon?.camera && config) {
|
||||||
@ -261,164 +287,35 @@ export default function PolygonItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleEnabled = useCallback(
|
const handleToggleEnabled = useCallback(
|
||||||
async (e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!polygon || !cameraConfig) {
|
if (!polygon) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEnabledState = polygon.enabled === false;
|
const isEnabled = isPolygonEnabled;
|
||||||
const updateTopicType =
|
const nextState = isEnabled ? "OFF" : "ON";
|
||||||
polygon.type === "zone"
|
|
||||||
? "zones"
|
|
||||||
: polygon.type === "motion_mask"
|
|
||||||
? "motion"
|
|
||||||
: polygon.type === "object_mask"
|
|
||||||
? "objects"
|
|
||||||
: polygon.type;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setLoadingPolygonIndex(index);
|
|
||||||
|
|
||||||
if (polygon.type === "zone") {
|
if (polygon.type === "zone") {
|
||||||
// Zones use query string format
|
sendZoneState(nextState);
|
||||||
const url = `cameras.${polygon.camera}.zones.${polygon.name}.enabled=${newEnabledState ? "True" : "False"}`;
|
|
||||||
|
|
||||||
await axios
|
|
||||||
.put(`config/set?${url}`, {
|
|
||||||
requires_restart: 0,
|
|
||||||
update_topic: `config/cameras/${polygon.camera}/${updateTopicType}`,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
updateConfig();
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
t("toast.save.error.title", {
|
|
||||||
ns: "common",
|
|
||||||
errorMessage: res.statusText,
|
|
||||||
}),
|
|
||||||
{ position: "top-center" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const errorMessage =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.response?.data?.detail ||
|
|
||||||
"Unknown error";
|
|
||||||
toast.error(
|
|
||||||
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
||||||
{ position: "top-center" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Motion masks and object masks use JSON body format
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let configUpdate: any = {};
|
|
||||||
|
|
||||||
if (polygon.type === "motion_mask") {
|
if (polygon.type === "motion_mask") {
|
||||||
configUpdate = {
|
sendMotionMaskState(nextState);
|
||||||
cameras: {
|
return;
|
||||||
[polygon.camera]: {
|
|
||||||
motion: {
|
|
||||||
mask: {
|
|
||||||
[polygon.name]: {
|
|
||||||
enabled: newEnabledState,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polygon.type === "object_mask") {
|
if (polygon.type === "object_mask") {
|
||||||
// Determine if this is a global mask or object-specific mask
|
sendObjectMaskState(nextState);
|
||||||
const isGlobalMask = !polygon.objects.length;
|
|
||||||
|
|
||||||
if (isGlobalMask) {
|
|
||||||
configUpdate = {
|
|
||||||
cameras: {
|
|
||||||
[polygon.camera]: {
|
|
||||||
objects: {
|
|
||||||
mask: {
|
|
||||||
[polygon.name]: {
|
|
||||||
enabled: newEnabledState,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
configUpdate = {
|
|
||||||
cameras: {
|
|
||||||
[polygon.camera]: {
|
|
||||||
objects: {
|
|
||||||
filters: {
|
|
||||||
[polygon.objects[0]]: {
|
|
||||||
mask: {
|
|
||||||
[polygon.name]: {
|
|
||||||
enabled: newEnabledState,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios
|
|
||||||
.put("config/set", {
|
|
||||||
config_data: configUpdate,
|
|
||||||
requires_restart: 0,
|
|
||||||
update_topic: `config/cameras/${polygon.camera}/${updateTopicType}`,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
updateConfig();
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
t("toast.save.error.title", {
|
|
||||||
ns: "common",
|
|
||||||
errorMessage: res.statusText,
|
|
||||||
}),
|
|
||||||
{ position: "top-center" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const errorMessage =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.response?.data?.detail ||
|
|
||||||
"Unknown error";
|
|
||||||
toast.error(
|
|
||||||
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
|
||||||
{ position: "top-center" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
setLoadingPolygonIndex(undefined);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
updateConfig,
|
isPolygonEnabled,
|
||||||
cameraConfig,
|
|
||||||
t,
|
|
||||||
polygon,
|
polygon,
|
||||||
setIsLoading,
|
sendZoneState,
|
||||||
index,
|
sendMotionMaskState,
|
||||||
setLoadingPolygonIndex,
|
sendObjectMaskState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -463,30 +360,27 @@ export default function PolygonItem({
|
|||||||
<PolygonItemIcon
|
<PolygonItemIcon
|
||||||
className="size-5"
|
className="size-5"
|
||||||
style={{
|
style={{
|
||||||
fill: toRGBColorString(
|
fill: toRGBColorString(polygon.color, isPolygonEnabled),
|
||||||
polygon.color,
|
|
||||||
polygon.enabled ?? true,
|
|
||||||
),
|
|
||||||
color: toRGBColorString(
|
color: toRGBColorString(
|
||||||
polygon.color,
|
polygon.color,
|
||||||
polygon.enabled ?? true,
|
isPolygonEnabled,
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{polygon.enabled === false
|
{isPolygonEnabled
|
||||||
? t("button.enable", { ns: "common" })
|
? t("button.disable", { ns: "common" })
|
||||||
: t("button.disable", { ns: "common" })}
|
: t("button.enable", { ns: "common" })}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
<p
|
<p
|
||||||
className={`cursor-default ${polygon.enabled === false ? "line-through" : ""}`}
|
className={`cursor-default ${!isPolygonEnabled ? "line-through" : ""}`}
|
||||||
>
|
>
|
||||||
{polygon.friendly_name ?? polygon.name}
|
{polygon.friendly_name ?? polygon.name}
|
||||||
{polygon.enabled === false && " (disabled)"}
|
{!isPolygonEnabled && " (disabled)"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
|
|||||||
44
web/src/hooks/use-polygon-states.ts
Normal file
44
web/src/hooks/use-polygon-states.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Polygon } from "@/types/canvas";
|
||||||
|
import { useWsState } from "@/api/ws";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get enabled state for a polygon from websocket state.
|
||||||
|
* Memoizes the lookup function to avoid unnecessary re-renders.
|
||||||
|
*/
|
||||||
|
export function usePolygonStates(polygons: Polygon[]) {
|
||||||
|
const wsState = useWsState();
|
||||||
|
|
||||||
|
// Create a memoized lookup map that only updates when relevant ws values change
|
||||||
|
return useMemo(() => {
|
||||||
|
const stateMap = new Map<string, boolean>();
|
||||||
|
|
||||||
|
polygons.forEach((polygon) => {
|
||||||
|
const topic =
|
||||||
|
polygon.type === "zone"
|
||||||
|
? `${polygon.camera}/zone/${polygon.name}/state`
|
||||||
|
: polygon.type === "motion_mask"
|
||||||
|
? `${polygon.camera}/motion_mask/${polygon.name}/state`
|
||||||
|
: `${polygon.camera}/object_mask/${polygon.name}/state`;
|
||||||
|
|
||||||
|
const wsValue = wsState[topic];
|
||||||
|
const enabled =
|
||||||
|
wsValue === "ON"
|
||||||
|
? true
|
||||||
|
: wsValue === "OFF"
|
||||||
|
? false
|
||||||
|
: (polygon.enabled ?? true);
|
||||||
|
stateMap.set(
|
||||||
|
`${polygon.camera}/${polygon.type}/${polygon.name}`,
|
||||||
|
enabled,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (polygon: Polygon) => {
|
||||||
|
return (
|
||||||
|
stateMap.get(`${polygon.camera}/${polygon.type}/${polygon.name}`) ??
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [polygons, wsState]);
|
||||||
|
}
|
||||||
@ -300,7 +300,6 @@ export default function MasksAndZonesView({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const globalObjectMaskIds = Object.keys(cameraConfig.objects.mask || {});
|
|
||||||
let objectMaskIndex = globalObjectMasks.length;
|
let objectMaskIndex = globalObjectMasks.length;
|
||||||
|
|
||||||
objectMasks = Object.entries(cameraConfig.objects.filters)
|
objectMasks = Object.entries(cameraConfig.objects.filters)
|
||||||
@ -311,8 +310,8 @@ export default function MasksAndZonesView({
|
|||||||
.flatMap(([objectName, filterConfig]): Polygon[] => {
|
.flatMap(([objectName, filterConfig]): Polygon[] => {
|
||||||
return Object.entries(filterConfig.mask || {}).flatMap(
|
return Object.entries(filterConfig.mask || {}).flatMap(
|
||||||
([maskId, maskData]) => {
|
([maskId, maskData]) => {
|
||||||
// Skip if this mask is already included in global masks
|
// Skip if this mask is a global mask (prefixed with "global_")
|
||||||
if (globalObjectMaskIds.includes(maskId)) {
|
if (maskId.startsWith("global_")) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user