import Heading from "@/components/ui/heading"; import { useCallback, useEffect, useMemo, useState } from "react"; 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"; import { FrigateConfig } from "@/types/frigateConfig"; import { useTranslation } from "react-i18next"; import CameraEditForm from "@/components/settings/CameraEditForm"; import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import { LuPlus } from "react-icons/lu"; import { IoMdArrowRoundBack } from "react-icons/io"; import { isDesktop } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { Switch } from "@/components/ui/switch"; import { Trans } from "react-i18next"; import { useEnabledState, useRestart } from "@/api/ws"; import { Label } from "@/components/ui/label"; import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator"; type CameraManagementViewProps = { setUnsavedChanges: React.Dispatch>; }; export default function CameraManagementView({ setUnsavedChanges, }: CameraManagementViewProps) { const { t } = useTranslation(["views/settings"]); const { data: config, mutate: updateConfig } = useSWR("config"); const [viewMode, setViewMode] = useState<"settings" | "add" | "edit">( "settings", ); // Control view state const [editCameraName, setEditCameraName] = useState( undefined, ); // Track camera being edited const [showWizard, setShowWizard] = useState(false); // State for restart dialog when enabling a disabled camera 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(); } return []; }, [config]); const disabledCameras = useMemo(() => { if (config) { return Object.keys(config.cameras) .filter((camera) => !config.cameras[camera].enabled_in_config) .sort(); } return []; }, [config]); useEffect(() => { document.title = t("documentTitle.cameraManagement"); }, [t]); // Handle back navigation from add/edit form const handleBack = useCallback(() => { setViewMode("settings"); setEditCameraName(undefined); setUnsavedChanges(false); updateConfig(); }, [updateConfig, setUnsavedChanges]); return ( <>
{viewMode === "settings" ? ( <> {t("cameraManagement.title")}
{enabledCameras.length > 0 && ( cameraManagement.streams.title } >
{enabledCameras.map((camera) => (
))}

cameraManagement.streams.enableDesc

{disabledCameras.length > 0 && (

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

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

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

)}
)}
) : ( <>
)}
setShowWizard(false)} /> setRestartDialogOpen(false)} onRestart={() => sendRestart("restart")} /> ); } type CameraEnableSwitchProps = { cameraName: string; }; function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) { const { payload: enabledState, send: sendEnabled } = useEnabledState(cameraName); return (
{ sendEnabled(isChecked ? "ON" : "OFF"); }} />
); } type CameraConfigEnableSwitchProps = { cameraName: string; setRestartDialogOpen: React.Dispatch>; onConfigChanged: () => Promise; }; 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: ( setRestartDialogOpen(true)}> ), }, ); } 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 (
{isSaving ? ( ) : ( )}
); }