mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-29 07:09:03 +03:00
Use identifier field for unknown license plates (#17123)
* backend * backend fixes * api for search queries * frontend * docs * add filterable scroll list to more filters pane for identifiers * always publish identifier
This commit is contained in:
@@ -333,6 +333,18 @@ function ObjectDetailsTab({
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
const identifierScore = useMemo(() => {
|
||||
if (!search) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (search.data.identifier && search.data?.identifier_score) {
|
||||
return Math.round((search.data?.identifier_score ?? 0) * 100);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
const averageEstimatedSpeed = useMemo(() => {
|
||||
if (!search || !search.data?.average_estimated_speed) {
|
||||
return undefined;
|
||||
@@ -538,6 +550,17 @@ function ObjectDetailsTab({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{search?.data.identifier && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Identifier</div>
|
||||
<div className="flex flex-col space-y-0.5 text-sm">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{search.data.identifier}{" "}
|
||||
{identifierScore && ` (${identifierScore}%)`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
|
||||
@@ -33,6 +33,14 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { LuCheck } from "react-icons/lu";
|
||||
|
||||
type SearchFilterDialogProps = {
|
||||
config?: FrigateConfig;
|
||||
@@ -77,7 +85,8 @@ export default function SearchFilterDialog({
|
||||
(currentFilter.max_score ?? 1) < 1 ||
|
||||
(currentFilter.max_speed ?? 150) < 150 ||
|
||||
(currentFilter.zones?.length ?? 0) > 0 ||
|
||||
(currentFilter.sub_labels?.length ?? 0) > 0),
|
||||
(currentFilter.sub_labels?.length ?? 0) > 0 ||
|
||||
(currentFilter.identifier?.length ?? 0) > 0),
|
||||
[currentFilter],
|
||||
);
|
||||
|
||||
@@ -119,6 +128,12 @@ export default function SearchFilterDialog({
|
||||
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
||||
}
|
||||
/>
|
||||
<IdentifierFilterContent
|
||||
identifiers={currentFilter.identifier}
|
||||
setIdentifiers={(identifiers) =>
|
||||
setCurrentFilter({ ...currentFilter, identifier: identifiers })
|
||||
}
|
||||
/>
|
||||
<ScoreFilterContent
|
||||
minScore={currentFilter.min_score}
|
||||
maxScore={currentFilter.max_score}
|
||||
@@ -192,6 +207,7 @@ export default function SearchFilterDialog({
|
||||
max_speed: undefined,
|
||||
has_snapshot: undefined,
|
||||
has_clip: undefined,
|
||||
identifier: undefined,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
@@ -830,3 +846,118 @@ export function SnapshotClipFilterContent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type IdentifierFilterContentProps = {
|
||||
identifiers: string[] | undefined;
|
||||
setIdentifiers: (identifiers: string[] | undefined) => void;
|
||||
};
|
||||
|
||||
export function IdentifierFilterContent({
|
||||
identifiers,
|
||||
setIdentifiers,
|
||||
}: IdentifierFilterContentProps) {
|
||||
const { data: allIdentifiers, error } = useSWR<string[]>("identifiers", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const [selectedIdentifiers, setSelectedIdentifiers] = useState<string[]>(
|
||||
identifiers || [],
|
||||
);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (identifiers) {
|
||||
setSelectedIdentifiers(identifiers);
|
||||
} else {
|
||||
setSelectedIdentifiers([]);
|
||||
}
|
||||
}, [identifiers]);
|
||||
|
||||
const handleSelect = (identifier: string) => {
|
||||
const newSelected = selectedIdentifiers.includes(identifier)
|
||||
? selectedIdentifiers.filter((id) => id !== identifier) // Deselect
|
||||
: [...selectedIdentifiers, identifier]; // Select
|
||||
|
||||
setSelectedIdentifiers(newSelected);
|
||||
if (newSelected.length === 0) {
|
||||
setIdentifiers(undefined); // Clear filter if no identifiers selected
|
||||
} else {
|
||||
setIdentifiers(newSelected);
|
||||
}
|
||||
};
|
||||
|
||||
if (!allIdentifiers || allIdentifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredIdentifiers =
|
||||
allIdentifiers?.filter((id) =>
|
||||
id.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden">
|
||||
<DropdownMenuSeparator className="mb-3" />
|
||||
<div className="mb-3 text-lg">Identifiers</div>
|
||||
{error ? (
|
||||
<p className="text-sm text-red-500">Failed to load identifiers</p>
|
||||
) : !allIdentifiers ? (
|
||||
<p className="text-sm text-muted-foreground">Loading identifiers...</p>
|
||||
) : (
|
||||
<>
|
||||
<Command className="border border-input bg-background">
|
||||
<CommandInput
|
||||
placeholder="Type to search identifiers..."
|
||||
value={inputValue}
|
||||
onValueChange={setInputValue}
|
||||
/>
|
||||
<CommandList className="max-h-[200px] overflow-auto">
|
||||
{filteredIdentifiers.length === 0 && inputValue && (
|
||||
<CommandEmpty>No identifiers found.</CommandEmpty>
|
||||
)}
|
||||
{filteredIdentifiers.map((identifier) => (
|
||||
<CommandItem
|
||||
key={identifier}
|
||||
value={identifier}
|
||||
onSelect={() => handleSelect(identifier)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<LuCheck
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedIdentifiers.includes(identifier)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{identifier}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
{selectedIdentifiers.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{selectedIdentifiers.map((id) => (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white"
|
||||
>
|
||||
{id}
|
||||
<button
|
||||
onClick={() => handleSelect(id)}
|
||||
className="ml-1 text-white hover:text-gray-200"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Select one or more identifiers from the list.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export default function Explore() {
|
||||
cameras: searchSearchParams["cameras"],
|
||||
labels: searchSearchParams["labels"],
|
||||
sub_labels: searchSearchParams["sub_labels"],
|
||||
identifier: searchSearchParams["identifier"],
|
||||
zones: searchSearchParams["zones"],
|
||||
before: searchSearchParams["before"],
|
||||
after: searchSearchParams["after"],
|
||||
@@ -140,6 +141,7 @@ export default function Explore() {
|
||||
cameras: searchSearchParams["cameras"],
|
||||
labels: searchSearchParams["labels"],
|
||||
sub_labels: searchSearchParams["sub_labels"],
|
||||
identifier: searchSearchParams["identifier"],
|
||||
zones: searchSearchParams["zones"],
|
||||
before: searchSearchParams["before"],
|
||||
after: searchSearchParams["after"],
|
||||
|
||||
@@ -58,6 +58,8 @@ export type SearchResult = {
|
||||
average_estimated_speed: number;
|
||||
velocity_angle: number;
|
||||
path_data: [number[], number][];
|
||||
identifier?: string;
|
||||
identifier_score?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,6 +68,7 @@ export type SearchFilter = {
|
||||
cameras?: string[];
|
||||
labels?: string[];
|
||||
sub_labels?: string[];
|
||||
identifier?: string[];
|
||||
zones?: string[];
|
||||
before?: number;
|
||||
after?: number;
|
||||
@@ -89,6 +92,7 @@ export type SearchQueryParams = {
|
||||
cameras?: string[];
|
||||
labels?: string[];
|
||||
sub_labels?: string[];
|
||||
identifier?: string[];
|
||||
zones?: string[];
|
||||
before?: string;
|
||||
after?: string;
|
||||
|
||||
@@ -121,6 +121,7 @@ export default function SearchView({
|
||||
}, [config, searchFilter]);
|
||||
|
||||
const { data: allSubLabels } = useSWR("sub_labels");
|
||||
const { data: allIdentifiers } = useSWR("identifiers");
|
||||
|
||||
const allZones = useMemo<string[]>(() => {
|
||||
if (!config) {
|
||||
@@ -160,12 +161,13 @@ export default function SearchView({
|
||||
max_score: ["100"],
|
||||
min_speed: ["1"],
|
||||
max_speed: ["150"],
|
||||
identifier: allIdentifiers,
|
||||
has_clip: ["yes", "no"],
|
||||
has_snapshot: ["yes", "no"],
|
||||
...(config?.plus?.enabled &&
|
||||
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
|
||||
}),
|
||||
[config, allLabels, allZones, allSubLabels, searchFilter],
|
||||
[config, allLabels, allZones, allSubLabels, allIdentifiers, searchFilter],
|
||||
);
|
||||
|
||||
// remove duplicate event ids
|
||||
|
||||
Reference in New Issue
Block a user