mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-13 03:47:34 +03:00
ui tweaks
This commit is contained in:
parent
3cdb40610f
commit
8072c991cd
@ -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."
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user