mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-17 05:38:25 +03:00
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
This commit is contained in:
parent
6accc38275
commit
1fec95f88e
@ -19,6 +19,8 @@ import { Button } from "../ui/button";
|
|||||||
import { FaCircleCheck } from "react-icons/fa6";
|
import { FaCircleCheck } from "react-icons/fa6";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { formatList } from "@/utils/stringUtil";
|
||||||
|
|
||||||
type AnimatedEventCardProps = {
|
type AnimatedEventCardProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
@ -50,26 +52,37 @@ export function AnimatedEventCard({
|
|||||||
fetchPreviews: !currentHour,
|
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(() => {
|
const tooltipText = useMemo(() => {
|
||||||
if (event?.data?.metadata?.title) {
|
if (event?.data?.metadata?.title) {
|
||||||
return event.data.metadata.title;
|
return event.data.metadata.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${[
|
`${formatList(
|
||||||
...new Set([
|
[
|
||||||
...(event.data.objects || []),
|
...new Set([
|
||||||
...(event.data.sub_labels || []),
|
...(event.data.objects || []).map((text) =>
|
||||||
...(event.data.audio || []),
|
text.replace("-verified", ""),
|
||||||
]),
|
),
|
||||||
]
|
...(event.data.sub_labels || []),
|
||||||
.filter((item) => item !== undefined && !item.includes("-verified"))
|
...(event.data.audio || []),
|
||||||
.map((text) => text.charAt(0).toUpperCase() + text.substring(1))
|
]),
|
||||||
.sort()
|
]
|
||||||
.join(", ")
|
.filter((item) => item !== undefined)
|
||||||
.replaceAll("-verified", "")} ` + t("detected")
|
.map((text) => getTranslatedLabel(text, getEventType(text)))
|
||||||
|
.sort(),
|
||||||
|
)} ` + t("detected")
|
||||||
);
|
);
|
||||||
}, [event, t]);
|
}, [event, getEventType, t]);
|
||||||
|
|
||||||
// visibility
|
// visibility
|
||||||
|
|
||||||
|
|||||||
@ -33,13 +33,14 @@ import axios from "axios";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
|
||||||
import { Button, buttonVariants } from "../ui/button";
|
import { Button, buttonVariants } from "../ui/button";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LuCircle } from "react-icons/lu";
|
import { LuCircle } from "react-icons/lu";
|
||||||
import { MdAutoAwesome } from "react-icons/md";
|
import { MdAutoAwesome } from "react-icons/md";
|
||||||
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
|
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
|
||||||
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { formatList } from "@/utils/stringUtil";
|
||||||
|
|
||||||
type ReviewCardProps = {
|
type ReviewCardProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
@ -123,6 +124,12 @@ export default function ReviewCard({
|
|||||||
}
|
}
|
||||||
}, [bypassDialogRef, onDelete]);
|
}, [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 = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
className="relative flex w-full cursor-pointer flex-col gap-1.5"
|
className="relative flex w-full cursor-pointer flex-col gap-1.5"
|
||||||
@ -197,20 +204,20 @@ export default function ReviewCard({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="smart-capitalize">
|
<TooltipContent className="smart-capitalize">
|
||||||
{[
|
{formatList(
|
||||||
...new Set([
|
[
|
||||||
...(event.data.objects || []),
|
...new Set([
|
||||||
...(event.data.sub_labels || []),
|
...(event.data.objects || []).map((text) =>
|
||||||
...(event.data.audio || []),
|
text.replace("-verified", ""),
|
||||||
]),
|
),
|
||||||
]
|
...(event.data.sub_labels || []),
|
||||||
.filter(
|
...(event.data.audio || []),
|
||||||
(item) => item !== undefined && !item.includes("-verified"),
|
]),
|
||||||
)
|
]
|
||||||
.map((text) => capitalizeFirstLetter(text))
|
.filter((item) => item !== undefined)
|
||||||
.sort()
|
.map((text) => getTranslatedLabel(text, getEventType(text)))
|
||||||
.join(", ")
|
.sort(),
|
||||||
.replaceAll("-verified", "")}
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<TimeAgo
|
<TimeAgo
|
||||||
|
|||||||
@ -371,22 +371,23 @@ export default function LivePlayer({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
</div>
|
</div>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent className="smart-capitalize">
|
<TooltipContent>
|
||||||
{formatList(
|
{formatList(
|
||||||
[
|
[
|
||||||
...new Set([
|
...new Set(
|
||||||
...(objects || []).map(({ label, sub_label }) =>
|
(objects || [])
|
||||||
label.endsWith("verified")
|
.map(({ label, sub_label }) => {
|
||||||
? sub_label
|
const isManual = label.endsWith("verified");
|
||||||
: label.replaceAll("_", " "),
|
const text = isManual ? sub_label : label;
|
||||||
),
|
const type = isManual ? "manual" : "object";
|
||||||
]),
|
return getTranslatedLabel(text, type);
|
||||||
]
|
})
|
||||||
.filter((label) => label?.includes("-verified") == false)
|
.filter(
|
||||||
.map((label) =>
|
(translated) =>
|
||||||
getTranslatedLabel(label.replace("-verified", "")),
|
translated && !translated.includes("-verified"),
|
||||||
)
|
),
|
||||||
.sort(),
|
),
|
||||||
|
].sort(),
|
||||||
)}
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { FaExclamationTriangle } from "react-icons/fa";
|
import { FaExclamationTriangle } from "react-icons/fa";
|
||||||
import { MdOutlinePersonSearch } from "react-icons/md";
|
import { MdOutlinePersonSearch } from "react-icons/md";
|
||||||
import { getTranslatedLabel } from "@/utils/i18n";
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { formatList } from "@/utils/stringUtil";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
@ -182,9 +183,8 @@ export default function PreviewThumbnailPlayer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getEventType = (text: string) => {
|
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.sub_labels?.includes(text)) return "manual";
|
||||||
|
if (review.data.audio.includes(text)) return "audio";
|
||||||
return "object";
|
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`}
|
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)}
|
onClick={() => onClick(review, false, true)}
|
||||||
>
|
>
|
||||||
{review.data.objects.sort().map((object) => {
|
{review.data.objects
|
||||||
return getIconForLabel(
|
.sort()
|
||||||
object,
|
.map((object, idx) =>
|
||||||
"object",
|
getIconForLabel(
|
||||||
"size-3 text-white",
|
object,
|
||||||
);
|
"object",
|
||||||
})}
|
"size-3 text-white",
|
||||||
|
`${object}-${idx}`,
|
||||||
|
),
|
||||||
|
)}
|
||||||
{review.data.audio.map((audio) => {
|
{review.data.audio.map((audio) => {
|
||||||
return getIconForLabel(
|
return getIconForLabel(
|
||||||
audio,
|
audio,
|
||||||
@ -288,23 +291,25 @@ export default function PreviewThumbnailPlayer({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
</div>
|
</div>
|
||||||
<TooltipContent className="smart-capitalize">
|
<TooltipContent>
|
||||||
{review.data.metadata
|
{review.data.metadata
|
||||||
? review.data.metadata.title
|
? review.data.metadata.title
|
||||||
: [
|
: formatList(
|
||||||
...new Set([
|
[
|
||||||
...(review.data.objects || []),
|
...new Set([
|
||||||
...(review.data.sub_labels || []),
|
...(review.data.objects || []).map((text) =>
|
||||||
...(review.data.audio || []),
|
text.replace("-verified", ""),
|
||||||
]),
|
),
|
||||||
]
|
...(review.data.sub_labels || []),
|
||||||
.filter(
|
...(review.data.audio || []),
|
||||||
(item) =>
|
]),
|
||||||
item !== undefined && !item.includes("-verified"),
|
]
|
||||||
)
|
.filter((item) => item !== undefined)
|
||||||
.map((text) => getTranslatedLabel(text, getEventType(text)))
|
.map((text) =>
|
||||||
.sort()
|
getTranslatedLabel(text, getEventType(text)),
|
||||||
.join(", ")}
|
)
|
||||||
|
.sort(),
|
||||||
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{!!(
|
{!!(
|
||||||
|
|||||||
@ -62,83 +62,86 @@ export function getIconForLabel(
|
|||||||
label: string,
|
label: string,
|
||||||
type: EventType = "object",
|
type: EventType = "object",
|
||||||
className?: string,
|
className?: string,
|
||||||
|
key?: string,
|
||||||
) {
|
) {
|
||||||
|
const iconKey = key || label;
|
||||||
|
|
||||||
if (label.endsWith("-verified")) {
|
if (label.endsWith("-verified")) {
|
||||||
return getVerifiedIcon(label, className, type);
|
return getVerifiedIcon(label, className, type, iconKey);
|
||||||
} else if (label.endsWith("-plate")) {
|
} else if (label.endsWith("-plate")) {
|
||||||
return getRecognizedPlateIcon(label, className, type);
|
return getRecognizedPlateIcon(label, className, type, iconKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (label) {
|
switch (label) {
|
||||||
// objects
|
// objects
|
||||||
case "bear":
|
case "bear":
|
||||||
return <GiPolarBear key={label} className={className} />;
|
return <GiPolarBear key={iconKey} className={className} />;
|
||||||
case "bicycle":
|
case "bicycle":
|
||||||
return <FaBicycle key={label} className={className} />;
|
return <FaBicycle key={iconKey} className={className} />;
|
||||||
case "bird":
|
case "bird":
|
||||||
return <PiBirdFill key={label} className={className} />;
|
return <PiBirdFill key={iconKey} className={className} />;
|
||||||
case "boat":
|
case "boat":
|
||||||
return <GiSailboat key={label} className={className} />;
|
return <GiSailboat key={iconKey} className={className} />;
|
||||||
case "bus":
|
case "bus":
|
||||||
case "school_bus":
|
case "school_bus":
|
||||||
return <FaBus key={label} className={className} />;
|
return <FaBus key={iconKey} className={className} />;
|
||||||
case "car":
|
case "car":
|
||||||
case "vehicle":
|
case "vehicle":
|
||||||
return <FaCarSide key={label} className={className} />;
|
return <FaCarSide key={iconKey} className={className} />;
|
||||||
case "cat":
|
case "cat":
|
||||||
return <FaCat key={label} className={className} />;
|
return <FaCat key={iconKey} className={className} />;
|
||||||
case "deer":
|
case "deer":
|
||||||
return <GiDeer key={label} className={className} />;
|
return <GiDeer key={iconKey} className={className} />;
|
||||||
case "animal":
|
case "animal":
|
||||||
case "bark":
|
case "bark":
|
||||||
case "dog":
|
case "dog":
|
||||||
return <FaDog key={label} className={className} />;
|
return <FaDog key={iconKey} className={className} />;
|
||||||
case "fox":
|
case "fox":
|
||||||
return <GiFox key={label} className={className} />;
|
return <GiFox key={iconKey} className={className} />;
|
||||||
case "goat":
|
case "goat":
|
||||||
return <GiGoat key={label} className={className} />;
|
return <GiGoat key={iconKey} className={className} />;
|
||||||
case "horse":
|
case "horse":
|
||||||
return <FaHorse key={label} className={className} />;
|
return <FaHorse key={iconKey} className={className} />;
|
||||||
case "kangaroo":
|
case "kangaroo":
|
||||||
return <GiKangaroo key={label} className={className} />;
|
return <GiKangaroo key={iconKey} className={className} />;
|
||||||
case "license_plate":
|
case "license_plate":
|
||||||
return <LuScanBarcode key={label} className={className} />;
|
return <LuScanBarcode key={iconKey} className={className} />;
|
||||||
case "motorcycle":
|
case "motorcycle":
|
||||||
return <FaMotorcycle key={label} className={className} />;
|
return <FaMotorcycle key={iconKey} className={className} />;
|
||||||
case "mouse":
|
case "mouse":
|
||||||
return <FaMouse key={label} className={className} />;
|
return <FaMouse key={iconKey} className={className} />;
|
||||||
case "package":
|
case "package":
|
||||||
return <LuBox key={label} className={className} />;
|
return <LuBox key={iconKey} className={className} />;
|
||||||
case "person":
|
case "person":
|
||||||
return <BsPersonWalking key={label} className={className} />;
|
return <BsPersonWalking key={iconKey} className={className} />;
|
||||||
case "rabbit":
|
case "rabbit":
|
||||||
return <GiRabbit key={label} className={className} />;
|
return <GiRabbit key={iconKey} className={className} />;
|
||||||
case "raccoon":
|
case "raccoon":
|
||||||
return <GiRaccoonHead key={label} className={className} />;
|
return <GiRaccoonHead key={iconKey} className={className} />;
|
||||||
case "robot_lawnmower":
|
case "robot_lawnmower":
|
||||||
return <FaHockeyPuck key={label} className={className} />;
|
return <FaHockeyPuck key={iconKey} className={className} />;
|
||||||
case "sports_ball":
|
case "sports_ball":
|
||||||
return <FaFootballBall key={label} className={className} />;
|
return <FaFootballBall key={iconKey} className={className} />;
|
||||||
case "skunk":
|
case "skunk":
|
||||||
return <GiSquirrel key={label} className={className} />;
|
return <GiSquirrel key={iconKey} className={className} />;
|
||||||
case "squirrel":
|
case "squirrel":
|
||||||
return <LuIcons.LuSquirrel key={label} className={className} />;
|
return <LuIcons.LuSquirrel key={iconKey} className={className} />;
|
||||||
case "umbrella":
|
case "umbrella":
|
||||||
return <FaUmbrella key={label} className={className} />;
|
return <FaUmbrella key={iconKey} className={className} />;
|
||||||
case "waste_bin":
|
case "waste_bin":
|
||||||
return <FaRegTrashAlt key={label} className={className} />;
|
return <FaRegTrashAlt key={iconKey} className={className} />;
|
||||||
// audio
|
// audio
|
||||||
case "crying":
|
case "crying":
|
||||||
case "laughter":
|
case "laughter":
|
||||||
case "scream":
|
case "scream":
|
||||||
case "speech":
|
case "speech":
|
||||||
case "yell":
|
case "yell":
|
||||||
return <MdRecordVoiceOver key={label} className={className} />;
|
return <MdRecordVoiceOver key={iconKey} className={className} />;
|
||||||
case "fire_alarm":
|
case "fire_alarm":
|
||||||
return <FaFire key={label} className={className} />;
|
return <FaFire key={iconKey} className={className} />;
|
||||||
// sub labels
|
// sub labels
|
||||||
case "amazon":
|
case "amazon":
|
||||||
return <FaAmazon key={label} className={className} />;
|
return <FaAmazon key={iconKey} className={className} />;
|
||||||
case "an_post":
|
case "an_post":
|
||||||
case "canada_post":
|
case "canada_post":
|
||||||
case "dpd":
|
case "dpd":
|
||||||
@ -148,20 +151,20 @@ export function getIconForLabel(
|
|||||||
case "postnord":
|
case "postnord":
|
||||||
case "purolator":
|
case "purolator":
|
||||||
case "royal_mail":
|
case "royal_mail":
|
||||||
return <GiPostStamp key={label} className={className} />;
|
return <GiPostStamp key={iconKey} className={className} />;
|
||||||
case "dhl":
|
case "dhl":
|
||||||
return <FaDhl key={label} className={className} />;
|
return <FaDhl key={iconKey} className={className} />;
|
||||||
case "fedex":
|
case "fedex":
|
||||||
return <FaFedex key={label} className={className} />;
|
return <FaFedex key={iconKey} className={className} />;
|
||||||
case "ups":
|
case "ups":
|
||||||
return <FaUps key={label} className={className} />;
|
return <FaUps key={iconKey} className={className} />;
|
||||||
case "usps":
|
case "usps":
|
||||||
return <FaUsps key={label} className={className} />;
|
return <FaUsps key={iconKey} className={className} />;
|
||||||
default:
|
default:
|
||||||
if (type === "audio") {
|
if (type === "audio") {
|
||||||
return <GiSoundWaves key={label} className={className} />;
|
return <GiSoundWaves key={iconKey} className={className} />;
|
||||||
}
|
}
|
||||||
return <LuLassoSelect key={label} className={className} />;
|
return <LuLassoSelect key={iconKey} className={className} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,11 +172,12 @@ function getVerifiedIcon(
|
|||||||
label: string,
|
label: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
type: EventType = "object",
|
type: EventType = "object",
|
||||||
|
key?: string,
|
||||||
) {
|
) {
|
||||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={label} className="relative flex items-center">
|
<div key={key} className="relative flex items-center">
|
||||||
{getIconForLabel(simpleLabel, type, className)}
|
{getIconForLabel(simpleLabel, type, className)}
|
||||||
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
|
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||||
</div>
|
</div>
|
||||||
@ -184,11 +188,12 @@ function getRecognizedPlateIcon(
|
|||||||
label: string,
|
label: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
type: EventType = "object",
|
type: EventType = "object",
|
||||||
|
key?: string,
|
||||||
) {
|
) {
|
||||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={label} className="relative inline-flex items-center">
|
<div key={key} className="relative inline-flex items-center">
|
||||||
{getIconForLabel(simpleLabel, type, className)}
|
{getIconForLabel(simpleLabel, type, className)}
|
||||||
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
|
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user