mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 23:25:25 +03:00
Add support for sub label filtering
This commit is contained in:
parent
2ce03c0ae5
commit
6c2d9b15a6
@ -18,9 +18,14 @@ import { SearchFilter, SearchSource } from "@/types/search";
|
|||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const SEARCH_FILTERS = ["cameras", "date", "general"] as const;
|
const SEARCH_FILTERS = ["cameras", "date", "general", "sub"] as const;
|
||||||
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
||||||
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = ["cameras", "date", "general"];
|
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
|
||||||
|
"cameras",
|
||||||
|
"date",
|
||||||
|
"general",
|
||||||
|
"sub",
|
||||||
|
];
|
||||||
|
|
||||||
type SearchFilterGroupProps = {
|
type SearchFilterGroupProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -72,6 +77,8 @@ export default function SearchFilterGroup({
|
|||||||
return [...labels].sort();
|
return [...labels].sort();
|
||||||
}, [config, filterList, filter]);
|
}, [config, filterList, filter]);
|
||||||
|
|
||||||
|
const { data: allSubLabels } = useSWR("sub_labels");
|
||||||
|
|
||||||
const allZones = useMemo<string[]>(() => {
|
const allZones = useMemo<string[]>(() => {
|
||||||
if (filterList?.zones) {
|
if (filterList?.zones) {
|
||||||
return filterList.zones;
|
return filterList.zones;
|
||||||
@ -181,6 +188,15 @@ export default function SearchFilterGroup({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{filters.includes("sub") && (
|
||||||
|
<SubFilterButton
|
||||||
|
allSubLabels={allSubLabels}
|
||||||
|
selectedSubLabels={filter?.subLabels}
|
||||||
|
updateSubLabelFilter={(newSubLabels) =>
|
||||||
|
onUpdateFilter({ ...filter, subLabels: newSubLabels })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -509,3 +525,175 @@ export function GeneralFilterContent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubFilterButtonProps = {
|
||||||
|
allSubLabels: string[];
|
||||||
|
selectedSubLabels: string[] | undefined;
|
||||||
|
updateSubLabelFilter: (labels: string[] | undefined) => void;
|
||||||
|
};
|
||||||
|
function SubFilterButton({
|
||||||
|
allSubLabels,
|
||||||
|
selectedSubLabels,
|
||||||
|
updateSubLabelFilter,
|
||||||
|
}: SubFilterButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [currentSubLabels, setCurrentSubLabels] = useState<
|
||||||
|
string[] | undefined
|
||||||
|
>(selectedSubLabels);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={selectedSubLabels?.length ? "select" : "default"}
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
>
|
||||||
|
<FaFilter
|
||||||
|
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`hidden md:block ${selectedSubLabels?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const content = (
|
||||||
|
<SubFilterContent
|
||||||
|
allSubLabels={allSubLabels}
|
||||||
|
selectedSubLabels={selectedSubLabels}
|
||||||
|
currentSubLabels={currentSubLabels}
|
||||||
|
setCurrentSubLabels={setCurrentSubLabels}
|
||||||
|
updateSubLabelFilter={updateSubLabelFilter}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentSubLabels(selectedSubLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
|
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||||
|
{content}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentSubLabels(selectedSubLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||||
|
<PopoverContent>{content}</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubFilterContentProps = {
|
||||||
|
allSubLabels: string[];
|
||||||
|
selectedSubLabels: string[] | undefined;
|
||||||
|
currentSubLabels: string[] | undefined;
|
||||||
|
updateSubLabelFilter: (labels: string[] | undefined) => void;
|
||||||
|
setCurrentSubLabels: (labels: string[] | undefined) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
export function SubFilterContent({
|
||||||
|
allSubLabels,
|
||||||
|
selectedSubLabels,
|
||||||
|
currentSubLabels,
|
||||||
|
updateSubLabelFilter,
|
||||||
|
setCurrentSubLabels,
|
||||||
|
onClose,
|
||||||
|
}: SubFilterContentProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
|
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||||
|
<Label
|
||||||
|
className="mx-2 cursor-pointer text-primary"
|
||||||
|
htmlFor="allLabels"
|
||||||
|
>
|
||||||
|
All Sub Labels
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
className="ml-1"
|
||||||
|
id="allLabels"
|
||||||
|
checked={currentSubLabels == undefined}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
setCurrentSubLabels(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
|
{allSubLabels.map((item) => (
|
||||||
|
<FilterSwitch
|
||||||
|
key={item}
|
||||||
|
label={item.replaceAll("_", " ")}
|
||||||
|
isChecked={currentSubLabels?.includes(item) ?? false}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
const updatedLabels = currentSubLabels
|
||||||
|
? [...currentSubLabels]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
updatedLabels.push(item);
|
||||||
|
setCurrentSubLabels(updatedLabels);
|
||||||
|
} else {
|
||||||
|
const updatedLabels = currentSubLabels
|
||||||
|
? [...currentSubLabels]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// can not deselect the last item
|
||||||
|
if (updatedLabels.length > 1) {
|
||||||
|
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
||||||
|
setCurrentSubLabels(updatedLabels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="flex items-center justify-evenly p-2">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedSubLabels != currentSubLabels) {
|
||||||
|
updateSubLabelFilter(currentSubLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSubLabels(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export default function Search() {
|
|||||||
query: similaritySearch.id,
|
query: similaritySearch.id,
|
||||||
cameras: searchSearchParams["cameras"],
|
cameras: searchSearchParams["cameras"],
|
||||||
labels: searchSearchParams["labels"],
|
labels: searchSearchParams["labels"],
|
||||||
|
sub_labels: searchSearchParams["subLabels"],
|
||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
@ -83,6 +84,7 @@ export default function Search() {
|
|||||||
query: searchTerm,
|
query: searchTerm,
|
||||||
cameras: searchSearchParams["cameras"],
|
cameras: searchSearchParams["cameras"],
|
||||||
labels: searchSearchParams["labels"],
|
labels: searchSearchParams["labels"],
|
||||||
|
sub_labels: searchSearchParams["subLabels"],
|
||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
@ -97,6 +99,7 @@ export default function Search() {
|
|||||||
{
|
{
|
||||||
cameras: searchSearchParams["cameras"],
|
cameras: searchSearchParams["cameras"],
|
||||||
labels: searchSearchParams["labels"],
|
labels: searchSearchParams["labels"],
|
||||||
|
sub_labels: searchSearchParams["subLabels"],
|
||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export type SearchResult = {
|
|||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
cameras?: string[];
|
cameras?: string[];
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
|
subLabels?: string[];
|
||||||
zones?: string[];
|
zones?: string[];
|
||||||
before?: number;
|
before?: number;
|
||||||
after?: number;
|
after?: number;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user