From 8b6105a8178b8c47ee181c90da22e03fd92aa469 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 10 Sep 2024 15:35:40 -0600 Subject: [PATCH] Add video and frigate plus tabs for search item --- frigate/api/event.py | 3 + web/src/components/card/SearchThumbnail.tsx | 279 +++++---------- .../overlay/detail/SearchDetailDialog.tsx | 333 ++++++++++++------ .../overlay/dialog/FrigatePlusDialog.tsx | 111 +++--- web/src/components/player/HlsVideoPlayer.tsx | 138 ++++---- web/src/pages/Search.tsx | 1 - web/src/types/search.ts | 4 +- web/src/views/search/SearchView.tsx | 21 +- 8 files changed, 451 insertions(+), 439 deletions(-) diff --git a/frigate/api/event.py b/frigate/api/event.py index efe9412df..ca821c07a 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -317,7 +317,10 @@ def events_search(): Event.zones, Event.start_time, Event.end_time, + Event.has_clip, + Event.has_snapshot, Event.data, + Event.plus_id, ReviewSegment.thumb_path, ] diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index 58efd4737..5ce653a4c 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -16,8 +16,6 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { SearchResult } from "@/types/search"; import useContextMenu from "@/hooks/use-contextmenu"; import { cn } from "@/lib/utils"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { Button } from "../ui/button"; type SearchThumbnailProps = { searchResult: SearchResult; @@ -44,9 +42,7 @@ export default function SearchThumbnail({ preventScrollOnSwipe: true, }); - useContextMenu(imgRef, () => { - onClick(searchResult, true); - }); + useContextMenu(imgRef, findSimilar); // Hover Details @@ -99,202 +95,89 @@ export default function SearchThumbnail({ ); return ( - { - if (!open) { - setDetails(false); - } - }} +
setIsHovered(true)} + onMouseLeave={isMobile ? undefined : () => setIsHovered(false)} + onClick={handleOnClick} + {...swipeHandlers} > - -
setIsHovered(true)} - onMouseLeave={isMobile ? undefined : () => setIsHovered(false)} - onClick={handleOnClick} - {...swipeHandlers} - > - -
- { - onImgLoad(); - }} - /> + +
+ { + onImgLoad(); + }} + /> -
- -
setTooltipHovering(true)} - onMouseLeave={() => setTooltipHovering(false)} - > - -
- { - <> - onClick(searchResult, true)} - > - {getIconForLabel( - searchResult.label, - "size-3 text-white", - )} - - - } -
-
-
- - {[...new Set([searchResult.label])] - .filter( - (item) => - item !== undefined && !item.includes("-verified"), - ) - .map((text) => capitalizeFirstLetter(text)) - .sort() - .join(", ") - .replaceAll("-verified", "")} - -
-
-
-
-
- {searchResult.end_time ? ( - - ) : ( -
- -
- )} - {formattedDate} -
-
-
- - - -
- - - ); -} - -type SearchDetailProps = { - search?: SearchResult; - findSimilar: () => void; -}; -function SearchDetails({ search, findSimilar }: SearchDetailProps) { - const { data: config } = useSWR("config", { - revalidateOnFocus: false, - }); - - const apiHost = useApiHost(); - - // data - - const formattedDate = useFormattedTimestamp( - search?.start_time ?? 0, - config?.ui.time_format == "24hour" - ? "%b %-d %Y, %H:%M" - : "%b %-d %Y, %I:%M %p", - ); - - const score = useMemo(() => { - if (!search) { - return 0; - } - - const value = search.score ?? search.data.top_score; - - return Math.round(value * 100); - }, [search]); - - const subLabelScore = useMemo(() => { - if (!search) { - return undefined; - } - - if (search.sub_label) { - return Math.round((search.data?.top_score ?? 0) * 100); - } else { - return undefined; - } - }, [search]); - - if (!search) { - return; - } - - return ( -
-
-
-
-
Label
-
- {getIconForLabel(search.label, "size-4 text-primary")} - {search.label} - {search.sub_label && ` (${search.sub_label})`} -
-
-
-
Score
-
- {score}%{subLabelScore && ` (${subLabelScore}%)`} -
-
-
-
Camera
-
- {search.camera.replaceAll("_", " ")} -
-
-
-
Timestamp
-
{formattedDate}
-
-
-
- + +
setTooltipHovering(true)} + onMouseLeave={() => setTooltipHovering(false)} + > + +
+ { + <> + onClick(searchResult, true)} + > + {getIconForLabel( + searchResult.label, + "size-3 text-white", + )} + + } - : undefined - } - draggable={false} - src={`${apiHost}api/events/${search.id}/thumbnail.jpg`} - /> - +
+
+
+ + {[...new Set([searchResult.label])] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => capitalizeFirstLetter(text)) + .sort() + .join(", ") + .replaceAll("-verified", "")} + +
+
+
+
+
+ {searchResult.end_time ? ( + + ) : ( +
+ +
+ )} + {formattedDate} +
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index bec7589db..fa22752bd 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -1,11 +1,4 @@ import { isDesktop, isIOS } from "react-device-detect"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "../../ui/sheet"; import { Drawer, DrawerContent, @@ -20,10 +13,27 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { getIconForLabel } from "@/utils/iconUtil"; import { useApiHost } from "@/api"; import { Button } from "../../ui/button"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import axios from "axios"; import { toast } from "sonner"; import { Textarea } from "../../ui/textarea"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import useOptimisticState from "@/hooks/use-optimistic-state"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog"; +import { Event } from "@/types/event"; +import HlsVideoPlayer from "@/components/player/HlsVideoPlayer"; +import { baseUrl } from "@/api/baseUrl"; + +const SEARCH_TABS = ["details", "Frigate+", "video"] as const; +type SearchTab = (typeof SEARCH_TABS)[number]; type SearchDetailDialogProps = { search?: SearchResult; @@ -39,6 +49,127 @@ export default function SearchDetailDialog({ revalidateOnFocus: false, }); + // tabs + + const [page, setPage] = useState("details"); + const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); + + const searchTabs = useMemo(() => { + if (!config || !search) { + return []; + } + + const views = [...SEARCH_TABS]; + + if (!config.plus.enabled || !search.has_snapshot) { + const index = views.indexOf("Frigate+"); + views.splice(index, 1); + } + + // TODO implement + //if (!config.semantic_search.enabled) { + // const index = views.indexOf("similar-calendar"); + // views.splice(index, 1); + // } + + return views; + }, [config, search]); + + if (!search) { + return; + } + + // content + + const Overlay = isDesktop ? Dialog : Drawer; + const Content = isDesktop ? DialogContent : DrawerContent; + const Header = isDesktop ? DialogHeader : DrawerHeader; + const Title = isDesktop ? DialogTitle : DrawerTitle; + const Description = isDesktop ? DialogDescription : DrawerDescription; + + return ( + { + if (!open) { + setSearch(undefined); + setPage("details"); + } + }} + > + +
+ Tracked Object Details + Tracked object details +
+ +
+ { + if (value) { + setPageToggle(value); + } + }} + > + {Object.values(searchTabs).map((item) => ( + +
{item}
+
+ ))} +
+ +
+
+ {page == "details" && ( + + )} + {page == "Frigate+" && ( + {}} + onEventUploaded={() => {}} + /> + )} + {page == "video" && } +
+
+ ); +} + +type ObjectDetailsTabProps = { + search: SearchResult; + config?: FrigateConfig; + setSearch: (search: SearchResult | undefined) => void; + setSimilarity?: () => void; +}; +function ObjectDetailsTab({ + search, + config, + setSearch, + setSimilarity, +}: ObjectDetailsTabProps) { const apiHost = useApiHost(); // data @@ -77,8 +208,6 @@ export default function SearchDetailDialog({ } }, [search]); - // api - const updateDescription = useCallback(() => { if (!search) { return; @@ -101,105 +230,95 @@ export default function SearchDetailDialog({ }); }, [desc, search]); - // content - - const Overlay = isDesktop ? Sheet : Drawer; - const Content = isDesktop ? SheetContent : DrawerContent; - const Header = isDesktop ? SheetHeader : DrawerHeader; - const Title = isDesktop ? SheetTitle : DrawerTitle; - const Description = isDesktop ? SheetDescription : DrawerDescription; - return ( - { - if (!open) { - setSearch(undefined); - } - }} - > - -
- Tracked Object Details - Tracked object details -
- {search && ( -
-
-
-
-
Label
-
- {getIconForLabel(search.label, "size-4 text-primary")} - {search.label} - {search.sub_label && ` (${search.sub_label})`} -
-
-
-
Score
-
- {score}%{subLabelScore && ` (${subLabelScore}%)`} -
-
-
-
Camera
-
- {search.camera.replaceAll("_", " ")} -
-
-
-
Timestamp
-
{formattedDate}
-
-
-
- - -
-
-
-
Description
-