import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; import axios from "axios"; import { toast } from "sonner"; import { Camera, Trash2 } from "lucide-react"; import type { FrigateConfig } from "@/types/frigateConfig"; import type { ProfileState } from "@/types/profile"; import { getProfileColor } from "@/utils/profileColors"; import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil"; import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; type ProfilesApiResponse = { profiles: string[]; active_profile: string | null; }; type ProfilesViewProps = { setUnsavedChanges?: React.Dispatch>; profileState?: ProfileState; profilesUIEnabled?: boolean; setProfilesUIEnabled?: React.Dispatch>; }; export default function ProfilesView({ profileState, profilesUIEnabled, setProfilesUIEnabled, }: ProfilesViewProps) { const { t } = useTranslation(["views/settings", "common"]); const { data: config, mutate: updateConfig } = useSWR("config"); const { data: profilesData, mutate: updateProfiles } = useSWR("profiles"); const [activating, setActivating] = useState(false); const [deleteProfile, setDeleteProfile] = useState(null); const [deleting, setDeleting] = useState(false); useEffect(() => { document.title = t("documentTitle.profiles", { ns: "views/settings", }); }, [t]); const allProfileNames = useMemo( () => profileState?.allProfileNames ?? [], [profileState?.allProfileNames], ); const activeProfile = profilesData?.active_profile ?? null; // Build overview data: for each profile, which cameras have which sections const profileOverviewData = useMemo(() => { if (!config || allProfileNames.length === 0) return {}; const data: Record> = {}; const cameras = Object.keys(config.cameras).sort(); for (const profile of allProfileNames) { data[profile] = {}; for (const camera of cameras) { const profileData = config.cameras[camera]?.profiles?.[profile]; if (!profileData) continue; const sections: string[] = []; for (const section of PROFILE_ELIGIBLE_SECTIONS) { if ( profileData[section as keyof typeof profileData] !== undefined && profileData[section as keyof typeof profileData] !== null ) { sections.push(section); } } if (profileData.enabled !== undefined && profileData.enabled !== null) { sections.push("enabled"); } if (sections.length > 0) { data[profile][camera] = sections; } } } return data; }, [config, allProfileNames]); const cameraCount = useMemo(() => { if (!config) return 0; return Object.keys(profileOverviewData).reduce((max, profile) => { const count = Object.keys(profileOverviewData[profile] ?? {}).length; return Math.max(max, count); }, 0); }, [config, profileOverviewData]); const handleActivateProfile = useCallback( async (profile: string | null) => { setActivating(true); try { await axios.put("profile/set", { profile: profile || null, }); await updateProfiles(); toast.success( profile ? t("profiles.activated", { ns: "views/settings", profile, }) : t("profiles.deactivated", { ns: "views/settings" }), { position: "top-center" }, ); } catch { toast.error(t("toast.save.error.title", { ns: "common" }), { position: "top-center", }); } finally { setActivating(false); } }, [updateProfiles, t], ); const handleDeleteProfile = useCallback(async () => { if (!deleteProfile || !config) return; setDeleting(true); try { // If this profile is active, deactivate it first if (activeProfile === deleteProfile) { await axios.put("profile/set", { profile: null }); } // Remove the profile from all cameras via config/set const configData: Record = {}; for (const camera of Object.keys(config.cameras)) { if (config.cameras[camera]?.profiles?.[deleteProfile]) { configData[camera] = { profiles: { [deleteProfile]: "" }, }; } } if (Object.keys(configData).length > 0) { await axios.put("config/set", { config_data: { cameras: configData }, }); } await updateConfig(); await updateProfiles(); toast.success( t("profiles.deleteSuccess", { ns: "views/settings", profile: deleteProfile, }), { position: "top-center" }, ); } catch { toast.error(t("toast.save.error.title", { ns: "common" }), { position: "top-center", }); } finally { setDeleting(false); setDeleteProfile(null); } }, [deleteProfile, activeProfile, config, updateConfig, updateProfiles, t]); if (!config || !profilesData) { return null; } const hasProfiles = allProfileNames.length > 0; return (
{t("profiles.title", { ns: "views/settings" })} {/* Enable Profiles Toggle — shown only when no profiles exist */} {!hasProfiles && setProfilesUIEnabled && (

{profilesUIEnabled ? t("profiles.enabledDescription", { ns: "views/settings" }) : t("profiles.disabledDescription", { ns: "views/settings" })}

)} {/* Active Profile Section — only when profiles exist */} {hasProfiles && (
{t("profiles.activeProfile", { ns: "views/settings" })}
{activeProfile && ( {t("profiles.active", { ns: "views/settings" })} )}
)} {/* Profile Cards */} {!hasProfiles ? (
{!profilesUIEnabled && (

{t("profiles.noProfiles", { ns: "views/settings" })}

)}
) : (
{allProfileNames.map((profile) => { const color = getProfileColor(profile, allProfileNames); const isActive = activeProfile === profile; const cameraData = profileOverviewData[profile] ?? {}; const cameras = Object.keys(cameraData).sort(); return (
{profile} {isActive && ( {t("profiles.active", { ns: "views/settings" })} )}
{cameras.length === 0 ? (

{t("profiles.noOverrides", { ns: "views/settings" })}

) : (
{cameras.map((camera) => { const sections = cameraData[camera]; return (
{camera}
{sections.map((section) => ( {section} ))}
); })}
)}
); })}
)} {/* Delete Profile Confirmation */} { if (!open) setDeleteProfile(null); }} > {t("profiles.deleteProfile", { ns: "views/settings" })} {t("profiles.deleteProfileConfirm", { ns: "views/settings", profile: deleteProfile, })} {t("button.cancel", { ns: "common" })} {t("button.delete", { ns: "common" })}
); }