Miscellaneous fixes (0.17 beta) (#21934)

* improve chip tooltip display

- use formatList to use i18n separators instead of commas
- ensure the correct event type is used so sublabels are not run through normalization
- remove smart-capitalization classes as translated labels use i18n (which includes capitalization)
- give icons an optional key so that the console doesn't complain about duplication when rendering

* Add grace period for recording segment checks to prevent spurious ffmpeg restarts

* add admin precedence to proxy role_map resolution to prevent downgrade

* clean up

* formatting

* work around radix pointer events issue when dialog is opened from drawer

fixes https://github.com/blakeblackshear/frigate/discussions/21940

* prevent console warnings about missing titles and descriptions

make these invisible with sr-only

* remove duplicate language

* Adjust handling for device sizes

* Cleanup

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2026-02-12 14:42:08 -06:00
committed by GitHub
parent e1005ac2a5
commit 67e3f8eefa
14 changed files with 268 additions and 122 deletions

View File

@@ -19,6 +19,8 @@ import { Button } from "../ui/button";
import { FaCircleCheck } from "react-icons/fa6";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type AnimatedEventCardProps = {
event: ReviewSegment;
@@ -50,26 +52,37 @@ export function AnimatedEventCard({
fetchPreviews: !currentHour,
});
const getEventType = useCallback(
(text: string) => {
if (event.data.sub_labels?.includes(text)) return "manual";
if (event.data.audio.includes(text)) return "audio";
return "object";
},
[event],
);
const tooltipText = useMemo(() => {
if (event?.data?.metadata?.title) {
return event.data.metadata.title;
}
return (
`${[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined && !item.includes("-verified"))
.map((text) => text.charAt(0).toUpperCase() + text.substring(1))
.sort()
.join(", ")
.replaceAll("-verified", "")} ` + t("detected")
`${formatList(
[
...new Set([
...(event.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)} ` + t("detected")
);
}, [event, t]);
}, [event, getEventType, t]);
// visibility

View File

