mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 16:42:18 +03:00
Miscellaneous fixes (#23238)
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
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
CI / AMD64 Build (push) Waiting to run
* start audio transcription post processor when enabled on any camera * Fetch embed key whenever an error occurs in case the llama server was restarted * mypy * add tooltips for colored dots in settings menu * add ability to reorder cameras from management pane * add ability to reorder birdseye * add reordering save text to camera management view * Include NPU in latency performance hint * Implement turbo for NPU on object detection * hide order fields * drop auto-derived field paths from camera value when unset globally * use correct field type for export hwaccel args * add debug replay to detail actions menu * clarify debug replay in docs * guard get_current_frame_time against missing camera state * Implement debug reply from export * Refactor debug replay to use sources for dynamic playback * Mypy * fix debug export replay source timestamp handling * skip replay cameras in stats immediately * broadcast debug replay state over ws and buffer pre-OPEN sends - push debug replay session state over the job_state ws topic so the status bar reacts instantly to start/stop without polling - fix child-effect-before-parent-effect race in WsProvider that silently dropped initial snapshot requests on cold load * fix debug replay test hang --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
co-authored by
Nicolas Mowen
parent
d968f00500
commit
43d97acd21
@@ -1,5 +1,5 @@
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
CONTROL_COLUMN_CLASS_NAME,
|
||||
SettingsGroupCard,
|
||||
@@ -14,7 +14,15 @@ import { useTranslation } from "react-i18next";
|
||||
import CameraEditForm from "@/components/settings/CameraEditForm";
|
||||
import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
|
||||
import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog";
|
||||
import { LuExternalLink, LuPencil, LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import {
|
||||
LuCheck,
|
||||
LuExternalLink,
|
||||
LuGripVertical,
|
||||
LuPencil,
|
||||
LuPlus,
|
||||
LuTrash2,
|
||||
} from "react-icons/lu";
|
||||
import { Reorder, useDragControls } from "framer-motion";
|
||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
@@ -45,6 +53,10 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const REORDER_SAVED_INDICATOR_MS = 1500;
|
||||
|
||||
type ReorderSaveStatus = "idle" | "saving" | "saved";
|
||||
|
||||
type CameraManagementViewProps = {
|
||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
profileState?: ProfileState;
|
||||
@@ -54,7 +66,7 @@ export default function CameraManagementView({
|
||||
setUnsavedChanges,
|
||||
profileState,
|
||||
}: CameraManagementViewProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const { t } = useTranslation(["views/settings", "common"]);
|
||||
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
@@ -72,16 +84,99 @@ export default function CameraManagementView({
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||
const { send: sendRestart } = useRestart();
|
||||
|
||||
// List of cameras for dropdown
|
||||
const enabledCameras = useMemo(() => {
|
||||
if (config) {
|
||||
return Object.keys(config.cameras)
|
||||
.filter((camera) => config.cameras[camera].enabled_in_config)
|
||||
.sort();
|
||||
.sort((a, b) => {
|
||||
const orderA = config.cameras[a].ui?.order ?? 0;
|
||||
const orderB = config.cameras[b].ui?.order ?? 0;
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}, [config]);
|
||||
|
||||
// Diverges from config during a drag and while the save is in flight.
|
||||
const [orderedCameras, setOrderedCameras] =
|
||||
useState<string[]>(enabledCameras);
|
||||
const orderedCamerasRef = useRef(orderedCameras);
|
||||
useEffect(() => {
|
||||
orderedCamerasRef.current = orderedCameras;
|
||||
}, [orderedCameras]);
|
||||
|
||||
useEffect(() => {
|
||||
setOrderedCameras((prev) => {
|
||||
if (
|
||||
prev.length === enabledCameras.length &&
|
||||
prev.every((cam, i) => cam === enabledCameras[i])
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return enabledCameras;
|
||||
});
|
||||
}, [enabledCameras]);
|
||||
|
||||
const [reorderSaveStatus, setReorderSaveStatus] =
|
||||
useState<ReorderSaveStatus>("idle");
|
||||
const reorderSavedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (reorderSavedTimerRef.current) {
|
||||
clearTimeout(reorderSavedTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleReorderDragEnd = useCallback(async () => {
|
||||
const current = orderedCamerasRef.current;
|
||||
if (
|
||||
current.length === enabledCameras.length &&
|
||||
current.every((cam, i) => cam === enabledCameras[i])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraUpdates: Record<string, { ui: { order: number } }> = {};
|
||||
current.forEach((cam, i) => {
|
||||
cameraUpdates[cam] = { ui: { order: i * 10 } };
|
||||
});
|
||||
|
||||
if (reorderSavedTimerRef.current) {
|
||||
clearTimeout(reorderSavedTimerRef.current);
|
||||
reorderSavedTimerRef.current = null;
|
||||
}
|
||||
setReorderSaveStatus("saving");
|
||||
|
||||
try {
|
||||
await axios.put("config/set", {
|
||||
requires_restart: 0,
|
||||
config_data: { cameras: cameraUpdates },
|
||||
});
|
||||
await updateConfig();
|
||||
setReorderSaveStatus("saved");
|
||||
reorderSavedTimerRef.current = setTimeout(() => {
|
||||
setReorderSaveStatus("idle");
|
||||
reorderSavedTimerRef.current = null;
|
||||
}, REORDER_SAVED_INDICATOR_MS);
|
||||
} catch (error) {
|
||||
setOrderedCameras(enabledCameras);
|
||||
setReorderSaveStatus("idle");
|
||||
const errorMessage =
|
||||
axios.isAxiosError(error) &&
|
||||
(error.response?.data?.message || error.response?.data?.detail)
|
||||
? error.response?.data?.message || error.response?.data?.detail
|
||||
: t("toast.save.error.noMessage", { ns: "common" });
|
||||
|
||||
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [enabledCameras, updateConfig, t]);
|
||||
|
||||
const disabledCameras = useMemo(() => {
|
||||
if (config) {
|
||||
return Object.keys(config.cameras)
|
||||
@@ -173,22 +268,26 @@ export default function CameraManagementView({
|
||||
</p>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
||||
{enabledCameras.map((camera) => (
|
||||
<div
|
||||
key={camera}
|
||||
className="flex flex-row items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<CameraNameLabel camera={camera} />
|
||||
<CameraFriendlyNameEditor
|
||||
cameraName={camera}
|
||||
onConfigChanged={updateConfig}
|
||||
/>
|
||||
</div>
|
||||
<CameraEnableSwitch cameraName={camera} />
|
||||
</div>
|
||||
))}
|
||||
<div className="max-w-md space-y-1.5">
|
||||
<Reorder.Group
|
||||
as="div"
|
||||
axis="y"
|
||||
values={orderedCameras}
|
||||
onReorder={setOrderedCameras}
|
||||
className="space-y-2 rounded-lg bg-secondary p-4"
|
||||
>
|
||||
{orderedCameras.map((camera) => (
|
||||
<EnabledCameraRow
|
||||
key={camera}
|
||||
camera={camera}
|
||||
onConfigChanged={updateConfig}
|
||||
onDragEnd={handleReorderDragEnd}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
<ReorderSaveStatusIndicator
|
||||
status={reorderSaveStatus}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground md:hidden">
|
||||
<Trans ns="views/settings">
|
||||
@@ -309,6 +408,80 @@ export default function CameraManagementView({
|
||||
);
|
||||
}
|
||||
|
||||
type ReorderSaveStatusIndicatorProps = {
|
||||
status: ReorderSaveStatus;
|
||||
};
|
||||
|
||||
function ReorderSaveStatusIndicator({
|
||||
status,
|
||||
}: ReorderSaveStatusIndicatorProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
return (
|
||||
<div
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
"flex h-4 items-center justify-start gap-1 text-xs transition-opacity duration-200",
|
||||
status === "idle" ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
{status === "saving" && (
|
||||
<span className="text-muted-foreground">
|
||||
{t("cameraManagement.streams.saving")}
|
||||
</span>
|
||||
)}
|
||||
{status === "saved" && (
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<LuCheck className="size-3.5" />
|
||||
{t("cameraManagement.streams.saved")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type EnabledCameraRowProps = {
|
||||
camera: string;
|
||||
onConfigChanged: () => Promise<unknown>;
|
||||
onDragEnd: () => void;
|
||||
};
|
||||
|
||||
function EnabledCameraRow({
|
||||
camera,
|
||||
onConfigChanged,
|
||||
onDragEnd,
|
||||
}: EnabledCameraRowProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const controls = useDragControls();
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
as="div"
|
||||
value={camera}
|
||||
dragListener={false}
|
||||
dragControls={controls}
|
||||
onDragEnd={onDragEnd}
|
||||
className="flex flex-row items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onPointerDown={(e) => controls.start(e)}
|
||||
className="-ml-1 cursor-grab touch-none rounded p-1 text-muted-foreground hover:text-primary active:cursor-grabbing"
|
||||
aria-label={t("cameraManagement.streams.reorderHandle")}
|
||||
>
|
||||
<LuGripVertical className="size-4" />
|
||||
</button>
|
||||
<CameraNameLabel camera={camera} />
|
||||
<CameraFriendlyNameEditor
|
||||
cameraName={camera}
|
||||
onConfigChanged={onConfigChanged}
|
||||
/>
|
||||
</div>
|
||||
<CameraEnableSwitch cameraName={camera} />
|
||||
</Reorder.Item>
|
||||
);
|
||||
}
|
||||
|
||||
type CameraEnableSwitchProps = {
|
||||
cameraName: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user