mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-15 23:55:25 +03:00
Merge branch 'embeddings-ui' of https://github.com/blakeblackshear/frigate into embeddings-ui
This commit is contained in:
commit
a894cd650e
@ -178,7 +178,7 @@ class Embeddings:
|
|||||||
embeddings = []
|
embeddings = []
|
||||||
|
|
||||||
for desc in event_descriptions.values():
|
for desc in event_descriptions.values():
|
||||||
embeddings.append(self.text_embedding([desc]))
|
embeddings.append(self.text_embedding([desc])[0])
|
||||||
|
|
||||||
ids = list(event_descriptions.keys())
|
ids = list(event_descriptions.keys())
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
@ -16,18 +15,11 @@ import {
|
|||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SearchSource,
|
SearchSource,
|
||||||
DEFAULT_TIME_RANGE_AFTER,
|
|
||||||
DEFAULT_TIME_RANGE_BEFORE,
|
|
||||||
} from "@/types/search";
|
} 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";
|
|
||||||
import { MdLabel } from "react-icons/md";
|
import { MdLabel } from "react-icons/md";
|
||||||
import SearchSourceIcon from "../icons/SearchSourceIcon";
|
|
||||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
import { FaArrowRight, FaClock } from "react-icons/fa";
|
|
||||||
import { useFormattedHour } from "@/hooks/use-date-utils";
|
|
||||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||||
|
|
||||||
@ -185,7 +177,6 @@ export default function SearchFilterGroup({
|
|||||||
config={config}
|
config={config}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
groups={groups}
|
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -364,503 +355,3 @@ export function GeneralFilterContent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 buttonText = useMemo(() => {
|
|
||||||
if (isMobile) {
|
|
||||||
return "Zones";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedZones || selectedZones.length == 0) {
|
|
||||||
return "All Zones";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedZones.length == 1) {
|
|
||||||
return selectedZones[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${selectedZones.length} Zones`;
|
|
||||||
}, [selectedZones]);
|
|
||||||
|
|
||||||
// ui
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentZones(selectedZones);
|
|
||||||
// only refresh when state changes
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [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={`${selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const content = (
|
|
||||||
<ZoneFilterContent
|
|
||||||
allZones={allZones}
|
|
||||||
selectedZones={selectedZones}
|
|
||||||
currentZones={currentZones}
|
|
||||||
setCurrentZones={setCurrentZones}
|
|
||||||
updateZoneFilter={updateZoneFilter}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlatformAwareDialog
|
|
||||||
trigger={trigger}
|
|
||||||
content={content}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setCurrentZones(selectedZones);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && (
|
|
||||||
<>
|
|
||||||
{isDesktop && <DropdownMenuSeparator />}
|
|
||||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
|
||||||
<Label
|
|
||||||
className="mx-2 cursor-pointer text-primary"
|
|
||||||
htmlFor="allZones"
|
|
||||||
>
|
|
||||||
All Zones
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
className="ml-1"
|
|
||||||
id="allZones"
|
|
||||||
checked={currentZones == undefined}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setCurrentZones(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="my-2.5 flex flex-col gap-2.5">
|
|
||||||
{allZones.map((item) => (
|
|
||||||
<FilterSwitch
|
|
||||||
key={item}
|
|
||||||
label={item.replaceAll("_", " ")}
|
|
||||||
isChecked={currentZones?.includes(item) ?? false}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
const updatedZones = currentZones
|
|
||||||
? [...currentZones]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
updatedZones.push(item);
|
|
||||||
setCurrentZones(updatedZones);
|
|
||||||
} else {
|
|
||||||
const updatedZones = currentZones
|
|
||||||
? [...currentZones]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (updatedZones.length > 1) {
|
|
||||||
updatedZones.splice(updatedZones.indexOf(item), 1);
|
|
||||||
setCurrentZones(updatedZones);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isDesktop && <DropdownMenuSeparator />}
|
|
||||||
<div className="flex items-center justify-evenly p-2">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
if (updateZoneFilter && selectedZones != currentZones) {
|
|
||||||
updateZoneFilter(currentZones);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentZones?.(undefined);
|
|
||||||
updateZoneFilter?.(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 buttonText = useMemo(() => {
|
|
||||||
if (isMobile) {
|
|
||||||
return "Sub Labels";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedSubLabels || selectedSubLabels.length == 0) {
|
|
||||||
return "All Sub Labels";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedSubLabels.length == 1) {
|
|
||||||
return selectedSubLabels[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${selectedSubLabels.length} Sub Labels`;
|
|
||||||
}, [selectedSubLabels]);
|
|
||||||
|
|
||||||
const trigger = (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={selectedSubLabels?.length ? "select" : "default"}
|
|
||||||
className="flex items-center gap-2 capitalize"
|
|
||||||
>
|
|
||||||
<SubFilterIcon
|
|
||||||
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`${selectedSubLabels?.length ? "text-selected-foreground" : "text-primary"}`}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const content = (
|
|
||||||
<SubFilterContent
|
|
||||||
allSubLabels={allSubLabels}
|
|
||||||
selectedSubLabels={selectedSubLabels}
|
|
||||||
currentSubLabels={currentSubLabels}
|
|
||||||
setCurrentSubLabels={setCurrentSubLabels}
|
|
||||||
updateSubLabelFilter={updateSubLabelFilter}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlatformAwareDialog
|
|
||||||
trigger={trigger}
|
|
||||||
content={content}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setCurrentSubLabels(selectedSubLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
{isDesktop && <DropdownMenuSeparator />}
|
|
||||||
<div className="flex items-center justify-evenly p-2">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedSubLabels != currentSubLabels) {
|
|
||||||
updateSubLabelFilter(currentSubLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
updateSubLabelFilter(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchTypeButtonProps = {
|
|
||||||
selectedSearchSources: SearchSource[] | undefined;
|
|
||||||
updateSearchSourceFilter: (sources: SearchSource[] | undefined) => void;
|
|
||||||
};
|
|
||||||
function SearchTypeButton({
|
|
||||||
selectedSearchSources,
|
|
||||||
updateSearchSourceFilter,
|
|
||||||
}: SearchTypeButtonProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const buttonText = useMemo(() => {
|
|
||||||
if (isMobile) {
|
|
||||||
return "Sources";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!selectedSearchSources ||
|
|
||||||
selectedSearchSources.length == 0 ||
|
|
||||||
selectedSearchSources.length == 2
|
|
||||||
) {
|
|
||||||
return "All Search Sources";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedSearchSources.length == 1) {
|
|
||||||
return selectedSearchSources[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${selectedSearchSources.length} Search Sources`;
|
|
||||||
}, [selectedSearchSources]);
|
|
||||||
|
|
||||||
const trigger = (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={selectedSearchSources?.length != 2 ? "select" : "default"}
|
|
||||||
className="flex items-center gap-2 capitalize"
|
|
||||||
>
|
|
||||||
<SearchSourceIcon
|
|
||||||
className={`${selectedSearchSources?.length != 2 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`${selectedSearchSources?.length != 2 ? "text-selected-foreground" : "text-primary"}`}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const content = (
|
|
||||||
<SearchTypeContent
|
|
||||||
selectedSearchSources={selectedSearchSources}
|
|
||||||
updateSearchSourceFilter={updateSearchSourceFilter}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlatformAwareDialog
|
|
||||||
trigger={trigger}
|
|
||||||
content={content}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchTypeContentProps = {
|
|
||||||
selectedSearchSources: SearchSource[] | undefined;
|
|
||||||
updateSearchSourceFilter: (sources: SearchSource[] | undefined) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
export function SearchTypeContent({
|
|
||||||
selectedSearchSources,
|
|
||||||
updateSearchSourceFilter,
|
|
||||||
onClose,
|
|
||||||
}: SearchTypeContentProps) {
|
|
||||||
const [currentSearchSources, setCurrentSearchSources] = useState<
|
|
||||||
SearchSource[] | undefined
|
|
||||||
>(selectedSearchSources);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
|
||||||
<div className="my-2.5 flex flex-col gap-2.5">
|
|
||||||
<FilterSwitch
|
|
||||||
label="Thumbnail Image"
|
|
||||||
isChecked={currentSearchSources?.includes("thumbnail") ?? false}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
const updatedSources = currentSearchSources
|
|
||||||
? [...currentSearchSources]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
updatedSources.push("thumbnail");
|
|
||||||
setCurrentSearchSources(updatedSources);
|
|
||||||
} else {
|
|
||||||
if (updatedSources.length > 1) {
|
|
||||||
const index = updatedSources.indexOf("thumbnail");
|
|
||||||
if (index !== -1) updatedSources.splice(index, 1);
|
|
||||||
setCurrentSearchSources(updatedSources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FilterSwitch
|
|
||||||
label="Description"
|
|
||||||
isChecked={currentSearchSources?.includes("description") ?? false}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
const updatedSources = currentSearchSources
|
|
||||||
? [...currentSearchSources]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
updatedSources.push("description");
|
|
||||||
setCurrentSearchSources(updatedSources);
|
|
||||||
} else {
|
|
||||||
if (updatedSources.length > 1) {
|
|
||||||
const index = updatedSources.indexOf("description");
|
|
||||||
if (index !== -1) updatedSources.splice(index, 1);
|
|
||||||
setCurrentSearchSources(updatedSources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isDesktop && <DropdownMenuSeparator />}
|
|
||||||
<div className="flex items-center justify-evenly p-2">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedSearchSources != currentSearchSources) {
|
|
||||||
updateSearchSourceFilter(currentSearchSources);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
updateSearchSourceFilter(undefined);
|
|
||||||
setCurrentSearchSources(["thumbnail", "description"]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { MobilePage, MobilePageContent } from "@/components/mobile/MobilePage";
|
import {
|
||||||
import { Button } from "@/components/ui/button";
|
MobilePage,
|
||||||
|
MobilePageContent,
|
||||||
|
MobilePageHeader,
|
||||||
|
MobilePageTitle,
|
||||||
|
} from "@/components/mobile/MobilePage";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@ -64,12 +68,20 @@ export function PlatformAwareSheet({
|
|||||||
}: PlatformAwareSheetProps) {
|
}: PlatformAwareSheetProps) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<MobilePage open={open} onOpenChange={onOpenChange}>
|
<div>
|
||||||
<Button asChild>{trigger}</Button>
|
<div onClick={() => onOpenChange(true)}>{trigger}</div>
|
||||||
<MobilePageContent className="max-h-[75dvh] overflow-hidden px-4">
|
<MobilePage open={open} onOpenChange={onOpenChange}>
|
||||||
{content}
|
<MobilePageContent className="h-full overflow-hidden">
|
||||||
</MobilePageContent>
|
<MobilePageHeader
|
||||||
</MobilePage>
|
className="mx-2"
|
||||||
|
onClose={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
<MobilePageTitle>More Filters</MobilePageTitle>
|
||||||
|
</MobilePageHeader>
|
||||||
|
<div className={contentClassName}>{content}</div>
|
||||||
|
</MobilePageContent>
|
||||||
|
</MobilePage>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,15 +10,20 @@ import {
|
|||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchSource,
|
SearchSource,
|
||||||
} from "@/types/search";
|
} from "@/types/search";
|
||||||
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop, isMobile, isMobileOnly } from "react-device-detect";
|
||||||
import { useFormattedHour } from "@/hooks/use-date-utils";
|
import { useFormattedHour } from "@/hooks/use-date-utils";
|
||||||
import Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
|
import FilterSwitch from "@/components/filter/FilterSwitch";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SearchFilterDialogProps = {
|
type SearchFilterDialogProps = {
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
@ -29,14 +34,12 @@ type SearchFilterDialogProps = {
|
|||||||
zones: string[];
|
zones: string[];
|
||||||
search_type: SearchSource[];
|
search_type: SearchSource[];
|
||||||
};
|
};
|
||||||
groups: [string, CameraGroupConfig][];
|
|
||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
};
|
};
|
||||||
export default function SearchFilterDialog({
|
export default function SearchFilterDialog({
|
||||||
config,
|
config,
|
||||||
filter,
|
filter,
|
||||||
filterValues,
|
filterValues,
|
||||||
groups,
|
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
}: SearchFilterDialogProps) {
|
}: SearchFilterDialogProps) {
|
||||||
// data
|
// data
|
||||||
@ -60,9 +63,53 @@ export default function SearchFilterDialog({
|
|||||||
config={config}
|
config={config}
|
||||||
timeRange={currentFilter.time_range}
|
timeRange={currentFilter.time_range}
|
||||||
updateTimeRange={(newRange) =>
|
updateTimeRange={(newRange) =>
|
||||||
setCurrentFilter({ time_range: newRange, ...currentFilter })
|
setCurrentFilter({ ...currentFilter, time_range: newRange })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ZoneFilterContent
|
||||||
|
allZones={filterValues.zones}
|
||||||
|
zones={currentFilter.zones}
|
||||||
|
updateZones={(newZones) =>
|
||||||
|
setCurrentFilter({ ...currentFilter, zones: newZones })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SubFilterContent
|
||||||
|
allSubLabels={allSubLabels}
|
||||||
|
subLabels={currentFilter.sub_labels}
|
||||||
|
setSubLabels={(newSubLabels) =>
|
||||||
|
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SearchTypeContent
|
||||||
|
searchSources={
|
||||||
|
currentFilter?.search_type ?? ["thumbnail", "description"]
|
||||||
|
}
|
||||||
|
setSearchSources={(newSearchSource) =>
|
||||||
|
onUpdateFilter({ ...currentFilter, search_type: newSearchSource })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isDesktop && <DropdownMenuSeparator />}
|
||||||
|
<div className="flex items-center justify-evenly p-2">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
if (currentFilter != filter) {
|
||||||
|
onUpdateFilter(currentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentFilter(filter ?? {});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -70,9 +117,18 @@ export default function SearchFilterDialog({
|
|||||||
<PlatformAwareSheet
|
<PlatformAwareSheet
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
content={content}
|
content={content}
|
||||||
contentClassName="w-auto"
|
contentClassName={cn(
|
||||||
|
"w-auto lg:w-[300px] scrollbar-container h-full overflow-auto px-4",
|
||||||
|
isMobileOnly && "pb-20",
|
||||||
|
)}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentFilter(filter ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -210,58 +266,183 @@ function TimeRangeFilterContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type ZoneFilterContentProps = {
|
||||||
* {filters.includes("date") && (
|
allZones?: string[];
|
||||||
<CalendarRangeFilterButton
|
zones?: string[];
|
||||||
range={
|
updateZones: (zones: string[] | undefined) => void;
|
||||||
filter?.after == undefined || filter?.before == undefined
|
};
|
||||||
? undefined
|
export function ZoneFilterContent({
|
||||||
: {
|
allZones,
|
||||||
from: new Date(filter.after * 1000),
|
zones,
|
||||||
to: new Date(filter.before * 1000),
|
updateZones,
|
||||||
}
|
}: ZoneFilterContentProps) {
|
||||||
}
|
return (
|
||||||
defaultText={isMobile ? "Dates" : "All Dates"}
|
<>
|
||||||
updateSelectedRange={onUpdateSelectedRange}
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
/>
|
{isDesktop && <DropdownMenuSeparator className="mb-2" />}
|
||||||
)}
|
<Heading as="h4">Zones</Heading>
|
||||||
{filters.includes("time") && (
|
{allZones && (
|
||||||
<TimeRangeFilterButton
|
<>
|
||||||
config={config}
|
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||||
timeRange={filter?.time_range}
|
<Label
|
||||||
updateTimeRange={(time_range) =>
|
className="mx-2 cursor-pointer text-primary"
|
||||||
onUpdateFilter({ ...filter, time_range })
|
htmlFor="allZones"
|
||||||
}
|
>
|
||||||
/>
|
All Zones
|
||||||
)}
|
</Label>
|
||||||
{filters.includes("zone") && allZones.length > 0 && (
|
<Switch
|
||||||
<ZoneFilterButton
|
className="ml-1"
|
||||||
allZones={filterValues.zones}
|
id="allZones"
|
||||||
selectedZones={filter?.zones}
|
checked={zones == undefined}
|
||||||
updateZoneFilter={(newZones) =>
|
onCheckedChange={(isChecked) => {
|
||||||
onUpdateFilter({ ...filter, zones: newZones })
|
if (isChecked) {
|
||||||
}
|
updateZones(undefined);
|
||||||
/>
|
}
|
||||||
)}
|
}}
|
||||||
{filters.includes("sub") && (
|
/>
|
||||||
<SubFilterButton
|
</div>
|
||||||
allSubLabels={allSubLabels}
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
selectedSubLabels={filter?.sub_labels}
|
{allZones.map((item) => (
|
||||||
updateSubLabelFilter={(newSubLabels) =>
|
<FilterSwitch
|
||||||
onUpdateFilter({ ...filter, sub_labels: newSubLabels })
|
key={item}
|
||||||
}
|
label={item.replaceAll("_", " ")}
|
||||||
/>
|
isChecked={zones?.includes(item) ?? false}
|
||||||
)}
|
onCheckedChange={(isChecked) => {
|
||||||
{config?.semantic_search?.enabled &&
|
if (isChecked) {
|
||||||
filters.includes("source") &&
|
const updatedZones = zones ? [...zones] : [];
|
||||||
!filter?.search_type?.includes("similarity") && (
|
|
||||||
<SearchTypeButton
|
updatedZones.push(item);
|
||||||
selectedSearchSources={
|
updateZones(updatedZones);
|
||||||
filter?.search_type ?? ["thumbnail", "description"]
|
} else {
|
||||||
}
|
const updatedZones = zones ? [...zones] : [];
|
||||||
updateSearchSourceFilter={(newSearchSource) =>
|
|
||||||
onUpdateFilter({ ...filter, search_type: newSearchSource })
|
// can not deselect the last item
|
||||||
}
|
if (updatedZones.length > 1) {
|
||||||
/>
|
updatedZones.splice(updatedZones.indexOf(item), 1);
|
||||||
|
updateZones(updatedZones);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
*/
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubFilterContentProps = {
|
||||||
|
allSubLabels: string[];
|
||||||
|
subLabels: string[] | undefined;
|
||||||
|
setSubLabels: (labels: string[] | undefined) => void;
|
||||||
|
};
|
||||||
|
export function SubFilterContent({
|
||||||
|
allSubLabels,
|
||||||
|
subLabels,
|
||||||
|
setSubLabels,
|
||||||
|
}: SubFilterContentProps) {
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
|
{isDesktop && <DropdownMenuSeparator className="mb-2" />}
|
||||||
|
<Heading as="h4">Sub Labels</Heading>
|
||||||
|
<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={subLabels == undefined}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
setSubLabels(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
|
{allSubLabels.map((item) => (
|
||||||
|
<FilterSwitch
|
||||||
|
key={item}
|
||||||
|
label={item.replaceAll("_", " ")}
|
||||||
|
isChecked={subLabels?.includes(item) ?? false}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
const updatedLabels = subLabels ? [...subLabels] : [];
|
||||||
|
|
||||||
|
updatedLabels.push(item);
|
||||||
|
setSubLabels(updatedLabels);
|
||||||
|
} else {
|
||||||
|
const updatedLabels = subLabels ? [...subLabels] : [];
|
||||||
|
|
||||||
|
// can not deselect the last item
|
||||||
|
if (updatedLabels.length > 1) {
|
||||||
|
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
||||||
|
setSubLabels(updatedLabels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchTypeContentProps = {
|
||||||
|
searchSources: SearchSource[] | undefined;
|
||||||
|
setSearchSources: (sources: SearchSource[] | undefined) => void;
|
||||||
|
};
|
||||||
|
export function SearchTypeContent({
|
||||||
|
searchSources,
|
||||||
|
setSearchSources,
|
||||||
|
}: SearchTypeContentProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
|
{isDesktop && <DropdownMenuSeparator className="mb-2" />}
|
||||||
|
<Heading as="h4">Search Sources</Heading>
|
||||||
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
|
<FilterSwitch
|
||||||
|
label="Thumbnail Image"
|
||||||
|
isChecked={searchSources?.includes("thumbnail") ?? false}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
const updatedSources = searchSources ? [...searchSources] : [];
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
updatedSources.push("thumbnail");
|
||||||
|
setSearchSources(updatedSources);
|
||||||
|
} else {
|
||||||
|
if (updatedSources.length > 1) {
|
||||||
|
const index = updatedSources.indexOf("thumbnail");
|
||||||
|
if (index !== -1) updatedSources.splice(index, 1);
|
||||||
|
setSearchSources(updatedSources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FilterSwitch
|
||||||
|
label="Description"
|
||||||
|
isChecked={searchSources?.includes("description") ?? false}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
const updatedSources = searchSources ? [...searchSources] : [];
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
updatedSources.push("description");
|
||||||
|
setSearchSources(updatedSources);
|
||||||
|
} else {
|
||||||
|
if (updatedSources.length > 1) {
|
||||||
|
const index = updatedSources.indexOf("description");
|
||||||
|
if (index !== -1) updatedSources.splice(index, 1);
|
||||||
|
setSearchSources(updatedSources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user