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({
open,
setOpen,
onFinish,
}: CreateFaceWizardDialogProps) {
const { t } = useTranslation("views/faceLibrary");
@ -142,7 +143,13 @@ export default function CreateFaceWizardDialog({
</Link>
</div>
<div className="flex justify-end">
<Button variant="select" onClick={() => handleReset()}>
<Button
variant="select"
onClick={() => {
onFinish();
handleReset();
}}
>
{t("button.done", { ns: "common" })}
</Button>
</div>

View File

@ -121,7 +121,11 @@ export default function FaceLibrary() {
const [selectedFaces, setSelectedFaces] = useState<string[]>([]);
const onClickFace = useCallback(
(imageId: string) => {
(imageId: string, ctrl: boolean) => {
if (selectedFaces.length == 0 && !ctrl) {
return;
}
const index = selectedFaces.indexOf(imageId);
if (index != -1) {
@ -143,39 +147,42 @@ export default function FaceLibrary() {
[selectedFaces, setSelectedFaces],
);
const onDelete = useCallback(() => {
axios
.post(`/faces/train/delete`, { ids: selectedFaces })
.then((resp) => {
setSelectedFaces([]);
const onDelete = useCallback(
(name: string, ids: string[]) => {
axios
.post(`/faces/${name}/delete`, { ids })
.then((resp) => {
setSelectedFaces([]);
if (resp.status == 200) {
toast.success(t("toast.success.deletedFace"), {
if (resp.status == 200) {
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",
});
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
useKeyboardListener(["a"], (key, modifiers) => {
useKeyboardListener(["a", "Escape"], (key, modifiers) => {
if (modifiers.repeat || !modifiers.down) {
return;
}
@ -186,6 +193,9 @@ export default function FaceLibrary() {
setSelectedFaces([...trainImages]);
}
break;
case "Escape":
setSelectedFaces([]);
break;
}
});
@ -258,7 +268,10 @@ export default function FaceLibrary() {
</ScrollArea>
{selectedFaces?.length > 0 ? (
<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" />
{t("button.deleteFaceAttempts")}
</Button>
@ -292,7 +305,7 @@ export default function FaceLibrary() {
<FaceGrid
faceImages={faceImages}
pageToggle={pageToggle}
onRefresh={refreshFaces}
onDelete={onDelete}
/>
))}
</div>
@ -304,7 +317,7 @@ type TrainingGridProps = {
attemptImages: string[];
faceNames: string[];
selectedFaces: string[];
onClickFace: (image: string) => void;
onClickFace: (image: string, ctrl: boolean) => void;
onRefresh: () => void;
};
function TrainingGrid({
@ -324,7 +337,7 @@ function TrainingGrid({
faceNames={faceNames}
threshold={config.face_recognition.recognition_threshold}
selected={selectedFaces.includes(image)}
onClick={() => onClickFace(image)}
onClick={(meta) => onClickFace(image, meta)}
onRefresh={onRefresh}
/>
))}
@ -337,7 +350,7 @@ type FaceAttemptProps = {
faceNames: string[];
threshold: number;
selected: boolean;
onClick: () => void;
onClick: (meta: boolean) => void;
onRefresh: () => void;
};
function FaceAttempt({
@ -415,7 +428,7 @@ function FaceAttempt({
? "shadow-selected outline-selected"
: "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">
<img className="size-44" src={`${baseUrl}clips/faces/train/${image}`} />
@ -479,9 +492,9 @@ function FaceAttempt({
type FaceGridProps = {
faceImages: string[];
pageToggle: string;
onRefresh: () => void;
onDelete: (name: string, ids: string[]) => void;
};
function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) {
return (
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll">
{faceImages.map((image: string) => (
@ -489,7 +502,7 @@ function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
key={image}
name={pageToggle}
image={image}
onRefresh={onRefresh}
onDelete={onDelete}
/>
))}
</div>
@ -499,31 +512,10 @@ function FaceGrid({ faceImages, pageToggle, onRefresh }: FaceGridProps) {
type FaceImageProps = {
name: 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 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 (
<div className="relative flex flex-col rounded-lg">
@ -540,7 +532,7 @@ function FaceImage({ name, image, onRefresh }: FaceImageProps) {
<TooltipTrigger>
<LuTrash2
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
onClick={onDelete}
onClick={() => onDelete(name, [image])}
/>
</TooltipTrigger>
<TooltipContent>{t("button.deleteFaceAttempts")}</TooltipContent>