mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 23:25:25 +03:00
Separate out filters and clean up detail pane
This commit is contained in:
parent
6c2d9b15a6
commit
75bab2d601
@ -17,13 +17,16 @@ import { CamerasFilterButton } from "./CamerasFilterButton";
|
|||||||
import { SearchFilter, SearchSource } from "@/types/search";
|
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";
|
||||||
|
import SubFilterIcon from "../icons/SubFilterIcon";
|
||||||
|
import { FaLocationDot } from "react-icons/fa6";
|
||||||
|
|
||||||
const SEARCH_FILTERS = ["cameras", "date", "general", "sub"] as const;
|
const SEARCH_FILTERS = ["cameras", "date", "general", "zone", "sub"] as const;
|
||||||
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
||||||
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
|
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
|
||||||
"cameras",
|
"cameras",
|
||||||
"date",
|
"date",
|
||||||
"general",
|
"general",
|
||||||
|
"zone",
|
||||||
"sub",
|
"sub",
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -172,22 +175,26 @@ export default function SearchFilterGroup({
|
|||||||
<GeneralFilterButton
|
<GeneralFilterButton
|
||||||
allLabels={filterValues.labels}
|
allLabels={filterValues.labels}
|
||||||
selectedLabels={filter?.labels}
|
selectedLabels={filter?.labels}
|
||||||
allZones={filterValues.zones}
|
|
||||||
selectedZones={filter?.zones}
|
|
||||||
selectedSearchSources={
|
selectedSearchSources={
|
||||||
filter?.search_type ?? ["thumbnail", "description"]
|
filter?.search_type ?? ["thumbnail", "description"]
|
||||||
}
|
}
|
||||||
updateLabelFilter={(newLabels) => {
|
updateLabelFilter={(newLabels) => {
|
||||||
onUpdateFilter({ ...filter, labels: newLabels });
|
onUpdateFilter({ ...filter, labels: newLabels });
|
||||||
}}
|
}}
|
||||||
updateZoneFilter={(newZones) =>
|
|
||||||
onUpdateFilter({ ...filter, zones: newZones })
|
|
||||||
}
|
|
||||||
updateSearchSourceFilter={(newSearchSource) =>
|
updateSearchSourceFilter={(newSearchSource) =>
|
||||||
onUpdateFilter({ ...filter, search_type: newSearchSource })
|
onUpdateFilter({ ...filter, search_type: newSearchSource })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{filters.includes("zone") && allZones.length > 0 && (
|
||||||
|
<ZoneFilterButton
|
||||||
|
allZones={filterValues.zones}
|
||||||
|
selectedZones={filter?.zones}
|
||||||
|
updateZoneFilter={(newZones) =>
|
||||||
|
onUpdateFilter({ ...filter, zones: newZones })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{filters.includes("sub") && (
|
{filters.includes("sub") && (
|
||||||
<SubFilterButton
|
<SubFilterButton
|
||||||
allSubLabels={allSubLabels}
|
allSubLabels={allSubLabels}
|
||||||
@ -204,30 +211,21 @@ export default function SearchFilterGroup({
|
|||||||
type GeneralFilterButtonProps = {
|
type GeneralFilterButtonProps = {
|
||||||
allLabels: string[];
|
allLabels: string[];
|
||||||
selectedLabels: string[] | undefined;
|
selectedLabels: string[] | undefined;
|
||||||
allZones: string[];
|
|
||||||
selectedZones?: string[];
|
|
||||||
selectedSearchSources: SearchSource[];
|
selectedSearchSources: SearchSource[];
|
||||||
updateLabelFilter: (labels: string[] | undefined) => void;
|
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||||
updateZoneFilter: (zones: string[] | undefined) => void;
|
|
||||||
updateSearchSourceFilter: (sources: SearchSource[]) => void;
|
updateSearchSourceFilter: (sources: SearchSource[]) => void;
|
||||||
};
|
};
|
||||||
function GeneralFilterButton({
|
function GeneralFilterButton({
|
||||||
allLabels,
|
allLabels,
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
allZones,
|
|
||||||
selectedZones,
|
|
||||||
selectedSearchSources,
|
selectedSearchSources,
|
||||||
updateLabelFilter,
|
updateLabelFilter,
|
||||||
updateZoneFilter,
|
|
||||||
updateSearchSourceFilter,
|
updateSearchSourceFilter,
|
||||||
}: GeneralFilterButtonProps) {
|
}: GeneralFilterButtonProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
);
|
);
|
||||||
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
|
|
||||||
selectedZones,
|
|
||||||
);
|
|
||||||
const [currentSearchSources, setCurrentSearchSources] = useState<
|
const [currentSearchSources, setCurrentSearchSources] = useState<
|
||||||
SearchSource[]
|
SearchSource[]
|
||||||
>(selectedSearchSources);
|
>(selectedSearchSources);
|
||||||
@ -235,16 +233,14 @@ function GeneralFilterButton({
|
|||||||
const trigger = (
|
const trigger = (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={
|
variant={selectedLabels?.length ? "select" : "default"}
|
||||||
selectedLabels?.length || selectedZones?.length ? "select" : "default"
|
|
||||||
}
|
|
||||||
className="flex items-center gap-2 capitalize"
|
className="flex items-center gap-2 capitalize"
|
||||||
>
|
>
|
||||||
<FaFilter
|
<FaFilter
|
||||||
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`hidden md:block ${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
|
className={`hidden md:block ${selectedLabels?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||||
>
|
>
|
||||||
Filter
|
Filter
|
||||||
</div>
|
</div>
|
||||||
@ -255,13 +251,8 @@ function GeneralFilterButton({
|
|||||||
allLabels={allLabels}
|
allLabels={allLabels}
|
||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
currentLabels={currentLabels}
|
currentLabels={currentLabels}
|
||||||
allZones={allZones}
|
|
||||||
selectedZones={selectedZones}
|
|
||||||
currentZones={currentZones}
|
|
||||||
selectedSearchSources={selectedSearchSources}
|
selectedSearchSources={selectedSearchSources}
|
||||||
currentSearchSources={currentSearchSources}
|
currentSearchSources={currentSearchSources}
|
||||||
setCurrentZones={setCurrentZones}
|
|
||||||
updateZoneFilter={updateZoneFilter}
|
|
||||||
setCurrentLabels={setCurrentLabels}
|
setCurrentLabels={setCurrentLabels}
|
||||||
updateLabelFilter={updateLabelFilter}
|
updateLabelFilter={updateLabelFilter}
|
||||||
setCurrentSearchSources={setCurrentSearchSources}
|
setCurrentSearchSources={setCurrentSearchSources}
|
||||||
@ -311,15 +302,10 @@ type GeneralFilterContentProps = {
|
|||||||
allLabels: string[];
|
allLabels: string[];
|
||||||
selectedLabels: string[] | undefined;
|
selectedLabels: string[] | undefined;
|
||||||
currentLabels: string[] | undefined;
|
currentLabels: string[] | undefined;
|
||||||
allZones?: string[];
|
|
||||||
selectedZones?: string[];
|
|
||||||
currentZones?: string[];
|
|
||||||
selectedSearchSources: SearchSource[];
|
selectedSearchSources: SearchSource[];
|
||||||
currentSearchSources: SearchSource[];
|
currentSearchSources: SearchSource[];
|
||||||
updateLabelFilter: (labels: string[] | undefined) => void;
|
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||||
setCurrentLabels: (labels: string[] | undefined) => void;
|
setCurrentLabels: (labels: string[] | undefined) => void;
|
||||||
updateZoneFilter?: (zones: string[] | undefined) => void;
|
|
||||||
setCurrentZones?: (zones: string[] | undefined) => void;
|
|
||||||
setCurrentSearchSources: (sources: SearchSource[]) => void;
|
setCurrentSearchSources: (sources: SearchSource[]) => void;
|
||||||
updateSearchSourceFilter: (sources: SearchSource[]) => void;
|
updateSearchSourceFilter: (sources: SearchSource[]) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -328,15 +314,10 @@ export function GeneralFilterContent({
|
|||||||
allLabels,
|
allLabels,
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
currentLabels,
|
currentLabels,
|
||||||
allZones,
|
|
||||||
selectedZones,
|
|
||||||
currentZones,
|
|
||||||
selectedSearchSources,
|
selectedSearchSources,
|
||||||
currentSearchSources,
|
currentSearchSources,
|
||||||
updateLabelFilter,
|
updateLabelFilter,
|
||||||
setCurrentLabels,
|
setCurrentLabels,
|
||||||
updateZoneFilter,
|
|
||||||
setCurrentZones,
|
|
||||||
setCurrentSearchSources,
|
setCurrentSearchSources,
|
||||||
updateSearchSourceFilter,
|
updateSearchSourceFilter,
|
||||||
onClose,
|
onClose,
|
||||||
@ -436,7 +417,137 @@ export function GeneralFilterContent({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="flex items-center justify-evenly p-2">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedLabels != currentLabels) {
|
||||||
|
updateLabelFilter(currentLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSearchSources != currentSearchSources) {
|
||||||
|
updateSearchSourceFilter(currentSearchSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentLabels(undefined);
|
||||||
|
updateLabelFilter(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneFilterButtonProps = {
|
||||||
|
allZones: string[];
|
||||||
|
selectedZones?: string[];
|
||||||
|
updateZoneFilter: (zones: string[] | undefined) => void;
|
||||||
|
};
|
||||||
|
function ZoneFilterButton({
|
||||||
|
allZones,
|
||||||
|
selectedZones,
|
||||||
|
updateZoneFilter,
|
||||||
|
}: ZoneFilterButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
|
||||||
|
selectedZones,
|
||||||
|
);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={selectedZones?.length ? "select" : "default"}
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
>
|
||||||
|
<FaLocationDot
|
||||||
|
className={`${selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`hidden md:block ${selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const content = (
|
||||||
|
<ZoneFilterContent
|
||||||
|
allZones={allZones}
|
||||||
|
selectedZones={selectedZones}
|
||||||
|
currentZones={currentZones}
|
||||||
|
setCurrentZones={setCurrentZones}
|
||||||
|
updateZoneFilter={updateZoneFilter}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentZones(selectedZones);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
|
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||||
|
{content}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentZones(selectedZones);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||||
|
<PopoverContent>{content}</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneFilterContentProps = {
|
||||||
|
allZones?: string[];
|
||||||
|
selectedZones?: string[];
|
||||||
|
currentZones?: string[];
|
||||||
|
updateZoneFilter?: (zones: string[] | undefined) => void;
|
||||||
|
setCurrentZones?: (zones: string[] | undefined) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
export function ZoneFilterContent({
|
||||||
|
allZones,
|
||||||
|
selectedZones,
|
||||||
|
currentZones,
|
||||||
|
updateZoneFilter,
|
||||||
|
setCurrentZones,
|
||||||
|
onClose,
|
||||||
|
}: ZoneFilterContentProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
{allZones && setCurrentZones && (
|
{allZones && setCurrentZones && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@ -495,18 +606,10 @@ export function GeneralFilterContent({
|
|||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedLabels != currentLabels) {
|
|
||||||
updateLabelFilter(currentLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateZoneFilter && selectedZones != currentZones) {
|
if (updateZoneFilter && selectedZones != currentZones) {
|
||||||
updateZoneFilter(currentZones);
|
updateZoneFilter(currentZones);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSearchSources != currentSearchSources) {
|
|
||||||
updateSearchSourceFilter(currentSearchSources);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -514,9 +617,7 @@ export function GeneralFilterContent({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentLabels(undefined);
|
|
||||||
setCurrentZones?.(undefined);
|
setCurrentZones?.(undefined);
|
||||||
updateLabelFilter(undefined);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
@ -547,7 +648,7 @@ function SubFilterButton({
|
|||||||
variant={selectedSubLabels?.length ? "select" : "default"}
|
variant={selectedSubLabels?.length ? "select" : "default"}
|
||||||
className="flex items-center gap-2 capitalize"
|
className="flex items-center gap-2 capitalize"
|
||||||
>
|
>
|
||||||
<FaFilter
|
<SubFilterIcon
|
||||||
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|||||||
25
web/src/components/icons/SubFilterIcon.tsx
Normal file
25
web/src/components/icons/SubFilterIcon.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FaCog, FaFilter } from "react-icons/fa";
|
||||||
|
|
||||||
|
type SubFilterIconProps = {
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubFilterIcon = forwardRef<HTMLDivElement, SubFilterIconProps>(
|
||||||
|
({ className, onClick }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative flex items-center", className)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<FaFilter className="size-full" />
|
||||||
|
<FaCog className="absolute size-3 translate-x-3 translate-y-3/4" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SubFilterIcon;
|
||||||
@ -114,12 +114,15 @@ export default function SearchDetailDialog({
|
|||||||
<div className="flex flex-row items-center gap-2 text-sm capitalize">
|
<div className="flex flex-row items-center gap-2 text-sm capitalize">
|
||||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||||
{search.label}
|
{search.label}
|
||||||
|
{search.sub_label && ` (${search.sub_label})`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="text-sm text-primary/40">Score</div>
|
<div className="text-sm text-primary/40">Score</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{Math.round(search.score * 100)}%
|
{Math.round(search.data.top_score * 100)}%
|
||||||
|
{search.sub_label &&
|
||||||
|
` (${Math.round((search.data.sub_label_score ?? 0) * 100)}%)`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
|
|||||||
@ -13,6 +13,16 @@ export type SearchResult = {
|
|||||||
zones: string[];
|
zones: string[];
|
||||||
search_source: SearchSource;
|
search_source: SearchSource;
|
||||||
search_distance: number;
|
search_distance: number;
|
||||||
|
data: {
|
||||||
|
top_score: number;
|
||||||
|
score: number;
|
||||||
|
sub_label_score?: number;
|
||||||
|
region: number[];
|
||||||
|
box: number[];
|
||||||
|
area: number;
|
||||||
|
ratio: number;
|
||||||
|
type: "object" | "audio" | "manual";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
|
|||||||
@ -16,15 +16,7 @@ import { Preview } from "@/types/preview";
|
|||||||
import { SearchFilter, SearchResult } from "@/types/search";
|
import { SearchFilter, SearchResult } from "@/types/search";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { isMobileOnly } from "react-device-detect";
|
import { isMobileOnly } from "react-device-detect";
|
||||||
import {
|
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
||||||
LuExternalLink,
|
|
||||||
LuImage,
|
|
||||||
LuSearchCheck,
|
|
||||||
LuSearchX,
|
|
||||||
LuText,
|
|
||||||
LuXCircle,
|
|
||||||
} from "react-icons/lu";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
type SearchViewProps = {
|
type SearchViewProps = {
|
||||||
@ -153,7 +145,7 @@ export default function SearchView({
|
|||||||
|
|
||||||
{hasExistingSearch && (
|
{hasExistingSearch && (
|
||||||
<SearchFilterGroup
|
<SearchFilterGroup
|
||||||
className={cn("", isMobileOnly && "w-full")}
|
className={cn("", isMobileOnly && "w-full justify-between")}
|
||||||
filter={searchFilter}
|
filter={searchFilter}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user