diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index f2709bf4f..742025ce7 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -17,13 +17,13 @@ }, "menu": { "general": "General", - "globalConfig": "Global Config", + "globalConfig": "Global configuration", "system": "System", "integrations": "Integrations", - "cameras": "Camera Configuration", + "cameras": "Camera configuration", "ui": "UI", "profileSettings": "Profile Settings", - "globalDetect": "Object Detection", + "globalDetect": "Object detection", "globalRecording": "Recording", "globalSnapshots": "Snapshots", "globalFfmpeg": "FFmpeg", @@ -47,13 +47,13 @@ "systemDetectorHardware": "Detector hardware", "systemDetectionModel": "Detection model", "systemMqtt": "MQTT", - "integrationSemanticSearch": "Semantic Search", + "integrationSemanticSearch": "Semantic search", "integrationGenerativeAi": "Generative AI", "integrationFaceRecognition": "Face recognition", - "integrationLpr": "License Plate Recognition", + "integrationLpr": "License plate recognition", "integrationObjectClassification": "Object classification", "integrationAudioTranscription": "Audio transcription", - "cameraDetect": "Object Detection", + "cameraDetect": "Object detection", "cameraFfmpeg": "FFmpeg", "cameraRecording": "Recording", "cameraSnapshots": "Snapshots", @@ -66,7 +66,7 @@ "cameraLivePlayback": "Live playback", "cameraBirdseye": "Birdseye", "cameraFaceRecognition": "Face recognition", - "cameraLpr": "License Plate Recognition", + "cameraLpr": "License plate recognition", "cameraMqttConfig": "MQTT", "cameraOnvif": "ONVIF", "cameraUi": "Camera UI", @@ -75,7 +75,7 @@ "cameraManagement": "Management", "cameraReview": "Review", "masksAndZones": "Masks / Zones", - "motionTuner": "Motion Tuner", + "motionTuner": "Motion tuner", "enrichments": "Enrichments", "users": "Users", "roles": "Roles", @@ -426,7 +426,11 @@ "backToSettings": "Back to Camera Settings", "streams": { "title": "Enable / Disable Cameras", - "desc": "Temporarily disable a camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
Note: This does not disable go2rtc restreams." + "enableLabel": "Enabled cameras", + "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.
Note: This does not disable go2rtc restreams.", + "disableLabel": "Disabled cameras", + "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.", + "enableSuccess": "Enabled {{cameraName}} in configuration" }, "cameraConfig": { "add": "Add Camera", diff --git a/web/src/components/config-form/theme/components/index.tsx b/web/src/components/config-form/theme/components/index.tsx index 0cac38209..85519313e 100644 --- a/web/src/components/config-form/theme/components/index.tsx +++ b/web/src/components/config-form/theme/components/index.tsx @@ -118,7 +118,7 @@ export function AdvancedCollapsible({ - + {children} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6958ee895..9e4d06c39 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -585,9 +585,7 @@ function MobileMenuItem({ }} >
- {label ?? ( -
{t("menu." + item.key)}
- )} + {label ??
{t("menu." + item.key)}
}
@@ -1048,7 +1046,7 @@ export default function Settings() { return (
-
{t("menu." + key)}
+
{t("menu." + key)}
{(showOverrideDot || showUnsavedDot) && (
{showOverrideDot && ( @@ -1104,9 +1102,7 @@ export default function Settings() {
{filteredItems.length > 1 && (

-
- {t("menu." + group.label)} -
+
{t("menu." + group.label)}

)} {filteredItems.map((item) => ( @@ -1275,7 +1271,7 @@ export default function Settings() { return (
-
+
{t("menu.settings", { ns: "common" })} @@ -1393,9 +1389,7 @@ export default function Settings() { : "text-sidebar-foreground/80", )} > -
- {t("menu." + group.label)} -
+
{t("menu." + group.label)}
{filteredItems.map((item) => ( diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 82312d9ff..5781046f7 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -1,6 +1,11 @@ import Heading from "@/components/ui/heading"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { SettingsGroupCard } from "@/components/card/SettingsGroupCard"; +import { + CONTROL_COLUMN_CLASS_NAME, + SettingsGroupCard, + SPLIT_ROW_CLASS_NAME, +} from "@/components/card/SettingsGroupCard"; +import { toast } from "sonner"; import { Toaster } from "@/components/ui/sonner"; import { Button } from "@/components/ui/button"; import useSWR from "swr"; @@ -15,6 +20,9 @@ import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { Switch } from "@/components/ui/switch"; import { Trans } from "react-i18next"; import { useEnabledState } from "@/api/ws"; +import { Label } from "@/components/ui/label"; +import axios from "axios"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; type CameraManagementViewProps = { setUnsavedChanges: React.Dispatch>; @@ -37,9 +45,20 @@ export default function CameraManagementView({ const [showWizard, setShowWizard] = useState(false); // List of cameras for dropdown - const cameras = useMemo(() => { + const enabledCameras = useMemo(() => { if (config) { - return Object.keys(config.cameras).sort(); + return Object.keys(config.cameras) + .filter((camera) => config.cameras[camera].enabled_in_config) + .sort(); + } + return []; + }, [config]); + + const disabledCameras = useMemo(() => { + if (config) { + return Object.keys(config.cameras) + .filter((camera) => !config.cameras[camera].enabled_in_config) + .sort(); } return []; }, [config]); @@ -82,7 +101,7 @@ export default function CameraManagementView({ {t("cameraManagement.addCamera")} - {cameras.length > 0 && ( + {enabledCameras.length > 0 && ( @@ -90,14 +109,22 @@ export default function CameraManagementView({ } > -
-
- - cameraManagement.streams.desc - +
+
+
- {cameras.map((camera) => ( + {enabledCameras.map((camera) => (
))}
+

+ + cameraManagement.streams.enableDesc + +

+ {disabledCameras.length > 0 && ( +
+
+ +

+ {t("cameraManagement.streams.disableDesc")} +

+
+
+
+ {disabledCameras.map((camera) => ( +
+ + +
+ ))} +
+

+ {t("cameraManagement.streams.disableDesc")} +

+
+
+ )} )}
@@ -169,3 +237,79 @@ function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) {
); } + +function CameraConfigEnableSwitch({ + cameraName, + onConfigChanged, +}: CameraEnableSwitchProps & { + onConfigChanged: () => Promise; +}) { + const { t } = useTranslation(["common", "views/settings"]); + 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: 0, + config_data: { + cameras: { + [cameraName]: { + enabled: true, + }, + }, + }, + update_topic: `config/cameras/${cameraName}/enabled`, + }); + + await onConfigChanged(); + + toast.success( + t("cameraManagement.streams.enableSuccess", { + 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); + } + }, + [cameraName, isSaving, onConfigChanged, t], + ); + + return ( +
+ {isSaving ? ( + + ) : ( + + )} +
+ ); +} diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index a1dd73e86..5b04aa0fe 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -25,6 +25,7 @@ import { DESCRIPTION_CLASS_NAME, CONTROL_COLUMN_CLASS_NAME, } from "@/components/card/SettingsGroupCard"; +import Heading from "@/components/ui/heading"; const WEEK_STARTS_ON = ["Sunday", "Monday"]; @@ -209,9 +210,12 @@ export default function UiSettingsView() { ]; return ( -
+
-
+ + {t("general.title")} + +