Add time selection

This commit is contained in:
Nicolas Mowen 2024-02-25 11:18:08 -07:00
parent 853b788194
commit 422324e5c5
6 changed files with 103 additions and 142 deletions

View File

@ -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>

View File

@ -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" />

View 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;
}

View File

@ -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}
/> />
); );
} }

View File

@ -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);

View File

@ -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,
}, },