@@ -33,13 +33,14 @@ import axios from "axios";
import { toast } from "sonner";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { Button, buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { LuCircle } from "react-icons/lu";
import { MdAutoAwesome } from "react-icons/md";
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type ReviewCardProps = {
event: ReviewSegment;
@@ -123,6 +124,12 @@ export default function ReviewCard({
}
}, [bypassDialogRef, onDelete]);
const getEventType = (text: string) => {
if (event.data.sub_labels?.includes(text)) return "manual";
if (event.data.audio.includes(text)) return "audio";
return "object";
};
const content = (
<div
className="relative flex w-full cursor-pointer flex-col gap-1.5"
@@ -197,20 +204,20 @@ export default function ReviewCard({
</div>
</TooltipTrigger>
<TooltipContent className="smart-capitalize">
{[
...new Set([
...(event.data.objects || []),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) => capitalizeFirstLetter(text))
.sort()
.join(", ")
.replaceAll("-verified", "")}
{formatList(
[
...new Set([
...(event.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)}
</TooltipContent>
</Tooltip>
<TimeAgo

View File

@@ -42,12 +42,20 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { isDesktop, isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerTitle,
DrawerTrigger,
} from "../ui/drawer";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { TooltipPortal } from "@radix-ui/react-tooltip";
@@ -194,6 +202,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "max-h-[75dvh] overflow-hidden p-2"
}
>
{!isDesktop && (
<>
<DrawerTitle className="sr-only">
{t("menu.settings")}
</DrawerTitle>
<DrawerDescription className="sr-only">
{t("menu.settings")}
</DrawerDescription>
</>
)}
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
{isMobile && (
<div className="mb-2">
@@ -355,6 +373,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "scrollbar-container max-h-[75dvh] w-[92%] overflow-y-scroll rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.languages")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.languages")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{languages.map(({ code, label }) => (
<MenuItem
@@ -395,6 +423,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.darkMode.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.darkMode.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
<MenuItem
className={
@@ -472,6 +510,16 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.theme.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.theme.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{colorSchemes.map((scheme) => (
<MenuItem

View File

@@ -4,6 +4,7 @@ import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
@@ -37,6 +38,12 @@ export default function RestartDialog({
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
const clearBodyPointerEvents = () => {
if (typeof document !== "undefined") {
document.body.style.pointerEvents = "";
}
};
useEffect(() => {
setRestartDialogOpen(isOpen);
}, [isOpen]);
@@ -74,14 +81,25 @@ export default function RestartDialog({
<>
<AlertDialog
open={restartDialogOpen}
onOpenChange={() => {
setRestartDialogOpen(false);
onClose();
onOpenChange={(open) => {
if (!open) {
setRestartDialogOpen(false);
onClose();
clearBodyPointerEvents();
}
}}
>
<AlertDialogContent>
<AlertDialogContent
onCloseAutoFocus={(event) => {
event.preventDefault();
clearBodyPointerEvents();
}}
>
<AlertDialogHeader>
<AlertDialogTitle>{t("restart.title")}</AlertDialogTitle>
<AlertDialogDescription className="sr-only">
{t("restart.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>

View File

@@ -371,22 +371,23 @@ export default function LivePlayer({
</TooltipTrigger>
</div>
<TooltipPortal>
<TooltipContent className="smart-capitalize">
<TooltipContent>
{formatList(
[
...new Set([
...(objects || []).map(({ label, sub_label }) =>
label.endsWith("verified")
? sub_label
: label.replaceAll("_", " "),
),
]),
]
.filter((label) => label?.includes("-verified") == false)
.map((label) =>
getTranslatedLabel(label.replace("-verified", "")),
)
.sort(),
...new Set(
(objects || [])
.map(({ label, sub_label }) => {
const isManual = label.endsWith("verified");
const text = isManual ? sub_label : label;
const type = isManual ? "manual" : "object";
return getTranslatedLabel(text, type);
})
.filter(
(translated) =>
translated && !translated.includes("-verified"),
),
),
].sort(),
)}
</TooltipContent>
</TooltipPortal>

View File

@@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
import { FaExclamationTriangle } from "react-icons/fa";
import { MdOutlinePersonSearch } from "react-icons/md";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type PreviewPlayerProps = {
review: ReviewSegment;
@@ -182,9 +183,8 @@ export default function PreviewThumbnailPlayer({
);
const getEventType = (text: string) => {
if (review.data.objects.includes(text)) return "object";
if (review.data.audio.includes(text)) return "audio";
if (review.data.sub_labels?.includes(text)) return "manual";
if (review.data.audio.includes(text)) return "audio";
return "object";
};
@@ -268,13 +268,16 @@ export default function PreviewThumbnailPlayer({
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} bg-gradient-to-br ${review.has_been_reviewed ? "bg-green-600 from-green-600 to-green-700" : "bg-gray-500 from-gray-400 to-gray-500"} z-0`}
onClick={() => onClick(review, false, true)}
>
{review.data.objects.sort().map((object) => {
return getIconForLabel(
object,
"object",
"size-3 text-white",
);
})}
{review.data.objects
.sort()
.map((object, idx) =>
getIconForLabel(
object,
"object",
"size-3 text-white",
`${object}-${idx}`,
),
)}
{review.data.audio.map((audio) => {
return getIconForLabel(
audio,
@@ -288,23 +291,25 @@ export default function PreviewThumbnailPlayer({
</div>
</TooltipTrigger>
</div>
<TooltipContent className="smart-capitalize">
<TooltipContent>
{review.data.metadata
? review.data.metadata.title
: [
...new Set([
...(review.data.objects || []),
...(review.data.sub_labels || []),
...(review.data.audio || []),
]),
]
.filter(
(item) =>
item !== undefined && !item.includes("-verified"),
)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort()
.join(", ")}
: formatList(
[
...new Set([
...(review.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(review.data.sub_labels || []),
...(review.data.audio || []),
]),
]
.filter((item) => item !== undefined)
.map((text) =>
getTranslatedLabel(text, getEventType(text)),
)
.sort(),
)}
</TooltipContent>
</Tooltip>
{!!(