From dbbe40bd2716c02ff2d1b7155b94992282be249d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:03:38 -0500 Subject: [PATCH] Improve Details Settings (#20718) * detail stream settings * fix mobile landscape * mobile landscape * tweak * tweaks --- web/public/locales/en/views/events.json | 7 +- web/public/locales/en/views/explore.json | 2 +- .../overlay/detail/AnnotationOffsetSlider.tsx | 87 ++++++++---- web/src/components/timeline/DetailStream.tsx | 124 +++++++++++++----- 4 files changed, 165 insertions(+), 55 deletions(-) diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index 333fca397..6e242c1e3 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -26,7 +26,12 @@ "aria": "Toggle detail view", "trackedObject_one": "object", "trackedObject_other": "objects", - "noObjectDetailData": "No object detail data available." + "noObjectDetailData": "No object detail data available.", + "settings": "Detail View Settings", + "alwaysExpandActive": { + "title": "Always expand active", + "desc": "Always expand the active review item's object details when available." + } }, "objectTrack": { "trackedPoint": "Tracked point", diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 0316ed2fe..3a0e7af00 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -71,7 +71,7 @@ }, "offset": { "label": "Annotation Offset", - "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. However, the annotation_offset field can be used to adjust this.", + "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", "millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0", "tips": "TIP: Imagine there is an event clip with a person walking from left to right. If the event timeline bounding box is consistently to the left of the person then the value should be decreased. Similarly, if a person is walking from left to right and the bounding box is consistently ahead of the person then the value should be increased.", "toast": { diff --git a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx index 0a2594d26..9f6b6efbd 100644 --- a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx +++ b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx @@ -1,11 +1,15 @@ import { useCallback, useState } from "react"; import { Slider } from "@/components/ui/slider"; import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; import { useDetailStream } from "@/context/detail-stream-context"; import axios from "axios"; import { useSWRConfig } from "swr"; import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; +import { LuInfo } from "react-icons/lu"; +import { cn } from "@/lib/utils"; +import { isMobile } from "react-device-detect"; type Props = { className?: string; @@ -65,30 +69,67 @@ export default function AnnotationOffsetSlider({ className }: Props) { return (
-
- Annotation offset (ms): {annotationOffset} +
+
+ + {t("trackingDetails.annotationSettings.offset.label")}: + + {annotationOffset} +
+
+ +
+
+ + +
-
- -
-
- - +
+ + trackingDetails.annotationSettings.offset.millisecondsToOffset + + + + + + + {t("trackingDetails.annotationSettings.offset.desc")} + +
); diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index b0749ae13..b5ce70446 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -16,13 +16,21 @@ import ActivityIndicator from "../indicators/activity-indicator"; import { Event } from "@/types/event"; import { getIconForLabel } from "@/utils/iconUtil"; import { ReviewSegment } from "@/types/review"; -import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu"; +import { + LuChevronDown, + LuCircle, + LuChevronRight, + LuSettings, +} from "react-icons/lu"; import { getTranslatedLabel } from "@/utils/i18n"; import EventMenu from "@/components/timeline/EventMenu"; import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog"; import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Link } from "react-router-dom"; +import { Switch } from "@/components/ui/switch"; +import { usePersistence } from "@/hooks/use-persistence"; +import { isDesktop } from "react-device-detect"; type DetailStreamProps = { reviewItems?: ReviewSegment[]; @@ -51,6 +59,11 @@ export default function DetailStream({ const effectiveTime = currentTime + annotationOffset / 1000; const [upload, setUpload] = useState(undefined); + const [controlsExpanded, setControlsExpanded] = useState(false); + const [alwaysExpandActive, setAlwaysExpandActive] = usePersistence( + "detailStreamActiveExpanded", + true, + ); const onSeekCheckPlaying = (timestamp: number) => { onSeek(timestamp, isPlaying); @@ -168,7 +181,7 @@ export default function DetailStream({ } return ( -
+ <> setUpload(undefined)} @@ -179,38 +192,80 @@ export default function DetailStream({ }} /> -
-
- {reviewItems?.length === 0 ? ( -
- {t("detail.noDataFound")} +
+
+
+ {reviewItems?.length === 0 ? ( +
+ {t("detail.noDataFound")} +
+ ) : ( + reviewItems?.map((review: ReviewSegment) => { + const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`; + return ( + setActiveReviewId(id)} + onOpenUpload={(e) => setUpload(e)} + alwaysExpandActive={alwaysExpandActive} + /> + ); + }) + )} +
+
+ +
+ + {controlsExpanded && ( +
+ +
+
+ + +
+
+ {t("detail.alwaysExpandActive.desc")} +
+
- ) : ( - reviewItems?.map((review: ReviewSegment) => { - const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`; - return ( - setActiveReviewId(id)} - onOpenUpload={(e) => setUpload(e)} - /> - ); - }) )}
- - -
+ ); } @@ -223,6 +278,7 @@ type ReviewGroupProps = { onActivate?: () => void; onOpenUpload?: (e: Event) => void; effectiveTime?: number; + alwaysExpandActive?: boolean; }; function ReviewGroup({ @@ -234,11 +290,19 @@ function ReviewGroup({ onActivate, onOpenUpload, effectiveTime, + alwaysExpandActive = false, }: ReviewGroupProps) { const { t } = useTranslation("views/events"); const [open, setOpen] = useState(false); const start = review.start_time ?? 0; + // Auto-expand when this review becomes active and alwaysExpandActive is enabled + useEffect(() => { + if (isActive && alwaysExpandActive) { + setOpen(true); + } + }, [isActive, alwaysExpandActive]); + const displayTime = formatUnixTimestampToDateTime(start, { timezone: config.ui.timezone, date_format: