mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
Allow filtering on detail level
This commit is contained in:
parent
dac8c1f975
commit
734a488db3
@ -1,4 +1,4 @@
|
|||||||
import { LuCheck, LuFilter, LuFocus } from "react-icons/lu";
|
import { LuCheck, LuFilter } from "react-icons/lu";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -8,6 +8,8 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
@ -50,10 +52,11 @@ export default function HistoryFilterPopover({
|
|||||||
[config, allLabels, allSubLabels]
|
[config, allLabels, allSubLabels]
|
||||||
);
|
);
|
||||||
const [selectedFilters, setSelectedFilters] = useState({
|
const [selectedFilters, setSelectedFilters] = useState({
|
||||||
cameras: filter == undefined ? [] : filter.cameras,
|
cameras: filter == undefined ? ["all"] : filter.cameras,
|
||||||
labels: filter == undefined ? [] : filter.labels,
|
labels: filter == undefined ? ["all"] : filter.labels,
|
||||||
before: filter?.before,
|
before: filter?.before,
|
||||||
after: filter?.after,
|
after: filter?.after,
|
||||||
|
detailLevel: filter?.detailLevel ?? "normal",
|
||||||
});
|
});
|
||||||
const dateRange = useMemo(() => {
|
const dateRange = useMemo(() => {
|
||||||
return selectedFilters?.before == undefined ||
|
return selectedFilters?.before == undefined ||
|
||||||
@ -65,6 +68,14 @@ export default function HistoryFilterPopover({
|
|||||||
};
|
};
|
||||||
}, [selectedFilters]);
|
}, [selectedFilters]);
|
||||||
|
|
||||||
|
const allItems = useMemo(() => {
|
||||||
|
return {
|
||||||
|
cameras:
|
||||||
|
JSON.stringify(selectedFilters.cameras) == JSON.stringify(["all"]),
|
||||||
|
labels: JSON.stringify(selectedFilters.labels) == JSON.stringify(["all"]),
|
||||||
|
};
|
||||||
|
}, [selectedFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={(open) => setOpen(open)}>
|
<Popover open={open} onOpenChange={(open) => setOpen(open)}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -78,7 +89,7 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="capitalize" variant="outline">
|
<Button className="capitalize" variant="outline">
|
||||||
{selectedFilters.cameras.length == 0
|
{allItems.cameras
|
||||||
? "All Cameras"
|
? "All Cameras"
|
||||||
: `${selectedFilters.cameras.length} Cameras`}
|
: `${selectedFilters.cameras.length} Cameras`}
|
||||||
</Button>
|
</Button>
|
||||||
@ -86,43 +97,50 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
|
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<FilterCheckBox
|
||||||
|
isChecked={allItems.cameras}
|
||||||
|
label="All Cameras"
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
setSelectedFilters({
|
||||||
|
...selectedFilters,
|
||||||
|
cameras: ["all"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
{filterValues.cameras.map((item) => (
|
{filterValues.cameras.map((item) => (
|
||||||
<FilterCheckBox
|
<FilterCheckBox
|
||||||
key={item}
|
key={item}
|
||||||
isChecked={
|
isChecked={selectedFilters.cameras.includes(item)}
|
||||||
selectedFilters.cameras.length == 0 ||
|
|
||||||
selectedFilters.cameras.includes(item)
|
|
||||||
}
|
|
||||||
label={item.replaceAll("_", " ")}
|
label={item.replaceAll("_", " ")}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
const selectedCameras = [...selectedFilters.cameras];
|
const selectedCameras = allItems.cameras
|
||||||
|
? []
|
||||||
|
: [...selectedFilters.cameras];
|
||||||
selectedCameras.push(item);
|
selectedCameras.push(item);
|
||||||
setSelectedFilters({
|
setSelectedFilters({
|
||||||
...selectedFilters,
|
...selectedFilters,
|
||||||
cameras: selectedCameras,
|
cameras: selectedCameras,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const selectedCameraList =
|
const selectedCameraList = [...selectedFilters.cameras];
|
||||||
selectedFilters.cameras.length == 0
|
|
||||||
? [...filterValues.cameras]
|
// can not deselect the last item
|
||||||
: [...selectedFilters.cameras];
|
if (selectedCameraList.length > 1) {
|
||||||
selectedCameraList.splice(
|
selectedCameraList.splice(
|
||||||
selectedCameraList.indexOf(item),
|
selectedCameraList.indexOf(item),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
setSelectedFilters({
|
setSelectedFilters({
|
||||||
...selectedFilters,
|
...selectedFilters,
|
||||||
cameras: selectedCameraList,
|
cameras: selectedCameraList,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSingleSelect={() => {
|
|
||||||
setSelectedFilters({
|
|
||||||
...selectedFilters,
|
|
||||||
cameras: [item],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@ -130,7 +148,7 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="capitalize" variant="outline">
|
<Button className="capitalize" variant="outline">
|
||||||
{selectedFilters.labels.length == 0
|
{allItems.labels
|
||||||
? "All Labels"
|
? "All Labels"
|
||||||
: `${selectedFilters.labels.length} Labels`}
|
: `${selectedFilters.labels.length} Labels`}
|
||||||
</Button>
|
</Button>
|
||||||
@ -138,6 +156,19 @@ export default function HistoryFilterPopover({
|
|||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
|
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<FilterCheckBox
|
||||||
|
isChecked={allItems.labels}
|
||||||
|
label="All Labels"
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
setSelectedFilters({
|
||||||
|
...selectedFilters,
|
||||||
|
labels: ["all"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
{filterValues.labels.map((item) => (
|
{filterValues.labels.map((item) => (
|
||||||
<FilterCheckBox
|
<FilterCheckBox
|
||||||
key={item}
|
key={item}
|
||||||
@ -148,37 +179,65 @@ export default function HistoryFilterPopover({
|
|||||||
label={item.replaceAll("_", " ")}
|
label={item.replaceAll("_", " ")}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
const selectedLabels = [...selectedFilters.labels];
|
const selectedLabels = allItems.labels
|
||||||
|
? []
|
||||||
|
: [...selectedFilters.labels];
|
||||||
selectedLabels.push(item);
|
selectedLabels.push(item);
|
||||||
setSelectedFilters({
|
setSelectedFilters({
|
||||||
...selectedFilters,
|
...selectedFilters,
|
||||||
labels: selectedLabels,
|
labels: selectedLabels,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const selectedLabelList =
|
const selectedLabelList = [...selectedFilters.labels];
|
||||||
selectedFilters.labels.length == 0
|
|
||||||
? [...filterValues.labels]
|
// can not deselect the last item
|
||||||
: selectedFilters.labels;
|
if (selectedLabelList.length > 1) {
|
||||||
selectedLabelList.splice(
|
selectedLabelList.splice(
|
||||||
selectedLabelList.indexOf(item),
|
selectedLabelList.indexOf(item),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
setSelectedFilters({
|
setSelectedFilters({
|
||||||
...selectedFilters,
|
...selectedFilters,
|
||||||
labels: selectedLabelList,
|
labels: selectedLabelList,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSingleSelect={() => {
|
|
||||||
setSelectedFilters({
|
|
||||||
...selectedFilters,
|
|
||||||
labels: [item],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button className="capitalize" variant="outline">
|
||||||
|
{selectedFilters.detailLevel}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Detail Level
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
value={selectedFilters.detailLevel}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedFilters({
|
||||||
|
...selectedFilters,
|
||||||
|
// @ts-ignore we know that value is one of the detailLevel
|
||||||
|
detailLevel: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuRadioItem value="normal">
|
||||||
|
Normal
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="extra">
|
||||||
|
Extra
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="full">Full</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="range"
|
mode="range"
|
||||||
@ -222,36 +281,25 @@ type FilterCheckBoxProps = {
|
|||||||
label: string;
|
label: string;
|
||||||
isChecked: boolean;
|
isChecked: boolean;
|
||||||
onCheckedChange: (isChecked: boolean) => void;
|
onCheckedChange: (isChecked: boolean) => void;
|
||||||
onSingleSelect: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function FilterCheckBox({
|
function FilterCheckBox({
|
||||||
label,
|
label,
|
||||||
isChecked,
|
isChecked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
onSingleSelect,
|
|
||||||
}: FilterCheckBoxProps) {
|
}: FilterCheckBoxProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<Button
|
||||||
className="capitalize flex justify-between items-center cursor-pointer"
|
className="capitalize flex justify-between items-center cursor-pointer w-full"
|
||||||
|
variant="ghost"
|
||||||
onClick={(_) => onCheckedChange(!isChecked)}
|
onClick={(_) => onCheckedChange(!isChecked)}
|
||||||
>
|
>
|
||||||
{isChecked ? (
|
{isChecked ? (
|
||||||
<LuCheck className="w-8 h-8" />
|
<LuCheck className="w-6 h-6" />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8" />
|
<div className="w-6 h-6" />
|
||||||
)}
|
)}
|
||||||
<div className="ml-1 w-full flex justify-start">{label}</div>
|
<div className="ml-1 w-full flex justify-start">{label}</div>
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSingleSelect();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuFocus className="text-primary w-6 h-6" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,6 @@ function History() {
|
|||||||
{ revalidateOnFocus: false }
|
{ revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const [detailLevel, _] = useState<"normal" | "extra" | "full">("normal");
|
|
||||||
const [playback, setPlayback] = useState<Card | undefined>();
|
const [playback, setPlayback] = useState<Card | undefined>();
|
||||||
|
|
||||||
const shouldAutoPlay = useMemo(() => {
|
const shouldAutoPlay = useMemo(() => {
|
||||||
@ -92,8 +91,11 @@ function History() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return getHourlyTimelineData(timelinePages, detailLevel);
|
return getHourlyTimelineData(
|
||||||
}, [detailLevel, timelinePages]);
|
timelinePages,
|
||||||
|
historyFilter?.detailLevel ?? "normal"
|
||||||
|
);
|
||||||
|
}, [historyFilter, timelinePages]);
|
||||||
|
|
||||||
const isDone =
|
const isDone =
|
||||||
(timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT;
|
(timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT;
|
||||||
|
|||||||
@ -44,4 +44,5 @@ interface HistoryFilter extends FilterType {
|
|||||||
labels: string[];
|
labels: string[];
|
||||||
before: number | undefined;
|
before: number | undefined;
|
||||||
after: number | undefined;
|
after: number | undefined;
|
||||||
|
detailLevel: "normal" | "extra" | "full";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user