Convert face selection to separate component

This commit is contained in:
Nicolas Mowen 2025-04-10 14:07:17 -06:00
parent 29ab7f247e
commit e1c2364877
3 changed files with 128 additions and 91 deletions

View File

@ -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 (
<div className={className ?? ""}>
{newFace && (
<TextEntryDialog
open={true}
setOpen={setNewFace}
title={t("createFaceLibrary.new")}
onSave={(newName) => onTrainAttempt(newName)}
/>
)}
<Tooltip>
<Selector>
<SelectorTrigger asChild>
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
</SelectorTrigger>
<SelectorContent
className={cn(
"max-h-[75dvh] overflow-hidden",
isMobile && "mx-1 gap-2 rounded-t-2xl px-4",
)}
>
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
<div
className={cn(
"flex flex-col",
isMobile && "gap-2 overflow-y-auto pb-4",
)}
>
<SelectorItem
className="flex cursor-pointer gap-2 capitalize"
onClick={() => setNewFace(true)}
>
<LuPlus />
{t("createFaceLibrary.new")}
</SelectorItem>
{faceNames.map((faceName) => (
<SelectorItem
key={faceName}
className="flex cursor-pointer gap-2 capitalize"
onClick={() => onTrainAttempt(faceName)}
>
<LuScanFace />
{faceName}
</SelectorItem>
))}
</div>
</SelectorContent>
</Selector>
<TooltipContent>{t("trainFace")}</TooltipContent>
</Tooltip>
</div>
);
}

View File

@ -57,7 +57,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; 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 { useTranslation } from "react-i18next";
import { TbFaceId } from "react-icons/tb"; import { TbFaceId } from "react-icons/tb";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import FaceSelectionDialog from "../FaceSelectionDialog";
const SEARCH_TABS = [ const SEARCH_TABS = [
"details", "details",
@ -844,30 +844,18 @@ function ObjectDetailsTab({
</Button> </Button>
)} )}
{hasFace && ( {hasFace && (
<DropdownMenu> <FaceSelectionDialog
<DropdownMenuTrigger asChild> className="w-full"
<Button className="w-full"> faceNames={faceNames}
<div className="flex gap-1"> onTrainAttempt={onTrainFace}
<TbFaceId /> >
{t("trainFace", { ns: "views/faceLibrary" })} <Button className="w-full">
</div> <div className="flex gap-1">
</Button> <TbFaceId />
</DropdownMenuTrigger> {t("trainFace", { ns: "views/faceLibrary" })}
<DropdownMenuContent> </div>
<DropdownMenuLabel> </Button>
{t("trainFaceAs", { ns: "views/faceLibrary" })} </FaceSelectionDialog>
</DropdownMenuLabel>
{faceNames.map((faceName) => (
<DropdownMenuItem
key={faceName}
className="cursor-pointer capitalize"
onClick={() => onTrainFace(faceName)}
>
{faceName}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)} )}
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog"; import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
import FaceSelectionDialog from "@/components/overlay/FaceSelectionDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -13,17 +14,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerTrigger,
} from "@/components/ui/drawer";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSeparator, DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@ -48,7 +42,6 @@ import { isDesktop, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
LuImagePlus, LuImagePlus,
LuPlus,
LuRefreshCw, LuRefreshCw,
LuScanFace, LuScanFace,
LuSearch, LuSearch,
@ -789,8 +782,6 @@ function FaceAttempt({
// interaction // interaction
const [newFace, setNewFace] = useState(false);
const imgRef = useRef<HTMLImageElement | null>(null); const imgRef = useRef<HTMLImageElement | null>(null);
useContextMenu(imgRef, () => { useContextMenu(imgRef, () => {
@ -848,22 +839,8 @@ function FaceAttempt({
}); });
}, [data, onRefresh, t]); }, [data, onRefresh, t]);
const Selector = isDesktop ? DropdownMenu : Drawer;
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent;
const SelectorItem = isDesktop ? DropdownMenuItem : DrawerClose;
return ( return (
<> <>
{newFace && (
<TextEntryDialog
open={true}
setOpen={setNewFace}
title={t("createFaceLibrary.new")}
onSave={(newName) => onTrainAttempt(newName)}
/>
)}
<div <div
className={cn( className={cn(
"relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]", "relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]",
@ -906,48 +883,12 @@ function FaceAttempt({
</div> </div>
</div> </div>
<div className="flex flex-row items-start justify-end gap-5 md:gap-4"> <div className="flex flex-row items-start justify-end gap-5 md:gap-4">
<Tooltip> <FaceSelectionDialog
<Selector> faceNames={faceNames}
<SelectorTrigger asChild> onTrainAttempt={onTrainAttempt}
<TooltipTrigger> >
<AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" /> <AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
</TooltipTrigger> </FaceSelectionDialog>
</SelectorTrigger>
<SelectorContent
className={cn(
"max-h-[75dvh] overflow-hidden",
isMobile && "mx-1 gap-2 rounded-t-2xl px-4",
)}
>
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
<div
className={cn(
"flex flex-col",
isMobile && "gap-2 overflow-y-auto pb-4",
)}
>
<SelectorItem
className="flex cursor-pointer gap-2 capitalize"
onClick={() => setNewFace(true)}
>
<LuPlus />
{t("createFaceLibrary.new")}
</SelectorItem>
{faceNames.map((faceName) => (
<SelectorItem
key={faceName}
className="flex cursor-pointer gap-2 capitalize"
onClick={() => onTrainAttempt(faceName)}
>
<LuScanFace />
{faceName}
</SelectorItem>
))}
</div>
</SelectorContent>
</Selector>
<TooltipContent>{t("trainFace")}</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<LuRefreshCw <LuRefreshCw