diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index ea3210eee..48bc6401c 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -40,12 +40,17 @@ "title": "Delete Name", "desc": "Are you sure you want to delete the collection {{name}}? This will permanently delete all associated faces." }, + "deleteFaceAttempts": { + "title": "Delete Faces", + "desc_one": "Are you sure you want to delete {{count}} face? This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} faces? This action cannot be undone." + }, "renameFace": { "title": "Rename Face", "desc": "Enter a new name for {{name}}" }, "button": { - "deleteFaceAttempts": "Delete Face Attempts", + "deleteFaceAttempts": "Delete Faces", "addFace": "Add Face", "renameFace": "Rename Face", "deleteFace": "Delete Face", diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 3a33cb204..d5ba4c26e 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -6,7 +6,17 @@ import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizard import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; import FaceSelectionDialog from "@/components/overlay/FaceSelectionDialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Dialog, DialogContent, @@ -44,7 +54,7 @@ import { TooltipPortal } from "@radix-ui/react-tooltip"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { LuFolderCheck, LuImagePlus, @@ -165,6 +175,11 @@ export default function FaceLibrary() { [selectedFaces, setSelectedFaces], ); + const [deleteDialogOpen, setDeleteDialogOpen] = useState<{ + name: string; + ids: string[]; + } | null>(null); + const onDelete = useCallback( (name: string, ids: string[], isName: boolean = false) => { axios @@ -191,7 +206,7 @@ export default function FaceLibrary() { if (faceImages.length == 1) { // face has been deleted - setPageToggle(""); + setPageToggle("train"); } refreshFaces(); @@ -244,29 +259,32 @@ export default function FaceLibrary() { // keyboard - useKeyboardListener( - page === "train" ? ["a", "Escape"] : [], - (key, modifiers) => { - if (modifiers.repeat || !modifiers.down) { - return; - } + useKeyboardListener(["a", "Escape"], (key, modifiers) => { + if (modifiers.repeat || !modifiers.down) { + return; + } - switch (key) { - case "a": - if (modifiers.ctrl) { - if (selectedFaces.length) { - setSelectedFaces([]); - } else { - setSelectedFaces([...trainImages]); - } + switch (key) { + case "a": + if (modifiers.ctrl) { + if (selectedFaces.length) { + setSelectedFaces([]); + } else { + setSelectedFaces([ + ...(pageToggle === "train" ? trainImages : faceImages), + ]); } - break; - case "Escape": - setSelectedFaces([]); - break; - } - }, - ); + } + break; + case "Escape": + setSelectedFaces([]); + break; + } + }); + + useEffect(() => { + setSelectedFaces([]); + }, [pageToggle]); if (!config) { return ; @@ -276,6 +294,41 @@ export default function FaceLibrary() {
+ setDeleteDialogOpen(null)} + > + + + {t("deleteFaceAttempts.title")} + + + + deleteFaceAttempts.desc + + + + + {t("button.cancel", { ns: "common" })} + + { + if (deleteDialogOpen) { + onDelete(deleteDialogOpen.name, deleteDialogOpen.ids); + setDeleteDialogOpen(null); + } + }} + > + {t("button.delete", { ns: "common" })} + + + + +
)} - {pageToggle && + {pageToggle && faceImages.length === 0 && pageToggle !== "train" ? ( +
+ + No faces available +
+ ) : ( + pageToggle && (pageToggle == "train" ? ( - ))} + )) + )} ); } @@ -1009,16 +1073,36 @@ function FaceAttempt({ type FaceGridProps = { faceImages: string[]; pageToggle: string; + selectedFaces: string[]; + onClickFaces: (images: string[], ctrl: boolean) => void; onDelete: (name: string, ids: string[]) => void; }; -function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) { - const sortedFaces = useMemo(() => faceImages.sort().reverse(), [faceImages]); +function FaceGrid({ + faceImages, + pageToggle, + selectedFaces, + onClickFaces, + onDelete, +}: FaceGridProps) { + const sortedFaces = useMemo( + () => (faceImages || []).sort().reverse(), + [faceImages], + ); + + if (sortedFaces.length === 0) { + return ( +
+ + No faces available +
+ ); + } return (
{sortedFaces.map((image: string) => ( @@ -1026,6 +1110,8 @@ function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) { key={image} name={pageToggle} image={image} + selected={selectedFaces.includes(image)} + onClickFaces={onClickFaces} onDelete={onDelete} /> ))} @@ -1036,22 +1122,44 @@ function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) { type FaceImageProps = { name: string; image: string; + selected: boolean; + onClickFaces: (images: string[], ctrl: boolean) => void; onDelete: (name: string, ids: string[]) => void; }; -function FaceImage({ name, image, onDelete }: FaceImageProps) { +function FaceImage({ + name, + image, + selected, + onClickFaces, + onDelete, +}: FaceImageProps) { const { t } = useTranslation(["views/faceLibrary"]); return ( -
+
{ + e.stopPropagation(); + onClickFaces([image], e.ctrlKey || e.metaKey); + }} + >
- +
-
+
{name}
@@ -1061,7 +1169,10 @@ function FaceImage({ name, image, onDelete }: FaceImageProps) { onDelete(name, [image])} + onClick={(e) => { + e.stopPropagation(); + onDelete(name, [image]); + }} /> {t("button.deleteFaceAttempts")}