From 5aee70ac7a24e9d29d9cf8bb8a3971c184917314 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:44:31 -0500 Subject: [PATCH] Explore view fixes (#13726) * return description consistently under data object * description type * navigate to history view from explore video tab --- frigate/api/event.py | 25 +++++-- frigate/api/review.py | 12 +++ .../overlay/detail/SearchDetailDialog.tsx | 75 ++++++++++++++----- web/src/types/search.ts | 2 +- 4 files changed, 89 insertions(+), 25 deletions(-) diff --git a/frigate/api/event.py b/frigate/api/event.py index 6e75602e9..fd3c4ad0b 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -302,8 +302,21 @@ def events_explore(): .dicts() ) - events = query.iterator() - return jsonify(list(events)) + events = list(query.iterator()) + + processed_events = [ + {k: v for k, v in event.items() if k != "data"} + | { + "data": { + k: v + for k, v in event["data"].items() + if k in ["type", "score", "top_score", "description"] + } + } + for event in events + ] + + return jsonify(processed_events) @EventBp.route("/event_ids") @@ -507,9 +520,11 @@ def events_search(): events = [ {k: v for k, v in event.items() if k != "data"} | { - k: v - for k, v in event["data"].items() - if k in ["type", "score", "top_score", "description"] + "data": { + k: v + for k, v in event["data"].items() + if k in ["type", "score", "top_score", "description"] + } } | { "search_distance": results[event["id"]]["distance"], diff --git a/frigate/api/review.py b/frigate/api/review.py index 6bb2a4800..d391828d5 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -94,6 +94,18 @@ def review(): return jsonify([r for r in review]) +@ReviewBp.route("/review/event/") +def get_review_from_event(id: str): + try: + return model_to_dict( + ReviewSegment.get( + ReviewSegment.data["detections"].cast("text") % f'*"{id}"*' + ) + ) + except DoesNotExist: + return "Review item not found", 404 + + @ReviewBp.route("/review/") def get_review(id: str): try: diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 7417cb3c4..e4651d6dd 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -27,7 +27,7 @@ import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record"; -import { FaImage, FaRegListAlt, FaVideo } from "react-icons/fa"; +import { FaHistory, FaImage, FaRegListAlt, FaVideo } from "react-icons/fa"; import { FaRotate } from "react-icons/fa6"; import ObjectLifecycle from "./ObjectLifecycle"; import { @@ -37,6 +37,14 @@ import { MobilePageHeader, MobilePageTitle, } from "@/components/mobile/MobilePage"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ReviewSegment } from "@/types/review"; +import { useNavigate } from "react-router-dom"; +import Chip from "@/components/indicators/Chip"; const SEARCH_TABS = [ "details", @@ -226,10 +234,10 @@ function ObjectDetailsTab({ // data - const [desc, setDesc] = useState(search?.description); + const [desc, setDesc] = useState(search?.data.description); // we have to make sure the current selected search item stays in sync - useEffect(() => setDesc(search?.description), [search]); + useEffect(() => setDesc(search?.data.description), [search]); const formattedDate = useFormattedTimestamp( search?.start_time ?? 0, @@ -279,7 +287,7 @@ function ObjectDetailsTab({ toast.error("Failed to update the description", { position: "top-center", }); - setDesc(search.description); + setDesc(search.data.description); }); }, [desc, search]); @@ -367,6 +375,11 @@ function VideoTab({ search, config }: VideoTabProps) { const endTime = useMemo(() => search.end_time ?? Date.now() / 1000, [search]); + const navigate = useNavigate(); + const { data: reviewItem } = useSWR([ + `review/event/${search.id}`, + ]); + const mainCameraAspect = useMemo(() => { const camera = config?.cameras?.[search.camera]; @@ -416,22 +429,46 @@ function VideoTab({ search, config }: VideoTabProps) { }, [mainCameraAspect]); return ( -
- {isLoading && ( - - )} -
- setIsLoading(false)} - /> +
+
+ {(isLoading || !reviewItem) && ( + + )} +
+ setIsLoading(false)} + /> +
+ {!isLoading && ( +
+ + + { + if (reviewItem?.id) { + const params = new URLSearchParams({ + id: reviewItem.id, + }).toString(); + navigate(`/review?${params}`); + } + }} + > + + + + View in History + +
+ )}
); } diff --git a/web/src/types/search.ts b/web/src/types/search.ts index 63b86229d..7cc7e3e91 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -3,7 +3,6 @@ export type SearchSource = "similarity" | "thumbnail" | "description"; export type SearchResult = { id: string; camera: string; - description?: string; start_time: number; end_time?: number; score: number; @@ -25,6 +24,7 @@ export type SearchResult = { area: number; ratio: number; type: "object" | "audio" | "manual"; + description?: string; }; };