mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-15 07:35:27 +03:00
Add time range to side filter
This commit is contained in:
parent
9bce6efddc
commit
af4a291cb2
@ -29,6 +29,7 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
|||||||
import { FaArrowRight, FaClock } from "react-icons/fa";
|
import { FaArrowRight, FaClock } from "react-icons/fa";
|
||||||
import { useFormattedHour } from "@/hooks/use-date-utils";
|
import { useFormattedHour } from "@/hooks/use-date-utils";
|
||||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||||
|
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||||
|
|
||||||
type SearchFilterGroupProps = {
|
type SearchFilterGroupProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -166,7 +167,22 @@ export default function SearchFilterGroup({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{filters.includes("date") && (
|
||||||
|
<CalendarRangeFilterButton
|
||||||
|
range={
|
||||||
|
filter?.after == undefined || filter?.before == undefined
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
from: new Date(filter.after * 1000),
|
||||||
|
to: new Date(filter.before * 1000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultText={isMobile ? "Dates" : "All Dates"}
|
||||||
|
updateSelectedRange={onUpdateSelectedRange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SearchFilterDialog
|
<SearchFilterDialog
|
||||||
|
config={config}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
@ -349,184 +365,6 @@ export function GeneralFilterContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeRangeFilterButtonProps = {
|
|
||||||
config?: FrigateConfig;
|
|
||||||
timeRange?: string;
|
|
||||||
updateTimeRange: (range: string | undefined) => void;
|
|
||||||
};
|
|
||||||
function TimeRangeFilterButton({
|
|
||||||
config,
|
|
||||||
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 [DEFAULT_TIME_RANGE_AFTER, DEFAULT_TIME_RANGE_BEFORE];
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeRange.split(",");
|
|
||||||
}, [timeRange]);
|
|
||||||
|
|
||||||
const [selectedAfterHour, setSelectedAfterHour] = useState(afterHour);
|
|
||||||
const [selectedBeforeHour, setSelectedBeforeHour] = useState(beforeHour);
|
|
||||||
|
|
||||||
// format based on locale
|
|
||||||
|
|
||||||
const formattedAfter = useFormattedHour(config, afterHour);
|
|
||||||
const formattedBefore = useFormattedHour(config, beforeHour);
|
|
||||||
const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
|
|
||||||
const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedAfterHour(afterHour);
|
|
||||||
setSelectedBeforeHour(beforeHour);
|
|
||||||
// only refresh when state changes
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [timeRange]);
|
|
||||||
|
|
||||||
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 ? `${formattedAfter} - ${formattedBefore}` : "All Times"}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const content = (
|
|
||||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
|
||||||
<div className="my-5 flex flex-row items-center justify-center gap-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);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formattedSelectedAfter}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="flex flex-row items-center justify-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);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formattedSelectedBefore}
|
|
||||||
</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 == "24:00" ? "23:59" : selectedBeforeHour
|
|
||||||
}
|
|
||||||
step="60"
|
|
||||||
onChange={(e) => {
|
|
||||||
const clock = e.target.value;
|
|
||||||
const [hour, minute, _] = clock.split(":");
|
|
||||||
setSelectedBeforeHour(`${hour}:${minute}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<div className="flex items-center justify-evenly p-2">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
selectedAfterHour == DEFAULT_TIME_RANGE_AFTER &&
|
|
||||||
selectedBeforeHour == DEFAULT_TIME_RANGE_BEFORE
|
|
||||||
) {
|
|
||||||
updateTimeRange(undefined);
|
|
||||||
} else {
|
|
||||||
updateTimeRange(`${selectedAfterHour},${selectedBeforeHour}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedAfterHour(DEFAULT_TIME_RANGE_AFTER);
|
|
||||||
setSelectedBeforeHour(DEFAULT_TIME_RANGE_BEFORE);
|
|
||||||
updateTimeRange(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
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[];
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
import { FaCog } from "react-icons/fa";
|
import { FaArrowRight, FaCog } from "react-icons/fa";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { PlatformAwareSheet } from "./PlatformAwareDialog";
|
import { PlatformAwareSheet } from "./PlatformAwareDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CamerasFilterContent } from "@/components/filter/CamerasFilterButton";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { SearchFilter, SearchSource } from "@/types/search";
|
import {
|
||||||
import { CameraGroupConfig } from "@/types/frigateConfig";
|
DEFAULT_TIME_RANGE_AFTER,
|
||||||
|
DEFAULT_TIME_RANGE_BEFORE,
|
||||||
|
SearchFilter,
|
||||||
|
SearchSource,
|
||||||
|
} from "@/types/search";
|
||||||
|
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import { useFormattedHour } from "@/hooks/use-date-utils";
|
||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
|
||||||
type SearchFilterDialogProps = {
|
type SearchFilterDialogProps = {
|
||||||
|
config?: FrigateConfig;
|
||||||
filter?: SearchFilter;
|
filter?: SearchFilter;
|
||||||
filterValues: {
|
filterValues: {
|
||||||
cameras: string[];
|
cameras: string[];
|
||||||
@ -20,6 +33,7 @@ type SearchFilterDialogProps = {
|
|||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
};
|
};
|
||||||
export default function SearchFilterDialog({
|
export default function SearchFilterDialog({
|
||||||
|
config,
|
||||||
filter,
|
filter,
|
||||||
filterValues,
|
filterValues,
|
||||||
groups,
|
groups,
|
||||||
@ -27,10 +41,8 @@ export default function SearchFilterDialog({
|
|||||||
}: SearchFilterDialogProps) {
|
}: SearchFilterDialogProps) {
|
||||||
// data
|
// data
|
||||||
|
|
||||||
|
const [currentFilter, setCurrentFilter] = useState(filter ?? {});
|
||||||
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
|
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
|
||||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
|
||||||
filter?.cameras,
|
|
||||||
);
|
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
@ -44,15 +56,12 @@ export default function SearchFilterDialog({
|
|||||||
);
|
);
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<CamerasFilterContent
|
<TimeRangeFilterContent
|
||||||
allCameras={filterValues.cameras}
|
config={config}
|
||||||
currentCameras={currentCameras}
|
timeRange={currentFilter.time_range}
|
||||||
groups={groups}
|
updateTimeRange={(newRange) =>
|
||||||
setCurrentCameras={setCurrentCameras}
|
setCurrentFilter({ time_range: newRange, ...currentFilter })
|
||||||
setOpen={setOpen}
|
}
|
||||||
updateCameraFilter={(newCameras) => {
|
|
||||||
onUpdateFilter({ ...filter, cameras: newCameras });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -68,6 +77,139 @@ export default function SearchFilterDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimeRangeFilterContentProps = {
|
||||||
|
config?: FrigateConfig;
|
||||||
|
timeRange?: string;
|
||||||
|
updateTimeRange: (range: string | undefined) => void;
|
||||||
|
};
|
||||||
|
function TimeRangeFilterContent({
|
||||||
|
config,
|
||||||
|
timeRange,
|
||||||
|
updateTimeRange,
|
||||||
|
}: TimeRangeFilterContentProps) {
|
||||||
|
const [startOpen, setStartOpen] = useState(false);
|
||||||
|
const [endOpen, setEndOpen] = useState(false);
|
||||||
|
|
||||||
|
const [afterHour, beforeHour] = useMemo(() => {
|
||||||
|
if (!timeRange || !timeRange.includes(",")) {
|
||||||
|
return [DEFAULT_TIME_RANGE_AFTER, DEFAULT_TIME_RANGE_BEFORE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeRange.split(",");
|
||||||
|
}, [timeRange]);
|
||||||
|
|
||||||
|
const [selectedAfterHour, setSelectedAfterHour] = useState(afterHour);
|
||||||
|
const [selectedBeforeHour, setSelectedBeforeHour] = useState(beforeHour);
|
||||||
|
|
||||||
|
// format based on locale
|
||||||
|
|
||||||
|
const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
|
||||||
|
const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedAfterHour(afterHour);
|
||||||
|
setSelectedBeforeHour(beforeHour);
|
||||||
|
// only refresh when state changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [timeRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedAfterHour == DEFAULT_TIME_RANGE_AFTER &&
|
||||||
|
selectedBeforeHour == DEFAULT_TIME_RANGE_BEFORE
|
||||||
|
) {
|
||||||
|
updateTimeRange(undefined);
|
||||||
|
} else {
|
||||||
|
updateTimeRange(`${selectedAfterHour},${selectedBeforeHour}`);
|
||||||
|
}
|
||||||
|
// only refresh when state changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedAfterHour, selectedBeforeHour]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
|
<Heading as="h4">Time Range</Heading>
|
||||||
|
<div className="my-5 flex flex-row items-center justify-center gap-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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formattedSelectedAfter}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="flex flex-row items-center justify-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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formattedSelectedBefore}
|
||||||
|
</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 == "24:00" ? "23:59" : selectedBeforeHour
|
||||||
|
}
|
||||||
|
step="60"
|
||||||
|
onChange={(e) => {
|
||||||
|
const clock = e.target.value;
|
||||||
|
const [hour, minute, _] = clock.split(":");
|
||||||
|
setSelectedBeforeHour(`${hour}:${minute}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {filters.includes("date") && (
|
* {filters.includes("date") && (
|
||||||
<CalendarRangeFilterButton
|
<CalendarRangeFilterButton
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user