import Heading from "@/components/ui/heading"; import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { useImproveContrast, useMotionContourArea, useMotionThreshold, } from "@/api/ws"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import { Separator } from "@/components/ui/separator"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; type MotionTunerViewProps = { selectedCamera: string; setUnsavedChanges: React.Dispatch>; }; type MotionSettings = { threshold?: number; contour_area?: number; improve_contrast?: boolean; }; export default function MotionTunerView({ selectedCamera, setUnsavedChanges, }: MotionTunerViewProps) { const { t } = useTranslation(["views/settings"]); const { getLocaleDocUrl } = useDocDomain(); const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); const { send: sendImproveContrast } = useImproveContrast(selectedCamera); const [motionSettings, setMotionSettings] = useState({ threshold: undefined, contour_area: undefined, improve_contrast: undefined, }); const [origMotionSettings, setOrigMotionSettings] = useState({ threshold: undefined, contour_area: undefined, improve_contrast: undefined, }); const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; } }, [config, selectedCamera]); useEffect(() => { if (cameraConfig) { setMotionSettings({ threshold: cameraConfig.motion.threshold, contour_area: cameraConfig.motion.contour_area, improve_contrast: cameraConfig.motion.improve_contrast, }); setOrigMotionSettings({ threshold: cameraConfig.motion.threshold, contour_area: cameraConfig.motion.contour_area, improve_contrast: cameraConfig.motion.improve_contrast, }); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCamera]); useEffect(() => { if (!motionSettings.threshold) return; sendMotionThreshold(motionSettings.threshold); }, [motionSettings.threshold, sendMotionThreshold]); useEffect(() => { if (!motionSettings.contour_area) return; sendMotionContourArea(motionSettings.contour_area); }, [motionSettings.contour_area, sendMotionContourArea]); useEffect(() => { if (motionSettings.improve_contrast === undefined) return; sendImproveContrast(motionSettings.improve_contrast ? "ON" : "OFF"); }, [motionSettings.improve_contrast, sendImproveContrast]); const handleMotionConfigChange = (newConfig: Partial) => { setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setUnsavedChanges(true); setChangedValue(true); }; const saveToConfig = useCallback(async () => { setIsLoading(true); axios .put( `config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast ? "True" : "False"}`, { requires_restart: 0 }, ) .then((res) => { if (res.status === 200) { toast.success(t("motionDetectionTuner.toast.success"), { position: "top-center", }); setChangedValue(false); updateConfig(); } else { toast.error( t("toast.save.error.title", { errorMessage: res.statusText, ns: "common", }), { position: "top-center", }, ); } }) .catch((error) => { toast.error( t("toast.save.error.title", { errorMessage: error.response.data.message, ns: "common", }), { position: "top-center" }, ); }) .finally(() => { setIsLoading(false); }); }, [ updateConfig, motionSettings.threshold, motionSettings.contour_area, motionSettings.improve_contrast, selectedCamera, t, ]); const onCancel = useCallback(() => { setMotionSettings(origMotionSettings); setChangedValue(false); removeMessage("motion_tuner", `motion_tuner_${selectedCamera}`); }, [origMotionSettings, removeMessage, selectedCamera]); useEffect(() => { if (changedValue) { addMessage( "motion_tuner", t("motionDetectionTuner.unsavedChanges", { camera: selectedCamera }), undefined, `motion_tuner_${selectedCamera}`, ); } else { removeMessage("motion_tuner", `motion_tuner_${selectedCamera}`); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [changedValue, selectedCamera]); useEffect(() => { document.title = t("documentTitle.motionTuner"); }, [t]); if (!cameraConfig && !selectedCamera) { return ; } return (
{t("motionDetectionTuner.title")}

{t("motionDetectionTuner.desc.title")}

{t("readTheDocumentation", { ns: "common" })}
motionDetectionTuner.Threshold.desc
{ handleMotionConfigChange({ threshold: value[0] }); }} />
{motionSettings.threshold}

motionDetectionTuner.contourArea.desc

{ handleMotionConfigChange({ contour_area: value[0] }); }} />
{motionSettings.contour_area}
motionDetectionTuner.improveContrast.desc
{ handleMotionConfigChange({ improve_contrast: isChecked }); }} />
{cameraConfig ? (
) : ( )}
); }