From 8d65e56f0e30606185cec0633e5227abc11c5dd2 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 22 Jun 2024 14:35:18 -0600 Subject: [PATCH] Move recordings view and open recordings when search is selected --- .../player/SearchThumbnailPlayer.tsx | 36 +++- web/src/pages/Events.tsx | 2 +- web/src/pages/Search.tsx | 169 ++++++++++-------- .../{events => recording}/RecordingView.tsx | 0 web/src/views/search/SearchView.tsx | 99 ++++++++++ 5 files changed, 224 insertions(+), 82 deletions(-) rename web/src/views/{events => recording}/RecordingView.tsx (100%) create mode 100644 web/src/views/search/SearchView.tsx diff --git a/web/src/components/player/SearchThumbnailPlayer.tsx b/web/src/components/player/SearchThumbnailPlayer.tsx index d5dbd2f12..8322e037d 100644 --- a/web/src/components/player/SearchThumbnailPlayer.tsx +++ b/web/src/components/player/SearchThumbnailPlayer.tsx @@ -17,13 +17,13 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { VideoPreview } from "../preview/ScrubbablePreview"; import { Preview } from "@/types/preview"; import { SearchResult } from "@/types/search"; +import { LuInfo } from "react-icons/lu"; type SearchPlayerProps = { searchResult: SearchResult; allPreviews?: Preview[]; scrollLock?: boolean; - onTimeUpdate?: (time: number | undefined) => void; - onClick: (searchResult: SearchResult, ctrl: boolean) => void; + onClick: (searchResult: SearchResult, detail: boolean) => void; }; export default function SearchThumbnailPlayer({ @@ -31,7 +31,6 @@ export default function SearchThumbnailPlayer({ allPreviews, scrollLock = false, onClick, - onTimeUpdate, }: SearchPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); @@ -136,10 +135,6 @@ export default function SearchThumbnailPlayer({ } setPlayback(false); - - if (onTimeUpdate) { - onTimeUpdate(undefined); - } } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -167,7 +162,6 @@ export default function SearchThumbnailPlayer({ relevantPreview={relevantPreview} setIgnoreClick={setIgnoreClick} isPlayingBack={setPlayback} - onTimeUpdate={onTimeUpdate} /> )} @@ -237,6 +231,32 @@ export default function SearchThumbnailPlayer({ +
+ +
setTooltipHovering(true)} + onMouseLeave={() => setTooltipHovering(false)} + > + +
+ { + <> + + + + + } +
+
+
+ + View Detection Details + +
+
{!playingBack && ( <>
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index ff998803c..df1a99d52 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -14,7 +14,7 @@ import { SegmentedReviewData, } from "@/types/review"; import EventView from "@/views/events/EventView"; -import { RecordingView } from "@/views/events/RecordingView"; +import { RecordingView } from "@/views/recording/RecordingView"; import axios from "axios"; import { useCallback, useEffect, useMemo, useState } from "react"; import useSWR from "swr"; diff --git a/web/src/pages/Search.tsx b/web/src/pages/Search.tsx index c8f39f420..f190fc04c 100644 --- a/web/src/pages/Search.tsx +++ b/web/src/pages/Search.tsx @@ -1,24 +1,29 @@ -import SearchFilterGroup from "@/components/filter/SearchFilterGroup"; -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer"; -import { Input } from "@/components/ui/input"; -import { Toaster } from "@/components/ui/sonner"; import useApiFilter from "@/hooks/use-api-filter"; import { useCameraPreviews } from "@/hooks/use-camera-previews"; -import { cn } from "@/lib/utils"; +import { useOverlayState } from "@/hooks/use-overlay-state"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { RecordingStartingPoint } from "@/types/record"; import { SearchFilter, SearchResult } from "@/types/search"; import { TimeRange } from "@/types/timeline"; +import { RecordingView } from "@/views/recording/RecordingView"; +import SearchView from "@/views/search/SearchView"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { LuSearchCheck, LuSearchX } from "react-icons/lu"; import useSWR from "swr"; export default function Search() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + // search field handler const [searchTimeout, setSearchTimeout] = useState(); const [search, setSearch] = useState(""); const [searchTerm, setSearchTerm] = useState(""); + const [recording, setRecording] = + useOverlayState("recording"); + // search filter const [searchFilter, setSearchFilter, searchSearchParams] = @@ -82,72 +87,90 @@ export default function Search() { autoRefresh: false, }); - return ( -
- + // selection -
- setSearch(e.target.value)} - /> - - -
- -
- {searchTerm.length == 0 && ( -
- - Search For Detections -
- )} - - {searchTerm.length > 0 && searchResults?.length == 0 && ( -
- - No Detections Found -
- )} - - {isLoading && ( - - )} - -
- {searchResults && - searchResults.map((value) => { - const selected = false; - - return ( -
-
- {}} - //onTimeUpdate={onPreviewTimeUpdate} - //onClick={onSelectReview} - /> -
-
-
- ); - })} -
-
-
+ const onSelectSearch = useCallback( + (item: SearchResult, detail: boolean) => { + if (detail) { + // TODO open detail + } else { + setRecording({ + camera: item.camera, + startTime: item.start_time, + severity: "alert", + }); + } + }, + [setRecording], ); + + const selectedReviewData = useMemo(() => { + if (!recording) { + return undefined; + } + + if (!config) { + return undefined; + } + + if (!searchResults) { + return undefined; + } + + const allCameras = searchFilter?.cameras ?? Object.keys(config.cameras); + + return { + camera: recording.camera, + start_time: recording.startTime, + allCameras: allCameras, + }; + + // previews will not update after item is selected + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recording, searchResults]); + + const selectedTimeRange = useMemo(() => { + if (!recording) { + return undefined; + } + + const time = new Date(recording.startTime * 1000); + time.setUTCMinutes(0, 0, 0); + const start = time.getTime() / 1000; + time.setHours(time.getHours() + 2); + const end = time.getTime() / 1000; + return { + after: start, + before: end, + }; + }, [recording]); + + if (recording) { + if (selectedReviewData && selectedTimeRange) { + return ( + + ); + } + } else { + return ( + + ); + } } diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx similarity index 100% rename from web/src/views/events/RecordingView.tsx rename to web/src/views/recording/RecordingView.tsx diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx new file mode 100644 index 000000000..503543ae0 --- /dev/null +++ b/web/src/views/search/SearchView.tsx @@ -0,0 +1,99 @@ +import SearchFilterGroup from "@/components/filter/SearchFilterGroup"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer"; +import { Input } from "@/components/ui/input"; +import { Toaster } from "@/components/ui/sonner"; +import { cn } from "@/lib/utils"; +import { Preview } from "@/types/preview"; +import { SearchFilter, SearchResult } from "@/types/search"; +import { LuSearchCheck, LuSearchX } from "react-icons/lu"; + +type SearchViewProps = { + search: string; + searchTerm: string; + searchFilter?: SearchFilter; + searchResults?: SearchResult[]; + allPreviews?: Preview[]; + isLoading: boolean; + setSearch: (search: string) => void; + onUpdateFilter: (filter: SearchFilter) => void; + onSelectItem: (item: SearchResult, detail: boolean) => void; +}; +export default function SearchView({ + search, + searchTerm, + searchFilter, + searchResults, + allPreviews, + isLoading, + setSearch, + onUpdateFilter, + onSelectItem, +}: SearchViewProps) { + return ( +
+ + +
+ setSearch(e.target.value)} + /> + + +
+ +
+ {searchTerm.length == 0 && ( +
+ + Search For Detections +
+ )} + + {searchTerm.length > 0 && searchResults?.length == 0 && ( +
+ + No Detections Found +
+ )} + + {isLoading && ( + + )} + +
+ {searchResults && + searchResults.map((value) => { + const selected = false; + + return ( +
+
+ onSelectItem(item, detail)} + /> +
+
+
+ ); + })} +
+
+
+ ); +}