// Camera Configuration View // Per-camera configuration with tab navigation and override indicators import { useMemo, useCallback, useState, memo } from "react"; import useSWR from "swr"; import { useTranslation } from "react-i18next"; import { DetectSection } from "@/components/config-form/sections/DetectSection"; import { RecordSection } from "@/components/config-form/sections/RecordSection"; import { SnapshotsSection } from "@/components/config-form/sections/SnapshotsSection"; import { MotionSection } from "@/components/config-form/sections/MotionSection"; import { ObjectsSection } from "@/components/config-form/sections/ObjectsSection"; import { ReviewSection } from "@/components/config-form/sections/ReviewSection"; import { AudioSection } from "@/components/config-form/sections/AudioSection"; import { NotificationsSection } from "@/components/config-form/sections/NotificationsSection"; import { LiveSection } from "@/components/config-form/sections/LiveSection"; import { TimestampSection } from "@/components/config-form/sections/TimestampSection"; import { useAllCameraOverrides } from "@/hooks/use-config-override"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import Heading from "@/components/ui/heading"; import { cn } from "@/lib/utils"; interface CameraConfigViewProps { /** Currently selected camera (from parent) */ selectedCamera?: string; /** Callback when unsaved changes state changes */ setUnsavedChanges?: React.Dispatch>; } export default function CameraConfigView({ selectedCamera: externalSelectedCamera, setUnsavedChanges, }: CameraConfigViewProps) { const { t } = useTranslation(["views/settings"]); const { data: config, mutate: refreshConfig } = useSWR("config"); // Get list of cameras const cameras = useMemo(() => { if (!config?.cameras) return []; return Object.keys(config.cameras).sort(); }, [config]); // Selected camera state (use external if provided, else internal) const [internalSelectedCamera, setInternalSelectedCamera] = useState( cameras[0] || "", ); const selectedCamera = externalSelectedCamera || internalSelectedCamera; // Get overridden sections for current camera const overriddenSections = useAllCameraOverrides(config, selectedCamera); const handleSave = useCallback(() => { refreshConfig(); setUnsavedChanges?.(false); }, [refreshConfig, setUnsavedChanges]); const handleCameraChange = useCallback((camera: string) => { setInternalSelectedCamera(camera); }, []); if (!config) { return (
); } if (cameras.length === 0) { return (
{t("configForm.camera.noCameras", { defaultValue: "No cameras configured", })}
); } return (
{t("configForm.camera.title", { defaultValue: "Camera Configuration", })}

{t("configForm.camera.description", { defaultValue: "Configure settings for individual cameras. Overridden settings are highlighted.", })}

{/* Camera Tabs - Only show if not externally controlled */} {!externalSelectedCamera && ( {cameras.map((camera) => { const cameraOverrides = overriddenSections.filter((s) => s.startsWith(camera), ); const hasOverrides = cameraOverrides.length > 0; const cameraConfig = config.cameras[camera]; const displayName = cameraConfig?.name || camera; return ( {displayName} {hasOverrides && ( {cameraOverrides.length} )} ); })} {cameras.map((camera) => ( ))} )} {/* Direct content when externally controlled */} {externalSelectedCamera && ( )}
); } interface CameraConfigContentProps { cameraName: string; config: FrigateConfig; overriddenSections: string[]; onSave: () => void; } const CameraConfigContent = memo(function CameraConfigContent({ cameraName, config, overriddenSections, onSave, }: CameraConfigContentProps) { const { t } = useTranslation([ "config/detect", "config/record", "config/snapshots", "config/motion", "config/objects", "config/review", "config/audio", "config/notifications", "config/live", "config/timestamp_style", "views/settings", "common", ]); const [activeSection, setActiveSection] = useState("detect"); const cameraConfig = config.cameras?.[cameraName]; if (!cameraConfig) { return (
{t("configForm.camera.notFound", { ns: "views/settings", defaultValue: "Camera not found", })}
); } const sections = [ { key: "detect", i18nNamespace: "config/detect", component: DetectSection, }, { key: "record", i18nNamespace: "config/record", component: RecordSection, }, { key: "snapshots", i18nNamespace: "config/snapshots", component: SnapshotsSection, }, { key: "motion", i18nNamespace: "config/motion", component: MotionSection, }, { key: "objects", i18nNamespace: "config/objects", component: ObjectsSection, }, { key: "review", i18nNamespace: "config/review", component: ReviewSection, }, { key: "audio", i18nNamespace: "config/audio", component: AudioSection }, { key: "notifications", i18nNamespace: "config/notifications", component: NotificationsSection, }, { key: "live", i18nNamespace: "config/live", component: LiveSection }, { key: "timestamp_style", i18nNamespace: "config/timestamp_style", component: TimestampSection, }, ]; return (
{/* Section Navigation */} {/* Section Content */}
{sections.map((section) => { const SectionComponent = section.component; return (
); })}
); });