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",
"face_recognition": "Face Recognition",
"lpr": "License Plate Recognition",
"birdseye": "Birdseye"
"birdseye": "Birdseye",
"masksAndZones": "Masks / Zones"
},
"detect": {
"title": "Detection Settings"
@ -1448,6 +1449,7 @@
"noActiveProfile": "No active profile",
"active": "Active",
"activated": "Profile '{{profile}}' activated",
"activateFailed": "Failed to set profile",
"deactivated": "Profile deactivated",
"noProfiles": "No profiles defined. Add a profile from any camera section.",
"noOverrides": "No overrides",
@ -1461,7 +1463,8 @@
"deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.",
"deleteSuccess": "Profile '{{profile}}' deleted",
"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",
"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."

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ 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 { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { cn } from "@/lib/utils";
import Heading from "@/components/ui/heading";
import { Button } from "@/components/ui/button";
@ -129,10 +130,15 @@ export default function ProfilesView({
: t("profiles.deactivated", { ns: "views/settings" }),
{ position: "top-center" },
);
} catch {
toast.error(t("toast.save.error.title", { ns: "common" }), {
position: "top-center",
});
} catch (err) {
const message =
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 {
setActivating(false);
}
@ -186,10 +192,15 @@ export default function ProfilesView({
}),
{ position: "top-center" },
);
} catch {
toast.error(t("toast.save.error.title", { ns: "common" }), {
position: "top-center",
});
} catch (err) {
const errorMessage =
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 {
setDeleting(false);
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" />
<div className="min-w-0">
<div className="truncate text-xs font-medium">
{camera}
{resolveCameraName(config, camera)}
</div>
<div className="mt-1 flex flex-wrap gap-1">
{sections.map((section) => (
@ -382,7 +393,10 @@ export default function ProfilesView({
color.bg,
)}
>
{section}
{t(`configForm.sections.${section}`, {
ns: "views/settings",
defaultValue: section,
})}
</span>
))}
</div>

View File

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