diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx index 2f2005d9c..28ad3b883 100644 --- a/web/src/components/camera/AutoUpdatingCameraImage.tsx +++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -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 ? Displaying at {fps}fps : null} diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index be978f7a5..1f2c28ade 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -36,12 +36,7 @@ export default function CameraImage({ }, [apiHost, name, imgRef, searchParams, config]); return ( -
+
{enabled ? (
diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/components/settings/MasksAndZones.tsx index 326eb444d..2657812f9 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/components/settings/MasksAndZones.tsx @@ -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 && (
-
+
{editPane == "zone" && ( - {(selectedZoneMask === undefined || - selectedZoneMask.includes("zone" as PolygonType)) && ( - <> -
-
Zones
- + + Masks / Zones + +
+ {(selectedZoneMask === undefined || + selectedZoneMask.includes("zone" as PolygonType)) && ( +
+
+
Zones
+ + + + + Add Zone + +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "zone" ? [{ polygon, index }] : [], + ) + .map(({ polygon, index }) => ( + + ))}
- {allPolygons - .flatMap((polygon, index) => - polygon.type === "zone" ? [{ polygon, index }] : [], - ) - .map(({ polygon, index }) => ( - - ))} - - )} -
- + )} + {(selectedZoneMask === undefined || + selectedZoneMask.includes( + "motion_mask" as PolygonType, + )) && ( +
+
+
Motion Masks
+ + + + + Add Motion Mask + +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "motion_mask" + ? [{ polygon, index }] + : [], + ) + .map(({ polygon, index }) => ( + + ))} +
+ )} + {(selectedZoneMask === undefined || + selectedZoneMask.includes( + "object_mask" as PolygonType, + )) && ( +
+
+
Object Masks
+ + + + + Add Object Mask + +
+ {allPolygons + .flatMap((polygon, index) => + polygon.type === "object_mask" + ? [{ polygon, index }] + : [], + ) + .map(({ polygon, index }) => ( + + ))} +
+ )}
- {(selectedZoneMask === undefined || - selectedZoneMask.includes("motion_mask" as PolygonType)) && ( - <> -
-
Motion Masks
- -
- {allPolygons - .flatMap((polygon, index) => - polygon.type === "motion_mask" - ? [{ polygon, index }] - : [], - ) - .map(({ polygon, index }) => ( - - ))} - - )} -
- -
- {(selectedZoneMask === undefined || - selectedZoneMask.includes("object_mask" as PolygonType)) && ( - <> -
-
Object Masks
- -
- {allPolygons - .flatMap((polygon, index) => - polygon.type === "object_mask" - ? [{ polygon, index }] - : [], - ) - .map(({ polygon, index }) => ( - - ))} - - )} )} {/* @@ -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" > -
+
{cameraConfig && scaledWidth && scaledHeight && @@ -677,7 +714,7 @@ export default function MasksAndZones({ selectedZoneMask={selectedZoneMask} /> ) : ( - + )}
@@ -725,7 +762,7 @@ function PolygonItem({ {isMobile && <>}
- + + + + + Edit +
handleCopyCoordinates(index)} > - + + + + + Copy coordinates +
setDeleteDialogOpen(true)} > - + + + + + Delete +
)} diff --git a/web/src/components/settings/MotionTuner.tsx b/web/src/components/settings/MotionTuner.tsx index e1af979cb..d7af8aac6 100644 --- a/web/src/components/settings/MotionTuner.tsx +++ b/web/src/components/settings/MotionTuner.tsx @@ -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("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 ( - <> - Motion Detection Tuner - -
- -
- {cameraConfig ? ( -
- -
-
- { - handleMotionConfigChange({ threshold: value[0] }); - }} - /> - -
-
- { - handleMotionConfigChange({ contour_area: value[0] }); - }} - /> - -
-
- { - handleMotionConfigChange({ improve_contrast: isChecked }); - }} - /> - -
+
+ +
+ + Motion Detection Tuner + -
- -
+
+
+ { + handleMotionConfigChange({ threshold: value[0] }); + }} + /> +
- {confirmationDialogOpen && ( - setConfirmationDialogOpen(false)} +
+ { + handleMotionConfigChange({ contour_area: value[0] }); + }} + /> + +
+
+ { + handleMotionConfigChange({ improve_contrast: isChecked }); + }} + /> + +
+ +
+ +
+
+ {confirmationDialogOpen && ( + setConfirmationDialogOpen(false)} + > + + + + You have unsaved changes on this camera. + + + Do you want to save your changes before continuing? + + + + handleDialog(false)}> + Cancel + + handleDialog(true)}> + Save + + + + + )} +
+ + {cameraConfig ? ( +
+
+ +
) : ( )} - +
); } diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index c29558e8f..f6d49a681 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -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 && ( - + <> + + + )} ), diff --git a/web/src/components/settings/PolygonDrawer.tsx b/web/src/components/settings/PolygonDrawer.tsx index 708dbbc22..7a9294cf3 100644 --- a/web/src/components/settings/PolygonDrawer.tsx +++ b/web/src/components/settings/PolygonDrawer.tsx @@ -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) => void; handleGroupDragEnd: (e: KonvaEventObject) => 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(); const [minMaxX, setMinMaxX] = useState([0, 0]); const [minMaxY, setMinMaxY] = useState([0, 0]); + const groupRef = useRef(null); const handleGroupMouseOver = ( e: Konva.KonvaEventObject, @@ -85,9 +93,12 @@ export default function PolygonDrawer({ [color], ); + // console.log(groupRef.current?.height()); + return ( ); })} + {groupRef.current && ( + + )} ); } diff --git a/web/src/components/settings/PolygonEditControls.tsx b/web/src/components/settings/PolygonEditControls.tsx index ed707aa74..88a859737 100644 --- a/web/src/components/settings/PolygonEditControls.tsx +++ b/web/src/components/settings/PolygonEditControls.tsx @@ -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>; - 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 ( -
- - +
+ + + + + Undo + + + + + + Reset +
); } diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 23167701a..03342ea4c 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -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>; 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({ Zone -
- + + {polygons && activePolygonIndex !== undefined && ( +
+
+ {polygons[activePolygonIndex].points.length} points +
+ {polygons[activePolygonIndex].isFinished ? <> : <>} + +
+ )} +
+ Click to draw a polygon on the image.
+ +
Name - + + + Name must be at least 2 characters and must not be the name of + a camera or another zone. + )} @@ -162,7 +189,11 @@ export function ZoneEditPane({ Inertia - + Specifies how many frames that an object must be in a zone @@ -182,7 +213,11 @@ export function ZoneEditPane({ Loitering Time - + Sets a minimum amount of time in seconds that the object must diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index c453e1a12..ff4193eaa 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -86,7 +86,7 @@ export default function Settings() { page == "masks / zones" || page == "motion tuner") && (
- {!isEditing && ( + {page == "masks / zones" && ( )}
-
+
{page == "general" && } {page == "objects" && <>} {page == "masks / zones" && ( @@ -114,7 +114,9 @@ export default function Settings() { setUnsavedChanges={setUnsavedChanges} /> )} - {page == "motion tuner" && } + {page == "motion tuner" && ( + + )}
); diff --git a/web/themes/theme-default.css b/web/themes/theme-default.css index 07b924956..fd45207fd 100644 --- a/web/themes/theme-default.css +++ b/web/themes/theme-default.css @@ -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%;