// Camera Configuration View // Per-camera configuration with tab navigation and override indicators import { useMemo, useCallback, useState } from "react"; import useSWR from "swr"; import { useTranslation } from "react-i18next"; import { DetectSection, RecordSection, SnapshotsSection, MotionSection, ObjectsSection, ReviewSection, AudioSection, NotificationsSection, LiveSection, TimestampSection, } from "@/components/config-form/sections"; 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 { 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; } function CameraConfigContent({ cameraName, config, overriddenSections, onSave, }: CameraConfigContentProps) { const { t } = useTranslation(["views/settings"]); const [activeSection, setActiveSection] = useState("detect"); const cameraConfig = config.cameras?.[cameraName]; if (!cameraConfig) { return (
{t("configForm.camera.notFound", { defaultValue: "Camera not found" })}
); } const sections = [ { key: "detect", label: "Detect", component: DetectSection }, { key: "record", label: "Record", component: RecordSection }, { key: "snapshots", label: "Snapshots", component: SnapshotsSection }, { key: "motion", label: "Motion", component: MotionSection }, { key: "objects", label: "Objects", component: ObjectsSection }, { key: "review", label: "Review", component: ReviewSection }, { key: "audio", label: "Audio", component: AudioSection }, { key: "notifications", label: "Notifications", component: NotificationsSection, }, { key: "live", label: "Live", component: LiveSection }, { key: "timestamp_style", label: "Timestamp", component: TimestampSection }, ]; return (
{/* Section Navigation */} {/* Section Content */}
{sections.map((section) => { const SectionComponent = section.component; return (
); })}
); }