mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 13:07:44 +03:00
Improve multi select
This commit is contained in:
parent
f21aef3a24
commit
9df276ba0d
@ -34,6 +34,7 @@ type CreateFaceWizardDialogProps = {
|
|||||||
export default function CreateFaceWizardDialog({
|
export default function CreateFaceWizardDialog({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
onFinish,
|
||||||
}: CreateFaceWizardDialogProps) {
|
}: CreateFaceWizardDialogProps) {
|
||||||
const { t } = useTranslation("views/faceLibrary");
|
const { t } = useTranslation("views/faceLibrary");
|
||||||
|
|
||||||
@ -142,7 +143,13 @@ export default function CreateFaceWizardDialog({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button variant="select" onClick={() => handleReset()}>
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
onFinish();
|
||||||
|
handleReset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t("button.done", { ns: "common" })}
|
{t("button.done", { ns: "common" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -121,7 +121,11 @@ export default function FaceLibrary() {
|
|||||||
const [selectedFaces, setSelectedFaces] = useState<string[]>([]);
|
const [selectedFaces, setSelectedFaces] = useState<string[]>([]);
|
||||||
|
|
||||||
const onClickFace = useCallback(
|
const onClickFace = useCallback(
|
||||||
(imageId: string) => {
|
(imageId: string, ctrl: boolean) => {
|
||||||
|
if (selectedFaces.length == 0 && !ctrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const index = selectedFaces.indexOf(imageId);
|
const index = selectedFaces.indexOf(imageId);
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
@ -143,39 +147,42 @@ export default function FaceLibrary() {
|
|||||||
[selectedFaces, setSelectedFaces],
|
[selectedFaces, setSelectedFaces],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDelete = useCallback(() => {
|
const onDelete = useCallback(
|
||||||
axios
|
(name: string, ids: string[]) => {
|
||||||
.post(`/faces/train/delete`, { ids: selectedFaces })
|
axios
|
||||||
.then((resp) => {
|
.post(`/faces/${name}/delete`, { ids })
|
||||||
setSelectedFaces([]);
|
.then((resp) => {
|
||||||
|
setSelectedFaces([]);
|
||||||
|
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
toast.success(t("toast.success.deletedFace"), {
|
toast.success(t("toast.success.deletedFace"), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (faceImages.length == 1) {
|
||||||
|
// face has been deleted
|
||||||
|
setPageToggle("");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFaces();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
"Unknown error";
|
||||||
|
toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (faceImages.length == 1) {
|
|
||||||
// face has been deleted
|
|
||||||
setPageToggle("");
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshFaces();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const errorMessage =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.response?.data?.detail ||
|
|
||||||
"Unknown error";
|
|
||||||
toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), {
|
|
||||||
position: "top-center",
|
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
}, [faceImages, selectedFaces, refreshFaces, setPageToggle, t]);
|
[faceImages, refreshFaces, setPageToggle, t],
|
||||||
|
);
|
||||||
|
|
||||||
// keyboard
|
// keyboard
|
||||||
|
|
||||||
useKeyboardListener(["a"], (key, modifiers) => {
|
useKeyboardListener(["a", "Escape"], (key, modifiers) => {
|
||||||
if (modifiers.repeat || !modifiers.down) {
|
if (modifiers.repeat || !modifiers.down) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -186,6 +193,9 @@ export default function FaceLibrary() {
|
|||||||
setSelectedFaces([...trainImages]);
|
setSelectedFaces([...trainImages]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "Escape":
|
||||||
|
setSelectedFaces([]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,7 +268,10 @@ export default function FaceLibrary() {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
{selectedFaces?.length > 0 ? (
|
{selectedFaces?.length > 0 ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Button className="flex gap-2" onClick={() => onDelete()}>
|
<Button
|
||||||
|
className="flex gap-2"
|
||||||
|
onClick={() => onDelete("train", selectedFaces)}
|
||||||
|
>
|
||||||
<LuTrash2 className="size-7 rounded-md p-1 text-secondary-foreground" />
|
<LuTrash2 className="size-7 rounded-md p-1 text-secondary-foreground" />
|
||||||
{t("button.deleteFaceAttempts")}
|
{t("button.deleteFaceAttempts")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -292,7 +305,7 @@ export default function FaceLibrary() {
|
|||||||
<FaceGrid
|
<FaceGrid
|
||||||
faceImages={faceImages}
|
faceImages={faceImages}
|
||||||
pageToggle={pageToggle}
|
pageToggle={pageToggle}
|
||||||
onRefresh={refreshFaces}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -304,7 +317,7 @@ type TrainingGridProps = {
|
|||||||
attemptImages: string[];
|
attemptImages: string[];
|
||||||
faceNames: string[];
|
faceNames: string[];
|
||||||
selectedFaces: string[];
|
selectedFaces: string[];
|
||||||
onClickFace: (image: string) => void;
|
onClickFace: (image: string, ctrl: boolean) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
};
|
};
|
||||||
function TrainingGrid({
|
function TrainingGrid({
|
||||||
@ -324,7 +337,7 @@ function TrainingGrid({
|
|||||||
faceNames={faceNames}
|
faceNames={faceNames}
|
||||||
threshold={config.face_recognition.recognition_threshold}
|
threshold={config.face_recognition.recognition_threshold}
|
||||||
selected={selectedFaces.includes(image)}
|
selected={selectedFaces.includes(image)}
|
||||||
onClick={() => onClickFace(image)}
|
onClick={(meta) => onClickFace(image, meta)}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -337,7 +350,7 @@ type FaceAttemptProps = {
|
|||||||
faceNames: string[];
|
faceNames: string[];
|
||||||
threshold: number;
|
threshold: number;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: (meta: boolean) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
};
|
};
|
||||||
function FaceAttempt({
|
function FaceAttempt({
|
||||||
@ -415,7 +428,7 @@ function FaceAttempt({
|
|||||||
? "shadow-selected outline-selected"
|
? "shadow-selected outline-selected"
|
||||||
: "outline-transparent duration-500",
|
: "outline-transparent duration-500",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={(e) => onClick(e.metaKey || e.ctrlKey)}
|
||||||
>
|
>
|
||||||
<div className="relative w-full overflow-hidden rounded-t-lg border border-t-0 *:text-card-foreground">
|
<div className="relative w-full overflow-hidden rounded-t-lg border border-t-0 *:text-card-foreground">
|
||||||
<img className="size-44" src={`${baseUrl}clips/faces/train/${image}`} />
|
<img className="size-44" src={`${baseUrl}clips/faces/train/${image}`} />
|
||||||
@ -479,9 +492,9 @@ function FaceAttempt({
|
|||||||
type FaceGridProps = {
|
type FaceGridProps = {
|
||||||
faceImages: string[];
|
faceImages: string[];
|
||||||
pageToggle: string;
|
pageToggle: string;
|
||||||
onRefresh: () => void;
|
onDelete: (name: string, ids: string[]) => void;
|
||||||
};
|
};
|
||||||
function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
|
function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) {
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll">
|
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll">
|
||||||
{faceImages.map((image: string) => (
|
{faceImages.map((image: string) => (
|
||||||
@ -489,7 +502,7 @@ function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
|
|||||||
key={image}
|
key={image}
|
||||||
name={pageToggle}
|
name={pageToggle}
|
||||||
image={image}
|
image={image}
|
||||||
onRefresh={onRefresh}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -499,31 +512,10 @@ function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
|
|||||||
type FaceImageProps = {
|
type FaceImageProps = {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
onRefresh: () => void;
|
onDelete: (name: string, ids: string[]) => void;
|
||||||
};
|
};
|
||||||
function FaceImage({ name, image, onRefresh }: FaceImageProps) {
|
function FaceImage({ name, image, onDelete }: FaceImageProps) {
|
||||||
const { t } = useTranslation(["views/faceLibrary"]);
|
const { t } = useTranslation(["views/faceLibrary"]);
|
||||||
const onDelete = useCallback(() => {
|
|
||||||
axios
|
|
||||||
.post(`/faces/${name}/delete`, { ids: [image] })
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp.status == 200) {
|
|
||||||
toast.success(t("toast.success.deletedFace"), {
|
|
||||||
position: "top-center",
|
|
||||||
});
|
|
||||||
onRefresh();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const errorMessage =
|
|
||||||
error.response?.data?.message ||
|
|
||||||
error.response?.data?.detail ||
|
|
||||||
"Unknown error";
|
|
||||||
toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), {
|
|
||||||
position: "top-center",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [name, image, onRefresh, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col rounded-lg">
|
<div className="relative flex flex-col rounded-lg">
|
||||||
@ -540,7 +532,7 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) {
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<LuTrash2
|
<LuTrash2
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
onClick={onDelete}
|
onClick={() => onDelete(name, [image])}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
|
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user