Improve Details Settings (#20718)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* detail stream settings

* fix mobile landscape

* mobile landscape

* tweak

* tweaks
This commit is contained in:
Josh Hawkins 2025-10-29 17:03:38 -05:00 committed by GitHub
parent 901002a0a5
commit dbbe40bd27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 165 additions and 55 deletions

View File

@ -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",

View File

@ -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 <code>annotation_offset</code> 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. <em>Default: 0</em>",
"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": {

View File

@ -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,13 +69,26 @@ export default function AnnotationOffsetSlider({ className }: Props) {
return (
<div
className={`absolute bottom-0 left-0 right-0 z-30 flex items-center gap-3 bg-background p-3 ${className ?? ""}`}
style={{ pointerEvents: "auto" }}
className={cn(
"flex flex-col gap-0.5",
isMobile && "landscape:gap-3",
className,
)}
>
<div className="w-56 text-sm">
Annotation offset (ms): {annotationOffset}
<div
className={cn(
"flex items-center gap-3",
isMobile &&
"landscape:flex-col landscape:items-start landscape:gap-4",
)}
>
<div className="flex max-w-28 flex-row items-center gap-2 text-sm md:max-w-48">
<span className="max-w-24 md:max-w-44">
{t("trackingDetails.annotationSettings.offset.label")}:
</span>
<span className="text-primary-variant">{annotationOffset}</span>
</div>
<div className="flex-1">
<div className="w-full flex-1 landscape:flex">
<Slider
value={[annotationOffset]}
min={-1500}
@ -82,7 +99,7 @@ export default function AnnotationOffsetSlider({ className }: Props) {
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={reset}>
Reset
{t("button.reset", { ns: "common" })}
</Button>
<Button size="sm" onClick={save} disabled={isSaving}>
{isSaving
@ -91,5 +108,29 @@ export default function AnnotationOffsetSlider({ className }: Props) {
</Button>
</div>
</div>
<div
className={cn(
"flex items-center gap-2 text-xs text-muted-foreground",
isMobile && "landscape:flex-col landscape:items-start",
)}
>
<Trans ns="views/explore">
trackingDetails.annotationSettings.offset.millisecondsToOffset
</Trans>
<Popover>
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label={t("trackingDetails.annotationSettings.offset.desc")}
>
<LuInfo className="size-4" />
</button>
</PopoverTrigger>
<PopoverContent className="w-80 text-sm">
{t("trackingDetails.annotationSettings.offset.desc")}
</PopoverContent>
</Popover>
</div>
</div>
);
}

View File

@ -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<Event | undefined>(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 (
<div className="relative">
<>
<FrigatePlusDialog
upload={upload}
onClose={() => setUpload(undefined)}
@ -179,9 +192,10 @@ export default function DetailStream({
}}
/>
<div className="relative flex h-full flex-col">
<div
ref={scrollRef}
className="scrollbar-container h-[calc(100vh-70px)] overflow-y-auto"
className="scrollbar-container flex-1 overflow-y-auto pb-14"
>
<div className="space-y-4 py-2">
{reviewItems?.length === 0 ? (
@ -202,6 +216,7 @@ export default function DetailStream({
isActive={activeReviewId == id}
onActivate={() => setActiveReviewId(id)}
onOpenUpload={(e) => setUpload(e)}
alwaysExpandActive={alwaysExpandActive}
/>
);
})
@ -209,8 +224,48 @@ export default function DetailStream({
</div>
</div>
<AnnotationOffsetSlider />
<div
className={cn(
"absolute bottom-0 left-0 right-0 z-30 rounded-t-md border border-secondary-highlight bg-background_alt shadow-md",
isDesktop && "border-b-0",
)}
>
<button
onClick={() => setControlsExpanded(!controlsExpanded)}
className="flex w-full items-center justify-between p-3"
>
<div className="flex items-center gap-2 text-sm font-medium">
<LuSettings className="size-4" />
<span>{t("detail.settings")}</span>
</div>
{controlsExpanded ? (
<LuChevronDown className="size-4 text-primary-variant" />
) : (
<LuChevronRight className="size-4 text-primary-variant" />
)}
</button>
{controlsExpanded && (
<div className="space-y-3 px-3 pb-3">
<AnnotationOffsetSlider />
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">
{t("detail.alwaysExpandActive.title")}
</label>
<Switch
checked={alwaysExpandActive}
onCheckedChange={setAlwaysExpandActive}
/>
</div>
<div className="text-xs text-muted-foreground">
{t("detail.alwaysExpandActive.desc")}
</div>
</div>
</div>
)}
</div>
</div>
</>
);
}
@ -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: