mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-15 07:35:27 +03:00
Add ability to filter by time range
This commit is contained in:
parent
8c540d7210
commit
52b562eed8
@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|||||||
import { DateRangePicker } from "../ui/calendar-range";
|
import { DateRangePicker } from "../ui/calendar-range";
|
||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
|
|
||||||
type CalendarFilterButtonProps = {
|
type CalendarFilterButtonProps = {
|
||||||
reviewSummary?: ReviewSummary;
|
reviewSummary?: ReviewSummary;
|
||||||
@ -24,6 +25,7 @@ export default function CalendarFilterButton({
|
|||||||
day,
|
day,
|
||||||
updateSelectedDay,
|
updateSelectedDay,
|
||||||
}: CalendarFilterButtonProps) {
|
}: CalendarFilterButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const selectedDate = useFormattedTimestamp(
|
const selectedDate = useFormattedTimestamp(
|
||||||
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
||||||
"%b %-d",
|
"%b %-d",
|
||||||
@ -65,20 +67,14 @@ export default function CalendarFilterButton({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent>{content}</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<PlatformAwareDialog
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
trigger={trigger}
|
||||||
<PopoverContent className="w-auto">{content}</PopoverContent>
|
content={content}
|
||||||
</Popover>
|
contentClassName="w-auto"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import FilterSwitch from "./FilterSwitch";
|
|||||||
import { FilterList } from "@/types/filter";
|
import { FilterList } from "@/types/filter";
|
||||||
import CalendarFilterButton from "./CalendarFilterButton";
|
import CalendarFilterButton from "./CalendarFilterButton";
|
||||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||||
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
|
|
||||||
const REVIEW_FILTERS = [
|
const REVIEW_FILTERS = [
|
||||||
"cameras",
|
"cameras",
|
||||||
@ -367,28 +368,10 @@ function GeneralFilterButton({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setCurrentLabels(selectedLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
|
||||||
{content}
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -397,10 +380,7 @@ function GeneralFilterButton({
|
|||||||
|
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
||||||
<PopoverContent>{content}</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,38 +6,26 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import FilterSwitch from "./FilterSwitch";
|
import FilterSwitch from "./FilterSwitch";
|
||||||
import { FilterList } from "@/types/filter";
|
import { FilterList } from "@/types/filter";
|
||||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||||
import { SearchFilter, SearchSource } from "@/types/search";
|
import {
|
||||||
|
DEFAULT_SEARCH_FILTERS,
|
||||||
|
SearchFilter,
|
||||||
|
SearchFilters,
|
||||||
|
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 SubFilterIcon from "../icons/SubFilterIcon";
|
||||||
import { FaLocationDot } from "react-icons/fa6";
|
import { FaLocationDot } from "react-icons/fa6";
|
||||||
import { MdLabel } from "react-icons/md";
|
import { MdLabel } from "react-icons/md";
|
||||||
import SearchSourceIcon from "../icons/SearchSourceIcon";
|
import SearchSourceIcon from "../icons/SearchSourceIcon";
|
||||||
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
const SEARCH_FILTERS = [
|
import { FaArrowRight, FaClock } from "react-icons/fa";
|
||||||
"cameras",
|
|
||||||
"date",
|
|
||||||
"general",
|
|
||||||
"zone",
|
|
||||||
"sub",
|
|
||||||
"source",
|
|
||||||
] as const;
|
|
||||||
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
|
||||||
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
|
|
||||||
"cameras",
|
|
||||||
"date",
|
|
||||||
"general",
|
|
||||||
"zone",
|
|
||||||
"sub",
|
|
||||||
"source",
|
|
||||||
];
|
|
||||||
|
|
||||||
type SearchFilterGroupProps = {
|
type SearchFilterGroupProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -48,7 +36,7 @@ type SearchFilterGroupProps = {
|
|||||||
};
|
};
|
||||||
export default function SearchFilterGroup({
|
export default function SearchFilterGroup({
|
||||||
className,
|
className,
|
||||||
filters = DEFAULT_REVIEW_FILTERS,
|
filters = DEFAULT_SEARCH_FILTERS,
|
||||||
filter,
|
filter,
|
||||||
filterList,
|
filterList,
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
@ -182,6 +170,14 @@ export default function SearchFilterGroup({
|
|||||||
updateSelectedRange={onUpdateSelectedRange}
|
updateSelectedRange={onUpdateSelectedRange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{filters.includes("time") && (
|
||||||
|
<TimeRangeFilterButton
|
||||||
|
timeRange={filter?.timeRange}
|
||||||
|
updateTimeRange={(timeRange) =>
|
||||||
|
onUpdateFilter({ ...filter, timeRange })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{filters.includes("zone") && allZones.length > 0 && (
|
{filters.includes("zone") && allZones.length > 0 && (
|
||||||
<ZoneFilterButton
|
<ZoneFilterButton
|
||||||
allZones={filterValues.zones}
|
allZones={filterValues.zones}
|
||||||
@ -291,28 +287,11 @@ function GeneralFilterButton({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setCurrentLabels(selectedLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
|
||||||
{content}
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
|
contentClassName={isDesktop ? "" : "max-h-[75dvh] overflow-hidden p-4"}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -321,10 +300,7 @@ function GeneralFilterButton({
|
|||||||
|
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
||||||
<PopoverContent>{content}</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +394,164 @@ export function GeneralFilterContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimeRangeFilterButtonProps = {
|
||||||
|
timeRange?: string;
|
||||||
|
updateTimeRange: (range: string | undefined) => void;
|
||||||
|
};
|
||||||
|
function TimeRangeFilterButton({
|
||||||
|
timeRange,
|
||||||
|
updateTimeRange,
|
||||||
|
}: TimeRangeFilterButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [startOpen, setStartOpen] = useState(false);
|
||||||
|
const [endOpen, setEndOpen] = useState(false);
|
||||||
|
|
||||||
|
const [afterHour, beforeHour] = useMemo(() => {
|
||||||
|
if (!timeRange || !timeRange.includes(",")) {
|
||||||
|
return ["00:00", "24:00"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeRange.split(",");
|
||||||
|
}, [timeRange]);
|
||||||
|
|
||||||
|
const [selectedAfterHour, setSelectedAfterHour] = useState(afterHour);
|
||||||
|
const [selectedBeforeHour, setSelectedBeforeHour] = useState(beforeHour);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={timeRange ? "select" : "default"}
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
>
|
||||||
|
<FaClock
|
||||||
|
className={`${timeRange ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`${timeRange ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
>
|
||||||
|
{timeRange ? `${afterHour} - ${beforeHour}` : "All Times"}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const content = (
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
|
<div
|
||||||
|
className={`mt-3 flex items-center rounded-lg bg-secondary text-secondary-foreground ${isDesktop ? "mx-8 gap-2 px-2" : "pl-2"}`}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
open={startOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setStartOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||||
|
variant={startOpen ? "select" : "default"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setStartOpen(true);
|
||||||
|
setEndOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedAfterHour}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="flex flex-col items-center">
|
||||||
|
<input
|
||||||
|
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
|
id="startTime"
|
||||||
|
type="time"
|
||||||
|
value={selectedAfterHour}
|
||||||
|
step="60"
|
||||||
|
onChange={(e) => {
|
||||||
|
const clock = e.target.value;
|
||||||
|
const [hour, minute, _] = clock.split(":");
|
||||||
|
setSelectedAfterHour(`${hour}:${minute}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FaArrowRight className="size-4 text-primary" />
|
||||||
|
<Popover
|
||||||
|
open={endOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setEndOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||||
|
variant={endOpen ? "select" : "default"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEndOpen(true);
|
||||||
|
setStartOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedBeforeHour}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="flex flex-col items-center">
|
||||||
|
<input
|
||||||
|
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
|
id="startTime"
|
||||||
|
type="time"
|
||||||
|
value={selectedBeforeHour}
|
||||||
|
step="60"
|
||||||
|
onChange={(e) => {
|
||||||
|
const clock = e.target.value;
|
||||||
|
const [hour, minute, _] = clock.split(":");
|
||||||
|
setSelectedBeforeHour(`${hour}:${minute}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-evenly p-2">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedAfterHour == "00:00" && selectedBeforeHour == "24:00") {
|
||||||
|
updateTimeRange(undefined);
|
||||||
|
} else {
|
||||||
|
updateTimeRange(`${selectedAfterHour},${selectedBeforeHour}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAfterHour("00:00");
|
||||||
|
setSelectedBeforeHour("24:00");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type ZoneFilterButtonProps = {
|
type ZoneFilterButtonProps = {
|
||||||
allZones: string[];
|
allZones: string[];
|
||||||
selectedZones?: string[];
|
selectedZones?: string[];
|
||||||
@ -485,28 +619,10 @@ function ZoneFilterButton({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
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 p-4">
|
|
||||||
{content}
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -515,10 +631,7 @@ function ZoneFilterButton({
|
|||||||
|
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
||||||
<PopoverContent>{content}</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,28 +792,10 @@ function SubFilterButton({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
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 p-4">
|
|
||||||
{content}
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -709,10 +804,7 @@ function SubFilterButton({
|
|||||||
|
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
||||||
<PopoverContent>{content}</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -863,32 +955,13 @@ function SearchTypeButton({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
|
||||||
{content}
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setOpen}
|
||||||
setOpen(open);
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
||||||
<PopoverContent>{content}</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
web/src/components/overlay/dialog/PlatformAwareDialog.tsx
Normal file
45
web/src/components/overlay/dialog/PlatformAwareDialog.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
|
||||||
|
type PlatformAwareDialogProps = {
|
||||||
|
trigger: JSX.Element;
|
||||||
|
content: JSX.Element;
|
||||||
|
triggerClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
export default function PlatformAwareDialog({
|
||||||
|
trigger,
|
||||||
|
content,
|
||||||
|
triggerClassName = "",
|
||||||
|
contentClassName = "",
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: PlatformAwareDialogProps) {
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
|
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||||
|
{content}
|
||||||
|
</DrawerContent> const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverTrigger asChild className={triggerClassName}>
|
||||||
|
{trigger}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className={contentClassName}>{content}</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { useEventUpdate } from "@/api/ws";
|
import { useEventUpdate } from "@/api/ws";
|
||||||
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||||
|
import { useTimezone } from "@/hooks/use-date-utils";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
||||||
import SearchView from "@/views/search/SearchView";
|
import SearchView from "@/views/search/SearchView";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { TbExclamationCircle } from "react-icons/tb";
|
import { TbExclamationCircle } from "react-icons/tb";
|
||||||
|
import useSWR from "swr";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
const API_LIMIT = 25;
|
const API_LIMIT = 25;
|
||||||
@ -11,6 +14,12 @@ const API_LIMIT = 25;
|
|||||||
export default function Explore() {
|
export default function Explore() {
|
||||||
// search field handler
|
// search field handler
|
||||||
|
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timezone = useTimezone(config);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter, searchSearchParams] =
|
const [searchFilter, setSearchFilter, searchSearchParams] =
|
||||||
@ -65,9 +74,11 @@ export default function Explore() {
|
|||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
|
time_range: searchSearchParams["timeRange"],
|
||||||
search_type: searchSearchParams["search_type"],
|
search_type: searchSearchParams["search_type"],
|
||||||
limit:
|
limit:
|
||||||
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
||||||
|
timezone,
|
||||||
in_progress: 0,
|
in_progress: 0,
|
||||||
include_thumbnails: 0,
|
include_thumbnails: 0,
|
||||||
},
|
},
|
||||||
@ -94,7 +105,7 @@ export default function Explore() {
|
|||||||
include_thumbnails: 0,
|
include_thumbnails: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [searchTerm, searchSearchParams, similaritySearch]);
|
}, [searchTerm, searchSearchParams, similaritySearch, timezone]);
|
||||||
|
|
||||||
// paging
|
// paging
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,23 @@
|
|||||||
|
const SEARCH_FILTERS = [
|
||||||
|
"cameras",
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"general",
|
||||||
|
"zone",
|
||||||
|
"sub",
|
||||||
|
"source",
|
||||||
|
] as const;
|
||||||
|
export type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
||||||
|
export const DEFAULT_SEARCH_FILTERS: SearchFilters[] = [
|
||||||
|
"cameras",
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"general",
|
||||||
|
"zone",
|
||||||
|
"sub",
|
||||||
|
"source",
|
||||||
|
];
|
||||||
|
|
||||||
export type SearchSource = "similarity" | "thumbnail" | "description";
|
export type SearchSource = "similarity" | "thumbnail" | "description";
|
||||||
|
|
||||||
export type SearchResult = {
|
export type SearchResult = {
|
||||||
@ -36,6 +56,7 @@ export type SearchFilter = {
|
|||||||
zones?: string[];
|
zones?: string[];
|
||||||
before?: number;
|
before?: number;
|
||||||
after?: number;
|
after?: number;
|
||||||
|
timeRange?: string;
|
||||||
search_type?: SearchSource[];
|
search_type?: SearchSource[];
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,13 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { SearchFilter, SearchResult, SearchSource } from "@/types/search";
|
import {
|
||||||
|
DEFAULT_SEARCH_FILTERS,
|
||||||
|
SearchFilter,
|
||||||
|
SearchFilters,
|
||||||
|
SearchResult,
|
||||||
|
SearchSource,
|
||||||
|
} from "@/types/search";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { isMobileOnly } from "react-device-detect";
|
import { isMobileOnly } from "react-device-detect";
|
||||||
import { LuImage, LuSearchX, LuText } from "react-icons/lu";
|
import { LuImage, LuSearchX, LuText } from "react-icons/lu";
|
||||||
@ -131,6 +137,20 @@ export default function SearchView({
|
|||||||
|
|
||||||
const [searchDetail, setSearchDetail] = useState<SearchResult>();
|
const [searchDetail, setSearchDetail] = useState<SearchResult>();
|
||||||
|
|
||||||
|
const selectedFilters = useMemo<SearchFilters[]>(() => {
|
||||||
|
const filters = [...DEFAULT_SEARCH_FILTERS];
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchFilter &&
|
||||||
|
(searchFilter?.query?.length || searchFilter?.event_id?.length)
|
||||||
|
) {
|
||||||
|
const index = filters.indexOf("time");
|
||||||
|
filters.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}, [searchFilter]);
|
||||||
|
|
||||||
// search interaction
|
// search interaction
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
@ -300,6 +320,7 @@ export default function SearchView({
|
|||||||
"w-full justify-between md:justify-start lg:justify-end",
|
"w-full justify-between md:justify-start lg:justify-end",
|
||||||
)}
|
)}
|
||||||
filter={searchFilter}
|
filter={searchFilter}
|
||||||
|
filters={selectedFilters as SearchFilters[]}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user