import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import useSWR from "swr"; import { cn } from "@/lib/utils"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import Heading from "@/components/ui/heading"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Toaster } from "@/components/ui/sonner"; import type { FrigateConfig } from "@/types/frigateConfig"; import type { SettingsPageProps } from "@/views/settings/SingleSectionPage"; import type { ConfigSectionData } from "@/types/configForm"; type ModelTab = "plus" | "custom"; type PageState = { detectors: ConfigSectionData; modelTab: ModelTab; plusModelId: string | undefined; customModel: ConfigSectionData; }; const STATUS_BAR_KEY = "detectors_and_model"; const deriveInitialState = (config: FrigateConfig): PageState => { const modelPath = config.model?.path; const plusEnabled = Boolean(config.plus?.enabled); let modelTab: ModelTab; if (typeof modelPath === "string" && modelPath.startsWith("plus://")) { modelTab = "plus"; } else if (typeof modelPath === "string" && modelPath.length > 0) { modelTab = "custom"; } else if (plusEnabled) { modelTab = "plus"; } else { modelTab = "custom"; } const plusModelId = config.model?.plus?.id; const { plus: _plus, ...modelWithoutPlus } = (config.model ?? {}) as Record< string, unknown >; return { detectors: (config.detectors ?? {}) as ConfigSectionData, modelTab, plusModelId: plusModelId ?? undefined, customModel: modelWithoutPlus as ConfigSectionData, }; }; export default function DetectorsAndModelSettingsView( _props: SettingsPageProps, ) { const { t } = useTranslation(["views/settings", "common"]); const { getLocaleDocUrl } = useDocDomain(); const { data: config } = useSWR("config"); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const [snapshot, setSnapshot] = useState(null); const [state, setState] = useState(null); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (!config || snapshot !== null) return; const initial = deriveInitialState(config); setSnapshot(initial); setState(initial); }, [config, snapshot]); const isDirty = useMemo(() => { if (!state || !snapshot) return false; return JSON.stringify(state) !== JSON.stringify(snapshot); }, [state, snapshot]); useEffect(() => { if (isDirty) { addMessage( STATUS_BAR_KEY, t("detectorsAndModel.unsavedChanges"), undefined, STATUS_BAR_KEY, ); } else { removeMessage(STATUS_BAR_KEY, STATUS_BAR_KEY); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDirty]); useEffect(() => { document.title = `${t("detectorsAndModel.title")} - Frigate`; }, [t]); const onSave = useCallback(async () => { // implemented in Task 9 setIsSaving(true); setIsSaving(false); }, []); const onUndo = useCallback(() => { if (snapshot) setState(snapshot); }, [snapshot]); if (!config || !state) { return ; } const saveDisabled = !isDirty || isSaving; return (
{t("detectorsAndModel.title")}
{t("detectorsAndModel.description")}
{t("readTheDocumentation", { ns: "common" })}
{isDirty && ( {t("button.modified", { ns: "common", defaultValue: "Modified" })} )}
{t("detectorsAndModel.cardTitles.detector")} — placeholder, filled in Task 5.
{t("detectorsAndModel.cardTitles.model")} — placeholder, filled in Tasks 6–8.
{isDirty && ( {t("unsavedChanges", { ns: "views/settings" })} )}
{isDirty && ( )}
); }