diff --git a/UI_LAYOUT_GUIDE.md b/UI_LAYOUT_GUIDE.md index 1a8093f..3e25e74 100644 --- a/UI_LAYOUT_GUIDE.md +++ b/UI_LAYOUT_GUIDE.md @@ -195,44 +195,6 @@ --- -## 显示与灯光页面(/display) - -### 区块 1:页面头部 -| 位置 | 组件 | 功能 | 说明 | -|------|------|------|------| -| 左侧 | 返回按钮 | 返回上一页 | 左箭头 | -| 中央 | 标题 | 显示 "显示与灯光" | 17px 字体 | - -### 区块 2:iOS 列表行 -| 序号 | 菜单项 | 右侧显示 | 功能 | -|------|--------|---------|------| -| 1 | 屏幕亮度 | 下拉选择 | 跳转 SelectPage 选择 | -| 2 | 自动锁屏 | 下拉选择 | 跳转 SelectPage 选择 | -| 3 | 旋钮亮度 | 下拉选择 | 跳转 SelectPage 选择 | -| 4 | 旋钮呼吸灯 | Toggle | 启用/禁用呼吸灯效果 | -| 5 | 按键灯 | Toggle | 启用/禁用按键灯 | - ---- - -## DSP & DAC 页面(/dsp) - -### 区块 1:页面头部 -| 位置 | 组件 | 功能 | 说明 | -|------|------|------|------| -| 左侧 | 返回按钮 | 返回上一页 | 左箭头 | -| 中央 | 标题 | 显示 "DSP & DAC" | 17px 字体 | - -### 区块 2:iOS 列表行 -| 序号 | 菜单项 | 右侧显示 | 功能 | -|------|--------|---------|------| -| 1 | DSP 引擎 | 下拉选择 | 跳转 SelectPage 选择 | -| 2 | DAC 模式 | 下拉选择 | 跳转 SelectPage 选择 | -| 3 | 模拟增益 | 下拉选择 | 跳转 SelectPage 选择 | -| 4 | XLR 端口阻抗 | 下拉选择 | 跳转 SelectPage 选择 | -| 5 | ARC 模式 | 下拉选择 | 跳转 SelectPage 选择 | - ---- - ## 蓝牙页面(/bluetooth) ### 区块 1:页面头部 diff --git a/client/src/App.tsx b/client/src/App.tsx index 31a393a..1c8727c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,8 +13,6 @@ import Dashboard from "./pages/Dashboard"; import AudioPage from "./pages/AudioPage"; import EffectsPage from "./pages/EffectsPage"; import BluetoothPage from "./pages/BluetoothPage"; -import DspPage from "./pages/DspPage"; -import DisplayPage from "./pages/DisplayPage"; import SystemPage from "./pages/SystemPage"; import IOPage from "./pages/IOPage"; import VUPage from "./pages/VUPage"; @@ -52,8 +50,6 @@ function AppRouter() { - - diff --git a/client/src/components/BottomNav.tsx b/client/src/components/BottomNav.tsx index 623bf1a..14745b0 100644 --- a/client/src/components/BottomNav.tsx +++ b/client/src/components/BottomNav.tsx @@ -11,8 +11,8 @@ import { cn } from "@/lib/utils"; import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext"; import { useIsLgUp } from "@/hooks/useIsLgUp"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; import { Activity, Bot, @@ -49,10 +49,8 @@ export default function BottomNav() { const { deviceState } = useDevice(); const navText = useMemo((): BottomNavLocale => { - const lang = deviceState?.language; - const pack = - lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale; + const pack = resolveLocalePack(deviceState?.language); + return (pack.bottomNav ?? localeEn.bottomNav) as BottomNavLocale; }, [deviceState?.language]); return ( diff --git a/client/src/components/ConnectionPlaceholder.tsx b/client/src/components/ConnectionPlaceholder.tsx index 8675c75..06efcc2 100644 --- a/client/src/components/ConnectionPlaceholder.tsx +++ b/client/src/components/ConnectionPlaceholder.tsx @@ -14,9 +14,6 @@ function pickConnectLocale(): ConnectLocale { if (lang === "0") return localeEn.connect; if (lang === "1") return localeZhHK.connect; if (lang === "2") return localeZh.connect; - const nav = navigator.language.toLowerCase(); - if (nav.startsWith("zh-hk") || nav.startsWith("zh-tw")) return localeZhHK.connect; - if (nav.startsWith("zh")) return localeZh.connect; return localeEn.connect; } diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 132aec7..c16fa3e 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -5,10 +5,8 @@ import { useDevice } from "@/contexts/DeviceContext"; import { cn } from "@/lib/utils"; import { - Activity, Bluetooth, ChevronRight, - Cpu, Gauge, Layers, LayoutDashboard, @@ -30,8 +28,6 @@ const NAV_ITEMS: NavItem[] = [ { path: "/eq", icon: Sliders, label: "Parametric EQ" }, { path: "/effects", icon: Layers, label: "Effects" }, { path: "/bluetooth", icon: Bluetooth, label: "Bluetooth" }, - { path: "/dsp", icon: Cpu, label: "DSP & DAC" }, - { path: "/display", icon: Activity, label: "Display & Lights" }, { path: "/system", icon: Settings, label: "System" }, ]; diff --git a/client/src/hooks/usePageTitleWithConnection.ts b/client/src/hooks/usePageTitleWithConnection.ts new file mode 100644 index 0000000..3c494eb --- /dev/null +++ b/client/src/hooks/usePageTitleWithConnection.ts @@ -0,0 +1,15 @@ +import { useMemo } from "react"; +import { useDevice } from "@/contexts/DeviceContext"; + +export function withDisconnectPageTitle(title: string): string { + return `${title}(Disconnect)`; +} + +/** Append "(Disconnect)" to the page title when the device is not connected. */ +export function usePageTitleWithConnection(title: string): string { + const { isConnected, isConnecting } = useDevice(); + return useMemo(() => { + if (isConnected || isConnecting) return title; + return withDisconnectPageTitle(title); + }, [title, isConnected, isConnecting]); +} diff --git a/client/src/locales/resolveLocale.ts b/client/src/locales/resolveLocale.ts new file mode 100644 index 0000000..502b8c4 --- /dev/null +++ b/client/src/locales/resolveLocale.ts @@ -0,0 +1,19 @@ +import localeEn from "./data-en.json"; +import localeZh from "./data-zh.json"; +import localeZhHK from "./data-zh-HK.json"; + +/** Device language index: 0=en, 1=zh-HK, 2=zh-CN */ +export const DEFAULT_DEVICE_LANGUAGE = 0; + +export type LocalePack = typeof localeEn; + +export function resolveDeviceLanguage(language: number | undefined): number { + return language ?? DEFAULT_DEVICE_LANGUAGE; +} + +export function resolveLocalePack(language: number | undefined): LocalePack { + const lang = resolveDeviceLanguage(language); + if (lang === 0) return localeEn; + if (lang === 1) return localeZhHK; + return localeZh; +} diff --git a/client/src/pages/AIPage.tsx b/client/src/pages/AIPage.tsx index ad8fdc0..9e2fc01 100644 --- a/client/src/pages/AIPage.tsx +++ b/client/src/pages/AIPage.tsx @@ -58,15 +58,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Streamdown } from "streamdown"; import { toast } from "sonner"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale"; type AILocale = NonNullable<(typeof localeZh)["ai"]>; function resolveAILocale(language: number | undefined): AILocale { - const loc = - language === 0 ? localeEn : language === 1 ? localeZhHK : localeZh; - return loc.ai as AILocale; + return resolveLocalePack(language).ai as AILocale; } function formatText(template: string, vars: Record): string { @@ -209,7 +207,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) const { setOpen: setAiPanelOpen } = useAIDrawer(); const { isConnected, deviceState, api } = useDevice(); const mac = deviceState?.mac ?? ""; - const language = deviceState?.language ?? 2; + const language = resolveDeviceLanguage(deviceState?.language); const deviceName = deviceState?.device ?? "Luxsin X9"; const aiText = useMemo(() => resolveAILocale(language), [language]); diff --git a/client/src/pages/AudioPage.tsx b/client/src/pages/AudioPage.tsx index c64cedd..72be4ab 100644 --- a/client/src/pages/AudioPage.tsx +++ b/client/src/pages/AudioPage.tsx @@ -20,9 +20,9 @@ import { parseFirmwareVersion, readBootSoundFromState, } from "@/lib/luxsinApi"; -import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: { value: number; @@ -128,12 +128,6 @@ type LocaleDac = { home?: { audioSet?: string }; }; -const LOCALE_BY_LANG: Record = { - 0: localeEn as LocaleDac, - 1: localeZhHK as LocaleDac, - 2: localeZh as LocaleDac, -}; - function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) { if (!items || items.length === 0) return fallback; const sorted = [...items].sort((a, b) => a.index - b.index); @@ -187,8 +181,7 @@ export default function AudioPage() { const { deviceState, updateSetting, refresh, isConnected, api } = useDevice(); const ds = deviceState; const vuSensor = (ds as ({ vuSensor?: number } | null))?.vuSensor; - const langIdx = ds?.language ?? 2; - const localeData = LOCALE_BY_LANG[langIdx] ?? (localeZh as LocaleDac); + const localeData = resolveLocalePack(ds?.language) as LocaleDac; const dacText = localeData.dac ?? {}; // balance 从接口获取的是乘以 10 后的值,需要除以 10 显示(例如:-130 → -13.0) @@ -246,6 +239,8 @@ export default function AudioPage() { "-12dB", ]); + const pageTitle = usePageTitleWithConnection(localeData.home?.audioSet ?? "Audio"); + const goSelect = (title: string, options: string[], selected: number, key: string) => { navigateToSelect(title, options, selected, "/audio", key); setLocation("/select"); @@ -258,7 +253,7 @@ export default function AudioPage() {

