Improve multi select

This commit is contained in:
Nicolas Mowen 2025-03-17 12:42:40 -06:00
parent f21aef3a24
commit 9df276ba0d
2 changed files with 60 additions and 61 deletions

View File

@ -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>

View File

@ -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>