mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-19 23:08:08 +02:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{!!(
|
||||
|
||||
Reference in New Issue
Block a user