From e1c236487727639872716929e5cb0cd778880e72 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 10 Apr 2025 14:07:17 -0600 Subject: [PATCH] Convert face selection to separate component --- .../overlay/FaceSelectionDialog.tsx | 108 ++++++++++++++++++ .../overlay/detail/SearchDetailDialog.tsx | 38 +++--- web/src/pages/FaceLibrary.tsx | 73 ++---------- 3 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 web/src/components/overlay/FaceSelectionDialog.tsx diff --git a/web/src/components/overlay/FaceSelectionDialog.tsx b/web/src/components/overlay/FaceSelectionDialog.tsx new file mode 100644 index 0000000000..4e9c644b83 --- /dev/null +++ b/web/src/components/overlay/FaceSelectionDialog.tsx @@ -0,0 +1,108 @@ +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop, isMobile } from "react-device-detect"; +import { LuPlus, LuScanFace } from "react-icons/lu"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import React, { ReactNode, useMemo, useState } from "react"; +import TextEntryDialog from "./dialog/TextEntryDialog"; +import { Button } from "../ui/button"; + +type FaceSelectionDialogProps = { + className?: string; + faceNames: string[]; + onTrainAttempt: (name: string) => void; + children: ReactNode; +}; +export default function FaceSelectionDialog({ + className, + faceNames, + onTrainAttempt, + children, +}: FaceSelectionDialogProps) { + const { t } = useTranslation(["views/faceLibrary"]); + + const isChildButton = useMemo( + () => React.isValidElement(children) && children.type === Button, + [children], + ); + + // control + const [newFace, setNewFace] = useState(false); + + // components + const Selector = isDesktop ? DropdownMenu : Drawer; + const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; + const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent; + const SelectorItem = isDesktop ? DropdownMenuItem : DrawerClose; + + return ( +
+ {newFace && ( + onTrainAttempt(newName)} + /> + )} + + + + + {children} + + + {t("trainFaceAs")} +
+ setNewFace(true)} + > + + {t("createFaceLibrary.new")} + + {faceNames.map((faceName) => ( + onTrainAttempt(faceName)} + > + + {faceName} + + ))} +
+
+
+ {t("trainFace")} +
+
+ ); +} diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 6762890d4a..afd52b6296 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -57,7 +57,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; @@ -77,6 +76,7 @@ import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import { useTranslation } from "react-i18next"; import { TbFaceId } from "react-icons/tb"; import { useIsAdmin } from "@/hooks/use-is-admin"; +import FaceSelectionDialog from "../FaceSelectionDialog"; const SEARCH_TABS = [ "details", @@ -844,30 +844,18 @@ function ObjectDetailsTab({ )} {hasFace && ( - - - - - - - {t("trainFaceAs", { ns: "views/faceLibrary" })} - - {faceNames.map((faceName) => ( - onTrainFace(faceName)} - > - {faceName} - - ))} - - + + + )} diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 4c7f59b83c..0057db094a 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -5,6 +5,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog"; 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 { Dialog, @@ -13,17 +14,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerTrigger, -} from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; @@ -48,7 +42,6 @@ import { isDesktop, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { LuImagePlus, - LuPlus, LuRefreshCw, LuScanFace, LuSearch, @@ -789,8 +782,6 @@ function FaceAttempt({ // interaction - const [newFace, setNewFace] = useState(false); - const imgRef = useRef(null); useContextMenu(imgRef, () => { @@ -848,22 +839,8 @@ function FaceAttempt({ }); }, [data, onRefresh, t]); - const Selector = isDesktop ? DropdownMenu : Drawer; - const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; - const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent; - const SelectorItem = isDesktop ? DropdownMenuItem : DrawerClose; - return ( <> - {newFace && ( - onTrainAttempt(newName)} - /> - )} -
- - - - - - - - - {t("trainFaceAs")} -
- setNewFace(true)} - > - - {t("createFaceLibrary.new")} - - {faceNames.map((faceName) => ( - onTrainAttempt(faceName)} - > - - {faceName} - - ))} -
-
-
- {t("trainFace")} -
+ + +