mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
detail stream settings
This commit is contained in:
parent
901002a0a5
commit
99eca4e88e
@ -26,7 +26,12 @@
|
|||||||
"aria": "Toggle detail view",
|
"aria": "Toggle detail view",
|
||||||
"trackedObject_one": "object",
|
"trackedObject_one": "object",
|
||||||
"trackedObject_other": "objects",
|
"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": {
|
"objectTrack": {
|
||||||
"trackedPoint": "Tracked point",
|
"trackedPoint": "Tracked point",
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"label": "Annotation 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>",
|
"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.",
|
"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": {
|
"toast": {
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
|
||||||
import { useDetailStream } from "@/context/detail-stream-context";
|
import { useDetailStream } from "@/context/detail-stream-context";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { LuInfo } from "react-icons/lu";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -64,31 +66,49 @@ export default function AnnotationOffsetSlider({ className }: Props) {
|
|||||||
}, [annotationOffset, camera, mutate, t]);
|
}, [annotationOffset, camera, mutate, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col gap-0.5">
|
||||||
className={`absolute bottom-0 left-0 right-0 z-30 flex items-center gap-3 bg-background p-3 ${className ?? ""}`}
|
<div className={`flex items-center gap-3 ${className ?? ""}`}>
|
||||||
style={{ pointerEvents: "auto" }}
|
<div className="flex flex-row gap-2 text-sm">
|
||||||
>
|
{t("trackingDetails.annotationSettings.offset.label")}:
|
||||||
<div className="w-56 text-sm">
|
<span className="text-primary-variant">{annotationOffset}</span>
|
||||||
Annotation offset (ms): {annotationOffset}
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Slider
|
||||||
|
value={[annotationOffset]}
|
||||||
|
min={-1500}
|
||||||
|
max={1500}
|
||||||
|
step={50}
|
||||||
|
onValueChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button size="sm" variant="ghost" onClick={reset}>
|
||||||
|
{t("button.reset", { ns: "common" })}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={save} disabled={isSaving}>
|
||||||
|
{isSaving
|
||||||
|
? t("button.saving", { ns: "common" })
|
||||||
|
: t("button.save", { ns: "common" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<Slider
|
<Trans ns="views/explore">
|
||||||
value={[annotationOffset]}
|
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||||
min={-1500}
|
</Trans>
|
||||||
max={1500}
|
<Popover>
|
||||||
step={50}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={handleChange}
|
<button
|
||||||
/>
|
className="focus:outline-none"
|
||||||
</div>
|
aria-label={t("trackingDetails.annotationSettings.offset.desc")}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Button size="sm" variant="ghost" onClick={reset}>
|
<LuInfo className="size-4" />
|
||||||
Reset
|
</button>
|
||||||
</Button>
|
</PopoverTrigger>
|
||||||
<Button size="sm" onClick={save} disabled={isSaving}>
|
<PopoverContent className="w-80 text-sm">
|
||||||
{isSaving
|
{t("trackingDetails.annotationSettings.offset.desc")}
|
||||||
? t("button.saving", { ns: "common" })
|
</PopoverContent>
|
||||||
: t("button.save", { ns: "common" })}
|
</Popover>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,13 +16,20 @@ import ActivityIndicator from "../indicators/activity-indicator";
|
|||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import { ReviewSegment } from "@/types/review";
|
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 { getTranslatedLabel } from "@/utils/i18n";
|
||||||
import EventMenu from "@/components/timeline/EventMenu";
|
import EventMenu from "@/components/timeline/EventMenu";
|
||||||
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
|
|
||||||
type DetailStreamProps = {
|
type DetailStreamProps = {
|
||||||
reviewItems?: ReviewSegment[];
|
reviewItems?: ReviewSegment[];
|
||||||
@ -51,6 +58,11 @@ export default function DetailStream({
|
|||||||
|
|
||||||
const effectiveTime = currentTime + annotationOffset / 1000;
|
const effectiveTime = currentTime + annotationOffset / 1000;
|
||||||
const [upload, setUpload] = useState<Event | undefined>(undefined);
|
const [upload, setUpload] = useState<Event | undefined>(undefined);
|
||||||
|
const [controlsExpanded, setControlsExpanded] = useState(false);
|
||||||
|
const [alwaysExpandActive, setAlwaysExpandActive] = usePersistence(
|
||||||
|
"detailStreamActiveExpanded",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const onSeekCheckPlaying = (timestamp: number) => {
|
const onSeekCheckPlaying = (timestamp: number) => {
|
||||||
onSeek(timestamp, isPlaying);
|
onSeek(timestamp, isPlaying);
|
||||||
@ -168,7 +180,7 @@ export default function DetailStream({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<>
|
||||||
<FrigatePlusDialog
|
<FrigatePlusDialog
|
||||||
upload={upload}
|
upload={upload}
|
||||||
onClose={() => setUpload(undefined)}
|
onClose={() => setUpload(undefined)}
|
||||||
@ -179,38 +191,76 @@ export default function DetailStream({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div className="relative flex h-full flex-col">
|
||||||
ref={scrollRef}
|
<div
|
||||||
className="scrollbar-container h-[calc(100vh-70px)] overflow-y-auto"
|
ref={scrollRef}
|
||||||
>
|
className="scrollbar-container flex-1 overflow-y-auto pb-14"
|
||||||
<div className="space-y-4 py-2">
|
>
|
||||||
{reviewItems?.length === 0 ? (
|
<div className="space-y-4 py-2">
|
||||||
<div className="py-8 text-center text-muted-foreground">
|
{reviewItems?.length === 0 ? (
|
||||||
{t("detail.noDataFound")}
|
<div className="py-8 text-center text-muted-foreground">
|
||||||
|
{t("detail.noDataFound")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
reviewItems?.map((review: ReviewSegment) => {
|
||||||
|
const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`;
|
||||||
|
return (
|
||||||
|
<ReviewGroup
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
review={review}
|
||||||
|
config={config}
|
||||||
|
onSeek={onSeekCheckPlaying}
|
||||||
|
effectiveTime={effectiveTime}
|
||||||
|
isActive={activeReviewId == id}
|
||||||
|
onActivate={() => setActiveReviewId(id)}
|
||||||
|
onOpenUpload={(e) => setUpload(e)}
|
||||||
|
alwaysExpandActive={alwaysExpandActive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapsible Controls Section - Absolutely positioned at bottom */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-30 rounded-t-md border border-secondary-highlight bg-background shadow-md">
|
||||||
|
<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>
|
||||||
) : (
|
|
||||||
reviewItems?.map((review: ReviewSegment) => {
|
|
||||||
const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`;
|
|
||||||
return (
|
|
||||||
<ReviewGroup
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
review={review}
|
|
||||||
config={config}
|
|
||||||
onSeek={onSeekCheckPlaying}
|
|
||||||
effectiveTime={effectiveTime}
|
|
||||||
isActive={activeReviewId == id}
|
|
||||||
onActivate={() => setActiveReviewId(id)}
|
|
||||||
onOpenUpload={(e) => setUpload(e)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<AnnotationOffsetSlider />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +273,7 @@ type ReviewGroupProps = {
|
|||||||
onActivate?: () => void;
|
onActivate?: () => void;
|
||||||
onOpenUpload?: (e: Event) => void;
|
onOpenUpload?: (e: Event) => void;
|
||||||
effectiveTime?: number;
|
effectiveTime?: number;
|
||||||
|
alwaysExpandActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ReviewGroup({
|
function ReviewGroup({
|
||||||
@ -234,11 +285,19 @@ function ReviewGroup({
|
|||||||
onActivate,
|
onActivate,
|
||||||
onOpenUpload,
|
onOpenUpload,
|
||||||
effectiveTime,
|
effectiveTime,
|
||||||
|
alwaysExpandActive = false,
|
||||||
}: ReviewGroupProps) {
|
}: ReviewGroupProps) {
|
||||||
const { t } = useTranslation("views/events");
|
const { t } = useTranslation("views/events");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const start = review.start_time ?? 0;
|
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, {
|
const displayTime = formatUnixTimestampToDateTime(start, {
|
||||||
timezone: config.ui.timezone,
|
timezone: config.ui.timezone,
|
||||||
date_format:
|
date_format:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user