mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-15 16:31:14 +03:00
* improve scroll handling for non-modal DropdownMenu in classification and face selection dialogs * clean up * fix incorrect key capitalization * fix profile array overrides not replacing base arrays don't use lodash merge(), it does positional merging and an empty source array doesn't override the destination, and shorter arrays leak destination elements through. backend is unaffected, so the saved config and actual backend functionality was right * only show audio debug tab when audio is enabled in config * move apple_compatibility out of advanced * remove retry_interval from UI 99% of users should never be changing this * hide switch in optionalfieldwidget if editing a profile * add override badges for cameras and profiles collect shared functions into the config util and separate hooks * Use new models endpoint info to determine modalities * clarify language * fix linter --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import {
|
|
Drawer,
|
|
DrawerClose,
|
|
DrawerContent,
|
|
DrawerDescription,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
DrawerTrigger,
|
|
} from "@/components/ui/drawer";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { isDesktop, isMobile } from "react-device-detect";
|
|
import { useTranslation } from "react-i18next";
|
|
import { cn } from "@/lib/utils";
|
|
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
|
import TextEntryDialog from "./dialog/TextEntryDialog";
|
|
import { Button } from "../ui/button";
|
|
|
|
type FaceSelectionDialogProps = {
|
|
className?: string;
|
|
faceNames: string[];
|
|
excludeName?: string;
|
|
dialogLabel?: string;
|
|
tooltipLabel?: string;
|
|
onTrainAttempt: (name: string) => void;
|
|
children: ReactNode;
|
|
};
|
|
export default function FaceSelectionDialog({
|
|
className,
|
|
faceNames,
|
|
excludeName,
|
|
dialogLabel,
|
|
tooltipLabel,
|
|
onTrainAttempt,
|
|
children,
|
|
}: FaceSelectionDialogProps) {
|
|
const { t } = useTranslation(["views/faceLibrary"]);
|
|
|
|
const filteredNames = useMemo(
|
|
() =>
|
|
excludeName ? faceNames.filter((n) => n !== excludeName) : faceNames,
|
|
[faceNames, excludeName],
|
|
);
|
|
|
|
const isChildButton = useMemo(
|
|
() => React.isValidElement(children) && children.type === Button,
|
|
[children],
|
|
);
|
|
|
|
// control
|
|
const [newFace, setNewFace] = useState(false);
|
|
|
|
// Non-modal Radix DropdownMenu doesn't propagate wheel events to nested
|
|
// scroll containers, so attach a non-passive listener that scrolls manually.
|
|
const scrollContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
if (!el || !isDesktop) return;
|
|
const handleWheel = (e: WheelEvent) => {
|
|
if (el.scrollHeight <= el.clientHeight) return;
|
|
e.preventDefault();
|
|
el.scrollTop += e.deltaY;
|
|
};
|
|
el.addEventListener("wheel", handleWheel, { passive: false });
|
|
return () => el.removeEventListener("wheel", handleWheel);
|
|
}, []);
|
|
|
|
// components
|
|
const Selector = isDesktop ? DropdownMenu : Drawer;
|
|
const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
|
|
const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent;
|
|
const SelectorItem = isDesktop
|
|
? DropdownMenuItem
|
|
: (props: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<DrawerClose asChild>
|
|
<div {...props} className={cn(props.className, "my-2")} />
|
|
</DrawerClose>
|
|
);
|
|
|
|
// keep modal false on desktop to prevent dismissable layer pointer events
|
|
// issue with dialog auto-close
|
|
return (
|
|
<div className={className ?? "flex"}>
|
|
{newFace && (
|
|
<TextEntryDialog
|
|
open={true}
|
|
setOpen={setNewFace}
|
|
title={t("createFaceLibrary.new")}
|
|
onSave={(newName) => onTrainAttempt(newName)}
|
|
/>
|
|
)}
|
|
<Selector {...(isDesktop ? { modal: false } : {})}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild={isChildButton}>
|
|
<SelectorTrigger asChild>{children}</SelectorTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{tooltipLabel ?? t("trainFace")}</TooltipContent>
|
|
</Tooltip>
|
|
<SelectorContent
|
|
ref={scrollContainerRef}
|
|
className={cn(
|
|
isDesktop && "scrollbar-container max-h-[40dvh] overflow-y-auto",
|
|
isMobile && "mx-1 gap-2 rounded-t-2xl px-4",
|
|
)}
|
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
>
|
|
{isMobile && (
|
|
<DrawerHeader className="sr-only">
|
|
<DrawerTitle>Details</DrawerTitle>
|
|
<DrawerDescription>Details</DrawerDescription>
|
|
</DrawerHeader>
|
|
)}
|
|
<DropdownMenuLabel>
|
|
{dialogLabel ?? t("trainFaceAs")}
|
|
</DropdownMenuLabel>
|
|
<div
|
|
className={cn(
|
|
"flex flex-col",
|
|
isMobile &&
|
|
"max-h-[40dvh] gap-2 overflow-y-auto overflow-x-hidden pb-4",
|
|
)}
|
|
>
|
|
{filteredNames.sort().map((faceName) => (
|
|
<SelectorItem
|
|
key={faceName}
|
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
|
onClick={() => onTrainAttempt(faceName)}
|
|
>
|
|
{faceName}
|
|
</SelectorItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<SelectorItem
|
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
|
onClick={() => setNewFace(true)}
|
|
>
|
|
{t("createFaceLibrary.new")}
|
|
</SelectorItem>
|
|
</div>
|
|
</SelectorContent>
|
|
</Selector>
|
|
</div>
|
|
);
|
|
}
|