mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 04:57:42 +03:00
Convert face selection to separate component
This commit is contained in:
parent
29ab7f247e
commit
e1c2364877
108
web/src/components/overlay/FaceSelectionDialog.tsx
Normal file
108
web/src/components/overlay/FaceSelectionDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user