mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
initial working konva
This commit is contained in:
parent
392ff1319d
commit
7556d08e1c
93
web/package-lock.json
generated
93
web/package-lock.json
generated
@ -37,6 +37,7 @@
|
|||||||
"hls.js": "^1.5.8",
|
"hls.js": "^1.5.8",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
|
"konva": "^9.3.6",
|
||||||
"lucide-react": "^0.368.0",
|
"lucide-react": "^0.368.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -47,6 +48,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.3",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
|
"react-konva": "^18.2.10",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
"react-tracked": "^1.7.14",
|
"react-tracked": "^1.7.14",
|
||||||
@ -2518,8 +2520,7 @@
|
|||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.11",
|
"version": "15.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.78",
|
"version": "18.2.78",
|
||||||
@ -2550,6 +2551,14 @@
|
|||||||
"react-icons": "*"
|
"react-icons": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-reconciler": {
|
||||||
|
"version": "0.28.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz",
|
||||||
|
"integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-transition-group": {
|
"node_modules/@types/react-transition-group": {
|
||||||
"version": "4.4.10",
|
"version": "4.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
|
||||||
@ -2559,6 +2568,11 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/scheduler": {
|
||||||
|
"version": "0.16.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||||
|
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
|
||||||
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.6",
|
"version": "7.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||||
@ -5046,6 +5060,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/its-fine": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-mncCA+yb6tuh5zK26cHqKlsSyxm4zdm4YgJpxycyx6p9fgxgK5PLu3iDVpKhzTn57Yrv3jk/r0aK0RFTT1OjFw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-reconciler": "^0.28.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-diff": {
|
"node_modules/jest-diff": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
|
||||||
@ -5177,6 +5202,25 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/konva": {
|
||||||
|
"version": "9.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/konva/-/konva-9.3.6.tgz",
|
||||||
|
"integrity": "sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@ -6289,6 +6333,51 @@
|
|||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-konva": {
|
||||||
|
"version": "18.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz",
|
||||||
|
"integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react-reconciler": "^0.28.2",
|
||||||
|
"its-fine": "^1.1.1",
|
||||||
|
"react-reconciler": "~0.29.0",
|
||||||
|
"scheduler": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"konva": "^8.0.1 || ^7.2.5 || ^9.0.0",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-dom": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-reconciler": {
|
||||||
|
"version": "0.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz",
|
||||||
|
"integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"hls.js": "^1.5.8",
|
"hls.js": "^1.5.8",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
|
"konva": "^9.3.6",
|
||||||
"lucide-react": "^0.368.0",
|
"lucide-react": "^0.368.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -52,6 +53,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.3",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
|
"react-konva": "^18.2.10",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
"react-tracked": "^1.7.14",
|
"react-tracked": "^1.7.14",
|
||||||
|
|||||||
BIN
web/public/space_landscape.jpg
Normal file
BIN
web/public/space_landscape.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@ -206,3 +206,31 @@ export function useAudioActivity(camera: string): { payload: number } {
|
|||||||
} = useWs(`${camera}/audio/rms`, "");
|
} = useWs(`${camera}/audio/rms`, "");
|
||||||
return { payload: payload as number };
|
return { payload: payload as number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMotionThreshold(camera: string): {
|
||||||
|
payload: string;
|
||||||
|
send: (payload: number, retain?: boolean) => void;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
value: { payload },
|
||||||
|
send,
|
||||||
|
} = useWs(
|
||||||
|
`${camera}/motion_threshold/state`,
|
||||||
|
`${camera}/motion_threshold/set`,
|
||||||
|
);
|
||||||
|
return { payload: payload as string, send };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMotionContourArea(camera: string): {
|
||||||
|
payload: string;
|
||||||
|
send: (payload: number, retain?: boolean) => void;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
value: { payload },
|
||||||
|
send,
|
||||||
|
} = useWs(
|
||||||
|
`${camera}/motion_contour_area/state`,
|
||||||
|
`${camera}/motion_contour_area/set`,
|
||||||
|
);
|
||||||
|
return { payload: payload as string, send };
|
||||||
|
}
|
||||||
|
|||||||
197
web/src/components/settings/DebugCanvas.tsx
Normal file
197
web/src/components/settings/DebugCanvas.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import React, { useMemo, useRef, useState, useEffect } from "react";
|
||||||
|
import PolygonDrawer from "./PolygonDrawer";
|
||||||
|
import { Stage, Layer, Image } from "react-konva";
|
||||||
|
import Konva from "konva";
|
||||||
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
const videoSource = "./space_landscape.jpg";
|
||||||
|
|
||||||
|
const wrapperStyle: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginTop: 20,
|
||||||
|
backgroundColor: "aliceblue",
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnStyle: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 20,
|
||||||
|
backgroundColor: "aliceblue",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DebugCanvas() {
|
||||||
|
const [image, setImage] = useState<HTMLImageElement | undefined>();
|
||||||
|
const imageRef = useRef<Konva.Image | null>(null);
|
||||||
|
const dataRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
|
const [points, setPoints] = useState<number[][]>([]);
|
||||||
|
const [size, setSize] = useState<{ width: number; height: number }>({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
const [flattenedPoints, setFlattenedPoints] = useState<number[]>([]);
|
||||||
|
const [position, setPosition] = useState([0, 0]);
|
||||||
|
const [isMouseOverPoint, setMouseOverPoint] = useState(false);
|
||||||
|
const [isPolyComplete, setPolyComplete] = useState(false);
|
||||||
|
|
||||||
|
const videoElement = useMemo(() => {
|
||||||
|
const element = new window.Image();
|
||||||
|
element.width = 650;
|
||||||
|
element.height = 302;
|
||||||
|
element.src = videoSource;
|
||||||
|
return element;
|
||||||
|
}, []); // No dependency needed here since videoSource is a constant
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onload = function () {
|
||||||
|
setSize({
|
||||||
|
width: videoElement.width,
|
||||||
|
height: videoElement.height,
|
||||||
|
});
|
||||||
|
setImage(videoElement);
|
||||||
|
if (imageRef.current) imageRef.current = videoElement;
|
||||||
|
};
|
||||||
|
videoElement.addEventListener("load", onload);
|
||||||
|
return () => {
|
||||||
|
videoElement.removeEventListener("load", onload);
|
||||||
|
};
|
||||||
|
}, [videoElement]);
|
||||||
|
|
||||||
|
const getMousePos = (stage: Konva.Stage) => {
|
||||||
|
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (isPolyComplete) return;
|
||||||
|
const stage = e.target.getStage()!;
|
||||||
|
const mousePos = getMousePos(stage);
|
||||||
|
if (isMouseOverPoint && points.length >= 3) {
|
||||||
|
setPolyComplete(true);
|
||||||
|
} else {
|
||||||
|
setPoints([...points, mousePos]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const stage = e.target.getStage()!;
|
||||||
|
const mousePos = getMousePos(stage);
|
||||||
|
setPosition(mousePos);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (isPolyComplete || points.length < 3) return;
|
||||||
|
e.currentTarget.scale({ x: 3, y: 3 });
|
||||||
|
setMouseOverPoint(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
e.currentTarget.scale({ x: 1, y: 1 });
|
||||||
|
setMouseOverPoint(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
if (stage) {
|
||||||
|
const index = e.target.index - 1;
|
||||||
|
const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
|
||||||
|
if (pos[0] < 0) pos[0] = 0;
|
||||||
|
if (pos[1] < 0) pos[1] = 0;
|
||||||
|
if (pos[0] > stage.width()) pos[0] = stage.width();
|
||||||
|
if (pos[1] > stage.height()) pos[1] = stage.height();
|
||||||
|
setPoints([...points.slice(0, index), pos, ...points.slice(index + 1)]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFlattenedPoints(
|
||||||
|
points
|
||||||
|
.concat(isPolyComplete ? [] : position)
|
||||||
|
.reduce((a, b) => a.concat(b), []),
|
||||||
|
);
|
||||||
|
}, [points, isPolyComplete, position]);
|
||||||
|
|
||||||
|
const undo = () => {
|
||||||
|
setPoints(points.slice(0, -1));
|
||||||
|
setPolyComplete(false);
|
||||||
|
setPosition(points[points.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setPoints([]);
|
||||||
|
setPolyComplete(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (e.target.name() === "polygon") {
|
||||||
|
const result: number[][] = [];
|
||||||
|
const copyPoints = [...points];
|
||||||
|
copyPoints.map((point) =>
|
||||||
|
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
|
||||||
|
);
|
||||||
|
e.target.position({ x: 0, y: 0 });
|
||||||
|
setPoints(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={wrapperStyle}>
|
||||||
|
<div style={columnStyle}>
|
||||||
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
|
width={size.width || 650}
|
||||||
|
height={size.height || 302}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<Layer>
|
||||||
|
<Image
|
||||||
|
ref={imageRef}
|
||||||
|
image={image}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={size.width}
|
||||||
|
height={size.height}
|
||||||
|
/>
|
||||||
|
<PolygonDrawer
|
||||||
|
points={points}
|
||||||
|
flattenedPoints={flattenedPoints}
|
||||||
|
handlePointDragMove={handlePointDragMove}
|
||||||
|
handleGroupDragEnd={handleGroupDragEnd}
|
||||||
|
handleMouseOverStartPoint={handleMouseOverStartPoint}
|
||||||
|
handleMouseOutStartPoint={handleMouseOutStartPoint}
|
||||||
|
isFinished={isPolyComplete}
|
||||||
|
/>
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button name="Undo" onClick={undo} />
|
||||||
|
<Button name="Reset" onClick={reset} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={dataRef}
|
||||||
|
style={{
|
||||||
|
width: 375,
|
||||||
|
height: 302,
|
||||||
|
boxShadow: ".5px .5px 5px .4em rgba(0,0,0,.1)",
|
||||||
|
marginTop: 20,
|
||||||
|
color: "black",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(points)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DebugCanvas;
|
||||||
141
web/src/components/settings/MotionTuner.tsx
Normal file
141
web/src/components/settings/MotionTuner.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useMotionContourArea, useMotionThreshold } from "@/api/ws";
|
||||||
|
|
||||||
|
export default function MotionTuner() {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const cameras = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(config.cameras)
|
||||||
|
.filter((conf) => conf.ui.dashboard && conf.enabled)
|
||||||
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const [selectedCamera, setSelectedCamera] = useState(cameras[0].name);
|
||||||
|
|
||||||
|
const cameraConfig = useMemo(() => {
|
||||||
|
if (config && selectedCamera) {
|
||||||
|
return config.cameras[selectedCamera];
|
||||||
|
}
|
||||||
|
}, [config, selectedCamera]);
|
||||||
|
|
||||||
|
const motionThreshold = useMemo(() => {
|
||||||
|
return cameraConfig?.motion.threshold ?? 0;
|
||||||
|
}, [cameraConfig?.motion.threshold]);
|
||||||
|
|
||||||
|
const motionContourArea = useMemo(
|
||||||
|
() => cameraConfig?.motion.contour_area ?? 0,
|
||||||
|
[cameraConfig?.motion.contour_area],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
|
||||||
|
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
|
||||||
|
|
||||||
|
const setMotionThreshold = useCallback(
|
||||||
|
(threshold: number) => {
|
||||||
|
if (cameraConfig && threshold != motionThreshold) {
|
||||||
|
cameraConfig.motion.threshold = threshold;
|
||||||
|
sendMotionThreshold(threshold);
|
||||||
|
console.log("setting motion threshold", threshold);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cameraConfig, motionThreshold, sendMotionThreshold],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setMotionContourArea = useCallback(
|
||||||
|
(contour_area: number) => {
|
||||||
|
if (cameraConfig && contour_area != motionContourArea) {
|
||||||
|
cameraConfig.motion.contour_area = contour_area;
|
||||||
|
sendMotionContourArea(contour_area);
|
||||||
|
console.log("setting motion contour area", contour_area);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cameraConfig, motionContourArea, sendMotionContourArea],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cameraConfig && !selectedCamera) {
|
||||||
|
return <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("selected camera", selectedCamera);
|
||||||
|
// console.log("threshold", motionThreshold);
|
||||||
|
// console.log("contour area", motionContourArea);
|
||||||
|
// console.log(cameraConfig);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading as="h2">Motion Detection Tuner</Heading>
|
||||||
|
<div className="flex items-center space-x-2 mt-5">
|
||||||
|
<Select value={selectedCamera} onValueChange={setSelectedCamera}>
|
||||||
|
<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>
|
||||||
|
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" />
|
||||||
|
<div className="flex flex-col justify-evenly w-full">
|
||||||
|
<div className="flex flex-row mb-5">
|
||||||
|
<Slider
|
||||||
|
id="motion-threshold"
|
||||||
|
className="w-[300px]"
|
||||||
|
value={[motionThreshold]}
|
||||||
|
min={10}
|
||||||
|
max={80}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => setMotionThreshold(value[0])}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="motion-threshold" className="px-2">
|
||||||
|
Threshold: {motionThreshold}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Slider
|
||||||
|
id="motion-contour-area"
|
||||||
|
className="w-[300px]"
|
||||||
|
value={[motionContourArea]}
|
||||||
|
min={10}
|
||||||
|
max={200}
|
||||||
|
step={5}
|
||||||
|
onValueChange={(value) => setMotionContourArea(value[0])}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="motion-contour-area" className="px-2">
|
||||||
|
Contour Area: {motionContourArea}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
180
web/src/components/settings/PolygonDrawer-off.tsx
Normal file
180
web/src/components/settings/PolygonDrawer-off.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Konva from "konva";
|
||||||
|
import { Stage, Layer, Line, Circle, Transformer } from "react-konva";
|
||||||
|
import useDoubleClick from "@/hooks/use-double-click";
|
||||||
|
|
||||||
|
export default function PolygonDrawer() {
|
||||||
|
const [points, setPoints] = useState<number[]>([]);
|
||||||
|
const [anchors, setAnchors] = useState<Konva.Circle[]>([]);
|
||||||
|
const [lastPointIndex, setLastPointIndex] = useState(0);
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [selectedAnchorIndex, setSelectedAnchorIndex] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
|
const layerRef = useRef<Konva.Layer>(null);
|
||||||
|
|
||||||
|
const handleMouseDown = () => {
|
||||||
|
if (!stageRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stage = stageRef.current.getStage();
|
||||||
|
const mousePos = stage?.getPointerPosition();
|
||||||
|
if (!mousePos) return;
|
||||||
|
|
||||||
|
const updatedPoints = [...points];
|
||||||
|
updatedPoints.push(mousePos.x, mousePos.y);
|
||||||
|
setPoints(updatedPoints);
|
||||||
|
|
||||||
|
const newAnchor = new Konva.Circle({
|
||||||
|
x: mousePos.x,
|
||||||
|
y: mousePos.y,
|
||||||
|
radius: 5,
|
||||||
|
fill: "blue",
|
||||||
|
draggable: true,
|
||||||
|
name: "anchor",
|
||||||
|
});
|
||||||
|
setAnchors((prevAnchors) => [...prevAnchors, newAnchor]);
|
||||||
|
layerRef.current?.add(newAnchor);
|
||||||
|
|
||||||
|
setIsDrawing(true);
|
||||||
|
};
|
||||||
|
console.log(isDrawing);
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
const stage = stageRef.current?.getStage();
|
||||||
|
const mousePos = stage?.getPointerPosition();
|
||||||
|
if (!mousePos || !isDrawing) return;
|
||||||
|
|
||||||
|
const updatedPoints = [...points];
|
||||||
|
updatedPoints[updatedPoints.length - 2] = mousePos.x;
|
||||||
|
updatedPoints[updatedPoints.length - 1] = mousePos.y;
|
||||||
|
setPoints(updatedPoints);
|
||||||
|
layerRef.current?.batchDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
alert("double clicked 1");
|
||||||
|
const layer = layerRef.current;
|
||||||
|
console.log(layer);
|
||||||
|
if (!layer) return;
|
||||||
|
console.log("double clicked");
|
||||||
|
document.body.style.background = "red";
|
||||||
|
|
||||||
|
const tempLine = layerRef.current.findOne(".temp");
|
||||||
|
if (tempLine) tempLine.destroy();
|
||||||
|
|
||||||
|
const polyObj = new Konva.Line({
|
||||||
|
points,
|
||||||
|
name: "poly",
|
||||||
|
fill: "green",
|
||||||
|
stroke: "red",
|
||||||
|
strokeWidth: 1,
|
||||||
|
draggable: true,
|
||||||
|
closed: true,
|
||||||
|
hitStrokeWidth: 10,
|
||||||
|
});
|
||||||
|
layerRef.current.add(polyObj);
|
||||||
|
layerRef.current.batchDraw();
|
||||||
|
|
||||||
|
setIsDrawing(false);
|
||||||
|
setLastPointIndex(0);
|
||||||
|
setPoints([]);
|
||||||
|
setAnchors([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStageClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
const clickedTarget = e.target;
|
||||||
|
if (clickedTarget instanceof Konva.Circle) {
|
||||||
|
const index = anchors.findIndex((anchor) => anchor === clickedTarget);
|
||||||
|
setSelectedAnchorIndex(index);
|
||||||
|
} else {
|
||||||
|
setSelectedAnchorIndex(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAnchorDragMove = (index: number) => {
|
||||||
|
const stage = stageRef.current?.getStage();
|
||||||
|
const mousePos = stage?.getPointerPosition();
|
||||||
|
if (!mousePos) return;
|
||||||
|
|
||||||
|
const updatedAnchors = [...anchors];
|
||||||
|
updatedAnchors[index] = new Konva.Circle({
|
||||||
|
...updatedAnchors[index].attrs,
|
||||||
|
x: mousePos.x,
|
||||||
|
y: mousePos.y,
|
||||||
|
});
|
||||||
|
setAnchors(updatedAnchors);
|
||||||
|
|
||||||
|
const updatedPoints = [...points];
|
||||||
|
updatedPoints[index * 2] = mousePos.x;
|
||||||
|
updatedPoints[index * 2 + 1] = mousePos.y;
|
||||||
|
setPoints(updatedPoints);
|
||||||
|
|
||||||
|
layerRef.current?.batchDraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stage = stageRef.current;
|
||||||
|
if (stage) {
|
||||||
|
console.log(stage);
|
||||||
|
stage.on("mousedown", handleMouseDown);
|
||||||
|
stage.on("mousemove", handleMouseMove);
|
||||||
|
stage.on("dblclick", handleDoubleClick);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (stage) {
|
||||||
|
stage.off("mousedown", handleMouseDown);
|
||||||
|
stage.off("mousemove", handleMouseMove);
|
||||||
|
stage.off("dblclick", handleDoubleClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
console.log(points);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="konva" ref={containerRef}>
|
||||||
|
<Stage
|
||||||
|
width={600}
|
||||||
|
height={480}
|
||||||
|
// onDblClick={(e) => {
|
||||||
|
// e.cancelBubble = true;
|
||||||
|
// console.log("dbl");
|
||||||
|
// handleDoubleClick();
|
||||||
|
// }}
|
||||||
|
// onMouseDown={handleMouseDown}
|
||||||
|
// onMouseMove={handleMouseMove}
|
||||||
|
// onClick={handleStageClick}
|
||||||
|
ref={stageRef}
|
||||||
|
>
|
||||||
|
<Layer ref={layerRef}>
|
||||||
|
{isDrawing && (
|
||||||
|
<Line
|
||||||
|
points={points}
|
||||||
|
name="temp"
|
||||||
|
fill="green"
|
||||||
|
stroke="red"
|
||||||
|
strokeWidth={1}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anchors.map((anchor, index) => (
|
||||||
|
<Circle
|
||||||
|
key={index}
|
||||||
|
x={anchor.x()}
|
||||||
|
y={anchor.y()}
|
||||||
|
radius={5}
|
||||||
|
fill={selectedAnchorIndex === index ? "red" : "blue"}
|
||||||
|
draggable
|
||||||
|
onDragMove={(e) => handleAnchorDragMove(index, e)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
<div id="debug">debug</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
128
web/src/components/settings/PolygonDrawer.tsx
Normal file
128
web/src/components/settings/PolygonDrawer.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Line, Circle, Group } from "react-konva";
|
||||||
|
import { minMax, dragBoundFunc } from "@/utils/canvasUtil";
|
||||||
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
|
import Konva from "konva";
|
||||||
|
import { Vector2d } from "konva/lib/types";
|
||||||
|
|
||||||
|
type PolygonDrawerProps = {
|
||||||
|
points: number[][];
|
||||||
|
flattenedPoints: number[];
|
||||||
|
isFinished: boolean;
|
||||||
|
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
|
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
|
handleMouseOverStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
|
handleMouseOutStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PolygonDrawer({
|
||||||
|
points,
|
||||||
|
flattenedPoints,
|
||||||
|
isFinished,
|
||||||
|
handlePointDragMove,
|
||||||
|
handleGroupDragEnd,
|
||||||
|
handleMouseOverStartPoint,
|
||||||
|
handleMouseOutStartPoint,
|
||||||
|
}: PolygonDrawerProps) {
|
||||||
|
const vertexRadius = 6;
|
||||||
|
|
||||||
|
const [stage, setStage] = useState<Konva.Stage>();
|
||||||
|
|
||||||
|
const [minMaxX, setMinMaxX] = useState<[number, number]>([0, 0]); //min and max in x axis
|
||||||
|
const [minMaxY, setMinMaxY] = useState<[number, number]>([0, 0]); //min and max in y axis
|
||||||
|
|
||||||
|
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (!isFinished) return;
|
||||||
|
e.target.getStage()!.container().style.cursor = "move";
|
||||||
|
setStage(e.target.getStage()!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupMouseOut = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (!e.target) return;
|
||||||
|
e.target.getStage()!.container().style.cursor = "default";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupDragStart = () => {
|
||||||
|
const arrX = points.map((p) => p[0]);
|
||||||
|
const arrY = points.map((p) => p[1]);
|
||||||
|
setMinMaxX(minMax(arrX));
|
||||||
|
setMinMaxY(minMax(arrY));
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupDragBound = (pos: Vector2d) => {
|
||||||
|
if (!stage) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
let { x, y } = pos;
|
||||||
|
const sw = stage.width();
|
||||||
|
const sh = stage.height();
|
||||||
|
if (minMaxY[0] + y < 0) y = -1 * minMaxY[0];
|
||||||
|
if (minMaxX[0] + x < 0) x = -1 * minMaxX[0];
|
||||||
|
if (minMaxY[1] + y > sh) y = sh - minMaxY[1];
|
||||||
|
if (minMaxX[1] + x > sw) x = sw - minMaxX[1];
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
// const flattenedPointsAsNumber = useMemo(
|
||||||
|
// () => flattenedPoints.flatMap((point) => [point.x, point.y]),
|
||||||
|
// [flattenedPoints],
|
||||||
|
// );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
name="polygon"
|
||||||
|
draggable={isFinished}
|
||||||
|
onDragStart={handleGroupDragStart}
|
||||||
|
onDragEnd={handleGroupDragEnd}
|
||||||
|
dragBoundFunc={groupDragBound}
|
||||||
|
onMouseOver={handleGroupMouseOver}
|
||||||
|
onMouseOut={handleGroupMouseOut}
|
||||||
|
>
|
||||||
|
<Line
|
||||||
|
points={flattenedPoints}
|
||||||
|
stroke="#00F1FF"
|
||||||
|
strokeWidth={3}
|
||||||
|
closed={isFinished}
|
||||||
|
fill="rgb(140,30,255,0.5)"
|
||||||
|
/>
|
||||||
|
{points.map((point, index) => {
|
||||||
|
const x = point[0] - vertexRadius / 2;
|
||||||
|
const y = point[1] - vertexRadius / 2;
|
||||||
|
const startPointAttr =
|
||||||
|
index === 0
|
||||||
|
? {
|
||||||
|
hitStrokeWidth: 12,
|
||||||
|
onMouseOver: handleMouseOverStartPoint,
|
||||||
|
onMouseOut: handleMouseOutStartPoint,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<Circle
|
||||||
|
key={index}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
radius={vertexRadius}
|
||||||
|
fill="#FF019A"
|
||||||
|
stroke="#00F1FF"
|
||||||
|
strokeWidth={2}
|
||||||
|
draggable
|
||||||
|
onDragMove={handlePointDragMove}
|
||||||
|
dragBoundFunc={(pos) => {
|
||||||
|
if (stage) {
|
||||||
|
return dragBoundFunc(
|
||||||
|
stage.width(),
|
||||||
|
stage.height(),
|
||||||
|
vertexRadius,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return pos; // Return original pos if stage is not defined
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...startPointAttr}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
web/src/components/settings/Zones.tsx
Normal file
79
web/src/components/settings/Zones.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useMotionContourArea, useMotionThreshold } from "@/api/ws";
|
||||||
|
import DebugCanvas from "./DebugCanvas";
|
||||||
|
|
||||||
|
export default function SettingsZones() {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const cameras = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(config.cameras)
|
||||||
|
.filter((conf) => conf.ui.dashboard && conf.enabled)
|
||||||
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const [selectedCamera, setSelectedCamera] = useState(cameras[0].name);
|
||||||
|
|
||||||
|
const cameraConfig = useMemo(() => {
|
||||||
|
if (config && selectedCamera) {
|
||||||
|
return config.cameras[selectedCamera];
|
||||||
|
}
|
||||||
|
}, [config, selectedCamera]);
|
||||||
|
|
||||||
|
if (!cameraConfig && !selectedCamera) {
|
||||||
|
return <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("selected camera", selectedCamera);
|
||||||
|
// console.log("threshold", motionThreshold);
|
||||||
|
// console.log("contour area", motionContourArea);
|
||||||
|
// console.log(cameraConfig);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading as="h2">Motion Detection Tuner</Heading>
|
||||||
|
<div className="flex items-center space-x-2 mt-5">
|
||||||
|
<Select value={selectedCamera} onValueChange={setSelectedCamera}>
|
||||||
|
<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>
|
||||||
|
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" />
|
||||||
|
<DebugCanvas />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
web/src/hooks/use-double-click.ts
Normal file
50
web/src/hooks/use-double-click.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect, MutableRefObject } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple React hook for differentiating single and double clicks on the same component.
|
||||||
|
*
|
||||||
|
* @param ref Dom node to watch for double clicks
|
||||||
|
* @param latency The amount of time (in milliseconds) to wait before differentiating a single from a double click
|
||||||
|
* @param onSingleClick A callback function for single click events
|
||||||
|
* @param onDoubleClick A callback function for double click events
|
||||||
|
*/
|
||||||
|
const useDoubleClick = ({
|
||||||
|
ref,
|
||||||
|
latency = 300,
|
||||||
|
onSingleClick = () => null,
|
||||||
|
onDoubleClick = () => null,
|
||||||
|
}: {
|
||||||
|
ref: MutableRefObject<HTMLElement>;
|
||||||
|
latency?: number;
|
||||||
|
onSingleClick?: (e: MouseEvent) => void;
|
||||||
|
onDoubleClick?: (e: MouseEvent) => void;
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const clickRef = ref.current;
|
||||||
|
let clickCount = 0;
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
clickCount += 1;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (clickCount === 1) onSingleClick(e);
|
||||||
|
else if (clickCount === 2) onDoubleClick(e);
|
||||||
|
|
||||||
|
clickCount = 0;
|
||||||
|
}, latency);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener for click events
|
||||||
|
if (clickRef) {
|
||||||
|
clickRef.addEventListener("click", handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove event listener
|
||||||
|
return () => {
|
||||||
|
if (clickRef) {
|
||||||
|
clickRef.removeEventListener("click", handleClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ref, latency, onSingleClick, onDoubleClick]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDoubleClick;
|
||||||
@ -10,8 +10,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import MotionTuner from "@/components/settings/MotionTuner";
|
||||||
|
import SettingsZones from "@/components/settings/Zones";
|
||||||
|
|
||||||
function Settings() {
|
function General() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading as="h2">Settings</Heading>
|
<Heading as="h2">Settings</Heading>
|
||||||
@ -41,4 +44,29 @@ function Settings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Settings;
|
export default function Settings() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs defaultValue="account" className="w-auto">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="objects">Objects</TabsTrigger>
|
||||||
|
<TabsTrigger value="zones">Zones</TabsTrigger>
|
||||||
|
<TabsTrigger value="masks">Masks</TabsTrigger>
|
||||||
|
<TabsTrigger value="motion">Motion</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="general">
|
||||||
|
<General />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="objects">Objects</TabsContent>
|
||||||
|
<TabsContent value="zones">
|
||||||
|
<SettingsZones />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="masks">Masks</TabsContent>
|
||||||
|
<TabsContent value="motion">
|
||||||
|
<MotionTuner />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
4
web/src/types/canvas.ts
Normal file
4
web/src/types/canvas.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type Point = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
45
web/src/utils/canvasUtil.ts
Normal file
45
web/src/utils/canvasUtil.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
export const getAveragePoint = (points: number[]): number => {
|
||||||
|
let totalX = 0;
|
||||||
|
let totalY = 0;
|
||||||
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
|
totalX += points[i];
|
||||||
|
totalY += points[i + 1];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: totalX / (points.length / 2),
|
||||||
|
y: totalY / (points.length / 2),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDistance = (node1: number[], node2: number[]): string => {
|
||||||
|
const diffX = Math.abs(node1[0] - node2[0]);
|
||||||
|
const diffY = Math.abs(node1[1] - node2[1]);
|
||||||
|
const distanceInPixel = Math.sqrt(diffX * diffX + diffY * diffY);
|
||||||
|
return distanceInPixel.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dragBoundFunc = (
|
||||||
|
stageWidth: number,
|
||||||
|
stageHeight: number,
|
||||||
|
vertexRadius: number,
|
||||||
|
pos: Point,
|
||||||
|
): Point => {
|
||||||
|
let x = pos.x;
|
||||||
|
let y = pos.y;
|
||||||
|
if (pos.x + vertexRadius > stageWidth) x = stageWidth;
|
||||||
|
if (pos.x - vertexRadius < 0) x = 0;
|
||||||
|
if (pos.y + vertexRadius > stageHeight) y = stageHeight;
|
||||||
|
if (pos.y - vertexRadius < 0) y = 0;
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const minMax = (points: number[]): [number, number] => {
|
||||||
|
return points.reduce(
|
||||||
|
(acc: [number | undefined, number | undefined], val) => {
|
||||||
|
acc[0] = acc[0] === undefined || val < acc[0] ? val : acc[0];
|
||||||
|
acc[1] = acc[1] === undefined || val > acc[1] ? val : acc[1];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[undefined, undefined],
|
||||||
|
) as [number, number];
|
||||||
|
};
|
||||||
@ -12,24 +12,24 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:5000",
|
target: "http://192.168.5.4:5000",
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
"/vod": {
|
"/vod": {
|
||||||
target: "http://localhost:5000",
|
target: "http://192.168.5.4:5000",
|
||||||
},
|
},
|
||||||
"/clips": {
|
"/clips": {
|
||||||
target: "http://localhost:5000",
|
target: "http://192.168.5.4:5000",
|
||||||
},
|
},
|
||||||
"/exports": {
|
"/exports": {
|
||||||
target: "http://localhost:5000",
|
target: "http://192.168.5.4:5000",
|
||||||
},
|
},
|
||||||
"/ws": {
|
"/ws": {
|
||||||
target: "ws://localhost:5000",
|
target: "ws://192.168.5.4:5000",
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
"/live": {
|
"/live": {
|
||||||
target: "ws://localhost:5000",
|
target: "ws://192.168.5.4:5000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user