import Heading from "@/components/ui/heading"; import { Label } from "@/components/ui/label"; import { useCallback, useContext, useEffect, useState } from "react"; import { Toaster } from "sonner"; import { Separator } from "../../components/ui/separator"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { toast } from "sonner"; import useSWR from "swr"; import axios from "axios"; import { FrigateConfig } from "@/types/frigateConfig"; import { CheckCircle2, XCircle } from "lucide-react"; import { Trans, useTranslation } from "react-i18next"; import { IoIosWarning } from "react-icons/io"; import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "@/components/ui/select"; type FrigatePlusModel = { id: string; type: string; supportedDetectors: string[]; trainDate: string; baseModel: string; width: number; height: number; }; type FrigatePlusSettings = { model: { id?: string; }; }; type FrigateSettingsViewProps = { setUnsavedChanges: React.Dispatch>; }; export default function FrigatePlusSettingsView({ setUnsavedChanges, }: FrigateSettingsViewProps) { const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const [frigatePlusSettings, setFrigatePlusSettings] = useState({ model: { id: undefined, }, }); const [origPlusSettings, setOrigPlusSettings] = useState( { model: { id: undefined, }, }, ); const { data: availableModels = {} } = useSWR< Record >("/plus/models", { fallbackData: {}, fetcher: async (url) => { const res = await axios.get(url, { withCredentials: true }); return res.data.reduce( (obj: Record, model: FrigatePlusModel) => { obj[model.id] = model; return obj; }, {}, ); }, }); useEffect(() => { if (config) { if (frigatePlusSettings?.model.id == undefined) { setFrigatePlusSettings({ model: { id: config.model.plus?.id, }, }); } setOrigPlusSettings({ model: { id: config.model.plus?.id, }, }); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); const handleFrigatePlusConfigChange = ( newConfig: Partial, ) => { setFrigatePlusSettings((prevConfig) => ({ model: { ...prevConfig.model, ...newConfig.model, }, })); setUnsavedChanges(true); setChangedValue(true); }; const saveToConfig = useCallback(async () => { setIsLoading(true); axios .put(`config/set?model.path=plus://${frigatePlusSettings.model.id}`, { requires_restart: 0, }) .then((res) => { if (res.status === 200) { toast.success(t("frigatePlus.toast.success"), { position: "top-center", }); setChangedValue(false); addMessage( "plus_restart", "Restart required (Frigate+ model changed)", undefined, "plus_restart", ); updateConfig(); } else { toast.error( t("frigatePlus.toast.error", { errorMessage: res.statusText }), { position: "top-center", }, ); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("toast.save.error.title", { errorMessage, ns: "common" }), { position: "top-center", }, ); }) .finally(() => { setIsLoading(false); }); }, [updateConfig, addMessage, frigatePlusSettings, t]); const onCancel = useCallback(() => { setFrigatePlusSettings(origPlusSettings); setChangedValue(false); removeMessage("plus_settings", "plus_settings"); }, [origPlusSettings, removeMessage]); useEffect(() => { if (changedValue) { addMessage( "plus_settings", `Unsaved Frigate+ settings changes`, undefined, "plus_settings", ); } else { removeMessage("plus_settings", "plus_settings"); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [changedValue]); useEffect(() => { document.title = t("documentTitle.frigatePlus"); }, [t]); const needCleanSnapshots = () => { if (!config) { return false; } return Object.values(config.cameras).some( (camera) => camera.snapshots.enabled && !camera.snapshots.clean_copy, ); }; if (!config) { return ; } return ( <>
{t("frigatePlus.title")} {t("frigatePlus.apiKey.title")}
{config?.plus?.enabled ? ( ) : ( )}

{t("frigatePlus.apiKey.desc")}

{!config?.model.plus && ( <>
{t("frigatePlus.apiKey.plusLink")}
)}
{config?.model.plus && ( <>
{t("frigatePlus.modelInfo.title")}
{!config?.model?.plus && (

{t("frigatePlus.modelInfo.loading")}

)} {config?.model?.plus === null && (

{t("frigatePlus.modelInfo.error")}

)} {config?.model?.plus && (

{config.model.plus.name} ( {config.model.plus.width + "x" + config.model.plus.height} )

{new Date( config.model.plus.trainDate, ).toLocaleString()}

{config.model.plus.baseModel}

{config.model.plus.supportedDetectors.join(", ")}

{t("frigatePlus.modelInfo.availableModels")}

frigatePlus.modelInfo.modelSelect

)}
)}
{t("frigatePlus.snapshotConfig.title")}

frigatePlus.snapshotConfig.desc

{t("frigatePlus.snapshotConfig.documentation")}
{config && (
{Object.entries(config.cameras).map( ([name, camera]) => ( ), )}
{t("frigatePlus.snapshotConfig.table.camera")} {t("frigatePlus.snapshotConfig.table.snapshots")} frigatePlus.snapshotConfig.table.cleanCopySnapshots
{name} {camera.snapshots.enabled ? ( ) : ( )} {camera.snapshots?.enabled && camera.snapshots?.clean_copy ? ( ) : ( )}
)} {needCleanSnapshots() && (
frigatePlus.snapshotConfig.cleanCopyWarning
)}
); }