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