ui tweaks

This commit is contained in:
Josh Hawkins 2026-03-11 12:35:13 -05:00
parent 3cdb40610f
commit 8072c991cd
6 changed files with 75 additions and 43 deletions

View File

@ -1342,7 +1342,8 @@
"genai": "GenAI", "genai": "GenAI",
"face_recognition": "Face Recognition", "face_recognition": "Face Recognition",
"lpr": "License Plate Recognition", "lpr": "License Plate Recognition",
"birdseye": "Birdseye" "birdseye": "Birdseye",
"masksAndZones": "Masks / Zones"
}, },
"detect": { "detect": {
"title": "Detection Settings" "title": "Detection Settings"
@ -1448,6 +1449,7 @@
"noActiveProfile": "No active profile", "noActiveProfile": "No active profile",
"active": "Active", "active": "Active",
"activated": "Profile '{{profile}}' activated", "activated": "Profile '{{profile}}' activated",
"activateFailed": "Failed to set profile",
"deactivated": "Profile deactivated", "deactivated": "Profile deactivated",
"noProfiles": "No profiles defined. Add a profile from any camera section.", "noProfiles": "No profiles defined. Add a profile from any camera section.",
"noOverrides": "No overrides", "noOverrides": "No overrides",
@ -1461,7 +1463,8 @@
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.", "deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
"deleteSuccess": "Profile '{{profile}}' deleted", "deleteSuccess": "Profile '{{profile}}' deleted",
"deleteSection": "Delete Section Overrides", "deleteSection": "Delete Section Overrides",
"deleteSectionConfirm": "Remove {{profile}}'s overrides for {{section}} on {{camera}}?", "deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?",
"deleteSectionSuccess": "Removed {{section}} overrides for {{profile}}",
"enableSwitch": "Enable Profiles", "enableSwitch": "Enable Profiles",
"enabledDescription": "Profiles are enabled. Navigate to a camera config section, create a new profile from the dropdown in the header, and save for changes to take effect.", "enabledDescription": "Profiles are enabled. Navigate to a camera config section, create a new profile from the dropdown in the header, and save for changes to take effect.",
"disabledDescription": "Profiles allow you to define named sets of camera config overrides (e.g., armed, away, night) that can be activated on demand." "disabledDescription": "Profiles allow you to define named sets of camera config overrides (e.g., armed, away, night) that can be activated on demand."

View File

@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { Check, ChevronDown, Plus, Trash2 } from "lucide-react"; import { Check, ChevronDown, Plus, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getProfileColor } from "@/utils/profileColors"; import { getProfileColor } from "@/utils/profileColors";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -52,6 +53,11 @@ export function ProfileSectionDropdown({
onDeleteProfileSection, onDeleteProfileSection,
}: ProfileSectionDropdownProps) { }: ProfileSectionDropdownProps) {
const { t } = useTranslation(["views/settings", "common"]); const { t } = useTranslation(["views/settings", "common"]);
const friendlyCameraName = useCameraFriendlyName(cameraName);
const friendlySectionName = t(`configForm.sections.${sectionKey}`, {
ns: "views/settings",
defaultValue: sectionKey,
});
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const [deleteConfirmProfile, setDeleteConfirmProfile] = useState< const [deleteConfirmProfile, setDeleteConfirmProfile] = useState<
string | null string | null
@ -153,21 +159,23 @@ export function ProfileSectionDropdown({
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={profile} key={profile}
className="group flex items-center justify-between gap-2" className="group flex items-start justify-between gap-2"
onClick={() => onSelectProfile(profile)} onClick={() => onSelectProfile(profile)}
> >
<div className="flex items-center gap-2"> <div className="flex flex-col items-center gap-2">
{isActive && <Check className="h-3.5 w-3.5 shrink-0" />} <div className="flex w-full flex-row items-center justify-start gap-2">
<span {isActive && <Check className="h-3.5 w-3.5 shrink-0" />}
className={cn( <span
"h-2 w-2 shrink-0 rounded-full", className={cn(
color.dot, "h-2 w-2 shrink-0 rounded-full",
!isActive && "ml-[22px]", color.dot,
)} !isActive && "ml-[22px]",
/> )}
<span>{profile}</span> />
<span>{profile}</span>
</div>
{!hasData && ( {!hasData && (
<span className="text-xs text-muted-foreground"> <span className="ml-[22px] text-xs text-muted-foreground">
{t("profiles.noOverrides", { ns: "views/settings" })} {t("profiles.noOverrides", { ns: "views/settings" })}
</span> </span>
)} )}
@ -260,8 +268,8 @@ export function ProfileSectionDropdown({
{t("profiles.deleteSectionConfirm", { {t("profiles.deleteSectionConfirm", {
ns: "views/settings", ns: "views/settings",
profile: deleteConfirmProfile, profile: deleteConfirmProfile,
section: sectionKey, section: friendlySectionName,
camera: cameraName, camera: friendlyCameraName,
})} })}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>

View File

@ -1050,14 +1050,13 @@ export default function Settings() {
// Profile state handlers // Profile state handlers
const handleSelectProfile = useCallback( const handleSelectProfile = useCallback(
(camera: string, section: string, profile: string | null) => { (camera: string, _section: string, profile: string | null) => {
const key = `${camera}::${section}`;
setEditingProfile((prev) => { setEditingProfile((prev) => {
if (profile === null) { if (profile === null) {
const { [key]: _, ...rest } = prev; const { [camera]: _, ...rest } = prev;
return rest; return rest;
} }
return { ...prev, [key]: profile }; return { ...prev, [camera]: profile };
}); });
}, },
[], [],
@ -1115,8 +1114,13 @@ export default function Settings() {
// Switch back to base config // Switch back to base config
handleSelectProfile(camera, section, null); handleSelectProfile(camera, section, null);
toast.success( toast.success(
t("toast.save.success", { t("profiles.deleteSectionSuccess", {
ns: "common", ns: "views/settings",
section: t(`configForm.sections.${section}`, {
ns: "views/settings",
defaultValue: section,
}),
profile,
}), }),
); );
} catch { } catch {
@ -1155,8 +1159,7 @@ export default function Settings() {
const headerEditingProfile = useMemo(() => { const headerEditingProfile = useMemo(() => {
if (!selectedCamera || !currentSectionKey) return null; if (!selectedCamera || !currentSectionKey) return null;
const key = `${selectedCamera}::${currentSectionKey}`; return editingProfile[selectedCamera] ?? null;
return editingProfile[key] ?? null;
}, [selectedCamera, currentSectionKey, editingProfile]); }, [selectedCamera, currentSectionKey, editingProfile]);
const showProfileDropdown = const showProfileDropdown =
@ -1230,7 +1233,15 @@ export default function Settings() {
}); });
await mutate("config"); await mutate("config");
handleSelectProfile(selectedCamera, "masksAndZones", null); handleSelectProfile(selectedCamera, "masksAndZones", null);
toast.success(t("toast.save.success", { ns: "common" })); toast.success(
t("profiles.deleteSectionSuccess", {
ns: "views/settings",
section: t("configForm.sections.masksAndZones", {
ns: "views/settings",
}),
profile: profileName,
}),
);
} catch { } catch {
toast.error(t("toast.save.error.title", { ns: "common" })); toast.error(t("toast.save.error.title", { ns: "common" }));
} }
@ -1639,7 +1650,7 @@ export default function Settings() {
</Badge> </Badge>
)} )}
</div> </div>
<div className="flex items-center gap-5"> <div className="flex items-center gap-2">
{hasPendingChanges && ( {hasPendingChanges && (
<div <div
className={cn( className={cn(

View File

@ -73,9 +73,8 @@ export default function MasksAndZonesView({
const [snapPoints, setSnapPoints] = useState(false); const [snapPoints, setSnapPoints] = useState(false);
// Profile state // Profile state
const profileSectionKey = `${selectedCamera}::masksAndZones`;
const currentEditingProfile = const currentEditingProfile =
profileState?.editingProfile[profileSectionKey] ?? null; profileState?.editingProfile[selectedCamera] ?? null;
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
if (config && selectedCamera) { if (config && selectedCamera) {

View File

@ -8,6 +8,7 @@ import type { FrigateConfig } from "@/types/frigateConfig";
import type { ProfileState } from "@/types/profile"; import type { ProfileState } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors"; import { getProfileColor } from "@/utils/profileColors";
import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil"; import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -129,10 +130,15 @@ export default function ProfilesView({
: t("profiles.deactivated", { ns: "views/settings" }), : t("profiles.deactivated", { ns: "views/settings" }),
{ position: "top-center" }, { position: "top-center" },
); );
} catch { } catch (err) {
toast.error(t("toast.save.error.title", { ns: "common" }), { const message =
position: "top-center", axios.isAxiosError(err) && err.response?.data?.message
}); ? String(err.response.data.message)
: undefined;
toast.error(
message || t("profiles.activateFailed", { ns: "views/settings" }),
{ position: "top-center" },
);
} finally { } finally {
setActivating(false); setActivating(false);
} }
@ -186,10 +192,15 @@ export default function ProfilesView({
}), }),
{ position: "top-center" }, { position: "top-center" },
); );
} catch { } catch (err) {
toast.error(t("toast.save.error.title", { ns: "common" }), { const errorMessage =
position: "top-center", axios.isAxiosError(err) && err.response?.data?.message
}); ? String(err.response.data.message)
: undefined;
toast.error(
errorMessage || t("toast.save.error.noMessage", { ns: "common" }),
{ position: "top-center" },
);
} finally { } finally {
setDeleting(false); setDeleting(false);
setDeleteProfile(null); setDeleteProfile(null);
@ -371,7 +382,7 @@ export default function ProfilesView({
<Camera className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" /> <Camera className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{camera} {resolveCameraName(config, camera)}
</div> </div>
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{sections.map((section) => ( {sections.map((section) => (
@ -382,7 +393,10 @@ export default function ProfilesView({
color.bg, color.bg,
)} )}
> >
{section} {t(`configForm.sections.${section}`, {
ns: "views/settings",
defaultValue: section,
})}
</span> </span>
))} ))}
</div> </div>

View File

@ -83,11 +83,8 @@ export function SingleSectionPage({
? getLocaleDocUrl(resolvedSectionConfig.sectionDocs) ? getLocaleDocUrl(resolvedSectionConfig.sectionDocs)
: undefined; : undefined;
const profileKey = selectedCamera const currentEditingProfile = selectedCamera
? `${selectedCamera}::${sectionKey}` ? (profileState?.editingProfile[selectedCamera] ?? null)
: undefined;
const currentEditingProfile = profileKey
? (profileState?.editingProfile[profileKey] ?? null)
: null; : null;
const handleSectionStatusChange = useCallback( const handleSectionStatusChange = useCallback(