- {localeData.home?.audioSet ?? "音频设置"} + {pageTitle}

diff --git a/client/src/pages/CommunityPage.tsx b/client/src/pages/CommunityPage.tsx index cf3c874..0719a46 100644 --- a/client/src/pages/CommunityPage.tsx +++ b/client/src/pages/CommunityPage.tsx @@ -2,8 +2,9 @@ import BottomNav from "@/components/BottomNav"; import { useDevice } from "@/contexts/DeviceContext"; import { useIsLgUp } from "@/hooks/useIsLgUp"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; import { ChevronLeft, Copy } from "lucide-react"; import { useMemo, useCallback } from "react"; import { useLocation } from "wouter"; @@ -19,11 +20,12 @@ export default function CommunityPage() { const { deviceState } = useDevice(); const text = useMemo((): CommunityPageLocale => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return (pack.communityPage ?? localeZh.communityPage) as CommunityPageLocale; + const pack = resolveLocalePack(deviceState?.language); + return (pack.communityPage ?? localeEn.communityPage) as CommunityPageLocale; }, [deviceState?.language]); + const pageTitle = usePageTitleWithConnection(text.title ?? "Community"); + const copyForumUrl = useCallback(async () => { try { await navigator.clipboard.writeText(FORUM_URL); @@ -76,7 +78,7 @@ export default function CommunityPage() { > -

{text.title}

+

{pageTitle}

diff --git a/client/src/pages/DisplayPage.tsx b/client/src/pages/DisplayPage.tsx deleted file mode 100644 index dcc132f..0000000 --- a/client/src/pages/DisplayPage.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/* ============================================================ - DISPLAY & LIGHTS PAGE - Design: iOS Settings style, reference luxsin_x8_设置.png - Sections: 显示 / 灯光 / 电源管理 - All selectable options → navigate to /select page - ============================================================ */ -import { useDevice } from "@/contexts/DeviceContext"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import { useLocation } from "wouter"; -import BottomNav from "@/components/BottomNav"; -import { navigateToSelect } from "./SelectPage"; - -function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - -function SectionHeader({ title }: { title: string }) { - return ( -
- {title} -
- ); -} - -function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: { - label: string; description?: string; value?: string; - onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void; -}) { - if (toggle) { - return ( -
-
-
{label}
- {description &&
{description}
} -
- {})} /> -
- ); - } - return ( - - ); -} - -const SCREEN_LIGHT_OPTIONS = ["亮", "中等", "暗"]; -const KNOB_LIGHT_OPTIONS = ["关闭", "亮", "中等", "暗"]; -const SCREEN_OFF_OPTIONS = ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]; -const SLEEP_OPTIONS = ["关闭", "1分钟", "5分钟", "10分钟"]; - -export default function DisplayPage() { - const [, setLocation] = useLocation(); - const { deviceState, updateSetting } = useDevice(); - const ds = deviceState; - - const screenLightIdx = ds?.screenLight ?? 1; - const knobLightIdx = ds?.knob_breathlight ?? 1; - const screenOffIdx = ds?.screenOff ?? 2; - const sleepIdx = ds?.sleep ?? 0; - const buttonLightOn = (ds?.buttonLight ?? 0) === 0; - - const goSelect = (title: string, options: string[], selected: number, key: string) => { - navigateToSelect(title, options, selected, "/display", key); - setLocation("/select"); - }; - - return ( -
-
- -

显示与灯光

-
-
- -
- {/* ── 显示 ── */} - -
- goSelect("屏幕亮度", SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} /> - goSelect("自动锁屏", SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} /> -
- - {/* ── 灯光 ── */} - -
- goSelect("旋钮亮度", KNOB_LIGHT_OPTIONS, knobLightIdx, "knob_breathlight")} /> - 0} - onToggle={(v) => updateSetting({ knob_breathlight: v ? 2 : 0 })} /> - updateSetting({ buttonLight: v ? 0 : 1 })} /> -
- - {/* ── 电源管理 ── */} - -
- goSelect("休眠时间", SLEEP_OPTIONS, sleepIdx, "sleep")} /> -
- - {/* ── 灯光预览 ── */} - -
-
- {/* Knob preview */} -
-
-
0 - ? `0 0 ${knobLightIdx * 10}px rgba(0,255,246,${knobLightIdx * 0.2})` - : "none", - }} /> -
0 ? `inset 0 0 8px rgba(0,255,246,0.15)` : "none", - }} /> -
- 旋钮 -
- {/* Button preview */} -
-
- {[1, 2, 3].map(i => ( -
- ))} -
- 按键 -
-
-
-
- - -
- ); -} diff --git a/client/src/pages/DspPage.tsx b/client/src/pages/DspPage.tsx deleted file mode 100644 index 1b168de..0000000 --- a/client/src/pages/DspPage.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* ============================================================ - DSP & DAC PAGE - Design: iOS Settings style - Sections: DSP引擎 / DAC配置 / 模拟级 / 音频格式 - All selectable options → navigate to /select page - ============================================================ */ -import { useDevice } from "@/contexts/DeviceContext"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import { useLocation } from "wouter"; -import BottomNav from "@/components/BottomNav"; -import { navigateToSelect } from "./SelectPage"; - -function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - -function SectionHeader({ title }: { title: string }) { - return ( -
- {title} -
- ); -} - -function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: { - label: string; description?: string; value?: string; - onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void; -}) { - if (toggle) { - return ( -
-
-
{label}
- {description &&
{description}
} -
- {})} /> -
- ); - } - return ( - - ); -} - -const DAC_ARC_OPTIONS = ["关闭", "模式1", "模式2", "模式3"]; -const DAC_GAIN_OPTIONS = ["0dB", "+3dB", "+6dB", "+9dB"]; -const DAC_IMP_OPTIONS = ["32Ω", "150Ω", "300Ω"]; -const ANALOG_GAIN_OPTIONS = ["低", "中低", "中高", "高"]; -const XLR_OPTIONS = ["正常", "反转"]; - -export default function DspPage() { - const [, setLocation] = useLocation(); - const { deviceState, updateSetting } = useDevice(); - const ds = deviceState; - - const dspOn = (ds?.dsp_enable ?? 0) === 0; - const peqOn = (ds?.peqEnable ?? 0) === 1; - const dacArcIdx = ds?.dacArc ?? 0; - const dacGainIdx = ds?.dacGain ?? 0; - const dacImpIdx = ds?.dacImpedance ?? 0; - const analogGainIdx = ds?.analogGain ?? 0; - const xlrIdx = ds?.xlr ?? 0; - - const goSelect = (title: string, options: string[], selected: number, key: string) => { - navigateToSelect(title, options, selected, "/dsp", key); - setLocation("/select"); - }; - - return ( -
-
- -

DSP & DAC

-
-
- -
- {/* ── DSP 引擎 ── */} - -
- updateSetting({ dsp_enable: v ? 0 : 1 })} /> - updateSetting({ peqEnable: v ? 1 : 0 })} /> -
- - {/* ── DAC 配置 ── */} - -
- goSelect("ARC 模式", DAC_ARC_OPTIONS, dacArcIdx, "dacArc")} /> - goSelect("DAC 增益", DAC_GAIN_OPTIONS, dacGainIdx, "dacGain")} /> - goSelect("DAC 阻抗", DAC_IMP_OPTIONS, dacImpIdx, "dacImpedance")} /> -
- - {/* ── 模拟级 ── */} - -
- goSelect("模拟增益", ANALOG_GAIN_OPTIONS, analogGainIdx, "analogGain")} /> - goSelect("XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} /> -
- - {/* ── 音频格式 ── */} - -
-
-
- - {ds?.audioFormat ?? "PCM 44.1kHz"} - -
-
-
- - -
- ); -} diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 24f86cd..899727f 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -29,8 +29,9 @@ import { } from "@/lib/luxsinApi"; import { getFilterType, getFilterShortName } from "@/lib/peqAudio"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; import { FreqChart } from "./eq/components/FreqChart"; import { CyanSlider, IOSToggle } from "./eq/components/EqPrimitives"; import { BandParamDialog } from "./eq/components/BandParamDialog"; @@ -95,18 +96,18 @@ export default function EQPage() { const eqOn = peqOn && !bypassOn; const eqUi = useMemo((): PeqEqUi => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; + const pack = resolveLocalePack(deviceState?.language); const peq = pack.peq as typeof localeZh.peq; - return (peq.eqUi ?? (localeZh.peq as typeof localeZh.peq).eqUi) as PeqEqUi; + return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi; }, [deviceState?.language]); const peqCardLabels = useMemo(() => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return pack.peq ?? localeZh.peq; + const pack = resolveLocalePack(deviceState?.language); + return pack.peq ?? localeEn.peq; }, [deviceState?.language]); + const eqPageTitle = usePageTitleWithConnection("HP-EQ"); + const [bandsByMode, setBandsByMode] = useState>({ A: cloneBands(DEFAULT_BANDS), B: cloneBands(DEFAULT_BANDS), @@ -1310,7 +1311,7 @@ export default function EQPage() { -

HP-EQ

+

{eqPageTitle}

{ diff --git a/client/src/pages/EffectsPage.tsx b/client/src/pages/EffectsPage.tsx index 594bb12..7a36e45 100644 --- a/client/src/pages/EffectsPage.tsx +++ b/client/src/pages/EffectsPage.tsx @@ -15,8 +15,9 @@ import { FeatureGate } from "@/components/FeatureGate"; import { navigateToSelect } from "./SelectPage"; import { cn } from "@/lib/utils"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; type EffectLocale = NonNullable<(typeof localeZh)["effect"]>; @@ -96,11 +97,12 @@ export default function EffectsPage() { const { deviceState, updateSetting } = useDevice(); const effectText = useMemo((): EffectLocale => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return (pack.effect ?? localeZh.effect) as EffectLocale; + const pack = resolveLocalePack(deviceState?.language); + return (pack.effect ?? localeEn.effect) as EffectLocale; }, [deviceState?.language]); + const pageTitle = usePageTitleWithConnection(effectText.pageTitle ?? "Effects"); + const sceneLabels = useMemo(() => { const opts = effectText.style?.options ?? []; return [...opts].sort((a, b) => a.index - b.index).map((o) => o.label); @@ -180,7 +182,7 @@ export default function EffectsPage() { -

{effectText.pageTitle}

+

{pageTitle}

{ diff --git a/client/src/pages/IOPage.tsx b/client/src/pages/IOPage.tsx index 0f87638..de7afeb 100644 --- a/client/src/pages/IOPage.tsx +++ b/client/src/pages/IOPage.tsx @@ -12,8 +12,9 @@ import { ChevronLeft } from "lucide-react"; import { useLocation } from "wouter"; import { useMemo } from "react"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; type SourceLocale = NonNullable<(typeof localeZh)["source"]>; @@ -180,8 +181,7 @@ export default function IOPage() { const { deviceState, setInput, setOutput } = useDevice(); const sourceText = useMemo((): SourceLocale => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; + const pack = resolveLocalePack(deviceState?.language); return (pack.source ?? {}) as SourceLocale; }, [deviceState?.language]); @@ -195,7 +195,9 @@ export default function IOPage() { [sourceText.outputOptions], ); - const ioPageTitle = sourceText.pageTitle ?? sourceText.label ?? "输入输出"; + const ioPageTitle = usePageTitleWithConnection( + sourceText.pageTitle ?? sourceText.label ?? "I/O", + ); const inputSectionTitle = sourceText.inputSection ?? "输入"; const outputSectionTitle = sourceText.outputSection ?? "输出"; diff --git a/client/src/pages/SelectPage.tsx b/client/src/pages/SelectPage.tsx index 7a904da..03410dc 100644 --- a/client/src/pages/SelectPage.tsx +++ b/client/src/pages/SelectPage.tsx @@ -26,8 +26,9 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; type DacVolumeDirectConfirmLocale = NonNullable< NonNullable<(typeof localeZh)["dac"]>["dacVolumeDirectConfirm"] @@ -98,10 +99,11 @@ export default function SelectPage() { const [saving, setSaving] = useState(false); const [volumeDirectConfirmIdx, setVolumeDirectConfirmIdx] = useState<1 | 2 | null>(null); + const pageTitle = usePageTitleWithConnection(state?.title ?? ""); + const volumeDirectConfirmText = useMemo((): DacVolumeDirectConfirmLocale => { - const lang = deviceState?.language; - const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return (pack.dac?.dacVolumeDirectConfirm ?? localeZh.dac!.dacVolumeDirectConfirm) as DacVolumeDirectConfirmLocale; + const pack = resolveLocalePack(deviceState?.language); + return (pack.dac?.dacVolumeDirectConfirm ?? localeEn.dac!.dacVolumeDirectConfirm) as DacVolumeDirectConfirmLocale; }, [deviceState?.language]); // Re-read global state whenever we enter /select (avoids stale key/options after remount) @@ -172,7 +174,7 @@ export default function SelectPage() { className="mr-4 text-white/60 active:text-white transition-colors"> -

{title}

+

{pageTitle}

diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx index 0170243..b2e7c92 100644 --- a/client/src/pages/SystemPage.tsx +++ b/client/src/pages/SystemPage.tsx @@ -20,9 +20,9 @@ import { parseFirmwareVersion, supportsAmbientLed, } from "@/lib/luxsinApi"; -import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { return ( @@ -160,12 +160,6 @@ type LocaleSettings = { }; }; -const LOCALE_BY_LANG: Record = { - 0: localeEn as LocaleSettings, - 1: localeZhHK as LocaleSettings, - 2: localeZh as LocaleSettings, -}; - function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) { if (!items || items.length === 0) return fallback; const sorted = [...items].sort((a, b) => a.index - b.index); @@ -201,8 +195,8 @@ export default function SystemPage() { const screenOffIdx = ds?.screenOff ?? 2; const sleepIdx = ds?.sleep ?? 0; const autoHomeIdx = ds?.autoHome ?? 1; - const rawLangIdx = ds?.language ?? 2; - const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : 2; + const rawLangIdx = resolveDeviceLanguage(ds?.language); + const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : DEFAULT_DEVICE_LANGUAGE; const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0; const firmwareVersion = parseFirmwareVersion(ds?.version); const showAmbientLed = supportsAmbientLed(firmwareVersion); @@ -227,7 +221,7 @@ export default function SystemPage() { if (!draggingLedBlue.current) setLocalLedBlue(clampLedChannel(ds?.led_blue)); }, [ds?.led_blue]); - const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh; + const localeData = resolveLocalePack(ds?.language) as LocaleSettings; const settingsText = localeData.settings; const ui = settingsText.systemPage; const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]); @@ -247,6 +241,8 @@ export default function SystemPage() { (localeEn as LocaleSettings).settings.sleep?.label ?? "Sleep"; + const pageTitle = usePageTitleWithConnection(settingsText.vu); + const goSelect = (title: string, options: string[], selected: number, key: string) => { navigateToSelect(title, options, selected, "/system", key); setLocation("/select"); @@ -258,7 +254,7 @@ export default function SystemPage() { -

{settingsText.vu}

+

{pageTitle}

diff --git a/client/src/pages/VUPage.tsx b/client/src/pages/VUPage.tsx index 772ca74..bbc7933 100644 --- a/client/src/pages/VUPage.tsx +++ b/client/src/pages/VUPage.tsx @@ -12,8 +12,9 @@ import { Check, ChevronLeft } from "lucide-react"; import { useLocation } from "wouter"; import { useMemo } from "react"; import localeZh from "@/locales/data-zh.json"; -import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; +import { resolveLocalePack } from "@/locales/resolveLocale"; +import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection"; type VUPageLocale = NonNullable<(typeof localeZh)["vuPage"]>; @@ -35,12 +36,12 @@ export default function VUPage() { const currentVU = deviceState?.vu ?? 0; const vuPageText = useMemo((): VUPageLocale => { - const lang = deviceState?.language; - const loc = - lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh; - return (loc.vuPage ?? {}) as VUPageLocale; + const loc = resolveLocalePack(deviceState?.language); + return (loc.vuPage ?? localeEn.vuPage ?? {}) as VUPageLocale; }, [deviceState?.language]); + const pageTitle = usePageTitleWithConnection(vuPageText.title ?? "VU"); + return (
@@ -48,7 +49,7 @@ export default function VUPage() {

- {vuPageText.title ?? "VU 表库"} + {pageTitle}

diff --git a/package.json b/package.json index d009a2e..02ba7e0 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "add": "^2.0.6", "autoprefixer": "^10.4.20", "esbuild": "^0.25.0", + "jiti": "^2.7.0", "pnpm": "^10.15.1", "postcss": "^8.4.47", "prettier": "^3.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90e8f0c..9d37d17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,13 +167,13 @@ importers: devDependencies: '@builder.io/vite-plugin-jsx-loc': specifier: ^0.1.1 - version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) + version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.20(tailwindcss@4.3.0) '@tailwindcss/vite': specifier: ^4.1.3 - version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) + version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) '@types/google.maps': specifier: ^3.58.1 version: 3.65.1 @@ -188,7 +188,7 @@ importers: version: 19.2.1(@types/react@19.2.17) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) + version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)) add: specifier: ^2.0.6 version: 2.0.6 @@ -198,6 +198,9 @@ importers: esbuild: specifier: ^0.25.0 version: 0.25.10 + jiti: + specifier: ^2.7.0 + version: 2.7.0 pnpm: specifier: ^10.15.1 version: 10.34.1 @@ -224,7 +227,7 @@ importers: version: 5.6.3 vite: specifier: ^7.1.7 - version: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) + version: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) vite-plugin-manus-runtime: specifier: ^0.0.57 version: 0.0.57 @@ -4164,10 +4167,10 @@ snapshots: estree-walker: 2.0.2 magic-string: 0.30.19 - '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': + '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': dependencies: '@builder.io/jsx-loc-internals': 0.0.1 - vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) '@chevrotain/cst-dts-gen@11.0.3': dependencies: @@ -5427,12 +5430,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.0 - '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': + '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) '@types/babel__core@7.20.5': dependencies: @@ -5623,7 +5626,7 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': + '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7) @@ -5631,7 +5634,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) + vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4) transitivePeerDependencies: - supports-color @@ -7903,7 +7906,7 @@ snapshots: lightningcss: 1.32.0 terser: 5.46.1 - vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4): + vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.3) @@ -7914,7 +7917,7 @@ snapshots: optionalDependencies: '@types/node': 24.13.1 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.46.1 tsx: 4.22.4