From 275ec2b2917a913251ca0352d7289c730d666fba Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:45:22 -0500 Subject: [PATCH] change layout --- .../overlay/detail/SearchDetailDialog.tsx | 398 +++++++--- .../overlay/detail/TrackingDetails.tsx | 694 +++++++++--------- 2 files changed, 630 insertions(+), 462 deletions(-) diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 0c0339793..d3815d11f 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -109,6 +109,7 @@ export default function SearchDetailDialog({ const { data: config } = useSWR("config", { revalidateOnFocus: false, }); + const apiHost = useApiHost(); // tabs @@ -118,6 +119,12 @@ export default function SearchDetailDialog({ 100, ); + // tracking details state + + const [trackingTimeIndex, setTrackingTimeIndex] = useState< + number | undefined + >(undefined); + // dialog and mobile page const [isOpen, setIsOpen] = useState(search != undefined); @@ -200,6 +207,9 @@ export default function SearchDetailDialog({ "scrollbar-container overflow-y-auto", isDesktop && "max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl", + isDesktop && + page == "tracking_details" && + "lg:max-w-[75%] xl:max-w-[80%]", isMobile && "px-4", )} > @@ -209,70 +219,213 @@ export default function SearchDetailDialog({ {t("trackedObjectDetails")} - -
- { - if (value) { - setPageToggle(value); - } - }} - > - {Object.values(searchTabs).map((item) => ( - - {item == "details" && } - {item == "snapshot" && } - {item == "video" && } - {item == "tracking_details" && } -
{t(`type.${item}`)}
-
- ))} -
- + {isDesktop ? ( +
+
+ {page === "snapshot" && search.has_snapshot && ( + { + search.plus_id = "new_upload"; + }} + /> + )} + {page === "video" && search.has_clip && ( + + )} + {page === "tracking_details" && ( + + )} + {(page === "details" || + (!search.has_snapshot && page === "snapshot") || + (!search.has_clip && page === "video")) && ( + + )} +
+
+ +
+ { + if (value) { + setPageToggle(value); + } + }} + > + {Object.values(searchTabs).map((item) => ( + + {item == "details" && ( + + )} + {item == "snapshot" && } + {item == "video" && } + {item == "tracking_details" && ( + + )} +
+ {t(`type.${item}`)} +
+
+ ))} +
+ +
+
+
+ {page == "details" && ( + + )} + {page == "snapshot" && ( + + )} + {page == "video" && ( + + )} + {page == "tracking_details" && ( + + )} +
+
- - {page == "details" && ( - - )} - {page == "snapshot" && ( - { - search.plus_id = "new_upload"; - }} - /> - )} - {page == "video" && } - {page == "tracking_details" && ( - {}} - /> + ) : ( + <> + +
+ { + if (value) { + setPageToggle(value); + } + }} + > + {Object.values(searchTabs).map((item) => ( + + {item == "details" && } + {item == "snapshot" && } + {item == "video" && } + {item == "tracking_details" && ( + + )} +
+ {t(`type.${item}`)} +
+
+ ))} +
+ +
+
+ {page == "details" && ( + + )} + {page == "snapshot" && ( + { + search.plus_id = "new_upload"; + }} + /> + )} + {page == "video" && } + {page == "tracking_details" && ( + + )} + )} @@ -285,6 +438,7 @@ type ObjectDetailsTabProps = { setSearch: (search: SearchResult | undefined) => void; setSimilarity?: () => void; setInputFocused: React.Dispatch>; + showThumbnail?: boolean; }; function ObjectDetailsTab({ search, @@ -292,6 +446,7 @@ function ObjectDetailsTab({ setSearch, setSimilarity, setInputFocused, + showThumbnail = true, }: ObjectDetailsTabProps) { const { t } = useTranslation(["views/explore", "views/faceLibrary"]); @@ -873,66 +1028,71 @@ function ObjectDetailsTab({
{formattedDate}
-
- -
- {config?.semantic_search.enabled && - setSimilarity != undefined && - search.data.type == "object" && ( - + )} + {hasFace && ( + { - setSearch(undefined); - setSimilarity(); - }} + faceNames={faceNames} + onTrainAttempt={onTrainFace} > -
- - {t("itemMenu.findSimilar.label")} -
- - )} - {hasFace && ( - - - - )} - {config?.cameras[search?.camera].audio_transcription.enabled && - search?.label == "speech" && - search?.end_time && ( - + +
)} + {config?.cameras[search?.camera].audio_transcription.enabled && + search?.label == "speech" && + search?.end_time && ( + + )} +
- + )}
{config?.cameras[search.camera].objects.genai.enabled && diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 77dce19e9..a95e3a287 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -5,7 +5,6 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import { Button } from "@/components/ui/button"; import { TrackingDetailsSequence } from "@/types/timeline"; import Heading from "@/components/ui/heading"; -import { ReviewDetailPaneType } from "@/types/review"; import { FrigateConfig } from "@/types/frigateConfig"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { getIconForLabel } from "@/utils/iconUtil"; @@ -18,7 +17,7 @@ import { LuSettings, LuTruck, } from "react-icons/lu"; -import { IoMdArrowRoundBack, IoMdExit } from "react-icons/io"; +import { IoMdExit } from "react-icons/io"; import { MdFaceUnlock, MdOutlineLocationOn, @@ -26,7 +25,7 @@ import { } from "react-icons/md"; import { cn } from "@/lib/utils"; import { useApiHost } from "@/api"; -import { isDesktop, isIOS, isSafari } from "react-device-detect"; +import { isIOS, isSafari } from "react-device-detect"; import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator"; import { Tooltip, @@ -63,14 +62,19 @@ type TrackingDetailsProps = { className?: string; event: Event; fullscreen?: boolean; - setPane: React.Dispatch>; + showImage?: boolean; + showLifecycle?: boolean; + timeIndex?: number; + setTimeIndex?: (index: number) => void; }; export default function TrackingDetails({ className, event, - fullscreen = false, - setPane, + showImage = true, + showLifecycle = false, + timeIndex: propTimeIndex, + setTimeIndex: propSetTimeIndex, }: TrackingDetailsProps) { const { t } = useTranslation(["views/explore"]); @@ -214,7 +218,11 @@ export default function TrackingDetails({ ); }, [savedPathPoints, eventSequencePoints, config, event]); - const [timeIndex, setTimeIndex] = useState(0); + const [localTimeIndex, setLocalTimeIndex] = useState(0); + + const timeIndex = + propTimeIndex !== undefined ? propTimeIndex : localTimeIndex; + const setTimeIndex = propSetTimeIndex || setLocalTimeIndex; const handleSetBox = useCallback( (box: number[], attrBox: number[] | undefined) => { @@ -257,15 +265,15 @@ export default function TrackingDetails({ const [hasError, setHasError] = useState(false); useEffect(() => { - if (timeIndex) { - const newSrc = `${apiHost}api/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`; + if (propTimeIndex !== undefined) { + const newSrc = `${apiHost}api/${event.camera}/recordings/${propTimeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`; setSrc(newSrc); } setImgLoaded(false); setHasError(false); // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [timeIndex, annotationOffset]); + }, [propTimeIndex, annotationOffset]); // carousels @@ -291,7 +299,7 @@ export default function TrackingDetails({ setLifecycleZones([]); } }, - [eventSequence, pathPoints, handleSetBox], + [eventSequence, pathPoints, handleSetBox, setTimeIndex], ); const formattedStart = config @@ -329,7 +337,7 @@ export default function TrackingDetails({ useEffect(() => { if (!eventSequence || eventSequence.length === 0) return; // If timeIndex hasn't been set to a non-zero value, prefer the first lifecycle timestamp - if (!timeIndex) { + if (timeIndex == null || timeIndex === 0) { setTimeIndex(eventSequence[0].timestamp); handleSetBox( eventSequence[0]?.data.box ?? [], @@ -337,12 +345,12 @@ export default function TrackingDetails({ ); setLifecycleZones(eventSequence[0]?.data.zones); } - }, [eventSequence, timeIndex, handleSetBox]); + }, [eventSequence, timeIndex, handleSetBox, setTimeIndex]); // When timeIndex changes or image finishes loading, sync the box/zones to matching lifecycle, else clear useEffect(() => { - if (!eventSequence || timeIndex == null) return; - const idx = eventSequence.findIndex((i) => i.timestamp === timeIndex); + if (!eventSequence || propTimeIndex == null) return; + const idx = eventSequence.findIndex((i) => i.timestamp === propTimeIndex); if (idx !== -1) { if (imgLoaded) { handleSetBox( @@ -356,7 +364,7 @@ export default function TrackingDetails({ setBoxStyle(null); setLifecycleZones([]); } - }, [timeIndex, imgLoaded, eventSequence, handleSetBox]); + }, [propTimeIndex, imgLoaded, eventSequence, handleSetBox]); const selectedLifecycle = useMemo(() => { if (!eventSequence || eventSequence.length === 0) return undefined; @@ -423,344 +431,344 @@ export default function TrackingDetails({ return (
- {!fullscreen && ( -
- -
- )} - -
- - {hasError && ( -
-
- - {t("trackingDetails.noImageFound")} -
-
- )} -
- - - setImgLoaded(true)} - onError={() => setHasError(true)} - /> - - {showZones && - imgRef.current?.width && - imgRef.current?.height && - lifecycleZones?.map((zone) => ( -
- - - -
- ))} - - {boxStyle && ( -
-
-
- )} - {attributeBoxStyle && ( -
- )} - {imgRef.current?.width && - imgRef.current?.height && - pathPoints && - pathPoints.length > 0 && ( -
- - - -
- )} - - - -
- navigate( - `/settings?page=masksAndZones&camera=${event.camera}&object_mask=${selectedLifecycle?.data.box}`, - ) + + + setImgLoaded(true)} + onError={() => setHasError(true)} + /> + + {showZones && + imgRef.current?.width && + imgRef.current?.height && + lifecycleZones?.map((zone) => ( +
+ + + +
+ ))} + + {boxStyle && ( +
+
+
+ )} + {attributeBoxStyle && ( +
+ )} + {imgRef.current?.width && + imgRef.current?.height && + pathPoints && + pathPoints.length > 0 && ( +
+ + + +
+ )} + + + +
+ navigate( + `/settings?page=masksAndZones&camera=${event.camera}&object_mask=${selectedLifecycle?.data.box}`, + ) + } + > +
+ {t("trackingDetails.createObjectMask")} +
+
+
+
+ +
+
+ )} + + {showLifecycle && ( + <> +
+ {t("trackingDetails.title")} + +
+ + + + + + + {t("trackingDetails.adjustAnnotationSettings")} + + + +
+
+ +
+
+ {t("trackingDetails.scrollViewTips")} +
+
+ {t("trackingDetails.count", { + first: selectedIndex + 1, + second: eventSequence?.length ?? 0, + })} +
+
+ {config?.cameras[event.camera]?.onvif.autotracking + .enabled_in_config && ( +
+ {t("trackingDetails.autoTrackingTips")} +
+ )} + {showControls && ( + + )} + +
+
+
+
{ + e.stopPropagation(); + setTimeIndex(event.start_time ?? 0); + }} + role="button" > -
- {t("trackingDetails.createObjectMask")} +
+ {getIconForLabel( + event.sub_label ? event.label + "-verified" : event.label, + "size-4 text-white", + )} +
+
+ {label} + + {formattedStart ?? ""} - {formattedEnd ?? ""} + + {event.data?.recognized_license_plate && ( + <> + · +
+ + {event.data.recognized_license_plate} + +
+ + )}
- - - -
-
- -
- {t("trackingDetails.title")} - -
- - - - - - - {t("trackingDetails.adjustAnnotationSettings")} - - - -
-
-
-
- {t("trackingDetails.scrollViewTips")} -
-
- {t("trackingDetails.count", { - first: selectedIndex + 1, - second: eventSequence?.length ?? 0, - })} -
-
- {config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && ( -
- {t("trackingDetails.autoTrackingTips")} -
- )} - {showControls && ( - - )} - -
-
-
-
{ - e.stopPropagation(); - setTimeIndex(event.start_time ?? 0); - }} - role="button" - > -
- {getIconForLabel( - event.sub_label ? event.label + "-verified" : event.label, - "size-4 text-white", - )}
-
- {label} - - {formattedStart ?? ""} - {formattedEnd ?? ""} - - {event.data?.recognized_license_plate && ( - <> - · -
- - {event.data.recognized_license_plate} - + +
+ {!eventSequence ? ( + + ) : eventSequence.length === 0 ? ( +
+ {t("detail.noObjectDetailData", { ns: "views/events" })} +
+ ) : ( +
+
+
+
+ {eventSequence.map((item, idx) => { + const isActive = + Math.abs( + (propTimeIndex ?? 0) - (item.timestamp ?? 0), + ) <= 0.5; + const formattedEventTimestamp = config + ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { + timezone: config.ui.timezone, + date_format: + config.ui.time_format == "24hour" + ? t( + "time.formattedTimestampHourMinuteSecond.24hour", + { ns: "common" }, + ) + : t( + "time.formattedTimestampHourMinuteSecond.12hour", + { ns: "common" }, + ), + time_style: "medium", + date_style: "medium", + }) + : ""; + + const ratio = + Array.isArray(item.data.box) && + item.data.box.length >= 4 + ? ( + aspectRatio * + (item.data.box[2] / item.data.box[3]) + ).toFixed(2) + : "N/A"; + const areaPx = + Array.isArray(item.data.box) && + item.data.box.length >= 4 + ? Math.round( + (config.cameras[event.camera]?.detect?.width ?? + 0) * + (config.cameras[event.camera]?.detect + ?.height ?? 0) * + (item.data.box[2] * item.data.box[3]), + ) + : undefined; + const areaPct = + Array.isArray(item.data.box) && + item.data.box.length >= 4 + ? (item.data.box[2] * item.data.box[3]).toFixed(4) + : undefined; + + return ( + { + setTimeIndex(item.timestamp ?? 0); + handleSetBox( + item.data.box ?? [], + item.data.attribute_box, + ); + setLifecycleZones(item.data.zones); + setSelectedZone(""); + }} + setSelectedZone={setSelectedZone} + getZoneColor={getZoneColor} + /> + ); + })}
- +
)}
- -
- {!eventSequence ? ( - - ) : eventSequence.length === 0 ? ( -
- {t("detail.noObjectDetailData", { ns: "views/events" })} -
- ) : ( -
-
-
-
- {eventSequence.map((item, idx) => { - const isActive = - Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5; - const formattedEventTimestamp = config - ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { - timezone: config.ui.timezone, - date_format: - config.ui.time_format == "24hour" - ? t( - "time.formattedTimestampHourMinuteSecond.24hour", - { ns: "common" }, - ) - : t( - "time.formattedTimestampHourMinuteSecond.12hour", - { ns: "common" }, - ), - time_style: "medium", - date_style: "medium", - }) - : ""; - - const ratio = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? ( - aspectRatio * - (item.data.box[2] / item.data.box[3]) - ).toFixed(2) - : "N/A"; - const areaPx = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? Math.round( - (config.cameras[event.camera]?.detect?.width ?? 0) * - (config.cameras[event.camera]?.detect?.height ?? - 0) * - (item.data.box[2] * item.data.box[3]), - ) - : undefined; - const areaPct = - Array.isArray(item.data.box) && item.data.box.length >= 4 - ? (item.data.box[2] * item.data.box[3]).toFixed(4) - : undefined; - - return ( - { - setTimeIndex(item.timestamp ?? 0); - handleSetBox( - item.data.box ?? [], - item.data.attribute_box, - ); - setLifecycleZones(item.data.zones); - setSelectedZone(""); - }} - setSelectedZone={setSelectedZone} - getZoneColor={getZoneColor} - /> - ); - })} -
-
- )} -
-
-
+ + )}
); }