Explore snapshot and clip filter (#14439)

* backend

* add ToggleButton component

* boolean type

* frontend

* allow setting filter in input

* better padding on dual slider

* use shadcn toggle group instead of custom component
This commit is contained in:
Josh Hawkins
2024-10-18 16:16:43 -05:00
committed by GitHub
parent b56f4c4558
commit 3c591ad8a9
7 changed files with 194 additions and 3 deletions

View File

@@ -296,6 +296,14 @@ export default function InputWithTags({
);
}
break;
case "has_snapshot":
if (!newFilters.has_snapshot) newFilters.has_snapshot = undefined;
newFilters.has_snapshot = value == "yes" ? 1 : 0;
break;
case "has_clip":
if (!newFilters.has_clip) newFilters.has_clip = undefined;
newFilters.has_clip = value == "yes" ? 1 : 0;
break;
case "event_id":
newFilters.event_id = value;
break;
@@ -341,6 +349,8 @@ export default function InputWithTags({
}`;
} else if (filterType === "min_score" || filterType === "max_score") {
return Math.round(Number(filterValues) * 100).toString() + "%";
} else if (filterType === "has_clip" || filterType === "has_snapshot") {
return filterValues ? "Yes" : "No";
} else {
return filterValues as string;
}

View File

@@ -25,6 +25,8 @@ import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { DualThumbSlider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type SearchFilterDialogProps = {
config?: FrigateConfig;
@@ -63,6 +65,8 @@ export default function SearchFilterDialog({
currentFilter &&
(currentFilter.time_range ||
(currentFilter.min_score ?? 0) > 0.5 ||
(currentFilter.has_snapshot ?? 0) === 1 ||
(currentFilter.has_clip ?? 0) === 1 ||
(currentFilter.max_score ?? 1) < 1 ||
(currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0),
@@ -113,6 +117,26 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, min_score: min, max_score: max })
}
/>
<SnapshotClipFilterContent
hasSnapshot={
currentFilter.has_snapshot !== undefined
? currentFilter.has_snapshot === 1
: undefined
}
hasClip={
currentFilter.has_clip !== undefined
? currentFilter.has_clip === 1
: undefined
}
setSnapshotClip={(snapshot, clip) =>
setCurrentFilter({
...currentFilter,
has_snapshot:
snapshot !== undefined ? (snapshot ? 1 : 0) : undefined,
has_clip: clip !== undefined ? (clip ? 1 : 0) : undefined,
})
}
/>
{isDesktop && <DropdownMenuSeparator />}
<div className="flex items-center justify-evenly p-2">
<Button
@@ -137,6 +161,8 @@ export default function SearchFilterDialog({
search_type: ["thumbnail", "description"],
min_score: undefined,
max_score: undefined,
has_snapshot: undefined,
has_clip: undefined,
}));
}}
>
@@ -440,7 +466,7 @@ export function ScoreFilterContent({
<div className="mb-3 text-lg">Score</div>
<div className="flex items-center gap-1">
<Input
className="w-12"
className="w-14 text-center"
inputMode="numeric"
value={Math.round((minScore ?? 0.5) * 100)}
onChange={(e) => {
@@ -452,7 +478,7 @@ export function ScoreFilterContent({
}}
/>
<DualThumbSlider
className="w-full"
className="mx-2 w-full"
min={0.5}
max={1.0}
step={0.01}
@@ -460,7 +486,7 @@ export function ScoreFilterContent({
onValueChange={([min, max]) => setScoreRange(min, max)}
/>
<Input
className="w-12"
className="w-14 text-center"
inputMode="numeric"
value={Math.round((maxScore ?? 1.0) * 100)}
onChange={(e) => {
@@ -475,3 +501,140 @@ export function ScoreFilterContent({
</div>
);
}
type SnapshotClipContentProps = {
hasSnapshot: boolean | undefined;
hasClip: boolean | undefined;
setSnapshotClip: (
snapshot: boolean | undefined,
clip: boolean | undefined,
) => void;
};
function SnapshotClipFilterContent({
hasSnapshot,
hasClip,
setSnapshotClip,
}: SnapshotClipContentProps) {
const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState(
hasSnapshot !== undefined,
);
const [isClipFilterActive, setIsClipFilterActive] = useState(
hasClip !== undefined,
);
useEffect(() => {
setIsSnapshotFilterActive(hasSnapshot !== undefined);
}, [hasSnapshot]);
useEffect(() => {
setIsClipFilterActive(hasClip !== undefined);
}, [hasClip]);
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">Features</div>
<div className="my-2.5 space-y-1">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="snapshot-filter"
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
checked={isSnapshotFilterActive}
onCheckedChange={(checked) => {
setIsSnapshotFilterActive(checked as boolean);
if (checked) {
setSnapshotClip(true, hasClip);
} else {
setSnapshotClip(undefined, hasClip);
}
}}
/>
<Label
htmlFor="snapshot-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
Has a snapshot
</Label>
</div>
<ToggleGroup
type="single"
value={
hasSnapshot === undefined ? undefined : hasSnapshot ? "yes" : "no"
}
onValueChange={(value) => {
if (value === "yes") setSnapshotClip(true, hasClip);
else if (value === "no") setSnapshotClip(false, hasClip);
}}
disabled={!isSnapshotFilterActive}
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
Yes
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
No
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
id="clip-filter"
checked={isClipFilterActive}
onCheckedChange={(checked) => {
setIsClipFilterActive(checked as boolean);
if (checked) {
setSnapshotClip(hasSnapshot, true);
} else {
setSnapshotClip(hasSnapshot, undefined);
}
}}
/>
<Label
htmlFor="clip-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
Has a video clip
</Label>
</div>
<ToggleGroup
type="single"
value={hasClip === undefined ? undefined : hasClip ? "yes" : "no"}
onValueChange={(value) => {
if (value === "yes") setSnapshotClip(hasSnapshot, true);
else if (value === "no") setSnapshotClip(hasSnapshot, false);
}}
disabled={!isClipFilterActive}
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
Yes
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
No
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</div>
);
}

View File

@@ -111,6 +111,8 @@ export default function Explore() {
search_type: searchSearchParams["search_type"],
min_score: searchSearchParams["min_score"],
max_score: searchSearchParams["max_score"],
has_snapshot: searchSearchParams["has_snapshot"],
has_clip: searchSearchParams["has_clip"],
limit:
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
timezone,
@@ -139,6 +141,8 @@ export default function Explore() {
search_type: searchSearchParams["search_type"],
min_score: searchSearchParams["min_score"],
max_score: searchSearchParams["max_score"],
has_snapshot: searchSearchParams["has_snapshot"],
has_clip: searchSearchParams["has_clip"],
event_id: searchSearchParams["event_id"],
timezone,
include_thumbnails: 0,

View File

@@ -59,6 +59,8 @@ export type SearchFilter = {
after?: number;
min_score?: number;
max_score?: number;
has_snapshot?: number;
has_clip?: number;
time_range?: string;
search_type?: SearchSource[];
event_id?: string;

View File

@@ -153,6 +153,8 @@ export default function SearchView({
after: [formatDateToLocaleString(-5)],
min_score: ["50"],
max_score: ["100"],
has_clip: ["yes", "no"],
has_snapshot: ["yes", "no"],
}),
[config, allLabels, allZones, allSubLabels],
);