From e9ba46759e2f8cf0a8236912708395ac48e2686e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:44:40 -0600 Subject: [PATCH] two column detail view --- .../overlay/detail/SearchDetailDialog.tsx | 486 +++++++++--------- 1 file changed, 255 insertions(+), 231 deletions(-) diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 4392203bf..29e824332 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -6,7 +6,7 @@ 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"; @@ -59,7 +59,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; -import { Card, CardContent } from "@/components/ui/card"; import useImageLoaded from "@/hooks/use-image-loaded"; import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator"; import { GenericVideoPlayer } from "@/components/player/GenericVideoPlayer"; @@ -326,9 +325,6 @@ export default function SearchDetailDialog({ : "not_enabled", } as unknown as Event } - onEventUploaded={() => { - search.plus_id = "new_upload"; - }} /> )} {page === "snapshot" && !search.has_snapshot && ( @@ -412,9 +408,6 @@ export default function SearchDetailDialog({ : "not_enabled", } as unknown as Event } - onEventUploaded={() => { - search.plus_id = "new_upload"; - }} /> )} {page == "snapshot" && !search.has_snapshot && ( @@ -472,7 +465,11 @@ function ObjectDetailsTab({ setInputFocused, showThumbnail = true, }: ObjectDetailsTabProps) { - const { t } = useTranslation(["views/explore", "views/faceLibrary"]); + const { t, i18n } = useTranslation([ + "views/explore", + "views/faceLibrary", + "components/dialog", + ]); const apiHost = useApiHost(); @@ -917,35 +914,160 @@ function ObjectDetailsTab({ }); }, [search, t]); + // frigate+ submission + + type SubmissionState = "reviewing" | "uploading" | "submitted"; + const [state, setState] = useState( + search?.plus_id ? "submitted" : "reviewing", + ); + + useEffect( + () => setState(search?.plus_id ? "submitted" : "reviewing"), + [search], + ); + + const onSubmitToPlus = useCallback( + async (falsePositive: boolean) => { + if (!search) { + return; + } + + falsePositive + ? axios.put(`events/${search.id}/false_positive`) + : axios.post(`events/${search.id}/plus`, { + include_annotation: 1, + }); + + setState("submitted"); + setSearch({ + ...search, + plus_id: "new_upload", + }); + }, + [search, setSearch], + ); + + const popoverContainerRef = useRef(null); + return ( -
+
-
-
{t("details.label")}
-
- {getIconForLabel(search.label, "size-4 text-primary")} - {getTranslatedLabel(search.label)} - {search.sub_label && ` (${search.sub_label})`} - {isAdmin && search.end_time && ( - - - - { - setIsSubLabelDialogOpen(true); - }} - /> - - - - - {t("details.editSubLabel.title")} - - - - )} +
+
+
+
+
+
+ {t("details.label")} +
+
+ {getIconForLabel(search.label, "size-4 text-primary")} + {getTranslatedLabel(search.label)} + {search.sub_label && ` (${search.sub_label})`} + {isAdmin && search.end_time && ( + + + + setIsSubLabelDialogOpen(true)} + /> + + + + + {t("details.editSubLabel.title")} + + + + )} +
+
+ +
+
+
+ {t("details.topScore.label")} + + +
+ + Info +
+
+ + {t("details.topScore.info")} + +
+
+
+
+ {topScore}%{subLabelScore && ` (${subLabelScore}%)`} +
+
+ +
+
+ {t("details.camera")} +
+
+ +
+
+
+
+ +
+
+ {snapScore != undefined && ( +
+
+
+ {t("details.snapshotScore.label")} +
+
+
{snapScore}%
+
+ )} + + {averageEstimatedSpeed && ( +
+
+ {t("details.estimatedSpeed")} +
+
+
+ {averageEstimatedSpeed}{" "} + {config?.ui.unit_system == "imperial" + ? t("unit.speed.mph", { ns: "common" }) + : t("unit.speed.kph", { ns: "common" })} + {velocityAngle != undefined && ( + + + + )} +
+
+
+ )} + +
+
+ {t("details.timestamp")} +
+
{formattedDate}
+
+
+
{search?.data.recognized_license_plate && ( @@ -964,9 +1086,7 @@ function ObjectDetailsTab({ { - setIsLPRDialogOpen(true); - }} + onClick={() => setIsLPRDialogOpen(true)} /> @@ -981,77 +1101,104 @@ function ObjectDetailsTab({
)} -
-
-
- {t("details.topScore.label")} - - -
- - Info -
-
- - {t("details.topScore.info")} - -
-
-
-
- {topScore}%{subLabelScore && ` (${subLabelScore}%)`} -
-
- {snapScore != undefined && ( -
-
-
- {t("details.snapshotScore.label")} +
+
+ +
+
+
+ {t("explore.plus.submitToPlus.label", { + ns: "components/dialog", + })} + + +
+ + Info
-
-
{snapScore}%
-
- )} - {averageEstimatedSpeed && ( -
-
- {t("details.estimatedSpeed")} -
-
- {averageEstimatedSpeed && ( -
- {averageEstimatedSpeed}{" "} - {config?.ui.unit_system == "imperial" - ? t("unit.speed.mph", { ns: "common" }) - : t("unit.speed.kph", { ns: "common" })}{" "} - {velocityAngle != undefined && ( - - - - )} -
- )} -
-
- )} -
-
{t("details.camera")}
-
- -
-
-
-
- {t("details.timestamp")} -
-
{formattedDate}
+ + + {t("explore.plus.submitToPlus.desc", { + ns: "components/dialog", + })} + +
+ +
+ {state == "reviewing" && ( + <> +
+ {i18n.language === "en" ? ( + // English with a/an logic plus label + <> + {/^[aeiou]/i.test(search?.label || "") ? ( + + explore.plus.review.question.ask_an + + ) : ( + + explore.plus.review.question.ask_a + + )} + + ) : ( + // For other languages + + explore.plus.review.question.ask_full + + )} +
+
+ + +
+ + )} + {state == "uploading" && } + {state == "submitted" && ( +
+ + {t("explore.plus.review.state.submitted")} +
+ )} +
+ {showThumbnail && (
void; }; -export function ObjectSnapshotTab({ - search, - onEventUploaded, -}: ObjectSnapshotTabProps) { - const { t, i18n } = useTranslation(["components/dialog"]); - type SubmissionState = "reviewing" | "uploading" | "submitted"; +export function ObjectSnapshotTab({ search }: ObjectSnapshotTabProps) { + const { t } = useTranslation(["components/dialog"]); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); - // upload - - const [state, setState] = useState( - search?.plus_id ? "submitted" : "reviewing", - ); - - useEffect( - () => setState(search?.plus_id ? "submitted" : "reviewing"), - [search], - ); - - const onSubmitToPlus = useCallback( - async (falsePositive: boolean) => { - if (!search) { - return; - } - - falsePositive - ? axios.put(`events/${search.id}/false_positive`) - : axios.post(`events/${search.id}/plus`, { - include_annotation: 1, - }); - - setState("submitted"); - onEventUploaded(); - }, - [search, onEventUploaded], - ); - return (
)} - {search.data.type == "object" && - search.plus_id !== "not_enabled" && - search.end_time && - search.label != "on_demand" && ( - - -
-
- {t("explore.plus.submitToPlus.label")} -
-
- {t("explore.plus.submitToPlus.desc")} -
-
- -
- {state == "reviewing" && ( - <> -
- {i18n.language === "en" ? ( - // English with a/an logic plus label - <> - {/^[aeiou]/i.test(search?.label || "") ? ( - - explore.plus.review.question.ask_an - - ) : ( - - explore.plus.review.question.ask_a - - )} - - ) : ( - // For other languages - - explore.plus.review.question.ask_full - - )} -
-
- - -
- - )} - {state == "uploading" && } - {state == "submitted" && ( -
- - {t("explore.plus.review.state.submitted")} -
- )} -
-
-
- )}