mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-19 14:48:22 +03:00
Add object selector
This commit is contained in:
parent
6749e9e121
commit
ced177d62c
@ -65,6 +65,8 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"typeState": "State",
|
"typeState": "State",
|
||||||
"typeObject": "Object",
|
"typeObject": "Object",
|
||||||
|
"objectLabel": "Object Label",
|
||||||
|
"objectLabelPlaceholder": "Select object type...",
|
||||||
"classificationType": "Classification Type",
|
"classificationType": "Classification Type",
|
||||||
"classificationSubLabel": "Sub Label",
|
"classificationSubLabel": "Sub Label",
|
||||||
"classificationAttribute": "Attribute",
|
"classificationAttribute": "Attribute",
|
||||||
@ -77,6 +79,7 @@
|
|||||||
"classRequired": "At least 1 class is required",
|
"classRequired": "At least 1 class is required",
|
||||||
"classesUnique": "Class names must be unique",
|
"classesUnique": "Class names must be unique",
|
||||||
"stateRequiresTwoClasses": "State models require at least 2 classes",
|
"stateRequiresTwoClasses": "State models require at least 2 classes",
|
||||||
|
"objectLabelRequired": "Please select an object label",
|
||||||
"objectTypeRequired": "Please select a classification type"
|
"objectTypeRequired": "Please select a classification type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,12 +10,23 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
import { LuX } from "react-icons/lu";
|
import { LuX } from "react-icons/lu";
|
||||||
import { MdAddBox } from "react-icons/md";
|
import { MdAddBox } from "react-icons/md";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
|
||||||
export type ModelType = "state" | "object";
|
export type ModelType = "state" | "object";
|
||||||
export type ObjectClassificationType = "sub_label" | "attribute";
|
export type ObjectClassificationType = "sub_label" | "attribute";
|
||||||
@ -23,6 +34,7 @@ export type ObjectClassificationType = "sub_label" | "attribute";
|
|||||||
export type Step1FormData = {
|
export type Step1FormData = {
|
||||||
modelName: string;
|
modelName: string;
|
||||||
modelType: ModelType;
|
modelType: ModelType;
|
||||||
|
objectLabel?: string;
|
||||||
objectType?: ObjectClassificationType;
|
objectType?: ObjectClassificationType;
|
||||||
classes: string[];
|
classes: string[];
|
||||||
};
|
};
|
||||||
@ -39,6 +51,27 @@ export default function Step1NameAndDefine({
|
|||||||
onCancel,
|
onCancel,
|
||||||
}: Step1NameAndDefineProps) {
|
}: Step1NameAndDefineProps) {
|
||||||
const { t } = useTranslation(["views/classificationModel"]);
|
const { t } = useTranslation(["views/classificationModel"]);
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const objectLabels = useMemo(() => {
|
||||||
|
if (!config) return [];
|
||||||
|
|
||||||
|
const labels = new Set<string>();
|
||||||
|
|
||||||
|
Object.values(config.cameras).forEach((cameraConfig) => {
|
||||||
|
if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraConfig.objects.track.forEach((label) => {
|
||||||
|
if (!config.model.all_attributes.includes(label)) {
|
||||||
|
labels.add(label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...labels].sort();
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
const step1FormData = z
|
const step1FormData = z
|
||||||
.object({
|
.object({
|
||||||
@ -50,6 +83,7 @@ export default function Step1NameAndDefine({
|
|||||||
message: t("wizard.step1.errors.nameOnlyNumbers"),
|
message: t("wizard.step1.errors.nameOnlyNumbers"),
|
||||||
}),
|
}),
|
||||||
modelType: z.enum(["state", "object"]),
|
modelType: z.enum(["state", "object"]),
|
||||||
|
objectLabel: z.string().optional(),
|
||||||
objectType: z.enum(["sub_label", "attribute"]).optional(),
|
objectType: z.enum(["sub_label", "attribute"]).optional(),
|
||||||
classes: z
|
classes: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
@ -86,7 +120,18 @@ export default function Step1NameAndDefine({
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// Object models require objectType to be selected
|
if (data.modelType === "object") {
|
||||||
|
return data.objectLabel !== undefined && data.objectLabel !== "";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: t("wizard.step1.errors.objectLabelRequired"),
|
||||||
|
path: ["objectLabel"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
if (data.modelType === "object") {
|
if (data.modelType === "object") {
|
||||||
return data.objectType !== undefined;
|
return data.objectType !== undefined;
|
||||||
}
|
}
|
||||||
@ -103,6 +148,7 @@ export default function Step1NameAndDefine({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
modelName: initialData?.modelName || "",
|
modelName: initialData?.modelName || "",
|
||||||
modelType: initialData?.modelType || "state",
|
modelType: initialData?.modelType || "state",
|
||||||
|
objectLabel: initialData?.objectLabel,
|
||||||
objectType: initialData?.objectType || "sub_label",
|
objectType: initialData?.objectType || "sub_label",
|
||||||
classes: initialData?.classes?.length ? initialData.classes : [""],
|
classes: initialData?.classes?.length ? initialData.classes : [""],
|
||||||
},
|
},
|
||||||
@ -209,52 +255,92 @@ export default function Step1NameAndDefine({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{watchedModelType === "object" && (
|
{watchedModelType === "object" && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="objectType"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="objectLabel"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>{t("wizard.step1.classificationType")}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>{t("wizard.step1.objectLabel")}</FormLabel>
|
||||||
<RadioGroup
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="flex flex-col gap-4 pt-2"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<FormControl>
|
||||||
<RadioGroupItem
|
<SelectTrigger className="h-8">
|
||||||
className={
|
<SelectValue
|
||||||
watchedObjectType === "sub_label"
|
placeholder={t(
|
||||||
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
"wizard.step1.objectLabelPlaceholder",
|
||||||
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
)}
|
||||||
}
|
/>
|
||||||
id="sub_label"
|
</SelectTrigger>
|
||||||
value="sub_label"
|
</FormControl>
|
||||||
/>
|
<SelectContent>
|
||||||
<Label className="cursor-pointer" htmlFor="sub_label">
|
{objectLabels.map((label) => (
|
||||||
{t("wizard.step1.classificationSubLabel")}
|
<SelectItem
|
||||||
</Label>
|
key={label}
|
||||||
</div>
|
value={label}
|
||||||
<div className="flex items-center gap-2">
|
className="cursor-pointer hover:bg-secondary-highlight"
|
||||||
<RadioGroupItem
|
>
|
||||||
className={
|
{getTranslatedLabel(label)}
|
||||||
watchedObjectType === "attribute"
|
</SelectItem>
|
||||||
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
))}
|
||||||
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
</SelectContent>
|
||||||
}
|
</Select>
|
||||||
id="attribute"
|
<FormMessage />
|
||||||
value="attribute"
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<Label className="cursor-pointer" htmlFor="attribute">
|
/>
|
||||||
{t("wizard.step1.classificationAttribute")}
|
|
||||||
</Label>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
</RadioGroup>
|
name="objectType"
|
||||||
</FormControl>
|
render={({ field }) => (
|
||||||
<FormMessage />
|
<FormItem>
|
||||||
</FormItem>
|
<FormLabel>
|
||||||
)}
|
{t("wizard.step1.classificationType")}
|
||||||
/>
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex flex-col gap-4 pt-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
watchedObjectType === "sub_label"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
id="sub_label"
|
||||||
|
value="sub_label"
|
||||||
|
/>
|
||||||
|
<Label className="cursor-pointer" htmlFor="sub_label">
|
||||||
|
{t("wizard.step1.classificationSubLabel")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
watchedObjectType === "attribute"
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
id="attribute"
|
||||||
|
value="attribute"
|
||||||
|
/>
|
||||||
|
<Label className="cursor-pointer" htmlFor="attribute">
|
||||||
|
{t("wizard.step1.classificationAttribute")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user