icon improvements

add type to getIconForLabel
provide default icon for audio events
This commit is contained in:
Josh Hawkins 2026-01-08 10:11:05 -06:00
parent 74d14cb8ca
commit 33eb5d031b
9 changed files with 81 additions and 30 deletions

View File

@ -181,7 +181,7 @@ export default function ReviewCard({
key={`${object}-${idx}`} key={`${object}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(object, "size-3 text-white")} {getIconForLabel(object, "object", "size-3 text-white")}
</div> </div>
))} ))}
{event.data.audio.map((audio, idx) => ( {event.data.audio.map((audio, idx) => (
@ -189,7 +189,7 @@ export default function ReviewCard({
key={`${audio}-${idx}`} key={`${audio}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(audio, "size-3 text-white")} {getIconForLabel(audio, "audio", "size-3 text-white")}
</div> </div>
))} ))}
</div> </div>

View File

@ -133,7 +133,11 @@ export default function SearchThumbnail({
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`} className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
onClick={() => onClick(searchResult, false, true)} onClick={() => onClick(searchResult, false, true)}
> >
{getIconForLabel(objectLabel, "size-3 text-white")} {getIconForLabel(
objectLabel,
searchResult.data.type,
"size-3 text-white",
)}
{Math.floor( {Math.floor(
(searchResult.data.score ?? (searchResult.data.score ??
searchResult.data.top_score ?? searchResult.data.top_score ??

View File

@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
{t("details.label")} {t("details.label")}
</div> </div>
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize"> <div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
{getIconForLabel(search.label, "size-4 text-primary")} {getIconForLabel(
search.label,
search.data.type,
"size-4 text-primary",
)}
{getTranslatedLabel(search.label, search.data.type)} {getTranslatedLabel(search.label, search.data.type)}
{search.sub_label && ` (${search.sub_label})`} {search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && ( {isAdmin && search.end_time && (

View File

@ -665,6 +665,7 @@ export function TrackingDetails({
> >
{getIconForLabel( {getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label, event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-4 text-white", "size-4 text-white",
)} )}
</div> </div>

View File

@ -358,7 +358,11 @@ export default function LivePlayer({
]), ]),
] ]
.map((label) => { .map((label) => {
return getIconForLabel(label, "size-3 text-white"); return getIconForLabel(
label,
"object",
"size-3 text-white",
);
}) })
.sort()} .sort()}
</Chip> </Chip>

View File

@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
onClick={() => onClick(review, false, true)} onClick={() => onClick(review, false, true)}
> >
{review.data.objects.sort().map((object) => { {review.data.objects.sort().map((object) => {
return getIconForLabel(object, "size-3 text-white"); return getIconForLabel(
object,
"object",
"size-3 text-white",
);
})} })}
{review.data.audio.map((audio) => { {review.data.audio.map((audio) => {
return getIconForLabel(audio, "size-3 text-white"); return getIconForLabel(
audio,
"audio",
"size-3 text-white",
);
})} })}
</Chip> </Chip>
</> </>

View File

@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { EventType } from "@/types/search";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu"; import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
@ -346,22 +347,29 @@ function ReviewGroup({
: null, : null,
); );
const rawIconLabels: string[] = [ const rawIconLabels: Array<{ label: string; type: EventType }> = [
...(fetchedEvents ...(fetchedEvents
? fetchedEvents.map((e) => ? fetchedEvents.map((e) => ({
e.sub_label ? e.label + "-verified" : e.label, label: e.sub_label ? e.label + "-verified" : e.label,
) type: e.data.type,
: (review.data?.objects ?? [])), }))
...(review.data?.audio ?? []), : (review.data?.objects ?? []).map((obj) => ({
label: obj,
type: "object" as EventType,
}))),
...(review.data?.audio ?? []).map((audio) => ({
label: audio,
type: "audio" as EventType,
})),
]; ];
// limit to 5 icons // limit to 5 icons
const seen = new Set<string>(); const seen = new Set<string>();
const iconLabels: string[] = []; const iconLabels: Array<{ label: string; type: EventType }> = [];
for (const lbl of rawIconLabels) { for (const item of rawIconLabels) {
if (!seen.has(lbl)) { if (!seen.has(item.label)) {
seen.add(lbl); seen.add(item.label);
iconLabels.push(lbl); iconLabels.push(item);
if (iconLabels.length >= 5) break; if (iconLabels.length >= 5) break;
} }
} }
@ -418,12 +426,12 @@ function ReviewGroup({
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="text-sm font-medium">{displayTime}</div> <div className="text-sm font-medium">{displayTime}</div>
<div className="relative flex items-center gap-2 text-white"> <div className="relative flex items-center gap-2 text-white">
{iconLabels.slice(0, 5).map((lbl, idx) => ( {iconLabels.slice(0, 5).map(({ label: lbl, type }, idx) => (
<div <div
key={`${lbl}-${idx}`} key={`${lbl}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(lbl, "size-3 text-white")} {getIconForLabel(lbl, type, "size-3 text-white")}
</div> </div>
))} ))}
</div> </div>
@ -516,7 +524,11 @@ function ReviewGroup({
> >
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium"> <div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
<div className="rounded-full bg-muted-foreground p-1"> <div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")} {getIconForLabel(
audioLabel,
"audio",
"size-3 text-white",
)}
</div> </div>
<span>{getTranslatedLabel(audioLabel, "audio")}</span> <span>{getTranslatedLabel(audioLabel, "audio")}</span>
</div> </div>
@ -618,6 +630,7 @@ function EventList({
> >
{getIconForLabel( {getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label, event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-3 text-white", "size-3 text-white",
)} )}
</div> </div>

View File

@ -1,5 +1,6 @@
import { IconName } from "@/components/icons/IconPicker"; import { IconName } from "@/components/icons/IconPicker";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { EventType } from "@/types/search";
import { BsPersonWalking } from "react-icons/bs"; import { BsPersonWalking } from "react-icons/bs";
import { import {
FaAmazon, FaAmazon,
@ -32,6 +33,7 @@ import {
GiRabbit, GiRabbit,
GiRaccoonHead, GiRaccoonHead,
GiSailboat, GiSailboat,
GiSoundWaves,
GiSquirrel, GiSquirrel,
} from "react-icons/gi"; } from "react-icons/gi";
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu"; import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is IconName {
return Object.keys(LuIcons).includes(value as IconName); return Object.keys(LuIcons).includes(value as IconName);
} }
export function getIconForLabel(label: string, className?: string) { export function getIconForLabel(
label: string,
type: EventType = "object",
className?: string,
) {
if (label.endsWith("-verified")) { if (label.endsWith("-verified")) {
return getVerifiedIcon(label, className); return getVerifiedIcon(label, className, type);
} else if (label.endsWith("-plate")) { } else if (label.endsWith("-plate")) {
return getRecognizedPlateIcon(label, className); return getRecognizedPlateIcon(label, className, type);
} }
switch (label) { switch (label) {
@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
case "usps": case "usps":
return <FaUsps key={label} className={className} />; return <FaUsps key={label} className={className} />;
default: default:
if (type === "audio") {
return <GiSoundWaves key={label} className={className} />;
}
return <LuLassoSelect key={label} className={className} />; return <LuLassoSelect key={label} className={className} />;
} }
} }
function getVerifiedIcon(label: string, className?: string) { function getVerifiedIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-")); const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return ( return (
<div key={label} className="flex items-center"> <div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)} {getIconForLabel(simpleLabel, type, className)}
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" /> <FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
</div> </div>
); );
} }
function getRecognizedPlateIcon(label: string, className?: string) { function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-")); const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return ( return (
<div key={label} className="flex items-center"> <div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)} {getIconForLabel(simpleLabel, type, className)}
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" /> <LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
</div> </div>
); );

View File

@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
: getColorForObjectName(obj.label), : getColorForObjectName(obj.label),
}} }}
> >
{getIconForLabel(obj.label, "size-5 text-white")} {getIconForLabel(obj.label, "object", "size-5 text-white")}
</div> </div>
<div className="ml-3 text-lg"> <div className="ml-3 text-lg">
{getTranslatedLabel(obj.label)} {getTranslatedLabel(obj.label)}
@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
<div className="flex flex-row items-center gap-3 pb-1"> <div className="flex flex-row items-center gap-3 pb-1">
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1"> <div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
<div className="rounded-lg bg-selected p-2"> <div className="rounded-lg bg-selected p-2">
{getIconForLabel(key, "size-5 text-white")} {getIconForLabel(key, "audio", "size-5 text-white")}
</div> </div>
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div> <div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
</div> </div>