diff --git a/src/Main.tsx b/src/Main.tsx index 23f967fb..7078fd62 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -4,6 +4,7 @@ import { I18nextProvider } from "react-i18next"; import { HashRouter, Route, Routes } from "react-router"; import { useShallow } from "zustand/react/shallow"; import ScrollToTop from "./components/ScrollToTop.js"; +import { TitleUpdater } from "./components/TitleUpdater.js"; import Toasts from "./components/Toasts.js"; import { ErrorBoundary } from "./ErrorBoundary.js"; import i18n from "./i18n/index.js"; @@ -52,6 +53,7 @@ function App() { } > + } /> } /> diff --git a/src/components/TitleUpdater.tsx b/src/components/TitleUpdater.tsx new file mode 100644 index 00000000..5f1a1e44 --- /dev/null +++ b/src/components/TitleUpdater.tsx @@ -0,0 +1,134 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router"; +import { useAppStore } from "../store.js"; +import { getValidSourceIdx } from "../utils.js"; + +const BASE_TITLE = "Zigbee2MQTT"; + +// Helper to convert text to title case +const toTitleCase = (text: string): string => { + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); +}; + +// Map device page tabs to translation keys +const deviceTabTranslationMap: Record = { + info: "about", + exposes: "exposes", + bind: "bind", + reporting: "reporting", + settings: "settings", + "settings-specific": "settings_specific", + state: "state", + clusters: "clusters", + groups: "groups", + scene: "scene", + "dev-console": "dev_console", +}; + +export function TitleUpdater() { + const location = useLocation(); + const { t: tNavbar } = useTranslation(["navbar"]); + const devices = useAppStore((state) => state.devices); + const groups = useAppStore((state) => state.groups); + + useEffect(() => { + let pageTitle = BASE_TITLE; + const pathname = location.pathname; + + if (pathname === "/" || pathname === "") { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.home)}`; + } else if (pathname.includes("/dashboard")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.dashboard)}`; + } else if (pathname.includes("/devices")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.devices)}`; + } else if (pathname.includes("/device/")) { + // Device detail page - extract params from pathname: /device/:sourceIdx/:deviceId/:tab? + const deviceMatch = pathname.match(/\/device\/(\d+)\/([^/]+)(?:\/(.+))?/); + if (deviceMatch) { + const [sourceIdx] = getValidSourceIdx(deviceMatch[1]); + const deviceId = deviceMatch[2]; + const tab = deviceMatch[3]; + + if (deviceId && devices[sourceIdx]) { + const device = devices[sourceIdx].find((d) => d.ieee_address === deviceId); + if (device) { + let title = `${BASE_TITLE} - ${device.friendly_name}`; + if (tab && deviceTabTranslationMap[tab]) { + const translationKey = deviceTabTranslationMap[tab]; + const translatedTab = tNavbar(($) => $[translationKey as keyof typeof $]); + title += ` - ${toTitleCase(translatedTab)}`; + } + pageTitle = title; + } + } + } + } else if (pathname.includes("/groups")) { + if (pathname.includes("/group/")) { + // Group detail page - extract params from pathname: /group/:sourceIdx/:groupId/:tab? + const groupMatch = pathname.match(/\/group\/(\d+)\/(\d+)(?:\/(.+))?/); + if (groupMatch) { + const [sourceIdx] = getValidSourceIdx(groupMatch[1]); + const groupId = Number.parseInt(groupMatch[2], 10); + const tab = groupMatch[3]; + + if (groups[sourceIdx]) { + const group = groups[sourceIdx].find((g) => g.id === groupId); + if (group) { + let title = `${BASE_TITLE} - ${group.friendly_name}`; + if (tab && deviceTabTranslationMap[tab]) { + const translationKey = deviceTabTranslationMap[tab]; + const translatedTab = tNavbar(($) => $[translationKey as keyof typeof $]); + title += ` - ${toTitleCase(translatedTab)}`; + } + pageTitle = title; + } + } + } + } else { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.groups)}`; + } + } else if (pathname.includes("/reporting")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.reporting)}`; + } else if (pathname.includes("/bindings")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.bindings)}`; + } else if (pathname.includes("/ota")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.ota)}`; + } else if (pathname.includes("/touchlink")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.touchlink)}`; + } else if (pathname.includes("/network")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.network)}`; + } else if (pathname.includes("/logs")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.logs)}`; + } else if (pathname.includes("/activity")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.activity)}`; + } else if (pathname.includes("/settings")) { + // Settings page - extract params from pathname: /settings/:sourceIdx?/:tab?/:subTab? + const settingsMatch = pathname.match(/\/settings(?:\/(\d+))?(?:\/([^/]+))?(?:\/(.+))?/); + if (settingsMatch) { + const tab = settingsMatch[2]; + const subTab = settingsMatch[3]; + + let title = `${BASE_TITLE} - ${tNavbar(($) => $.settings)}`; + + if (tab) { + title += ` - ${toTitleCase(tab)}`; + if (subTab) { + title += ` - ${toTitleCase(subTab)}`; + } + } + pageTitle = title; + } else { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.settings)}`; + } + } else if (pathname.includes("/frontend-settings")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.frontend_settings)}`; + } else if (pathname.includes("/contribute")) { + pageTitle = `${BASE_TITLE} - ${tNavbar(($) => $.contribute)}`; + } + + document.title = pageTitle; + }, [location.pathname, devices, groups, tNavbar]); + + return null; +}