mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
Add time selection
This commit is contained in:
parent
853b788194
commit
422324e5c5
@ -3,7 +3,7 @@ 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";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -13,7 +13,11 @@ import {
|
|||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import { Calendar } from "../ui/calendar";
|
import { Calendar } from "../ui/calendar";
|
||||||
import { ReviewFilter } from "@/types/review";
|
import { ReviewFilter } from "@/types/review";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import {
|
||||||
|
formatUnixTimestampToDateTime,
|
||||||
|
getEndOfDayTimestamp,
|
||||||
|
} from "@/utils/dateUtil";
|
||||||
|
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||||
|
|
||||||
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||||
|
|
||||||
@ -55,6 +59,19 @@ export default function ReviewFilterGroup({
|
|||||||
[config, allLabels]
|
[config, allLabels]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// handle updating filters
|
||||||
|
|
||||||
|
const onUpdateSelectedDay = useCallback(
|
||||||
|
(day?: Date) => {
|
||||||
|
onUpdateFilter({
|
||||||
|
...filter,
|
||||||
|
after: day == undefined ? undefined : day.getTime() / 1000,
|
||||||
|
before: day == undefined ? undefined : getEndOfDayTimestamp(day),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onUpdateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
<CamerasFilterButton
|
<CamerasFilterButton
|
||||||
@ -64,7 +81,12 @@ export default function ReviewFilterGroup({
|
|||||||
onUpdateFilter({ ...filter, cameras: newCameras });
|
onUpdateFilter({ ...filter, cameras: newCameras });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CalendarFilterButton before={filter?.before} after={filter?.after} />
|
<CalendarFilterButton
|
||||||
|
day={
|
||||||
|
filter?.after == undefined ? undefined : new Date(filter.after * 1000)
|
||||||
|
}
|
||||||
|
updateSelectedDay={onUpdateSelectedDay}
|
||||||
|
/>
|
||||||
<GeneralFilterButton
|
<GeneralFilterButton
|
||||||
allLabels={filterValues.labels}
|
allLabels={filterValues.labels}
|
||||||
selectedLabels={filter?.labels}
|
selectedLabels={filter?.labels}
|
||||||
@ -80,107 +102,6 @@ export default function ReviewFilterGroup({
|
|||||||
);
|
);
|
||||||
|
|
||||||
/*return (
|
/*return (
|
||||||
<Popover open={open} onOpenChange={(open) => setOpen(open)}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<LuFilter className="mx-1" />
|
|
||||||
Filter
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-screen sm:w-[340px]">
|
|
||||||
<div className="flex justify-around">
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button className="capitalize" variant="outline">
|
|
||||||
{allItems.labels
|
|
||||||
? "All Labels"
|
|
||||||
: `${selectedFilters.labels.length} Labels`}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<FilterCheckBox
|
|
||||||
isChecked={allItems.labels}
|
|
||||||
label="All Labels"
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setSelectedFilters({
|
|
||||||
...selectedFilters,
|
|
||||||
labels: ["all"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{filterValues.labels.map((item) => (
|
|
||||||
<FilterCheckBox
|
|
||||||
key={item}
|
|
||||||
isChecked={
|
|
||||||
selectedFilters.labels.length == 0 ||
|
|
||||||
selectedFilters.labels.includes(item)
|
|
||||||
}
|
|
||||||
label={item.replaceAll("_", " ")}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
const selectedLabels = allItems.labels
|
|
||||||
? []
|
|
||||||
: [...selectedFilters.labels];
|
|
||||||
selectedLabels.push(item);
|
|
||||||
setSelectedFilters({
|
|
||||||
...selectedFilters,
|
|
||||||
labels: selectedLabels,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const selectedLabelList = [...selectedFilters.labels];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (selectedLabelList.length > 1) {
|
|
||||||
selectedLabelList.splice(
|
|
||||||
selectedLabelList.indexOf(item),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
setSelectedFilters({
|
|
||||||
...selectedFilters,
|
|
||||||
labels: selectedLabelList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</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"
|
||||||
@ -297,10 +218,14 @@ function CamerasFilterButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CalendarFilterButtonProps = {
|
type CalendarFilterButtonProps = {
|
||||||
before: number | undefined;
|
day?: Date;
|
||||||
after: number | undefined;
|
updateSelectedDay: (day?: Date) => void;
|
||||||
};
|
};
|
||||||
function CalendarFilterButton({ before, after }: CalendarFilterButtonProps) {
|
function CalendarFilterButton({
|
||||||
|
day,
|
||||||
|
updateSelectedDay,
|
||||||
|
}: CalendarFilterButtonProps) {
|
||||||
|
const [selectedDay, setSelectedDay] = useState(day);
|
||||||
const disabledDates = useMemo(() => {
|
const disabledDates = useMemo(() => {
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
|
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
|
||||||
@ -308,28 +233,34 @@ function CalendarFilterButton({ before, after }: CalendarFilterButtonProps) {
|
|||||||
future.setFullYear(tomorrow.getFullYear() + 10);
|
future.setFullYear(tomorrow.getFullYear() + 10);
|
||||||
return { from: tomorrow, to: future };
|
return { from: tomorrow, to: future };
|
||||||
}, []);
|
}, []);
|
||||||
// @ts-ignore
|
const selectedDate = useFormattedTimestamp(
|
||||||
const dateRange = useMemo(() => {
|
day == undefined ? 0 : day?.getTime() / 1000,
|
||||||
return before == undefined || after == undefined
|
"%b %-d"
|
||||||
? undefined
|
);
|
||||||
: {
|
|
||||||
from: new Date(after * 1000),
|
|
||||||
to: new Date(before * 1000),
|
|
||||||
};
|
|
||||||
}, [before, after]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
updateSelectedDay(selectedDay);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button className="mx-1" variant="secondary">
|
<Button className="mx-1" variant="secondary">
|
||||||
<LuCalendar className=" mr-[10px]" />
|
<LuCalendar className=" mr-[10px]" />
|
||||||
{formatUnixTimestampToDateTime(Date.now() / 1000, {
|
{day == undefined ? "Last 24 Hours" : selectedDate}
|
||||||
strftime_fmt: "%b %-d",
|
|
||||||
})}
|
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<Calendar mode="single" disabled={disabledDates} />
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
disabled={disabledDates}
|
||||||
|
selected={selectedDay}
|
||||||
|
onSelect={(day) => {
|
||||||
|
setSelectedDay(day);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
@ -364,11 +295,18 @@ function GeneralFilterButton({
|
|||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
updateLabelFilter={updateLabelFilter}
|
updateLabelFilter={updateLabelFilter}
|
||||||
/>
|
/>
|
||||||
<FilterCheckBox
|
<Button
|
||||||
label="Show Reviewed"
|
className="capitalize flex justify-between items-center cursor-pointer w-full"
|
||||||
isChecked={showReviewed}
|
variant="secondary"
|
||||||
onCheckedChange={(isChecked) => setShowReviewed(isChecked)}
|
onClick={(_) => setShowReviewed(!showReviewed)}
|
||||||
/>
|
>
|
||||||
|
{showReviewed ? (
|
||||||
|
<LuCheck className="w-6 h-6" />
|
||||||
|
) : (
|
||||||
|
<div className="w-6 h-6" />
|
||||||
|
)}
|
||||||
|
<div className="ml-1 w-full flex justify-start">Show Reviewed</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
} from "../ui/context-menu";
|
} from "../ui/context-menu";
|
||||||
import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
|
import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
@ -92,6 +93,13 @@ export default function PreviewThumbnailPlayer({
|
|||||||
[hoverTimeout, review]
|
[hoverTimeout, review]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// date
|
||||||
|
|
||||||
|
const formattedDate = useFormattedTimestamp(
|
||||||
|
review.start_time,
|
||||||
|
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
@ -137,13 +145,7 @@ export default function PreviewThumbnailPlayer({
|
|||||||
{!playingBack && (
|
{!playingBack && (
|
||||||
<div className="absolute left-[6px] right-[6px] bottom-1 flex justify-between text-white">
|
<div className="absolute left-[6px] right-[6px] bottom-1 flex justify-between text-white">
|
||||||
<TimeAgo time={review.start_time * 1000} dense />
|
<TimeAgo time={review.start_time * 1000} dense />
|
||||||
{config &&
|
{formattedDate}
|
||||||
formatUnixTimestampToDateTime(review.start_time, {
|
|
||||||
strftime_fmt:
|
|
||||||
config.ui.time_format == "24hour"
|
|
||||||
? "%b %-d, %H:%M"
|
|
||||||
: "%b %-d, %I:%M %p",
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" />
|
||||||
|
|||||||
12
web/src/hooks/use-date-utils.ts
Normal file
12
web/src/hooks/use-date-utils.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function useFormattedTimestamp(timestamp: number, format: string) {
|
||||||
|
const formattedTimestamp = useMemo(() => {
|
||||||
|
return formatUnixTimestampToDateTime(timestamp, {
|
||||||
|
strftime_fmt: format,
|
||||||
|
});
|
||||||
|
}, [format, timestamp]);
|
||||||
|
|
||||||
|
return formattedTimestamp;
|
||||||
|
}
|
||||||
@ -21,6 +21,11 @@ export default function Events() {
|
|||||||
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
||||||
useApiFilter<ReviewFilter>();
|
useApiFilter<ReviewFilter>();
|
||||||
|
|
||||||
|
const onUpdateFilter = useCallback((newFilter: ReviewFilter) => {
|
||||||
|
setSize(1);
|
||||||
|
setReviewFilter(newFilter);
|
||||||
|
}, [])
|
||||||
|
|
||||||
// review paging
|
// review paging
|
||||||
|
|
||||||
const timeRange = useMemo(() => {
|
const timeRange = useMemo(() => {
|
||||||
@ -34,7 +39,6 @@ export default function Events() {
|
|||||||
|
|
||||||
const getKey = useCallback(
|
const getKey = useCallback(
|
||||||
(index: number, prevData: ReviewSegment[]) => {
|
(index: number, prevData: ReviewSegment[]) => {
|
||||||
console.log("The params are " + JSON.stringify(reviewSearchParams))
|
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const lastDate = prevData[prevData.length - 1].start_time;
|
const lastDate = prevData[prevData.length - 1].start_time;
|
||||||
reviewSearchParams;
|
reviewSearchParams;
|
||||||
@ -137,7 +141,7 @@ export default function Events() {
|
|||||||
|
|
||||||
return newData;
|
return newData;
|
||||||
},
|
},
|
||||||
{ revalidate: false }
|
{ revalidate: false, populateCache: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -209,7 +213,7 @@ export default function Events() {
|
|||||||
markItemAsReviewed={markItemAsReviewed}
|
markItemAsReviewed={markItemAsReviewed}
|
||||||
onSelectReview={setSelectedReviewId}
|
onSelectReview={setSelectedReviewId}
|
||||||
pullLatestData={updateSegments}
|
pullLatestData={updateSegments}
|
||||||
updateFilter={setReviewFilter}
|
updateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -293,6 +293,11 @@ export function endOfHourOrCurrentTime(timestamp: number) {
|
|||||||
return Math.min(timestamp, now.getTime() / 1000);
|
return Math.min(timestamp, now.getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEndOfDayTimestamp(date: Date) {
|
||||||
|
date.setHours(23, 59, 59, 999);
|
||||||
|
return date.getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
export function isCurrentHour(timestamp: number) {
|
export function isCurrentHour(timestamp: number) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setMinutes(0, 0, 0);
|
now.setMinutes(0, 0, 0);
|
||||||
|
|||||||
@ -12,24 +12,24 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5000',
|
target: 'http://192.168.50.106:5000',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
'/vod': {
|
'/vod': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/clips': {
|
'/clips': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/exports': {
|
'/exports': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://192.168.50.106:5000'
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://localhost:5000',
|
target: 'ws://192.168.50.106:5000',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
'/live': {
|
'/live': {
|
||||||
target: 'ws://localhost:5000',
|
target: 'ws://192.168.50.106:5000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user