frigate/web/src/components/settings/ZoneControls.tsx

265 lines
7.1 KiB
TypeScript
Raw Normal View History

2024-04-08 04:57:15 +03:00
import { Polygon } from "@/types/canvas";
2024-04-09 16:46:26 +03:00
import {
Dialog,
DialogContent,
2024-04-10 20:03:46 +03:00
DialogDescription,
2024-04-09 16:46:26 +03:00
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
2024-04-10 20:03:46 +03:00
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { useMemo, useState } from "react";
2024-04-09 16:46:26 +03:00
import { Input } from "../ui/input";
2024-04-10 20:03:46 +03:00
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
import { FaObjectGroup } from "react-icons/fa";
import { Button } from "../ui/button";
import { ATTRIBUTES, FrigateConfig } from "@/types/frigateConfig";
2024-04-09 16:46:26 +03:00
import useSWR from "swr";
2024-04-10 20:03:46 +03:00
import { isMobile } from "react-device-detect";
type ZoneObjectSelectorProps = {
camera: string;
zoneName: string;
allLabels: string[];
updateLabelFilter: (labels: string[] | undefined) => void;
};
export function ZoneObjectSelector({
camera,
zoneName,
allLabels,
updateLabelFilter,
}: ZoneObjectSelectorProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const [open, setOpen] = useState(false);
const cameraConfig = useMemo(() => {
if (config && camera) {
return config.cameras[camera];
}
}, [config, camera]);
const zoneLabels = useMemo<string[]>(() => {
if (!cameraConfig || !zoneName) {
return [];
}
const labels = new Set<string>();
cameraConfig.objects.track.forEach((label) => {
if (!ATTRIBUTES.includes(label)) {
labels.add(label);
}
});
2024-04-11 07:08:34 +03:00
if (cameraConfig.zones[zoneName]) {
cameraConfig.zones[zoneName].objects.forEach((label) => {
if (!ATTRIBUTES.includes(label)) {
labels.add(label);
}
});
}
2024-04-10 20:03:46 +03:00
return [...labels].sort() || [];
}, [cameraConfig, zoneName]);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
zoneLabels,
);
const trigger = (
<Button
className={`flex items-center gap-2 capitalize ${false ? "bg-selected hover:bg-selected" : ""}`}
size="sm"
>
<FaObjectGroup
className={`${false ? "text-background dark:text-primary" : "text-secondary-foreground"}`}
/>
</Button>
);
const content = (
<GeneralFilterContent
allLabels={allLabels}
selectedLabels={zoneLabels}
currentLabels={currentLabels}
updateLabelFilter={updateLabelFilter}
setCurrentLabels={setCurrentLabels}
onClose={() => setOpen(false)}
/>
);
if (isMobile) {
return (
<Drawer
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentLabels(zoneLabels);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<Popover
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentLabels(zoneLabels);
}
setOpen(open);
}}
>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}
2024-04-08 04:57:15 +03:00
2024-04-10 20:03:46 +03:00
type ZoneControlsProps = {
2024-04-08 04:57:15 +03:00
camera: string;
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | null;
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
};
2024-04-10 20:03:46 +03:00
export function ZoneControls({
camera,
2024-04-08 04:57:15 +03:00
polygons,
setPolygons,
activePolygonIndex,
setActivePolygonIndex,
2024-04-10 20:03:46 +03:00
}: ZoneControlsProps) {
2024-04-09 16:46:26 +03:00
const { data: config } = useSWR("config");
const [zoneName, setZoneName] = useState<string | null>();
const [invalidName, setInvalidName] = useState<boolean>();
const [dialogOpen, setDialogOpen] = useState(false);
2024-04-08 04:57:15 +03:00
const undo = () => {
if (activePolygonIndex !== null && polygons) {
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (activePolygon.points.length > 0) {
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: activePolygon.points.slice(0, -1),
isFinished: false,
};
setPolygons(updatedPolygons);
}
}
};
const reset = () => {
if (activePolygonIndex !== null) {
const updatedPolygons = [...polygons];
updatedPolygons[activePolygonIndex] = {
points: [],
isFinished: false,
2024-04-11 07:08:34 +03:00
name: updatedPolygons[activePolygonIndex].name,
2024-04-10 20:03:46 +03:00
camera: camera,
2024-04-10 03:04:23 +03:00
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
2024-04-08 04:57:15 +03:00
};
setPolygons(updatedPolygons);
}
};
2024-04-09 16:46:26 +03:00
const handleNewPolygon = (zoneName: string) => {
2024-04-08 04:57:15 +03:00
setPolygons([
...(polygons || []),
{
points: [],
isFinished: false,
2024-04-09 16:46:26 +03:00
name: zoneName,
2024-04-10 20:03:46 +03:00
camera: camera,
2024-04-10 03:04:23 +03:00
color: [220, 0, 0],
2024-04-08 04:57:15 +03:00
},
]);
setActivePolygonIndex(polygons.length);
};
return (
<div className="flex flex-col">
<div className="flex justify-between items-center my-5">
<Button className="mr-5" variant="secondary" onClick={undo}>
Undo
</Button>
<Button variant="secondary" onClick={reset}>
Reset
</Button>
2024-04-09 16:46:26 +03:00
<Button variant="secondary" onClick={() => setDialogOpen(true)}>
New Zone
2024-04-08 04:57:15 +03:00
</Button>
2024-04-09 16:46:26 +03:00
<Dialog
open={dialogOpen}
onOpenChange={(open) => {
setDialogOpen(open);
if (!open) {
setZoneName("");
}
}}
>
<DialogContent>
2024-04-11 07:08:34 +03:00
{isMobile && <span tabIndex={0} className="sr-only" />}
2024-04-09 16:46:26 +03:00
<DialogTitle>New Zone</DialogTitle>
2024-04-10 20:03:46 +03:00
<DialogDescription>
2024-04-11 07:08:34 +03:00
Enter a unique label for your zone. Do not include spaces, and
don't use the name of a camera.
2024-04-10 20:03:46 +03:00
</DialogDescription>
2024-04-09 16:46:26 +03:00
<>
<Input
2024-04-11 07:08:34 +03:00
className={`mt-3 ${isMobile && "text-md"}`}
2024-04-09 16:46:26 +03:00
type="search"
value={zoneName ?? ""}
onChange={(e) => {
setInvalidName(
2024-04-10 20:03:46 +03:00
Object.keys(config.cameras).includes(e.target.value) ||
2024-04-11 07:08:34 +03:00
e.target.value.includes(" ") ||
polygons
.map((item) => item.name)
.includes(e.target.value),
2024-04-09 16:46:26 +03:00
);
setZoneName(e.target.value);
}}
/>
{invalidName && (
2024-04-11 07:08:34 +03:00
<div className="text-danger text-sm">Invalid zone name.</div>
2024-04-09 16:46:26 +03:00
)}
<DialogFooter>
<Button
size="sm"
variant="select"
disabled={invalidName || (zoneName?.length ?? 0) == 0}
onClick={() => {
if (zoneName) {
setDialogOpen(false);
handleNewPolygon(zoneName);
}
setZoneName(null);
}}
>
Continue
</Button>
</DialogFooter>
</>
</DialogContent>
</Dialog>
2024-04-08 04:57:15 +03:00
</div>
</div>
);
}
2024-04-10 20:03:46 +03:00
export default ZoneControls;