From 9c94640ffbdc69d3663a9d210a3421fbb20ff8e2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:00:47 -0500 Subject: [PATCH] allow changing camera friendly_name from camera management pane --- web/public/locales/en/views/settings.json | 8 +- .../overlay/dialog/TextEntryDialog.tsx | 23 +++- .../views/settings/CameraManagementView.tsx | 113 +++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 5a50e4daf..7dfd0ea98 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -457,7 +457,13 @@ "enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
Note: This does not disable go2rtc restreams.", "disableLabel": "Disabled cameras", "disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.", - "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes." + "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.", + "friendlyName": { + "edit": "Edit camera display name", + "title": "Edit Display Name", + "description": "Set the friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", + "rename": "Rename" + } }, "cameraConfig": { "add": "Add Camera", diff --git a/web/src/components/overlay/dialog/TextEntryDialog.tsx b/web/src/components/overlay/dialog/TextEntryDialog.tsx index 4ee0876fc..38fcee657 100644 --- a/web/src/components/overlay/dialog/TextEntryDialog.tsx +++ b/web/src/components/overlay/dialog/TextEntryDialog.tsx @@ -1,3 +1,4 @@ +import ActivityIndicator from "@/components/indicators/activity-indicator"; import TextEntry from "@/components/input/TextEntry"; import { Button } from "@/components/ui/button"; import { @@ -19,7 +20,9 @@ type TextEntryDialogProps = { setOpen: (open: boolean) => void; onSave: (text: string) => void; defaultValue?: string; + placeholder?: string; allowEmpty?: boolean; + isSaving?: boolean; regexPattern?: RegExp; regexErrorMessage?: string; forbiddenPattern?: RegExp; @@ -33,7 +36,9 @@ export default function TextEntryDialog({ setOpen, onSave, defaultValue = "", + placeholder, allowEmpty = false, + isSaving = false, regexPattern, regexErrorMessage, forbiddenPattern, @@ -50,6 +55,7 @@ export default function TextEntryDialog({ - - diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 86e9b7a31..472aee923 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import CameraEditForm from "@/components/settings/CameraEditForm"; import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog"; -import { LuPlus, LuTrash2 } from "react-icons/lu"; +import { LuPencil, LuPlus, LuTrash2 } from "react-icons/lu"; import { IoMdArrowRoundBack } from "react-icons/io"; import { isDesktop } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; @@ -26,6 +26,12 @@ import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator"; +import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { ProfileState } from "@/types/profile"; import { getProfileColor } from "@/utils/profileColors"; import { cn } from "@/lib/utils"; @@ -161,7 +167,13 @@ export default function CameraManagementView({ key={camera} className="flex flex-row items-center justify-between" > - +
+ + +
))} @@ -297,6 +309,103 @@ function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) { ); } +type CameraFriendlyNameEditorProps = { + cameraName: string; + onConfigChanged: () => Promise; +}; + +function CameraFriendlyNameEditor({ + cameraName, + onConfigChanged, +}: CameraFriendlyNameEditorProps) { + const { t } = useTranslation(["views/settings", "common"]); + const { data: config } = useSWR("config"); + const [open, setOpen] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + const currentFriendlyName = config?.cameras?.[cameraName]?.friendly_name; + + const onSave = useCallback( + async (text: string) => { + if (isSaving) return; + setIsSaving(true); + + try { + await axios.put("config/set", { + requires_restart: 0, + config_data: { + cameras: { + [cameraName]: { + friendly_name: text.trim() || null, + }, + }, + }, + }); + + await onConfigChanged(); + setOpen(false); + + toast.success(t("toast.save.success", { ns: "common" }), { + position: "top-center", + }); + } catch (error) { + const errorMessage = + axios.isAxiosError(error) && + (error.response?.data?.message || error.response?.data?.detail) + ? error.response?.data?.message || error.response?.data?.detail + : t("toast.save.error.noMessage", { ns: "common" }); + + toast.error( + t("toast.save.error.title", { errorMessage, ns: "common" }), + { position: "top-center" }, + ); + } finally { + setIsSaving(false); + } + }, + [cameraName, isSaving, onConfigChanged, t], + ); + + const renameLabel = t("cameraManagement.streams.friendlyName.rename", { + ns: "views/settings", + }); + + return ( + <> + + + + + {renameLabel} + + + + ); +} + type CameraConfigEnableSwitchProps = { cameraName: string; setRestartDialogOpen: React.Dispatch>;