2024-09-04 16:46:49 +03:00
|
|
|
import { Event } from "@/types/event";
|
|
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
import { useCallback, useState } from "react";
|
2026-03-07 16:50:00 +03:00
|
|
|
import { LuExternalLink, LuMinus, LuPlus } from "react-icons/lu";
|
2024-09-04 16:46:49 +03:00
|
|
|
import { Link } from "react-router-dom";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import useSWR from "swr";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
2026-03-07 16:50:00 +03:00
|
|
|
import { Slider } from "@/components/ui/slider";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { Trans, useTranslation } from "react-i18next";
|
2025-05-28 15:10:45 +03:00
|
|
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
2025-12-11 17:23:34 +03:00
|
|
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
2024-09-04 16:46:49 +03:00
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
const OFFSET_MIN = -2500;
|
|
|
|
|
const OFFSET_MAX = 2500;
|
|
|
|
|
const OFFSET_STEP = 50;
|
|
|
|
|
|
2024-09-04 16:46:49 +03:00
|
|
|
type AnnotationSettingsPaneProps = {
|
|
|
|
|
event: Event;
|
|
|
|
|
annotationOffset: number;
|
|
|
|
|
setAnnotationOffset: React.Dispatch<React.SetStateAction<number>>;
|
|
|
|
|
};
|
|
|
|
|
export function AnnotationSettingsPane({
|
|
|
|
|
event,
|
|
|
|
|
annotationOffset,
|
|
|
|
|
setAnnotationOffset,
|
|
|
|
|
}: AnnotationSettingsPaneProps) {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["views/explore"]);
|
2025-12-11 17:23:34 +03:00
|
|
|
const isAdmin = useIsAdmin();
|
2025-05-28 15:10:45 +03:00
|
|
|
const { getLocaleDocUrl } = useDocDomain();
|
2025-03-16 18:36:20 +03:00
|
|
|
|
2024-09-04 16:46:49 +03:00
|
|
|
const { data: config, mutate: updateConfig } =
|
|
|
|
|
useSWR<FrigateConfig>("config");
|
|
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
const handleSliderChange = useCallback(
|
|
|
|
|
(values: number[]) => {
|
|
|
|
|
if (!values || values.length === 0) return;
|
|
|
|
|
setAnnotationOffset(values[0]);
|
2024-09-04 16:46:49 +03:00
|
|
|
},
|
2026-03-07 16:50:00 +03:00
|
|
|
[setAnnotationOffset],
|
|
|
|
|
);
|
2024-09-04 16:46:49 +03:00
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
const stepOffset = useCallback(
|
|
|
|
|
(delta: number) => {
|
|
|
|
|
setAnnotationOffset((prev) => {
|
|
|
|
|
const next = prev + delta;
|
|
|
|
|
return Math.max(OFFSET_MIN, Math.min(OFFSET_MAX, next));
|
|
|
|
|
});
|
2024-09-04 16:46:49 +03:00
|
|
|
},
|
2026-03-07 16:50:00 +03:00
|
|
|
[setAnnotationOffset],
|
2024-09-04 16:46:49 +03:00
|
|
|
);
|
|
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
const reset = useCallback(() => {
|
|
|
|
|
setAnnotationOffset(0);
|
|
|
|
|
}, [setAnnotationOffset]);
|
|
|
|
|
|
|
|
|
|
const saveToConfig = useCallback(async () => {
|
|
|
|
|
if (!config || !event) return;
|
2024-09-04 16:46:49 +03:00
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
setIsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await axios.put(
|
|
|
|
|
`config/set?cameras.${event.camera}.detect.annotation_offset=${annotationOffset}`,
|
|
|
|
|
{ requires_restart: 0 },
|
|
|
|
|
);
|
|
|
|
|
if (res.status === 200) {
|
|
|
|
|
toast.success(
|
|
|
|
|
t("trackingDetails.annotationSettings.offset.toast.success", {
|
|
|
|
|
camera: event.camera,
|
|
|
|
|
}),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
updateConfig();
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(
|
|
|
|
|
t("toast.save.error.title", {
|
|
|
|
|
errorMessage: res.statusText,
|
|
|
|
|
ns: "common",
|
|
|
|
|
}),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
const err = error as {
|
|
|
|
|
response?: { data?: { message?: string; detail?: string } };
|
|
|
|
|
};
|
|
|
|
|
const errorMessage =
|
|
|
|
|
err?.response?.data?.message ||
|
|
|
|
|
err?.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
|
|
|
|
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
2024-09-04 16:46:49 +03:00
|
|
|
}
|
2026-03-07 16:50:00 +03:00
|
|
|
}, [annotationOffset, config, event, updateConfig, t]);
|
2024-09-04 16:46:49 +03:00
|
|
|
|
|
|
|
|
return (
|
2025-11-06 19:22:52 +03:00
|
|
|
<div className="p-4">
|
|
|
|
|
<div className="text-md mb-2">
|
2025-10-26 21:12:20 +03:00
|
|
|
{t("trackingDetails.annotationSettings.title")}
|
2024-09-04 16:46:49 +03:00
|
|
|
</div>
|
2025-11-06 19:22:52 +03:00
|
|
|
|
|
|
|
|
<Separator className="mb-4 flex bg-secondary" />
|
2026-03-07 16:50:00 +03:00
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
<div className="text-sm font-medium">
|
|
|
|
|
{t("trackingDetails.annotationSettings.offset.label")}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-muted-foreground">
|
|
|
|
|
<Trans ns="views/explore">
|
|
|
|
|
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
|
|
|
|
</Trans>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-8 shrink-0"
|
|
|
|
|
aria-label="-50ms"
|
|
|
|
|
onClick={() => stepOffset(-OFFSET_STEP)}
|
|
|
|
|
disabled={annotationOffset <= OFFSET_MIN}
|
|
|
|
|
>
|
|
|
|
|
<LuMinus className="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Slider
|
|
|
|
|
value={[annotationOffset]}
|
|
|
|
|
min={OFFSET_MIN}
|
|
|
|
|
max={OFFSET_MAX}
|
|
|
|
|
step={OFFSET_STEP}
|
|
|
|
|
onValueChange={handleSliderChange}
|
|
|
|
|
className="flex-1"
|
2024-09-04 16:46:49 +03:00
|
|
|
/>
|
2026-03-07 16:50:00 +03:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="size-8 shrink-0"
|
|
|
|
|
aria-label="+50ms"
|
|
|
|
|
onClick={() => stepOffset(OFFSET_STEP)}
|
|
|
|
|
disabled={annotationOffset >= OFFSET_MAX}
|
|
|
|
|
>
|
|
|
|
|
<LuPlus className="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2024-09-04 16:46:49 +03:00
|
|
|
|
2026-03-07 16:50:00 +03:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="font-mono text-sm tabular-nums text-primary-variant">
|
|
|
|
|
{annotationOffset > 0 ? "+" : ""}
|
|
|
|
|
{annotationOffset}ms
|
|
|
|
|
</span>
|
|
|
|
|
<Button type="button" variant="ghost" size="sm" onClick={reset}>
|
|
|
|
|
{t("button.reset", { ns: "common" })}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-sm text-secondary-foreground">
|
|
|
|
|
{t("trackingDetails.annotationSettings.offset.tips")}
|
|
|
|
|
<div className="mt-2 flex items-center text-primary-variant">
|
|
|
|
|
<Link
|
|
|
|
|
to={getLocaleDocUrl(
|
|
|
|
|
"troubleshooting/dummy-camera#annotation-offset",
|
2025-12-11 17:23:34 +03:00
|
|
|
)}
|
2026-03-07 16:50:00 +03:00
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline"
|
|
|
|
|
>
|
|
|
|
|
{t("readTheDocumentation", { ns: "common" })}
|
|
|
|
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
|
|
|
|
</Link>
|
2024-09-04 16:46:49 +03:00
|
|
|
</div>
|
2026-03-07 16:50:00 +03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<>
|
|
|
|
|
<Separator className="bg-secondary" />
|
|
|
|
|
<Button
|
|
|
|
|
variant="select"
|
|
|
|
|
aria-label={t("button.save", { ns: "common" })}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
onClick={saveToConfig}
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex flex-row items-center gap-2">
|
|
|
|
|
<ActivityIndicator />
|
|
|
|
|
<span>{t("button.saving", { ns: "common" })}</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
t("button.save", { ns: "common" })
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2024-09-04 16:46:49 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|