mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +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 ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { snapPointToLines } from "@/utils/canvasUtil";
|
||||
import { usePolygonStates } from "@/hooks/use-polygon-states";
|
||||
|
||||
type PolygonCanvasProps = {
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
@ -40,6 +41,7 @@ export function PolygonCanvas({
|
||||
const imageRef = useRef<Konva.Image | null>(null);
|
||||
const stageRef = useRef<Konva.Stage>(null);
|
||||
const apiHost = useApiHost();
|
||||
const getPolygonEnabled = usePolygonStates(polygons);
|
||||
|
||||
const videoElement = useMemo(() => {
|
||||
if (camera && width && height) {
|
||||
@ -321,7 +323,7 @@ export function PolygonCanvas({
|
||||
isActive={index === activePolygonIndex}
|
||||
isHovered={index === hoveredPolygonIndex}
|
||||
isFinished={polygon.isFinished}
|
||||
enabled={polygon.enabled}
|
||||
enabled={getPolygonEnabled(polygon)}
|
||||
color={polygon.color}
|
||||
handlePointDragMove={handlePointDragMove}
|
||||
handleGroupDragEnd={handleGroupDragEnd}
|
||||
@ -351,7 +353,7 @@ export function PolygonCanvas({
|
||||
isActive={true}
|
||||
isHovered={activePolygonIndex === hoveredPolygonIndex}
|
||||
isFinished={polygons[activePolygonIndex].isFinished}
|
||||
enabled={polygons[activePolygonIndex].enabled}
|
||||
enabled={getPolygonEnabled(polygons[activePolygonIndex])}
|
||||
color={polygons[activePolygonIndex].color}
|
||||
handlePointDragMove={handlePointDragMove}
|
||||
handleGroupDragEnd={handleGroupDragEnd}
|
||||
|
||||
@ -34,6 +34,7 @@ import { buttonVariants } from "../ui/button";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMotionMaskState, useObjectMaskState, useZoneState } from "@/api/ws";
|
||||
|
||||
type PolygonItemProps = {
|
||||
polygon: Polygon;
|
||||
@ -66,6 +67,31 @@ export default function PolygonItem({
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
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(() => {
|
||||
if (polygon?.camera && config) {
|
||||
@ -261,164 +287,35 @@ export default function PolygonItem({
|
||||
};
|
||||
|
||||
const handleToggleEnabled = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!polygon || !cameraConfig) {
|
||||
if (!polygon) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newEnabledState = polygon.enabled === false;
|
||||
const updateTopicType =
|
||||
polygon.type === "zone"
|
||||
? "zones"
|
||||
: polygon.type === "motion_mask"
|
||||
? "motion"
|
||||
: polygon.type === "object_mask"
|
||||
? "objects"
|
||||
: polygon.type;
|
||||
|
||||
setIsLoading(true);
|
||||
setLoadingPolygonIndex(index);
|
||||
const isEnabled = isPolygonEnabled;
|
||||
const nextState = isEnabled ? "OFF" : "ON";
|
||||
|
||||
if (polygon.type === "zone") {
|
||||
// Zones use query string format
|
||||
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);
|
||||
});
|
||||
sendZoneState(nextState);
|
||||
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") {
|
||||
configUpdate = {
|
||||
cameras: {
|
||||
[polygon.camera]: {
|
||||
motion: {
|
||||
mask: {
|
||||
[polygon.name]: {
|
||||
enabled: newEnabledState,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
sendMotionMaskState(nextState);
|
||||
return;
|
||||
}
|
||||
|
||||
if (polygon.type === "object_mask") {
|
||||
// Determine if this is a global mask or object-specific mask
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
sendObjectMaskState(nextState);
|
||||
}
|
||||
|
||||
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,
|
||||
cameraConfig,
|
||||
t,
|
||||
isPolygonEnabled,
|
||||
polygon,
|
||||
setIsLoading,
|
||||
index,
|
||||
setLoadingPolygonIndex,
|
||||
sendZoneState,
|
||||
sendMotionMaskState,
|
||||
sendObjectMaskState,
|
||||
],
|
||||
);
|
||||
|
||||
@ -463,30 +360,27 @@ export default function PolygonItem({
|
||||
<PolygonItemIcon
|
||||
className="size-5"
|
||||
style={{
|
||||
fill: toRGBColorString(
|
||||
polygon.color,
|
||||
polygon.enabled ?? true,
|
||||
),
|
||||
fill: toRGBColorString(polygon.color, isPolygonEnabled),
|
||||
color: toRGBColorString(
|
||||
polygon.color,
|
||||
polygon.enabled ?? true,
|
||||
isPolygonEnabled,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{polygon.enabled === false
|
||||
? t("button.enable", { ns: "common" })
|
||||
: t("button.disable", { ns: "common" })}
|
||||
{isPolygonEnabled
|
||||
? t("button.disable", { ns: "common" })
|
||||
: t("button.enable", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<p
|
||||
className={`cursor-default ${polygon.enabled === false ? "line-through" : ""}`}
|
||||
className={`cursor-default ${!isPolygonEnabled ? "line-through" : ""}`}
|
||||
>
|
||||
{polygon.friendly_name ?? polygon.name}
|
||||
{polygon.enabled === false && " (disabled)"}
|
||||
{!isPolygonEnabled && " (disabled)"}
|
||||
</p>
|
||||
</div>
|
||||
<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;
|
||||
|
||||
objectMasks = Object.entries(cameraConfig.objects.filters)
|
||||
@ -311,8 +310,8 @@ export default function MasksAndZonesView({
|
||||
.flatMap(([objectName, filterConfig]): Polygon[] => {
|
||||
return Object.entries(filterConfig.mask || {}).flatMap(
|
||||
([maskId, maskData]) => {
|
||||
// Skip if this mask is already included in global masks
|
||||
if (globalObjectMaskIds.includes(maskId)) {
|
||||
// Skip if this mask is a global mask (prefixed with "global_")
|
||||
if (maskId.startsWith("global_")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user