mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
progress
This commit is contained in:
parent
cf96f7b706
commit
8d0385c991
@ -67,8 +67,8 @@ export function PolygonCanvas({
|
|||||||
return distance < 15;
|
return distance < 15;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseDown = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
if (!activePolygonIndex || !polygons) {
|
if (activePolygonIndex == null || !polygons) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,18 +100,28 @@ export function PolygonCanvas({
|
|||||||
// }
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseOverStartPoint = (
|
||||||
if (activePolygonIndex !== null && polygons) {
|
e: KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => {
|
||||||
|
if (activePolygonIndex == null || !polygons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const activePolygon = polygons[activePolygonIndex];
|
const activePolygon = polygons[activePolygonIndex];
|
||||||
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
|
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
|
||||||
e.currentTarget.scale({ x: 2, y: 2 });
|
e.currentTarget.scale({ x: 2, y: 2 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseOutStartPoint = (
|
||||||
|
e: KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => {
|
||||||
e.currentTarget.scale({ x: 1, y: 1 });
|
e.currentTarget.scale({ x: 1, y: 1 });
|
||||||
if (activePolygonIndex !== null && polygons) {
|
|
||||||
|
if (activePolygonIndex == null || !polygons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const activePolygon = polygons[activePolygonIndex];
|
const activePolygon = polygons[activePolygonIndex];
|
||||||
if (
|
if (
|
||||||
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
|
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
|
||||||
@ -119,11 +129,15 @@ export function PolygonCanvas({
|
|||||||
) {
|
) {
|
||||||
e.currentTarget.scale({ x: 1, y: 1 });
|
e.currentTarget.scale({ x: 1, y: 1 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
const handlePointDragMove = (
|
||||||
if (activePolygonIndex !== null && polygons) {
|
e: KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => {
|
||||||
|
if (activePolygonIndex == null || !polygons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedPolygons = [...polygons];
|
const updatedPolygons = [...polygons];
|
||||||
const activePolygon = updatedPolygons[activePolygonIndex];
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
@ -144,14 +158,13 @@ export function PolygonCanvas({
|
|||||||
};
|
};
|
||||||
setPolygons(updatedPolygons);
|
setPolygons(updatedPolygons);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const flattenPoints = (points: number[][]): number[] => {
|
const flattenPoints = (points: number[][]): number[] => {
|
||||||
return points.reduce((acc, point) => [...acc, ...point], []);
|
return points.reduce((acc, point) => [...acc, ...point], []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
|
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
if (activePolygonIndex !== null && e.target.name() === "polygon") {
|
if (activePolygonIndex !== null && e.target.name() === "polygon") {
|
||||||
const updatedPolygons = [...polygons];
|
const updatedPolygons = [...polygons];
|
||||||
const activePolygon = updatedPolygons[activePolygonIndex];
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
@ -174,6 +187,7 @@ export function PolygonCanvas({
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleMouseDown}
|
||||||
>
|
>
|
||||||
<Layer>
|
<Layer>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@ -11,10 +11,14 @@ type PolygonDrawerProps = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isFinished: boolean;
|
isFinished: boolean;
|
||||||
color: number[];
|
color: number[];
|
||||||
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
|
handlePointDragMove: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
||||||
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
|
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent | TouchEvent>) => void;
|
||||||
handleMouseOverStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
|
handleMouseOverStartPoint: (
|
||||||
handleMouseOutStartPoint: (e: KonvaEventObject<MouseEvent>) => void;
|
e: KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => void;
|
||||||
|
handleMouseOutStartPoint: (
|
||||||
|
e: KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PolygonDrawer({
|
export default function PolygonDrawer({
|
||||||
@ -33,13 +37,17 @@ export default function PolygonDrawer({
|
|||||||
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
||||||
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
||||||
|
|
||||||
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
const handleGroupMouseOver = (
|
||||||
|
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => {
|
||||||
if (!isFinished) return;
|
if (!isFinished) return;
|
||||||
e.target.getStage()!.container().style.cursor = "move";
|
e.target.getStage()!.container().style.cursor = "move";
|
||||||
setStage(e.target.getStage()!);
|
setStage(e.target.getStage()!);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupMouseOut = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
const handleGroupMouseOut = (
|
||||||
|
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||||
|
) => {
|
||||||
if (!e.target) return;
|
if (!e.target) return;
|
||||||
e.target.getStage()!.container().style.cursor = "default";
|
e.target.getStage()!.container().style.cursor = "default";
|
||||||
};
|
};
|
||||||
@ -87,6 +95,7 @@ export default function PolygonDrawer({
|
|||||||
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
||||||
dragBoundFunc={isActive ? groupDragBound : undefined}
|
dragBoundFunc={isActive ? groupDragBound : undefined}
|
||||||
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
||||||
|
onTouchStart={isActive ? handleGroupMouseOver : undefined}
|
||||||
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
||||||
>
|
>
|
||||||
<Line
|
<Line
|
||||||
|
|||||||
@ -43,11 +43,8 @@ export function ZoneObjectSelector({
|
|||||||
if (!cameraConfig || !zoneName) {
|
if (!cameraConfig || !zoneName) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
console.log(zoneName);
|
|
||||||
|
|
||||||
const labels = new Set<string>();
|
const labels = new Set<string>();
|
||||||
// console.log("zone name", zoneName);
|
|
||||||
// console.log(cameraConfig.zones[zoneName].objects);
|
|
||||||
|
|
||||||
cameraConfig.objects.track.forEach((label) => {
|
cameraConfig.objects.track.forEach((label) => {
|
||||||
if (!ATTRIBUTES.includes(label)) {
|
if (!ATTRIBUTES.includes(label)) {
|
||||||
@ -55,9 +52,13 @@ export function ZoneObjectSelector({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cameraConfig.zones[zoneName]) {
|
||||||
cameraConfig.zones[zoneName].objects.forEach((label) => {
|
cameraConfig.zones[zoneName].objects.forEach((label) => {
|
||||||
|
if (!ATTRIBUTES.includes(label)) {
|
||||||
labels.add(label);
|
labels.add(label);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return [...labels].sort() || [];
|
return [...labels].sort() || [];
|
||||||
}, [cameraConfig, zoneName]);
|
}, [cameraConfig, zoneName]);
|
||||||
@ -166,7 +167,7 @@ export function ZoneControls({
|
|||||||
updatedPolygons[activePolygonIndex] = {
|
updatedPolygons[activePolygonIndex] = {
|
||||||
points: [],
|
points: [],
|
||||||
isFinished: false,
|
isFinished: false,
|
||||||
name: "new",
|
name: updatedPolygons[activePolygonIndex].name,
|
||||||
camera: camera,
|
camera: camera,
|
||||||
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
|
color: updatedPolygons[activePolygonIndex].color ?? [220, 0, 0],
|
||||||
};
|
};
|
||||||
@ -210,29 +211,31 @@ export function ZoneControls({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
{isMobile && <span tabIndex={0} className="sr-only" />}
|
||||||
<DialogTitle>New Zone</DialogTitle>
|
<DialogTitle>New Zone</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Enter a label for your zone. Do not include spaces, and don't use
|
Enter a unique label for your zone. Do not include spaces, and
|
||||||
the name of a camera.
|
don't use the name of a camera.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
className="mt-3"
|
className={`mt-3 ${isMobile && "text-md"}`}
|
||||||
type="search"
|
type="search"
|
||||||
value={zoneName ?? ""}
|
value={zoneName ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setInvalidName(
|
setInvalidName(
|
||||||
Object.keys(config.cameras).includes(e.target.value) ||
|
Object.keys(config.cameras).includes(e.target.value) ||
|
||||||
e.target.value.includes(" "),
|
e.target.value.includes(" ") ||
|
||||||
|
polygons
|
||||||
|
.map((item) => item.name)
|
||||||
|
.includes(e.target.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
setZoneName(e.target.value);
|
setZoneName(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{invalidName && (
|
{invalidName && (
|
||||||
<div className="text-danger text-sm">
|
<div className="text-danger text-sm">Invalid zone name.</div>
|
||||||
Zone name is not valid.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { PolygonCanvas } from "./PolygonCanvas";
|
import { PolygonCanvas } from "./PolygonCanvas";
|
||||||
import { Polygon } from "@/types/canvas";
|
import { Polygon } from "@/types/canvas";
|
||||||
import { interpolatePoints } from "@/utils/canvasUtil";
|
import { interpolatePoints } from "@/utils/canvasUtil";
|
||||||
@ -43,9 +43,16 @@ const parseCoordinates = (coordinatesString: string) => {
|
|||||||
return points;
|
return points;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ZoneObjects = {
|
||||||
|
camera: string;
|
||||||
|
zoneName: string;
|
||||||
|
objects: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsZones() {
|
export default function SettingsZones() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
|
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
|
||||||
|
const [zoneObjects, setZoneObjects] = useState<ZoneObjects[]>([]);
|
||||||
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
|
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@ -85,6 +92,51 @@ export default function SettingsZones() {
|
|||||||
return [...labels].sort();
|
return [...labels].sort();
|
||||||
}, [cameras]);
|
}, [cameras]);
|
||||||
|
|
||||||
|
// const saveZoneObjects = useCallback(
|
||||||
|
// (camera: string, zoneName: string, newObjects?: string[]) => {
|
||||||
|
// setZoneObjects((prevZoneObjects) =>
|
||||||
|
// prevZoneObjects.map((zoneObject) => {
|
||||||
|
// if (
|
||||||
|
// zoneObject.camera === camera &&
|
||||||
|
// zoneObject.zoneName === zoneName
|
||||||
|
// ) {
|
||||||
|
// console.log("found", camera, "with", zoneName);
|
||||||
|
// console.log("new objects", newObjects);
|
||||||
|
// console.log("new zoneobject", {
|
||||||
|
// ...zoneObject,
|
||||||
|
// objects: newObjects ?? [],
|
||||||
|
// });
|
||||||
|
// // Replace objects with newObjects if provided
|
||||||
|
// return {
|
||||||
|
// ...zoneObject,
|
||||||
|
// objects: newObjects ?? [],
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return zoneObject; // Keep original object
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// [setZoneObjects],
|
||||||
|
// );
|
||||||
|
|
||||||
|
const saveZoneObjects = useCallback(
|
||||||
|
(camera: string, zoneName: string, objects?: string[]) => {
|
||||||
|
setZoneObjects((prevZoneObjects) => {
|
||||||
|
const updatedZoneObjects = prevZoneObjects.map((zoneObject) => {
|
||||||
|
if (
|
||||||
|
zoneObject.camera === camera &&
|
||||||
|
zoneObject.zoneName === zoneName
|
||||||
|
) {
|
||||||
|
return { ...zoneObject, objects: objects || [] };
|
||||||
|
}
|
||||||
|
return zoneObject;
|
||||||
|
});
|
||||||
|
return updatedZoneObjects;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setZoneObjects],
|
||||||
|
);
|
||||||
|
|
||||||
const grow = useMemo(() => {
|
const grow = useMemo(() => {
|
||||||
if (!cameraConfig) {
|
if (!cameraConfig) {
|
||||||
return;
|
return;
|
||||||
@ -105,6 +157,14 @@ export default function SettingsZones() {
|
|||||||
}
|
}
|
||||||
}, [cameraConfig]);
|
}, [cameraConfig]);
|
||||||
|
|
||||||
|
const handleCameraChange = useCallback(
|
||||||
|
(camera: string) => {
|
||||||
|
setSelectedCamera(camera);
|
||||||
|
setActivePolygonIndex(null);
|
||||||
|
},
|
||||||
|
[setSelectedCamera, setActivePolygonIndex],
|
||||||
|
);
|
||||||
|
|
||||||
const [{ width: containerWidth, height: containerHeight }] =
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
useResizeObserver(containerRef);
|
useResizeObserver(containerRef);
|
||||||
|
|
||||||
@ -159,20 +219,40 @@ export default function SettingsZones() {
|
|||||||
color: zoneData.color,
|
color: zoneData.color,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [cameraConfig, containerRef]);
|
}, [cameraConfig, containerRef]);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
if (!cameraConfig && !selectedCamera) {
|
if (!cameraConfig && !selectedCamera) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="overflow-auto">
|
||||||
<Heading as="h2">Zones</Heading>
|
<Heading as="h2">Zones</Heading>
|
||||||
<div className="flex items-center space-x-2 mt-5">
|
<div className="flex items-center space-x-2 mt-5">
|
||||||
<Select value={selectedCamera} onValueChange={setSelectedCamera}>
|
<Select value={selectedCamera} onValueChange={handleCameraChange}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Camera" />
|
<SelectValue placeholder="Camera" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -194,9 +274,9 @@ export default function SettingsZones() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cameraConfig && (
|
{cameraConfig && (
|
||||||
<div className="flex flex-row justify-evenly">
|
<div className="flex flex-col justify-evenly">
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col justify-center items-center w-[60%] ${grow}`}
|
className={`flex flex-col justify-center items-center w-full md:w-[60%] ${grow}`}
|
||||||
>
|
>
|
||||||
<div ref={containerRef} className="size-full">
|
<div ref={containerRef} className="size-full">
|
||||||
{cameraConfig ? (
|
{cameraConfig ? (
|
||||||
@ -213,7 +293,7 @@ export default function SettingsZones() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[30%]">
|
<div className="w-full md:w-[30%]">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -254,7 +334,9 @@ export default function SettingsZones() {
|
|||||||
camera={polygon.camera}
|
camera={polygon.camera}
|
||||||
zoneName={polygon.name}
|
zoneName={polygon.name}
|
||||||
allLabels={allLabels}
|
allLabels={allLabels}
|
||||||
updateLabelFilter={(objects) => console.log(objects)}
|
updateLabelFilter={(objects) =>
|
||||||
|
saveZoneObjects(polygon.camera, polygon.name, objects)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -294,6 +376,6 @@ export default function SettingsZones() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,9 @@ function General() {
|
|||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full h-full">
|
||||||
|
<div className="flex h-full">
|
||||||
|
<div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
|
||||||
<Tabs defaultValue="general" className="w-auto">
|
<Tabs defaultValue="general" className="w-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@ -67,6 +69,8 @@ export default function Settings() {
|
|||||||
<MotionTuner />
|
<MotionTuner />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,13 +21,7 @@ export interface BirdseyeConfig {
|
|||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ATTRIBUTES = [
|
export const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||||
"amazon",
|
|
||||||
"face",
|
|
||||||
"fedex",
|
|
||||||
"license_plate",
|
|
||||||
"ups",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export interface CameraConfig {
|
export interface CameraConfig {
|
||||||
audio: {
|
audio: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user