motion tuner, edit controls, tooltips

This commit is contained in:
Josh Hawkins 2024-04-14 18:36:39 -05:00
parent 8deec1c9b6
commit 6ae3e24157
12 changed files with 507 additions and 362 deletions

View File

@ -6,6 +6,7 @@ type AutoUpdatingCameraImageProps = {
searchParams?: URLSearchParams;
showFps?: boolean;
className?: string;
cameraClasses?: string;
reloadInterval?: number;
};
@ -16,6 +17,7 @@ export default function AutoUpdatingCameraImage({
searchParams = undefined,
showFps = true,
className,
cameraClasses,
reloadInterval = MIN_LOAD_TIMEOUT_MS,
}: AutoUpdatingCameraImageProps) {
const [key, setKey] = useState(Date.now());
@ -68,6 +70,7 @@ export default function AutoUpdatingCameraImage({
camera={camera}
onload={handleLoad}
searchParams={`cache=${key}&${searchParams}`}
className={cameraClasses}
/>
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
</div>

View File

@ -36,12 +36,7 @@ export default function CameraImage({
}, [apiHost, name, imgRef, searchParams, config]);
return (
<div
className={`relative w-full h-full flex justify-center ${
className || ""
}`}
ref={containerRef}
>
<div className={className} ref={containerRef}>
{enabled ? (
<img
ref={imgRef}

View File

@ -53,6 +53,7 @@ export default function DebugCameraImage({
<AutoUpdatingCameraImage
camera={cameraConfig.name}
searchParams={searchParams}
cameraClasses="relative w-full h-full flex justify-center"
/>
<Button onClick={handleToggleSettings} variant="link" size="sm">
<span className="w-5 h-5">

View File

@ -163,6 +163,7 @@ export default function LivePlayer({
camera={cameraConfig.name}
showFps={false}
reloadInterval={stillReloadInterval}
cameraClasses="relative w-full h-full flex justify-center"
/>
</div>

View File

@ -5,16 +5,19 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon, PolygonType } from "@/types/canvas";
import { interpolatePoints, toRGBColorString } from "@/utils/canvasUtil";
import { isDesktop, isMobile } from "react-device-detect";
import { isMobile } from "react-device-detect";
import { Skeleton } from "../ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
import { LuCopy, LuPencil, LuPlusSquare, LuTrash } from "react-icons/lu";
import { LuCopy, LuPencil, LuPlus } from "react-icons/lu";
import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa";
import { BsPersonBoundingBox } from "react-icons/bs";
import { HiTrash } from "react-icons/hi";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { Toaster } from "../ui/sonner";
import { ZoneEditPane } from "./ZoneEditPane";
import { Button } from "../ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import {
AlertDialog,
AlertDialogAction,
@ -25,8 +28,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { Separator } from "../ui/separator";
import { BsPersonBoundingBox } from "react-icons/bs";
import Heading from "../ui/heading";
const parseCoordinates = (coordinatesString: string) => {
const coordinates = coordinatesString.split(",");
@ -196,7 +198,7 @@ export default function MasksAndZones({
}, [config, selectedCamera]);
const stretch = true;
const fitAspect = 1;
const fitAspect = 16 / 9;
const scaledHeight = useMemo(() => {
if (containerRef.current && aspectRatio && detectHeight) {
@ -247,13 +249,18 @@ export default function MasksAndZones({
};
const handleCancel = useCallback(() => {
console.log("handling cancel");
setEditPane(undefined);
setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
console.log("all", allPolygons);
console.log("editing", editingPolygons);
// setAllPolygons(allPolygons.filter((poly) => !poly.isUnsaved));
setEditingPolygons([...allPolygons]);
setActivePolygonIndex(undefined);
setHoveredPolygonIndex(null);
}, [allPolygons]);
}, [allPolygons, editingPolygons]);
const handleSave = useCallback(() => {
console.log("handling save");
setAllPolygons([...(editingPolygons ?? [])]);
setActivePolygonIndex(undefined);
setEditPane(undefined);
@ -368,20 +375,27 @@ export default function MasksAndZones({
: [],
);
console.log("setting all and editing");
setAllPolygons([
...zones,
...motionMasks,
...globalObjectMasks,
...objectMasks,
]);
setEditingPolygons([
...zones,
...motionMasks,
...globalObjectMasks,
...objectMasks,
]);
setZoneObjects(
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
// setZoneObjects(
// Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
// camera: cameraConfig.name,
// zoneName: name,
// objects: Object.keys(zoneData.filters),
// })),
// );
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -391,23 +405,25 @@ export default function MasksAndZones({
if (editPane === undefined) {
setEditingPolygons([...allPolygons]);
setIsEditing(false);
console.log(allPolygons);
console.log("edit pane undefined, all", allPolygons);
} else {
setIsEditing(true);
}
}, [setEditingPolygons, setIsEditing, allPolygons, editPane]);
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setEditingPolygons, setIsEditing, allPolygons]);
useEffect(() => {
console.log(
"config zone objects",
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
camera: cameraConfig.name,
zoneName: name,
objects: Object.keys(zoneData.filters),
})),
);
console.log("component zone objects", zoneObjects);
}, [zoneObjects]);
// useEffect(() => {
// console.log(
// "config zone objects",
// Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
// camera: cameraConfig.name,
// zoneName: name,
// objects: Object.keys(zoneData.filters),
// })),
// );
// console.log("component zone objects", zoneObjects);
// }, [zoneObjects]);
useEffect(() => {
if (selectedCamera) {
@ -421,13 +437,14 @@ export default function MasksAndZones({
return (
<>
{cameraConfig && allPolygons && (
{cameraConfig && editingPolygons && (
<div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" />
<div className="flex flex-col w-full overflow-y-auto md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
<div className="flex flex-col h-full w-full overflow-y-auto mt-2 md:mt-0 md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
{editPane == "zone" && (
<ZoneEditPane
polygons={editingPolygons}
setPolygons={setEditingPolygons}
activePolygonIndex={activePolygonIndex}
onCancel={handleCancel}
onSave={handleSave}
@ -436,6 +453,7 @@ export default function MasksAndZones({
{editPane == "motion_mask" && (
<ZoneEditPane
polygons={editingPolygons}
setPolygons={setEditingPolygons}
activePolygonIndex={activePolygonIndex}
onCancel={handleCancel}
onSave={handleSave}
@ -444,6 +462,7 @@ export default function MasksAndZones({
{editPane == "object_mask" && (
<ZoneEditPane
polygons={editingPolygons}
setPolygons={setEditingPolygons}
activePolygonIndex={activePolygonIndex}
onCancel={handleCancel}
onSave={handleSave}
@ -451,21 +470,30 @@ export default function MasksAndZones({
)}
{editPane === undefined && (
<>
<Heading as="h3" className="my-2">
Masks / Zones
</Heading>
<div className="flex flex-col w-full">
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("zone" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center my-2">
<div className="mt-0 pt-0">
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Zones</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="h-8 px-0"
variant="secondary"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
onClick={() => {
setEditPane("zone");
handleNewPolygon("zone");
}}
>
<LuPlusSquare />
<LuPlus />
</Button>
</TooltipTrigger>
<TooltipContent>Add Zone</TooltipContent>
</Tooltip>
</div>
{allPolygons
.flatMap((polygon, index) =>
@ -487,26 +515,30 @@ export default function MasksAndZones({
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
)}
<div className="flex my-2">
<Separator className="bg-secondary" />
</div>
)}
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("motion_mask" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center my-2">
selectedZoneMask.includes(
"motion_mask" as PolygonType,
)) && (
<div className="first:mt-0 mt-3 first:pt-0 pt-3 border-t-[1px] first:border-transparent border-secondary">
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Motion Masks</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="h-8 px-0"
variant="secondary"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
onClick={() => {
setEditPane("motion_mask");
handleNewPolygon("motion_mask");
}}
>
<LuPlusSquare />
<LuPlus />
</Button>
</TooltipTrigger>
<TooltipContent>Add Motion Mask</TooltipContent>
</Tooltip>
</div>
{allPolygons
.flatMap((polygon, index) =>
@ -530,26 +562,30 @@ export default function MasksAndZones({
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
)}
<div className="flex my-2">
<Separator className="bg-secondary" />
</div>
)}
{(selectedZoneMask === undefined ||
selectedZoneMask.includes("object_mask" as PolygonType)) && (
<>
<div className="flex flex-row justify-between items-center my-2">
selectedZoneMask.includes(
"object_mask" as PolygonType,
)) && (
<div className="first:mt-0 mt-3 first:pt-0 pt-3 border-t-[1px] first:border-transparent border-secondary">
<div className="flex flex-row justify-between items-center my-3">
<div className="text-md">Object Masks</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="h-8 px-0"
variant="secondary"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
onClick={() => {
setEditPane("object_mask");
handleNewPolygon("object_mask");
}}
>
<LuPlusSquare />
<LuPlus />
</Button>
</TooltipTrigger>
<TooltipContent>Add Object Mask</TooltipContent>
</Tooltip>
</div>
{allPolygons
.flatMap((polygon, index) =>
@ -573,8 +609,9 @@ export default function MasksAndZones({
handleCopyCoordinates={handleCopyCoordinates}
/>
))}
</>
</div>
)}
</div>
</>
)}
{/* <Table>
@ -661,7 +698,7 @@ export default function MasksAndZones({
ref={containerRef}
className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full"
>
<div className="size-full">
<div className="flex flex-row justify-center mx-auto size-full">
{cameraConfig &&
scaledWidth &&
scaledHeight &&
@ -677,7 +714,7 @@ export default function MasksAndZones({
selectedZoneMask={selectedZoneMask}
/>
) : (
<Skeleton className="w-full h-full" />
<Skeleton className="size-full" />
)}
</div>
</div>
@ -725,7 +762,7 @@ function PolygonItem({
{isMobile && <></>}
<div
className={`flex items-center ${
activePolygonIndex === index
hoveredPolygonIndex === index
? "text-primary"
: "text-muted-foreground"
}`}
@ -779,37 +816,52 @@ function PolygonItem({
setEditPane(polygon.type);
}}
>
<Tooltip>
<TooltipTrigger>
<LuPencil
className={`size-4 ${
activePolygonIndex === index
hoveredPolygonIndex === index
? "text-primary"
: "text-muted-foreground"
}`}
/>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
</div>
<div
className="cursor-pointer"
onClick={() => handleCopyCoordinates(index)}
>
<Tooltip>
<TooltipTrigger>
<LuCopy
className={`size-4 ${
activePolygonIndex === index
hoveredPolygonIndex === index
? "text-primary"
: "text-muted-foreground"
}`}
/>
</TooltipTrigger>
<TooltipContent>Copy coordinates</TooltipContent>
</Tooltip>
</div>
<div
className="cursor-pointer"
onClick={() => setDeleteDialogOpen(true)}
>
<LuTrash
<Tooltip>
<TooltipTrigger>
<HiTrash
className={`size-4 ${
activePolygonIndex === index
hoveredPolygonIndex === index
? "text-primary fill-primary"
: "text-muted-foreground fill-muted-foreground"
}`}
/>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</div>
</div>
)}

View File

@ -1,13 +1,4 @@
import Heading from "@/components/ui/heading";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
@ -37,13 +28,17 @@ import { Switch } from "../ui/switch";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
type MotionTunerProps = {
selectedCamera: string;
};
type MotionSettings = {
threshold?: number;
contour_area?: number;
improve_contrast?: boolean;
};
export default function MotionTuner() {
export default function MotionTuner({ selectedCamera }: MotionTunerProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
@ -60,7 +55,7 @@ export default function MotionTuner() {
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
// const [selectedCamera, setSelectedCamera] = useState(cameras[0]?.name);
const [nextSelectedCamera, setNextSelectedCamera] = useState("");
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
@ -169,11 +164,11 @@ export default function MotionTuner() {
setNextSelectedCamera(camera);
setConfirmationDialogOpen(true);
} else {
setSelectedCamera(camera);
// setSelectedCamera(camera);
setNextSelectedCamera("");
}
},
[setSelectedCamera, changedValue],
[changedValue],
);
const handleDialog = useCallback(
@ -181,12 +176,12 @@ export default function MotionTuner() {
if (save) {
saveToConfig();
}
setSelectedCamera(nextSelectedCamera);
// setSelectedCamera(nextSelectedCamera);
setNextSelectedCamera("");
setConfirmationDialogOpen(false);
setChangedValue(false);
},
[saveToConfig, setSelectedCamera, nextSelectedCamera],
[saveToConfig],
);
if (!cameraConfig && !selectedCamera) {
@ -194,41 +189,14 @@ export default function MotionTuner() {
}
return (
<>
<Heading as="h2">Motion Detection Tuner</Heading>
<Toaster />
<div className="flex items-center space-x-2 mt-5">
<Select
value={selectedCamera}
onValueChange={handleSelectedCameraChange}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Camera" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Choose a camera</SelectLabel>
{cameras.map((camera) => (
<SelectItem
key={camera.name}
value={camera.name}
className="capitalize"
>
{camera.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
{cameraConfig ? (
<div className="flex flex-col justify-start">
<AutoUpdatingCameraImage
camera={cameraConfig.name}
searchParams={new URLSearchParams([["motion", "1"]])}
className="w-[50%]"
/>
<div className="flex flex-row justify-evenly w-full">
<div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" />
<div className="flex flex-col w-full overflow-y-auto mt-2 md:mt-0 md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
<Heading as="h3" className="my-2">
Motion Detection Tuner
</Heading>
<div className="flex flex-col w-full space-y-10">
<div className="flex flex-row mb-5">
<Slider
id="motion-threshold"
@ -312,9 +280,22 @@ export default function MotionTuner() {
</AlertDialog>
)}
</div>
{cameraConfig ? (
<div className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full">
<div className="size-full min-h-10">
<AutoUpdatingCameraImage
camera={cameraConfig.name}
searchParams={new URLSearchParams([["motion", "1"]])}
showFps={false}
className="size-full"
cameraClasses="relative w-full h-full flex flex-col justify-start"
/>
</div>
</div>
) : (
<Skeleton className="size-full rounded-2xl" />
)}
</>
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { useMemo, useRef, useState, useEffect } from "react";
import PolygonDrawer from "./PolygonDrawer";
import { Stage, Layer, Image, Text } from "react-konva";
import { Stage, Layer, Image, Text, Circle } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { Polygon, PolygonType } from "@/types/canvas";
@ -216,23 +216,43 @@ export function PolygonCanvas({
isHovered={index === hoveredPolygonIndex}
isFinished={polygon.isFinished}
color={polygon.color}
name={polygon.name}
handlePointDragMove={handlePointDragMove}
handleGroupDragEnd={handleGroupDragEnd}
handleMouseOverStartPoint={handleMouseOverStartPoint}
handleMouseOutStartPoint={handleMouseOutStartPoint}
/>
{index === hoveredPolygonIndex && (
<>
<Circle
x={
getAveragePoint(flattenPoints(polygon.points)).x //-
//(polygon.name.length * 16 * 0.6) / 2
}
y={
getAveragePoint(flattenPoints(polygon.points)).y //-
//16 / 2
}
radius={2}
fill="red"
/>
<Text
text={polygon.name}
align="left"
verticalAlign="top"
// align="left"
// verticalAlign="top"
align="center"
verticalAlign="middle"
x={
getAveragePoint(flattenPoints(polygon.points)).x
// - (polygon.name.length * 16 * 0.6) / 2
getAveragePoint(flattenPoints(polygon.points)).x //-
//(polygon.name.length * 16 * 0.6) / 2
}
y={
getAveragePoint(flattenPoints(polygon.points)).y //-
//16 / 2
}
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
fontSize={16}
/>
</>
)}
</React.Fragment>
),

View File

@ -1,6 +1,11 @@
import { useCallback, useState } from "react";
import { Line, Circle, Group } from "react-konva";
import { minMax, toRGBColorString, dragBoundFunc } from "@/utils/canvasUtil";
import { useCallback, useRef, useState } from "react";
import { Line, Circle, Group, Text } from "react-konva";
import {
minMax,
toRGBColorString,
dragBoundFunc,
getAveragePoint,
} from "@/utils/canvasUtil";
import type { KonvaEventObject } from "konva/lib/Node";
import Konva from "konva";
import { Vector2d } from "konva/lib/types";
@ -12,6 +17,7 @@ type PolygonDrawerProps = {
isHovered: boolean;
isFinished: boolean;
color: number[];
name: string;
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
handleMouseOverStartPoint: (
@ -28,6 +34,7 @@ export default function PolygonDrawer({
isActive,
isHovered,
isFinished,
name,
color,
handlePointDragMove,
handleGroupDragEnd,
@ -38,6 +45,7 @@ export default function PolygonDrawer({
const [stage, setStage] = useState<Konva.Stage>();
const [minMaxX, setMinMaxX] = useState([0, 0]);
const [minMaxY, setMinMaxY] = useState([0, 0]);
const groupRef = useRef<Konva.Group>(null);
const handleGroupMouseOver = (
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
@ -85,9 +93,12 @@ export default function PolygonDrawer({
[color],
);
// console.log(groupRef.current?.height());
return (
<Group
name="polygon"
ref={groupRef}
draggable={isActive && isFinished}
onDragStart={isActive ? handleGroupDragStart : undefined}
onDragEnd={isActive ? handleGroupDragEnd : undefined}
@ -145,6 +156,26 @@ export default function PolygonDrawer({
/>
);
})}
{groupRef.current && (
<Text
text={name}
// align="left"
// verticalAlign="top"
width={groupRef.current.width()}
height={groupRef.current.height()}
align="center"
verticalAlign="middle"
x={
getAveragePoint(flattenedPoints).x //-
//(polygon.name.length * 16 * 0.6) / 2
}
y={
getAveragePoint(flattenedPoints).y //-
//16 / 2
}
fontSize={16}
/>
)}
</Group>
);
}

View File

@ -1,10 +1,12 @@
import { Polygon } from "@/types/canvas";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
import { Button } from "../ui/button";
type PolygonEditControlsProps = {
polygons: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex: number | null;
activePolygonIndex: number | undefined;
};
export default function PolygonEditControls({
@ -13,39 +15,61 @@ export default function PolygonEditControls({
activePolygonIndex,
}: PolygonEditControlsProps) {
const undo = () => {
if (activePolygonIndex !== null && polygons) {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (activePolygon.points.length > 0) {
updatedPolygons[activePolygonIndex] = {
...activePolygon,
points: activePolygon.points.slice(0, -1),
points: [...activePolygon.points.slice(0, -1)],
isFinished: false,
};
setPolygons(updatedPolygons);
}
}
};
const reset = () => {
if (activePolygonIndex !== null) {
if (activePolygonIndex === undefined || !polygons) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
updatedPolygons[activePolygonIndex] = {
...updatedPolygons[activePolygonIndex],
...activePolygon,
points: [],
isFinished: false,
};
setPolygons(updatedPolygons);
}
};
return (
<div className="flex">
<Button className="mr-5" variant="secondary" onClick={undo}>
Undo
<div className="flex flex-row justify-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
onClick={undo}
>
<MdUndo />
</Button>
<Button variant="secondary" onClick={reset}>
Reset
</TooltipTrigger>
<TooltipContent>Undo</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
onClick={reset}
>
<MdOutlineRestartAlt />
</Button>
</TooltipTrigger>
<TooltipContent>Reset</TooltipContent>
</Tooltip>
</div>
);
}

View File

@ -21,9 +21,11 @@ import { z } from "zod";
import { Polygon } from "@/types/canvas";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import PolygonEditControls from "./PolygonEditControls";
type ZoneEditPaneProps = {
polygons?: Polygon[];
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
activePolygonIndex?: number;
onSave?: () => void;
onCancel?: () => void;
@ -31,6 +33,7 @@ type ZoneEditPaneProps = {
export function ZoneEditPane({
polygons,
setPolygons,
activePolygonIndex,
onSave,
onCancel,
@ -133,9 +136,25 @@ export function ZoneEditPane({
<Heading as="h3" className="my-2">
Zone
</Heading>
<div className="flex my-3">
<Separator className="bg-secondary" />
<Separator className="my-3 bg-secondary" />
{polygons && activePolygonIndex !== undefined && (
<div className="flex flex-row my-2 text-sm w-full justify-between">
<div className="my-1">
{polygons[activePolygonIndex].points.length} points
</div>
{polygons[activePolygonIndex].isFinished ? <></> : <></>}
<PolygonEditControls
polygons={polygons}
setPolygons={setPolygons}
activePolygonIndex={activePolygonIndex}
/>
</div>
)}
<div className="mb-3 text-sm text-muted-foreground">
Click to draw a polygon on the image.
</div>
<Separator className="my-3 bg-secondary" />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@ -146,8 +165,16 @@ export function ZoneEditPane({
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Enter a name..." {...field} />
<Input
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="Enter a name..."
{...field}
/>
</FormControl>
<FormDescription>
Name must be at least 2 characters and must not be the name of
a camera or another zone.
</FormDescription>
<FormMessage />
</FormItem>
)}
@ -162,7 +189,11 @@ export function ZoneEditPane({
<FormItem>
<FormLabel>Inertia</FormLabel>
<FormControl>
<Input placeholder="3" {...field} />
<Input
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="3"
{...field}
/>
</FormControl>
<FormDescription>
Specifies how many frames that an object must be in a zone
@ -182,7 +213,11 @@ export function ZoneEditPane({
<FormItem>
<FormLabel>Loitering Time</FormLabel>
<FormControl>
<Input placeholder="0" {...field} />
<Input
className="w-full p-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0"
{...field}
/>
</FormControl>
<FormDescription>
Sets a minimum amount of time in seconds that the object must

View File

@ -86,7 +86,7 @@ export default function Settings() {
page == "masks / zones" ||
page == "motion tuner") && (
<div className="flex items-center gap-2">
{!isEditing && (
{page == "masks / zones" && (
<ZoneMaskFilterButton
selectedZoneMask={filterZoneMask}
updateZoneMaskFilter={setFilterZoneMask}
@ -101,7 +101,7 @@ export default function Settings() {
</div>
)}
</div>
<div className="mt-2 flex flex-col items-start w-full h-dvh md:pb-24">
<div className="mt-2 flex flex-col items-start w-full h-full md:h-dvh pb-9 md:pb-24">
{page == "general" && <General />}
{page == "objects" && <></>}
{page == "masks / zones" && (
@ -114,7 +114,9 @@ export default function Settings() {
setUnsavedChanges={setUnsavedChanges}
/>
)}
{page == "motion tuner" && <MotionTuner />}
{page == "motion tuner" && (
<MotionTuner selectedCamera={selectedCamera} />
)}
</div>
</div>
);

View File

@ -133,8 +133,8 @@
--secondary-highlight: hsl(0, 0%, 25%);
--secondary-highlight: 0 0% 25%;
--muted: hsl(0, 0%, 8%);
--muted: 0 0% 8%;
--muted: hsl(0, 0%, 15%);
--muted: 0 0% 15%;
--muted-foreground: hsl(0, 0%, 32%);
--muted-foreground: 0 0% 32%;