mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-04 11:01:14 +03:00
restructure camera enable/disable pane
This commit is contained in:
parent
ec44398b1c
commit
497084ada1
@ -457,7 +457,7 @@
|
|||||||
},
|
},
|
||||||
"cameraManagement": {
|
"cameraManagement": {
|
||||||
"title": "Manage Cameras",
|
"title": "Manage Cameras",
|
||||||
"description": "Add, edit, and delete cameras, control which cameras are enabled, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.",
|
"description": "Add, edit, and delete cameras, control the state of each camera, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.",
|
||||||
"addCamera": "Add New Camera",
|
"addCamera": "Add New Camera",
|
||||||
"deleteCamera": "Delete Camera",
|
"deleteCamera": "Delete Camera",
|
||||||
"deleteCameraDialog": {
|
"deleteCameraDialog": {
|
||||||
@ -475,12 +475,17 @@
|
|||||||
"selectCamera": "Select a Camera",
|
"selectCamera": "Select a Camera",
|
||||||
"backToSettings": "Back to Camera Settings",
|
"backToSettings": "Back to Camera Settings",
|
||||||
"streams": {
|
"streams": {
|
||||||
"title": "Enable / Disable Cameras",
|
"title": "Camera State",
|
||||||
"enableLabel": "Enabled cameras",
|
"label": "Camera state",
|
||||||
"enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.<br /> <em>Note: This does not disable go2rtc restreams.</em><br /><br />Drag the handle to reorder the cameras as they appear in the UI. The order of enabled cameras will be reflected throughout the UI including the Live dashboard and camera selection dropdowns.",
|
"description": "Set the operating state for each camera.<br /><br /><strong>On</strong>: streams are processed normally.<br /><strong>Off</strong>: temporarily pauses processing. Does not persist across Frigate restarts.<br /><strong>Disabled</strong>: stops processing and saves the change to your configuration. A restart is required to re-enable a disabled camera.<br /><br /><em>Note: Disabling does not affect go2rtc restreams.</em><br /><br />Drag the handle to reorder active cameras as they appear throughout the UI, including the Live dashboard and camera selection dropdowns.",
|
||||||
"disableLabel": "Disabled cameras",
|
"disabledSubheading": "Disabled in configuration",
|
||||||
"disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
|
"status": {
|
||||||
"enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.",
|
"on": "On",
|
||||||
|
"off": "Off",
|
||||||
|
"disabled": "Disabled"
|
||||||
|
},
|
||||||
|
"enableSuccess": "Enabled {{cameraName}}. Restart Frigate to apply.",
|
||||||
|
"disableSuccess": "Disabled {{cameraName}} and saved to configuration.",
|
||||||
"reorderHandle": "Drag to reorder",
|
"reorderHandle": "Drag to reorder",
|
||||||
"saving": "Saving…",
|
"saving": "Saving…",
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
@ -527,10 +532,10 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"title": "Profile Camera Overrides",
|
"title": "Profile Camera Overrides",
|
||||||
"selectLabel": "Select profile",
|
"selectLabel": "Select profile",
|
||||||
"description": "Configure which cameras are enabled or disabled when a profile is activated. Cameras set to \"Inherit\" keep their base enabled state.",
|
"description": "Configure which cameras are turned on or off when a profile is activated. Cameras set to \"Inherit\" keep their default state.",
|
||||||
"inherit": "Inherit",
|
"inherit": "Inherit",
|
||||||
"enabled": "Enabled",
|
"on": "On",
|
||||||
"disabled": "Disabled"
|
"off": "Off"
|
||||||
},
|
},
|
||||||
"cameraType": {
|
"cameraType": {
|
||||||
"title": "Camera Type",
|
"title": "Camera Type",
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
LuGripVertical,
|
LuGripVertical,
|
||||||
LuPencil,
|
LuPencil,
|
||||||
LuPlus,
|
LuPlus,
|
||||||
|
LuRefreshCcw,
|
||||||
LuTrash2,
|
LuTrash2,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { Reorder, useDragControls } from "framer-motion";
|
import { Reorder, useDragControls } from "framer-motion";
|
||||||
@ -28,7 +29,6 @@ import { Link } from "react-router-dom";
|
|||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { useEnabledState, useRestart } from "@/api/ws";
|
import { useEnabledState, useRestart } from "@/api/ws";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@ -275,7 +275,7 @@ export default function CameraManagementView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{enabledCameras.length > 0 && (
|
{(enabledCameras.length > 0 || disabledCameras.length > 0) && (
|
||||||
<SettingsGroupCard
|
<SettingsGroupCard
|
||||||
title={
|
title={
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">
|
||||||
@ -285,83 +285,66 @@ export default function CameraManagementView({
|
|||||||
>
|
>
|
||||||
<div className={SPLIT_ROW_CLASS_NAME}>
|
<div className={SPLIT_ROW_CLASS_NAME}>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label
|
<Label>{t("cameraManagement.streams.label")}</Label>
|
||||||
className="cursor-pointer"
|
<p className="hidden text-sm text-muted-foreground md:block">
|
||||||
htmlFor={"enabled-cameras-switch"}
|
<Trans ns="views/settings">
|
||||||
>
|
cameraManagement.streams.description
|
||||||
{t("cameraManagement.streams.enableLabel")}
|
</Trans>
|
||||||
<p className="hidden text-sm text-muted-foreground md:block">
|
</p>
|
||||||
<Trans ns="views/settings">
|
|
||||||
cameraManagement.streams.enableDesc
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-md space-y-1.5">
|
<div className="max-w-md space-y-1.5">
|
||||||
<Reorder.Group
|
<div className="space-y-3 rounded-lg bg-secondary p-4">
|
||||||
as="div"
|
{orderedCameras.length > 0 && (
|
||||||
axis="y"
|
<Reorder.Group
|
||||||
values={orderedCameras}
|
as="div"
|
||||||
onReorder={setOrderedCameras}
|
axis="y"
|
||||||
className="space-y-2 rounded-lg bg-secondary p-4"
|
values={orderedCameras}
|
||||||
>
|
onReorder={setOrderedCameras}
|
||||||
{orderedCameras.map((camera) => (
|
className="space-y-2"
|
||||||
<EnabledCameraRow
|
>
|
||||||
key={camera}
|
{orderedCameras.map((camera) => (
|
||||||
camera={camera}
|
<ActiveCameraRow
|
||||||
onConfigChanged={updateConfig}
|
key={camera}
|
||||||
onDragEnd={handleReorderDragEnd}
|
camera={camera}
|
||||||
/>
|
onConfigChanged={updateConfig}
|
||||||
))}
|
onDragEnd={handleReorderDragEnd}
|
||||||
</Reorder.Group>
|
setRestartDialogOpen={setRestartDialogOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Reorder.Group>
|
||||||
|
)}
|
||||||
|
{orderedCameras.length > 0 &&
|
||||||
|
disabledCameras.length > 0 && (
|
||||||
|
<div className="border-t border-border/40" />
|
||||||
|
)}
|
||||||
|
{disabledCameras.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"cameraManagement.streams.disabledSubheading",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{disabledCameras.map((camera) => (
|
||||||
|
<DisabledCameraRow
|
||||||
|
key={camera}
|
||||||
|
camera={camera}
|
||||||
|
onConfigChanged={updateConfig}
|
||||||
|
setRestartDialogOpen={setRestartDialogOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ReorderSaveStatusIndicator
|
<ReorderSaveStatusIndicator
|
||||||
status={reorderSaveStatus}
|
status={reorderSaveStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground md:hidden">
|
<p className="text-sm text-muted-foreground md:hidden">
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">
|
||||||
cameraManagement.streams.enableDesc
|
cameraManagement.streams.description
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{disabledCameras.length > 0 && (
|
|
||||||
<div className={SPLIT_ROW_CLASS_NAME}>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label
|
|
||||||
className="cursor-pointer"
|
|
||||||
htmlFor={"disabled-cameras-switch"}
|
|
||||||
>
|
|
||||||
{t("cameraManagement.streams.disableLabel")}
|
|
||||||
<RestartRequiredIndicator className="ml-1" />
|
|
||||||
</Label>
|
|
||||||
<p className="hidden text-sm text-muted-foreground md:block">
|
|
||||||
{t("cameraManagement.streams.disableDesc")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${CONTROL_COLUMN_CLASS_NAME} space-y-1.5`}
|
|
||||||
>
|
|
||||||
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
|
||||||
{disabledCameras.map((camera) => (
|
|
||||||
<div
|
|
||||||
key={camera}
|
|
||||||
className="flex flex-row items-center justify-between"
|
|
||||||
>
|
|
||||||
<CameraNameLabel camera={camera} />
|
|
||||||
<CameraConfigEnableSwitch
|
|
||||||
cameraName={camera}
|
|
||||||
onConfigChanged={updateConfig}
|
|
||||||
setRestartDialogOpen={setRestartDialogOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground md:hidden">
|
|
||||||
{t("cameraManagement.streams.disableDesc")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SettingsGroupCard>
|
</SettingsGroupCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -468,17 +451,19 @@ function ReorderSaveStatusIndicator({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnabledCameraRowProps = {
|
type ActiveCameraRowProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
onConfigChanged: () => Promise<unknown>;
|
onConfigChanged: () => Promise<unknown>;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function EnabledCameraRow({
|
function ActiveCameraRow({
|
||||||
camera,
|
camera,
|
||||||
onConfigChanged,
|
onConfigChanged,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
}: EnabledCameraRowProps) {
|
setRestartDialogOpen,
|
||||||
|
}: ActiveCameraRowProps) {
|
||||||
const { t } = useTranslation(["views/settings"]);
|
const { t } = useTranslation(["views/settings"]);
|
||||||
const controls = useDragControls();
|
const controls = useDragControls();
|
||||||
|
|
||||||
@ -506,38 +491,226 @@ function EnabledCameraRow({
|
|||||||
onConfigChanged={onConfigChanged}
|
onConfigChanged={onConfigChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CameraEnableSwitch cameraName={camera} />
|
<CameraStatusSelect
|
||||||
|
cameraName={camera}
|
||||||
|
isDisabledInConfig={false}
|
||||||
|
onConfigChanged={onConfigChanged}
|
||||||
|
setRestartDialogOpen={setRestartDialogOpen}
|
||||||
|
/>
|
||||||
</Reorder.Item>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CameraEnableSwitchProps = {
|
type DisabledCameraRowProps = {
|
||||||
cameraName: string;
|
camera: string;
|
||||||
|
onConfigChanged: () => Promise<unknown>;
|
||||||
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) {
|
function DisabledCameraRow({
|
||||||
const { payload: enabledState, send: sendEnabled } =
|
camera,
|
||||||
useEnabledState(cameraName);
|
onConfigChanged,
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
setRestartDialogOpen,
|
||||||
|
}: DisabledCameraRowProps) {
|
||||||
const isChecked =
|
|
||||||
enabledState === "ON" || enabledState === "OFF"
|
|
||||||
? enabledState === "ON"
|
|
||||||
: (config?.cameras?.[cameraName]?.enabled ?? false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<Switch
|
<div className="flex items-center gap-1">
|
||||||
id={`camera-enabled-${cameraName}`}
|
<CameraNameLabel camera={camera} className="text-muted-foreground" />
|
||||||
checked={isChecked}
|
<CameraDetailsEditor
|
||||||
onCheckedChange={(isChecked) => {
|
cameraName={camera}
|
||||||
sendEnabled(isChecked ? "ON" : "OFF");
|
onConfigChanged={onConfigChanged}
|
||||||
}}
|
/>
|
||||||
|
</div>
|
||||||
|
<CameraStatusSelect
|
||||||
|
cameraName={camera}
|
||||||
|
isDisabledInConfig={true}
|
||||||
|
onConfigChanged={onConfigChanged}
|
||||||
|
setRestartDialogOpen={setRestartDialogOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CameraStatus = "on" | "off" | "disabled";
|
||||||
|
|
||||||
|
type CameraStatusSelectProps = {
|
||||||
|
cameraName: string;
|
||||||
|
isDisabledInConfig: boolean;
|
||||||
|
onConfigChanged: () => Promise<unknown>;
|
||||||
|
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CameraStatusSelect({
|
||||||
|
cameraName,
|
||||||
|
isDisabledInConfig,
|
||||||
|
onConfigChanged,
|
||||||
|
setRestartDialogOpen,
|
||||||
|
}: CameraStatusSelectProps) {
|
||||||
|
const { t } = useTranslation([
|
||||||
|
"views/settings",
|
||||||
|
"components/dialog",
|
||||||
|
"common",
|
||||||
|
]);
|
||||||
|
const { payload: enabledState, send: sendEnabled } =
|
||||||
|
useEnabledState(cameraName);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const currentStatus: CameraStatus = isDisabledInConfig
|
||||||
|
? "disabled"
|
||||||
|
: enabledState === "OFF"
|
||||||
|
? "off"
|
||||||
|
: "on";
|
||||||
|
|
||||||
|
const restartLabel = t("configForm.restartRequiredField", {
|
||||||
|
ns: "views/settings",
|
||||||
|
defaultValue: "Restart required",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
async (newStatus: string) => {
|
||||||
|
if (newStatus === currentStatus || isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === "on" && !isDisabledInConfig) {
|
||||||
|
sendEnabled("ON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === "off" && !isDisabledInConfig) {
|
||||||
|
sendEnabled("OFF");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === "on" && isDisabledInConfig) {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await axios.put("config/set", {
|
||||||
|
requires_restart: 1,
|
||||||
|
config_data: {
|
||||||
|
cameras: { [cameraName]: { enabled: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await onConfigChanged();
|
||||||
|
toast.success(
|
||||||
|
t("cameraManagement.streams.enableSuccess", {
|
||||||
|
ns: "views/settings",
|
||||||
|
cameraName,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
action: (
|
||||||
|
<a onClick={() => setRestartDialogOpen(true)}>
|
||||||
|
<Button>
|
||||||
|
{t("restart.button", { ns: "components/dialog" })}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
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" },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === "disabled" && !isDisabledInConfig) {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
// Stop runtime processing immediately before persisting the
|
||||||
|
// disable so the camera stops working without waiting for
|
||||||
|
// a restart. The config write below makes the change durable.
|
||||||
|
sendEnabled("OFF");
|
||||||
|
await axios.put("config/set", {
|
||||||
|
requires_restart: 0,
|
||||||
|
config_data: {
|
||||||
|
cameras: { [cameraName]: { enabled: false } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await onConfigChanged();
|
||||||
|
toast.success(
|
||||||
|
t("cameraManagement.streams.disableSuccess", {
|
||||||
|
ns: "views/settings",
|
||||||
|
cameraName,
|
||||||
|
}),
|
||||||
|
{ position: "top-center" },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
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" },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
cameraName,
|
||||||
|
currentStatus,
|
||||||
|
isDisabledInConfig,
|
||||||
|
isSaving,
|
||||||
|
onConfigChanged,
|
||||||
|
sendEnabled,
|
||||||
|
setRestartDialogOpen,
|
||||||
|
t,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSaving) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-7 w-[110px] flex-row items-center justify-end">
|
||||||
|
<ActivityIndicator className="size-4" size={16} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={currentStatus} onValueChange={handleChange}>
|
||||||
|
<SelectTrigger className="h-7 w-[110px] text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="on">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{t("cameraManagement.streams.status.on")}
|
||||||
|
{isDisabledInConfig && (
|
||||||
|
<LuRefreshCcw
|
||||||
|
className="size-3 text-muted-foreground"
|
||||||
|
aria-label={restartLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
{!isDisabledInConfig && (
|
||||||
|
<SelectItem value="off">
|
||||||
|
{t("cameraManagement.streams.status.off")}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
<SelectItem value="disabled">
|
||||||
|
{t("cameraManagement.streams.status.disabled")}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type CameraDetailsEditorProps = {
|
type CameraDetailsEditorProps = {
|
||||||
cameraName: string;
|
cameraName: string;
|
||||||
onConfigChanged: () => Promise<unknown>;
|
onConfigChanged: () => Promise<unknown>;
|
||||||
@ -783,97 +956,6 @@ function CameraDetailsEditor({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CameraConfigEnableSwitchProps = {
|
|
||||||
cameraName: string;
|
|
||||||
setRestartDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
onConfigChanged: () => Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function CameraConfigEnableSwitch({
|
|
||||||
cameraName,
|
|
||||||
onConfigChanged,
|
|
||||||
setRestartDialogOpen,
|
|
||||||
}: CameraConfigEnableSwitchProps) {
|
|
||||||
const { t } = useTranslation([
|
|
||||||
"common",
|
|
||||||
"views/settings",
|
|
||||||
"components/dialog",
|
|
||||||
]);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const onCheckedChange = useCallback(
|
|
||||||
async (isChecked: boolean) => {
|
|
||||||
if (!isChecked || isSaving) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put("config/set", {
|
|
||||||
requires_restart: 1,
|
|
||||||
config_data: {
|
|
||||||
cameras: {
|
|
||||||
[cameraName]: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await onConfigChanged();
|
|
||||||
|
|
||||||
toast.success(
|
|
||||||
t("cameraManagement.streams.enableSuccess", {
|
|
||||||
ns: "views/settings",
|
|
||||||
cameraName,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
position: "top-center",
|
|
||||||
action: (
|
|
||||||
<a onClick={() => setRestartDialogOpen(true)}>
|
|
||||||
<Button>
|
|
||||||
{t("restart.button", { ns: "components/dialog" })}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cameraName, isSaving, onConfigChanged, setRestartDialogOpen, t],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
{isSaving ? (
|
|
||||||
<ActivityIndicator className="h-5 w-8" size={16} />
|
|
||||||
) : (
|
|
||||||
<Switch
|
|
||||||
id={`camera-enabled-${cameraName}`}
|
|
||||||
checked={false}
|
|
||||||
onCheckedChange={onCheckedChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type CameraTypeSectionProps = {
|
type CameraTypeSectionProps = {
|
||||||
cameras: string[];
|
cameras: string[];
|
||||||
config: FrigateConfig | undefined;
|
config: FrigateConfig | undefined;
|
||||||
@ -1231,12 +1313,12 @@ function ProfileCameraEnableSection({
|
|||||||
})}
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="enabled">
|
<SelectItem value="enabled">
|
||||||
{t("cameraManagement.profiles.enabled", {
|
{t("cameraManagement.profiles.on", {
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="disabled">
|
<SelectItem value="disabled">
|
||||||
{t("cameraManagement.profiles.disabled", {
|
{t("cameraManagement.profiles.off", {
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user