mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +03:00
motion tuner, edit controls, tooltips
This commit is contained in:
parent
8deec1c9b6
commit
6ae3e24157
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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,130 +470,148 @@ export default function MasksAndZones({
|
||||
)}
|
||||
{editPane === undefined && (
|
||||
<>
|
||||
{(selectedZoneMask === undefined ||
|
||||
selectedZoneMask.includes("zone" as PolygonType)) && (
|
||||
<>
|
||||
<div className="flex flex-row justify-between items-center my-2">
|
||||
<div className="text-md">Zones</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-0"
|
||||
onClick={() => {
|
||||
setEditPane("zone");
|
||||
handleNewPolygon("zone");
|
||||
}}
|
||||
>
|
||||
<LuPlusSquare />
|
||||
</Button>
|
||||
<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="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="secondary"
|
||||
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||
onClick={() => {
|
||||
setEditPane("zone");
|
||||
handleNewPolygon("zone");
|
||||
}}
|
||||
>
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Zone</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "zone" ? [{ polygon, index }] : [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "zone" ? [{ polygon, index }] : [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<div className="flex my-2">
|
||||
<Separator className="bg-secondary" />
|
||||
)}
|
||||
{(selectedZoneMask === undefined ||
|
||||
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="secondary"
|
||||
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||
onClick={() => {
|
||||
setEditPane("motion_mask");
|
||||
handleNewPolygon("motion_mask");
|
||||
}}
|
||||
>
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Motion Mask</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "motion_mask"
|
||||
? [{ polygon, index }]
|
||||
: [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(selectedZoneMask === undefined ||
|
||||
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="secondary"
|
||||
className="size-6 p-1 rounded-md text-background bg-secondary-foreground"
|
||||
onClick={() => {
|
||||
setEditPane("object_mask");
|
||||
handleNewPolygon("object_mask");
|
||||
}}
|
||||
>
|
||||
<LuPlus />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add Object Mask</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "object_mask"
|
||||
? [{ polygon, index }]
|
||||
: [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(selectedZoneMask === undefined ||
|
||||
selectedZoneMask.includes("motion_mask" as PolygonType)) && (
|
||||
<>
|
||||
<div className="flex flex-row justify-between items-center my-2">
|
||||
<div className="text-md">Motion Masks</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-0"
|
||||
onClick={() => {
|
||||
setEditPane("motion_mask");
|
||||
handleNewPolygon("motion_mask");
|
||||
}}
|
||||
>
|
||||
<LuPlusSquare />
|
||||
</Button>
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "motion_mask"
|
||||
? [{ polygon, index }]
|
||||
: [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
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">
|
||||
<div className="text-md">Object Masks</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-0"
|
||||
onClick={() => {
|
||||
setEditPane("object_mask");
|
||||
handleNewPolygon("object_mask");
|
||||
}}
|
||||
>
|
||||
<LuPlusSquare />
|
||||
</Button>
|
||||
</div>
|
||||
{allPolygons
|
||||
.flatMap((polygon, index) =>
|
||||
polygon.type === "object_mask"
|
||||
? [{ polygon, index }]
|
||||
: [],
|
||||
)
|
||||
.map(({ polygon, index }) => (
|
||||
<PolygonItem
|
||||
key={index}
|
||||
polygon={polygon}
|
||||
index={index}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
hoveredPolygonIndex={hoveredPolygonIndex}
|
||||
setHoveredPolygonIndex={setHoveredPolygonIndex}
|
||||
deleteDialogOpen={deleteDialogOpen}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
setAllPolygons={setAllPolygons}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* <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);
|
||||
}}
|
||||
>
|
||||
<LuPencil
|
||||
className={`size-4 ${
|
||||
activePolygonIndex === index
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuPencil
|
||||
className={`size-4 ${
|
||||
hoveredPolygonIndex === index
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Edit</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleCopyCoordinates(index)}
|
||||
>
|
||||
<LuCopy
|
||||
className={`size-4 ${
|
||||
activePolygonIndex === index
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuCopy
|
||||
className={`size-4 ${
|
||||
hoveredPolygonIndex === index
|
||||
? "text-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy coordinates</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<LuTrash
|
||||
className={`size-4 ${
|
||||
activePolygonIndex === index
|
||||
? "text-primary fill-primary"
|
||||
: "text-muted-foreground fill-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HiTrash
|
||||
className={`size-4 ${
|
||||
hoveredPolygonIndex === index
|
||||
? "text-primary fill-primary"
|
||||
: "text-muted-foreground fill-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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,127 +189,113 @@ 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-row mb-5">
|
||||
<Slider
|
||||
id="motion-threshold"
|
||||
className="w-[300px]"
|
||||
disabled={motionSettings.threshold === undefined}
|
||||
value={[motionSettings.threshold ?? 0]}
|
||||
min={10}
|
||||
max={80}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
handleMotionConfigChange({ threshold: value[0] });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="motion-threshold" className="px-2">
|
||||
Threshold: {motionSettings.threshold}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Slider
|
||||
id="motion-contour-area"
|
||||
className="w-[300px]"
|
||||
disabled={motionSettings.contour_area === undefined}
|
||||
value={[motionSettings.contour_area ?? 0]}
|
||||
min={10}
|
||||
max={200}
|
||||
step={5}
|
||||
onValueChange={(value) => {
|
||||
handleMotionConfigChange({ contour_area: value[0] });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="motion-contour-area" className="px-2">
|
||||
Contour Area: {motionSettings.contour_area}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Switch
|
||||
id="improve-contrast"
|
||||
disabled={motionSettings.improve_contrast === undefined}
|
||||
checked={motionSettings.improve_contrast === true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
handleMotionConfigChange({ improve_contrast: isChecked });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="improve-contrast">Improve Contrast</Label>
|
||||
</div>
|
||||
<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">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isLoading ? "ghost" : "select"}
|
||||
disabled={!changedValue || isLoading}
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save to Config"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col w-full space-y-10">
|
||||
<div className="flex flex-row mb-5">
|
||||
<Slider
|
||||
id="motion-threshold"
|
||||
className="w-[300px]"
|
||||
disabled={motionSettings.threshold === undefined}
|
||||
value={[motionSettings.threshold ?? 0]}
|
||||
min={10}
|
||||
max={80}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
handleMotionConfigChange({ threshold: value[0] });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="motion-threshold" className="px-2">
|
||||
Threshold: {motionSettings.threshold}
|
||||
</Label>
|
||||
</div>
|
||||
{confirmationDialogOpen && (
|
||||
<AlertDialog
|
||||
open={confirmationDialogOpen}
|
||||
onOpenChange={() => setConfirmationDialogOpen(false)}
|
||||
<div className="flex flex-row">
|
||||
<Slider
|
||||
id="motion-contour-area"
|
||||
className="w-[300px]"
|
||||
disabled={motionSettings.contour_area === undefined}
|
||||
value={[motionSettings.contour_area ?? 0]}
|
||||
min={10}
|
||||
max={200}
|
||||
step={5}
|
||||
onValueChange={(value) => {
|
||||
handleMotionConfigChange({ contour_area: value[0] });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="motion-contour-area" className="px-2">
|
||||
Contour Area: {motionSettings.contour_area}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Switch
|
||||
id="improve-contrast"
|
||||
disabled={motionSettings.improve_contrast === undefined}
|
||||
checked={motionSettings.improve_contrast === true}
|
||||
onCheckedChange={(isChecked) => {
|
||||
handleMotionConfigChange({ improve_contrast: isChecked });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="improve-contrast">Improve Contrast</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isLoading ? "ghost" : "select"}
|
||||
disabled={!changedValue || isLoading}
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
You have unsaved changes on this camera.
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to save your changes before continuing?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => handleDialog(false)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => handleDialog(true)}>
|
||||
Save
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{isLoading ? "Saving..." : "Save to Config"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{confirmationDialogOpen && (
|
||||
<AlertDialog
|
||||
open={confirmationDialogOpen}
|
||||
onOpenChange={() => setConfirmationDialogOpen(false)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
You have unsaved changes on this camera.
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to save your changes before continuing?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => handleDialog(false)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => handleDialog(true)}>
|
||||
Save
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
<Text
|
||||
text={polygon.name}
|
||||
align="left"
|
||||
verticalAlign="top"
|
||||
x={
|
||||
getAveragePoint(flattenPoints(polygon.points)).x
|
||||
// - (polygon.name.length * 16 * 0.6) / 2
|
||||
}
|
||||
y={getAveragePoint(flattenPoints(polygon.points)).y} //- 16 / 2}
|
||||
fontSize={16}
|
||||
/>
|
||||
<>
|
||||
<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="center"
|
||||
verticalAlign="middle"
|
||||
x={
|
||||
getAveragePoint(flattenPoints(polygon.points)).x //-
|
||||
//(polygon.name.length * 16 * 0.6) / 2
|
||||
}
|
||||
y={
|
||||
getAveragePoint(flattenPoints(polygon.points)).y //-
|
||||
//16 / 2
|
||||
}
|
||||
fontSize={16}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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);
|
||||
}
|
||||
if (activePolygonIndex === undefined || !polygons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPolygons = [...polygons];
|
||||
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||
updatedPolygons[activePolygonIndex] = {
|
||||
...activePolygon,
|
||||
points: [...activePolygon.points.slice(0, -1)],
|
||||
isFinished: false,
|
||||
};
|
||||
setPolygons(updatedPolygons);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (activePolygonIndex !== null) {
|
||||
const updatedPolygons = [...polygons];
|
||||
updatedPolygons[activePolygonIndex] = {
|
||||
...updatedPolygons[activePolygonIndex],
|
||||
points: [],
|
||||
};
|
||||
setPolygons(updatedPolygons);
|
||||
if (activePolygonIndex === undefined || !polygons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPolygons = [...polygons];
|
||||
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||
updatedPolygons[activePolygonIndex] = {
|
||||
...activePolygon,
|
||||
points: [],
|
||||
isFinished: false,
|
||||
};
|
||||
setPolygons(updatedPolygons);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button className="mr-5" variant="secondary" onClick={undo}>
|
||||
Undo
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={reset}>
|
||||
Reset
|
||||
</Button>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,10 +136,26 @@ 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">
|
||||
<FormField
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user