From 9801534f11c4230aeb68fb70393323434732f5bc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 25 Feb 2024 12:04:44 -0700 Subject: [PATCH] Implement Review Filters (#10031) * Get cameras filter working * Implement label and review filters * Fix * Add time selection * Cleanup * Cleanup * cleanup * remove commented code * Fix --- frigate/http.py | 26 +- web/src/components/card/TimelineItemCard.tsx | 144 ------- .../filter/HistoryFilterPopover.tsx | 307 --------------- .../components/filter/ReviewFilterGroup.tsx | 371 ++++++++++++++++++ .../player/PreviewThumbnailPlayer.tsx | 18 +- web/src/hooks/use-api-filter.ts | 10 +- web/src/hooks/use-date-utils.ts | 12 + web/src/pages/Events.tsx | 51 ++- web/src/types/review.ts | 8 + web/src/utils/dateUtil.ts | 5 + web/src/views/events/DesktopEventView.tsx | 54 +-- 11 files changed, 473 insertions(+), 533 deletions(-) delete mode 100644 web/src/components/card/TimelineItemCard.tsx delete mode 100644 web/src/components/filter/HistoryFilterPopover.tsx create mode 100644 web/src/components/filter/ReviewFilterGroup.tsx create mode 100644 web/src/hooks/use-date-utils.ts diff --git a/frigate/http.py b/frigate/http.py index 7f29f3f94..bb5675b00 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -2395,7 +2395,9 @@ def vod_event(id): @bp.route("/review") def review(): - camera = request.args.get("camera", "all") + cameras = request.args.get("cameras", "all") + labels = request.args.get("labels", "all") + reviewed = request.args.get("reviewed", default=False) limit = request.args.get("limit", 100) severity = request.args.get("severity", None) @@ -2406,8 +2408,26 @@ def review(): clauses = [((ReviewSegment.start_time > after) & (ReviewSegment.end_time < before))] - if camera != "all": - clauses.append((ReviewSegment.camera == camera)) + if cameras != "all": + camera_list = cameras.split(",") + clauses.append((ReviewSegment.camera << camera_list)) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + ) + + label_clause = reduce(operator.or_, label_clauses) + clauses.append((label_clause)) + + if not reviewed: + clauses.append((ReviewSegment.has_been_reviewed == False)) if severity: clauses.append((ReviewSegment.severity == severity)) diff --git a/web/src/components/card/TimelineItemCard.tsx b/web/src/components/card/TimelineItemCard.tsx deleted file mode 100644 index 9ebbc2c88..000000000 --- a/web/src/components/card/TimelineItemCard.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { getTimelineItemDescription } from "@/utils/timelineUtil"; -import { Button } from "../ui/button"; -import Logo from "../Logo"; -import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; -import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; -import VideoPlayer from "../player/VideoPlayer"; -import { Card } from "../ui/card"; -import { useApiHost } from "@/api"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "../ui/alert-dialog"; -import { useCallback } from "react"; -import axios from "axios"; - -type TimelineItemCardProps = { - timeline: Timeline; - relevantPreview: Preview | undefined; - onSelect: () => void; -}; -export default function TimelineItemCard({ - timeline, - relevantPreview, - onSelect, -}: TimelineItemCardProps) { - const { data: config } = useSWR("config"); - const apiHost = useApiHost(); - - const onSubmitToPlus = useCallback( - async (falsePositive: boolean) => { - falsePositive - ? await axios.put(`events/${timeline.source_id}/false_positive`) - : await axios.post(`events/${timeline.source_id}/plus`, { - include_annotation: 1, - }); - }, - [timeline] - ); - - return ( - -
- { - if (relevantPreview) { - player.pause(); // autoplay + pause is required for iOS - player.currentTime(timeline.timestamp - relevantPreview.start); - } - }} - /> -
-
-
- {getTimelineItemDescription(timeline)} -
-
- {formatUnixTimestampToDateTime(timeline.timestamp, { - strftime_fmt: - config?.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S %p", - time_style: "medium", - date_style: "medium", - })} -
- {timeline.source == "tracked_object" && ( - - - - - - - Submit To Frigate+ - - Objects in locations you want to avoid are not false - positives. Submitting them as false positives will confuse the - model. - - - {`${timeline.data.label}`} - - Cancel - onSubmitToPlus(false)} - > - This is a {timeline.data.label} - - onSubmitToPlus(true)} - > - This is not a {timeline.data.label} - - - - - )} -
-
- ); -} diff --git a/web/src/components/filter/HistoryFilterPopover.tsx b/web/src/components/filter/HistoryFilterPopover.tsx deleted file mode 100644 index d293e266e..000000000 --- a/web/src/components/filter/HistoryFilterPopover.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { LuCheck, LuFilter } from "react-icons/lu"; -import { Button } from "../ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; -import { useMemo, useState } from "react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { Calendar } from "../ui/calendar"; - -type HistoryFilterPopoverProps = { - // @ts-ignore - filter: HistoryFilter | undefined; - // @ts-ignore - onUpdateFilter: (filter: HistoryFilter) => void; -}; - -export default function HistoryFilterPopover({ - filter, - onUpdateFilter, -}: HistoryFilterPopoverProps) { - const { data: config } = useSWR("config"); - - const [open, setOpen] = useState(false); - const disabledDates = useMemo(() => { - const tomorrow = new Date(); - tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); - const future = new Date(); - future.setFullYear(2032); - return { from: tomorrow, to: future }; - }, []); - - const { data: allLabels } = useSWR(["labels"], { - revalidateOnFocus: false, - }); - const { data: allSubLabels } = useSWR( - ["sub_labels", { split_joined: 1 }], - { - revalidateOnFocus: false, - } - ); - const filterValues = useMemo( - () => ({ - cameras: Object.keys(config?.cameras || {}), - labels: Object.values(allLabels || {}), - }), - [config, allLabels, allSubLabels] - ); - const [selectedFilters, setSelectedFilters] = useState({ - cameras: filter == undefined ? ["all"] : filter.cameras, - labels: filter == undefined ? ["all"] : filter.labels, - before: filter?.before, - after: filter?.after, - detailLevel: filter?.detailLevel ?? "normal", - }); - const dateRange = useMemo(() => { - return selectedFilters?.before == undefined || - selectedFilters?.after == undefined - ? undefined - : { - from: new Date(selectedFilters.after * 1000), - to: new Date(selectedFilters.before * 1000), - }; - }, [selectedFilters]); - - const allItems = useMemo(() => { - return { - cameras: - JSON.stringify(selectedFilters.cameras) == JSON.stringify(["all"]), - labels: JSON.stringify(selectedFilters.labels) == JSON.stringify(["all"]), - }; - }, [selectedFilters]); - - return ( - setOpen(open)}> - - - - -
- - - - - - Filter Cameras - - { - if (isChecked) { - setSelectedFilters({ - ...selectedFilters, - cameras: ["all"], - }); - } - }} - /> - - {filterValues.cameras.map((item) => ( - { - if (isChecked) { - const selectedCameras = allItems.cameras - ? [] - : [...selectedFilters.cameras]; - selectedCameras.push(item); - setSelectedFilters({ - ...selectedFilters, - cameras: selectedCameras, - }); - } else { - const selectedCameraList = [...selectedFilters.cameras]; - - // can not deselect the last item - if (selectedCameraList.length > 1) { - selectedCameraList.splice( - selectedCameraList.indexOf(item), - 1 - ); - setSelectedFilters({ - ...selectedFilters, - cameras: selectedCameraList, - }); - } - } - }} - /> - ))} - - - - - - - - Filter Labels - - { - if (isChecked) { - setSelectedFilters({ - ...selectedFilters, - labels: ["all"], - }); - } - }} - /> - - {filterValues.labels.map((item) => ( - { - 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, - }); - } - } - }} - /> - ))} - - - - - - - - - Detail Level - - - { - setSelectedFilters({ - ...selectedFilters, - // @ts-ignore we know that value is one of the detailLevel - detailLevel: value, - }); - }} - > - - Normal - - - Extra - - Full - - - -
- { - let afterTime = undefined; - if (range?.from != undefined) { - afterTime = range.from.getTime() / 1000; - } - - // need to make sure the day selected for before covers the entire day - let beforeTime = undefined; - if (range?.from != undefined) { - const beforeDate = range.to ?? range.from; - beforeDate.setHours(beforeDate.getHours() + 24, -1, 0, 0); - beforeTime = beforeDate.getTime() / 1000; - } - - setSelectedFilters({ - ...selectedFilters, - after: afterTime, - before: beforeTime, - }); - }} - /> - -
-
- ); -} - -type FilterCheckBoxProps = { - label: string; - isChecked: boolean; - onCheckedChange: (isChecked: boolean) => void; -}; - -function FilterCheckBox({ - label, - isChecked, - onCheckedChange, -}: FilterCheckBoxProps) { - return ( - - ); -} diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx new file mode 100644 index 000000000..ef51da673 --- /dev/null +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -0,0 +1,371 @@ +import { LuCalendar, LuCheck, LuFilter, LuVideo } from "react-icons/lu"; +import { Button } from "../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useCallback, useMemo, useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Calendar } from "../ui/calendar"; +import { ReviewFilter } from "@/types/review"; +import { getEndOfDayTimestamp } from "@/utils/dateUtil"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; + +const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"]; + +type ReviewFilterGroupProps = { + filter?: ReviewFilter; + onUpdateFilter: (filter: ReviewFilter) => void; +}; + +export default function ReviewFilterGroup({ + filter, + onUpdateFilter, +}: ReviewFilterGroupProps) { + const { data: config } = useSWR("config"); + + const allLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + const cameras = filter?.cameras || Object.keys(config.cameras); + + cameras.forEach((camera) => { + config.cameras[camera].objects.track.forEach((label) => { + if (!ATTRIBUTES.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels]; + }, [config, filter]); + + const filterValues = useMemo( + () => ({ + cameras: Object.keys(config?.cameras || {}), + labels: Object.values(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 ( +
+ { + onUpdateFilter({ ...filter, cameras: newCameras }); + }} + /> + + { + onUpdateFilter({ ...filter, labels: newLabels }); + }} + showReviewed={filter?.showReviewed || false} + setShowReviewed={(reviewed) => + onUpdateFilter({ ...filter, showReviewed: reviewed }) + } + /> +
+ ); +} + +type CameraFilterButtonProps = { + allCameras: string[]; + selectedCameras: string[] | undefined; + updateCameraFilter: (cameras: string[] | undefined) => void; +}; +function CamerasFilterButton({ + allCameras, + selectedCameras, + updateCameraFilter, +}: CameraFilterButtonProps) { + const [currentCameras, setCurrentCameras] = useState( + selectedCameras + ); + + return ( + { + if (!open) { + updateCameraFilter(currentCameras); + } + }} + > + + + + + Filter Cameras + + { + if (isChecked) { + setCurrentCameras(undefined); + } + }} + /> + + {allCameras.map((item) => ( + { + if (isChecked) { + const updatedCameras = currentCameras + ? [...currentCameras] + : []; + + updatedCameras.push(item); + setCurrentCameras(updatedCameras); + } else { + const updatedCameras = currentCameras + ? [...currentCameras] + : []; + + // can not deselect the last item + if (updatedCameras.length > 1) { + updatedCameras.splice(updatedCameras.indexOf(item), 1); + setCurrentCameras(updatedCameras); + } + } + }} + /> + ))} + + + + ); +} + +type CalendarFilterButtonProps = { + day?: Date; + updateSelectedDay: (day?: Date) => void; +}; +function CalendarFilterButton({ + day, + updateSelectedDay, +}: CalendarFilterButtonProps) { + const [selectedDay, setSelectedDay] = useState(day); + const disabledDates = useMemo(() => { + const tomorrow = new Date(); + tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); + const future = new Date(); + future.setFullYear(tomorrow.getFullYear() + 10); + return { from: tomorrow, to: future }; + }, []); + const selectedDate = useFormattedTimestamp( + day == undefined ? 0 : day?.getTime() / 1000, + "%b %-d" + ); + + return ( + { + if (!open) { + updateSelectedDay(selectedDay); + } + }} + > + + + + + { + setSelectedDay(day); + }} + /> + + + ); +} + +type GeneralFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; + showReviewed: boolean; + setShowReviewed: (reviewed: boolean) => void; +}; +function GeneralFilterButton({ + allLabels, + selectedLabels, + updateLabelFilter, + showReviewed, + setShowReviewed, +}: GeneralFilterButtonProps) { + return ( + + + + + +
+ + +
+
+
+ ); +} + +type LabelFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; +}; +function LabelsFilterButton({ + allLabels, + selectedLabels, + updateLabelFilter, +}: LabelFilterButtonProps) { + const [currentLabels, setCurrentLabels] = useState( + selectedLabels + ); + + return ( + { + if (!open) { + updateLabelFilter(currentLabels); + } + }} + > + + + + + Filter Labels + + { + if (isChecked) { + setCurrentLabels(undefined); + } + }} + /> + + {allLabels.map((item) => ( + { + if (isChecked) { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + updatedLabels.push(item); + setCurrentLabels(updatedLabels); + } else { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice(updatedLabels.indexOf(item), 1); + setCurrentLabels(updatedLabels); + } + } + }} + /> + ))} + + + + ); +} + +type FilterCheckBoxProps = { + label: string; + isChecked: boolean; + onCheckedChange: (isChecked: boolean) => void; +}; + +function FilterCheckBox({ + label, + isChecked, + onCheckedChange, +}: FilterCheckBoxProps) { + return ( + + ); +} diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 598aa234b..dda4fc2a8 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useApiHost } from "@/api"; -import { formatUnixTimestampToDateTime, isCurrentHour } from "@/utils/dateUtil"; +import { isCurrentHour } from "@/utils/dateUtil"; import { ReviewSegment } from "@/types/review"; import { Slider } from "../ui/slider"; import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil"; @@ -18,6 +18,7 @@ import { } from "../ui/context-menu"; import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu"; import axios from "axios"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; type PreviewPlayerProps = { review: ReviewSegment; @@ -92,6 +93,13 @@ export default function PreviewThumbnailPlayer({ [hoverTimeout, review] ); + // date + + const formattedDate = useFormattedTimestamp( + review.start_time, + config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p" + ); + return ( @@ -137,13 +145,7 @@ export default function PreviewThumbnailPlayer({ {!playingBack && (
- {config && - formatUnixTimestampToDateTime(review.start_time, { - strftime_fmt: - config.ui.time_format == "24hour" - ? "%b %-d, %H:%M" - : "%b %-d, %I:%M %p", - })} + {formattedDate}
)}
diff --git a/web/src/hooks/use-api-filter.ts b/web/src/hooks/use-api-filter.ts index 85e3a11b3..f0f0de3a6 100644 --- a/web/src/hooks/use-api-filter.ts +++ b/web/src/hooks/use-api-filter.ts @@ -3,11 +3,9 @@ import { useMemo, useState } from "react"; type useApiFilterReturn = [ filter: F | undefined, setFilter: (filter: F) => void, - searchParams: - | { - [key: string]: any; - } - | undefined, + searchParams: { + [key: string]: any; + }, ]; export default function useApiFilter< @@ -16,7 +14,7 @@ export default function useApiFilter< const [filter, setFilter] = useState(undefined); const searchParams = useMemo(() => { if (filter == undefined) { - return undefined; + return {}; } const search: { [key: string]: string } = {}; diff --git a/web/src/hooks/use-date-utils.ts b/web/src/hooks/use-date-utils.ts new file mode 100644 index 000000000..99ee55f59 --- /dev/null +++ b/web/src/hooks/use-date-utils.ts @@ -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; +} diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 77c2a3f61..6e7e10526 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,5 +1,6 @@ +import useApiFilter from "@/hooks/use-api-filter"; import useOverlayState from "@/hooks/use-overlay-state"; -import { ReviewSegment } from "@/types/review"; +import { ReviewFilter, ReviewSegment } from "@/types/review"; import DesktopEventView from "@/views/events/DesktopEventView"; import DesktopRecordingView from "@/views/events/DesktopRecordingView"; import MobileEventView from "@/views/events/MobileEventView"; @@ -15,6 +16,16 @@ export default function Events() { // recordings viewer const [selectedReviewId, setSelectedReviewId] = useOverlayState("review"); + // review filter + + const [reviewFilter, setReviewFilter, reviewSearchParams] = + useApiFilter(); + + const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { + setSize(1); + setReviewFilter(newFilter); + }, []) + // review paging const timeRange = useMemo(() => { @@ -26,30 +37,30 @@ export default function Events() { return axios.get(path, { params }).then((res) => res.data); }, []); - const reviewSearchParams = {}; const getKey = useCallback( (index: number, prevData: ReviewSegment[]) => { if (index > 0) { const lastDate = prevData[prevData.length - 1].start_time; - const pagedParams = reviewSearchParams - ? { before: lastDate, after: timeRange.after, limit: API_LIMIT } - : { - ...reviewSearchParams, - before: lastDate, - after: timeRange.after, - limit: API_LIMIT, - }; + reviewSearchParams; + const pagedParams = { + cameras: reviewSearchParams["cameras"], + labels: reviewSearchParams["labels"], + reviewed: reviewSearchParams["showReviewed"] || false, + before: lastDate, + after: reviewSearchParams["after"] || timeRange.after, + limit: API_LIMIT, + }; return ["review", pagedParams]; } - const params = reviewSearchParams - ? { limit: API_LIMIT, before: timeRange.before, after: timeRange.after } - : { - ...reviewSearchParams, - limit: API_LIMIT, - before: timeRange.before, - after: timeRange.after, - }; + const params = { + cameras: reviewSearchParams["cameras"], + labels: reviewSearchParams["labels"], + reviewed: reviewSearchParams["showReviewed"] || false, + limit: API_LIMIT, + before: reviewSearchParams["before"] || timeRange.before, + after: reviewSearchParams["after"] || timeRange.after, + }; return ["review", params]; }, [reviewSearchParams] @@ -130,7 +141,7 @@ export default function Events() { return newData; }, - { revalidate: false } + { revalidate: false, populateCache: true } ); } }, @@ -197,10 +208,12 @@ export default function Events() { timeRange={timeRange} reachedEnd={isDone} isValidating={isValidating} + filter={reviewFilter} loadNextPage={() => setSize(size + 1)} markItemAsReviewed={markItemAsReviewed} onSelectReview={setSelectedReviewId} pullLatestData={updateSegments} + updateFilter={onUpdateFilter} /> ); } diff --git a/web/src/types/review.ts b/web/src/types/review.ts index 55a08a819..287c89573 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -19,3 +19,11 @@ export type ReviewData = { significant_motion_areas: number[]; zones: string[]; }; + +export type ReviewFilter = { + cameras?: string[]; + labels?: string[]; + before?: number; + after?: number; + showReviewed?: boolean; +}; diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index f17a261fc..244dcb586 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -293,6 +293,11 @@ export function endOfHourOrCurrentTime(timestamp: number) { 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) { const now = new Date(); now.setMinutes(0, 0, 0); diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index bed88be6a..c0e376984 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -1,20 +1,14 @@ import { useFrigateEvents } from "@/api/ws"; +import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { FrigateConfig } from "@/types/frigateConfig"; -import { ReviewSegment, ReviewSeverity } from "@/types/review"; -import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { LuCalendar, LuFilter, LuRefreshCcw, LuVideo } from "react-icons/lu"; +import { LuRefreshCcw } from "react-icons/lu"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; @@ -24,10 +18,12 @@ type DesktopEventViewProps = { timeRange: { before: number; after: number }; reachedEnd: boolean; isValidating: boolean; + filter?: ReviewFilter; loadNextPage: () => void; markItemAsReviewed: (reviewId: string) => void; onSelectReview: (reviewId: string) => void; pullLatestData: () => void; + updateFilter: (filter: ReviewFilter) => void; }; export default function DesktopEventView({ reviewPages, @@ -35,10 +31,12 @@ export default function DesktopEventView({ timeRange, reachedEnd, isValidating, + filter, loadNextPage, markItemAsReviewed, onSelectReview, pullLatestData, + updateFilter, }: DesktopEventViewProps) { const { data: config } = useSWR("config"); const [severity, setSeverity] = useState("alert"); @@ -234,17 +232,7 @@ export default function DesktopEventView({ Motion -
- - - -
+
@@ -334,29 +322,3 @@ export default function DesktopEventView({
); } - -function ReviewCalendarButton() { - const disabledDates = useMemo(() => { - const tomorrow = new Date(); - tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); - const future = new Date(); - future.setFullYear(tomorrow.getFullYear() + 10); - return { from: tomorrow, to: future }; - }, []); - - return ( - - - - - - - - - ); -}