From 765bd2c98892f79fb113bf41068eb55ddeda532e Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 22 Jun 2024 15:10:48 -0600 Subject: [PATCH] Implement detail pane --- .../components/overlay/SearchDetailDialog.tsx | 122 ++++++++++++++++++ .../player/SearchThumbnailPlayer.tsx | 1 + web/src/pages/Search.tsx | 20 ++- web/src/views/search/SearchView.tsx | 26 +++- 4 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 web/src/components/overlay/SearchDetailDialog.tsx diff --git a/web/src/components/overlay/SearchDetailDialog.tsx b/web/src/components/overlay/SearchDetailDialog.tsx new file mode 100644 index 000000000..308291e1b --- /dev/null +++ b/web/src/components/overlay/SearchDetailDialog.tsx @@ -0,0 +1,122 @@ +import { isDesktop, isIOS } from "react-device-detect"; +import { Sheet, SheetContent } from "../ui/sheet"; +import { Drawer, DrawerContent } from "../ui/drawer"; +import { SearchResult } from "@/types/search"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { useApiHost } from "@/api"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { useState } from "react"; + +type SearchDetailDialogProps = { + search?: SearchResult; + setSearch: (search: SearchResult | undefined) => void; +}; +export default function SearchDetailDialog({ + search, + setSearch, +}: SearchDetailDialogProps) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const apiHost = useApiHost(); + + // data + + const [desc, setDesc] = useState(search?.description); + const formattedDate = useFormattedTimestamp( + search?.start_time ?? 0, + config?.ui.time_format == "24hour" + ? "%b %-d %Y, %H:%M" + : "%b %-d %Y, %I:%M %p", + ); + + // content + + const Overlay = isDesktop ? Sheet : Drawer; + const Content = isDesktop ? SheetContent : DrawerContent; + + return ( + { + if (!open) { + setSearch(undefined); + } + }} + > + + {search && ( +
+
+
+
+
Label
+
+ {getIconForLabel(search.label, "size-4 text-white")} + {search.label} +
+
+
+
Score
+
+ {Math.round(search.score * 100)}% +
+
+
+
Camera
+
+ {search.camera.replaceAll("_", " ")} +
+
+
+
Timestamp
+
{formattedDate}
+
+
+
+ + +
+
+
+
Description
+ setDesc(e.target.value)} + /> +
+ +
+
+
+ )} +
+
+ ); +} diff --git a/web/src/components/player/SearchThumbnailPlayer.tsx b/web/src/components/player/SearchThumbnailPlayer.tsx index 8322e037d..69f315669 100644 --- a/web/src/components/player/SearchThumbnailPlayer.tsx +++ b/web/src/components/player/SearchThumbnailPlayer.tsx @@ -244,6 +244,7 @@ export default function SearchThumbnailPlayer({ <> onClick(searchResult, true)} > diff --git a/web/src/pages/Search.tsx b/web/src/pages/Search.tsx index f190fc04c..59e43b70c 100644 --- a/web/src/pages/Search.tsx +++ b/web/src/pages/Search.tsx @@ -89,17 +89,13 @@ export default function Search() { // selection - const onSelectSearch = useCallback( - (item: SearchResult, detail: boolean) => { - if (detail) { - // TODO open detail - } else { - setRecording({ - camera: item.camera, - startTime: item.start_time, - severity: "alert", - }); - } + const onOpenSearch = useCallback( + (item: SearchResult) => { + setRecording({ + camera: item.camera, + startTime: item.start_time, + severity: "alert", + }); }, [setRecording], ); @@ -169,7 +165,7 @@ export default function Search() { isLoading={isLoading} setSearch={setSearch} onUpdateFilter={onUpdateFilter} - onSelectItem={onSelectSearch} + onOpenSearch={onOpenSearch} /> ); } diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 503543ae0..5e6c82e16 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -1,11 +1,13 @@ import SearchFilterGroup from "@/components/filter/SearchFilterGroup"; import ActivityIndicator from "@/components/indicators/activity-indicator"; +import SearchDetailDialog from "@/components/overlay/SearchDetailDialog"; 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 { useCallback, useState } from "react"; import { LuSearchCheck, LuSearchX } from "react-icons/lu"; type SearchViewProps = { @@ -17,7 +19,7 @@ type SearchViewProps = { isLoading: boolean; setSearch: (search: string) => void; onUpdateFilter: (filter: SearchFilter) => void; - onSelectItem: (item: SearchResult, detail: boolean) => void; + onOpenSearch: (item: SearchResult) => void; }; export default function SearchView({ search, @@ -28,11 +30,29 @@ export default function SearchView({ isLoading, setSearch, onUpdateFilter, - onSelectItem, + onOpenSearch, }: SearchViewProps) { + // detail + + const [searchDetail, setSearchDetail] = useState(); + + // search interaction + + const onSelectSearch = useCallback( + (item: SearchResult, detail: boolean) => { + if (detail) { + setSearchDetail(item); + } else { + onOpenSearch(item); + } + }, + [onOpenSearch], + ); + return (
+
onSelectItem(item, detail)} + onClick={onSelectSearch} />