当设备未连接时,各页面默认显示英文语言的文本,并在标题追加 (Disconnect)

This commit is contained in:
eafonyang
2026-07-01 11:23:12 +08:00
parent 1e5e6f5548
commit ac9bac4d18
21 changed files with 113 additions and 460 deletions
-4
View File
@@ -12,8 +12,6 @@ import NotFound from "./pages/NotFound";
import Dashboard from "./pages/Dashboard";
import AudioPage from "./pages/AudioPage";
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";
@@ -64,8 +62,6 @@ function AppRouter() {
<Route path="/eq" component={EQPageRoute} />
<Route path="/effects" component={EffectsPageRoute} />
<Route path="/bluetooth" component={BluetoothPage} />
<Route path="/dsp" component={DspPage} />
<Route path="/display" component={DisplayPage} />
<Route path="/system" component={SystemPage} />
<Route path="/io" component={IOPage} />
<Route path="/vu" component={VUPage} />
+3 -5
View File
@@ -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 (
@@ -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;
}
-4
View File
@@ -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" },
];
@@ -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]);
}
+19
View File
@@ -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;
}
+3 -5
View File
@@ -59,15 +59,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, string | number>): string {
@@ -210,7 +208,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]);
+6 -11
View File
@@ -19,9 +19,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;
@@ -123,12 +123,6 @@ type LocaleDac = {
home?: { audioSet?: string };
};
const LOCALE_BY_LANG: Record<number, LocaleDac> = {
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);
@@ -182,8 +176,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
@@ -241,6 +234,8 @@ export default function AudioPage() {
const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]);
const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]);
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");
@@ -253,7 +248,7 @@ export default function AudioPage() {
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
{localeData.home?.audioSet ?? "音频设置"}
{pageTitle}
</h1>
<div className="w-8" />
</div>
+7 -5
View File
@@ -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() {
>
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{text.title}</h1>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
<div className="w-8" />
</div>
-168
View File
@@ -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 (
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
</label>
);
}
function SectionHeader({ title }: { title: string }) {
return (
<div className="px-1 pt-5 pb-2">
<span className="text-[14px] font-semibold" style={{ color: "#00FFF6" }}>{title}</span>
</div>
);
}
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 (
<div className="ios-list-row">
<div className="flex-1 min-w-0">
<div className="text-[16px] text-white">{label}</div>
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
</div>
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
</div>
);
}
return (
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
<div className="flex-1 min-w-0">
<div className="text-[16px] text-white">{label}</div>
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
</div>
<div className="flex items-center gap-1">
{value && <span className="ios-row-value">{value}</span>}
<ChevronRight size={16} className="ios-chevron ml-1" />
</div>
</button>
);
}
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 (
<div className="min-h-screen bg-black">
<div className="page-header">
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white"></h1>
<div className="w-8" />
</div>
<div className="px-4 pb-32">
{/* ── 显示 ── */}
<SectionHeader title="显示" />
<div className="ios-list-group">
<ListRow label="屏幕亮度" description="调节屏幕的整体亮度"
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
onClick={() => goSelect("屏幕亮度", SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
<ListRow label="自动锁屏" description="设备无操作后自动关闭屏幕"
value={SCREEN_OFF_OPTIONS[screenOffIdx]}
onClick={() => goSelect("自动锁屏", SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />
</div>
{/* ── 灯光 ── */}
<SectionHeader title="灯光" />
<div className="ios-list-group">
<ListRow label="旋钮亮度" description="调节旋钮指示灯的亮度"
value={KNOB_LIGHT_OPTIONS[knobLightIdx]}
onClick={() => goSelect("旋钮亮度", KNOB_LIGHT_OPTIONS, knobLightIdx, "knob_breathlight")} />
<ListRow label="旋钮熄屏呼吸灯" description="锁屏后旋钮是否显示呼吸灯效果"
toggle checked={knobLightIdx > 0}
onToggle={(v) => updateSetting({ knob_breathlight: v ? 2 : 0 })} />
<ListRow label="按键灯" description="前面板按键背光"
toggle checked={buttonLightOn}
onToggle={(v) => updateSetting({ buttonLight: v ? 0 : 1 })} />
</div>
{/* ── 电源管理 ── */}
<SectionHeader title="电源管理" />
<div className="ios-list-group">
<ListRow label="休眠" description="设备进入低功耗模式的时间"
value={SLEEP_OPTIONS[sleepIdx]}
onClick={() => goSelect("休眠时间", SLEEP_OPTIONS, sleepIdx, "sleep")} />
</div>
{/* ── 灯光预览 ── */}
<SectionHeader title="灯光预览" />
<div className="ios-list-group p-5">
<div className="flex items-center justify-center gap-10 py-2">
{/* Knob preview */}
<div className="flex flex-col items-center gap-3">
<div className="relative w-16 h-16">
<div className="w-16 h-16 rounded-full"
style={{
background: "radial-gradient(circle, #1a2a2a 40%, #0d1117 100%)",
border: "2px solid rgba(255,255,255,0.1)",
boxShadow: knobLightIdx > 0
? `0 0 ${knobLightIdx * 10}px rgba(0,255,246,${knobLightIdx * 0.2})`
: "none",
}} />
<div className="absolute inset-1 rounded-full"
style={{
border: `2px solid rgba(0,255,246,${knobLightIdx === 0 ? 0 : knobLightIdx === 1 ? 0.9 : knobLightIdx === 2 ? 0.55 : 0.3})`,
boxShadow: knobLightIdx > 0 ? `inset 0 0 8px rgba(0,255,246,0.15)` : "none",
}} />
</div>
<span className="text-[12px] text-white/40"></span>
</div>
{/* Button preview */}
<div className="flex flex-col items-center gap-3">
<div className="flex gap-2">
{[1, 2, 3].map(i => (
<div key={i} className="w-7 h-7 rounded-[8px]"
style={{
background: buttonLightOn ? "rgba(0,255,246,0.25)" : "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.1)",
boxShadow: buttonLightOn ? "0 0 8px rgba(0,255,246,0.35)" : "none",
}} />
))}
</div>
<span className="text-[12px] text-white/40"></span>
</div>
</div>
</div>
</div>
<BottomNav />
</div>
);
}
-149
View File
@@ -1,149 +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 (
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
</label>
);
}
function SectionHeader({ title }: { title: string }) {
return (
<div className="px-1 pt-5 pb-2">
<span className="text-[14px] font-semibold" style={{ color: "#00FFF6" }}>{title}</span>
</div>
);
}
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 (
<div className="ios-list-row">
<div className="flex-1 min-w-0">
<div className="text-[16px] text-white">{label}</div>
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
</div>
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
</div>
);
}
return (
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
<div className="flex-1 min-w-0">
<div className="text-[16px] text-white">{label}</div>
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
</div>
<div className="flex items-center gap-1">
{value && <span className="ios-row-value">{value}</span>}
<ChevronRight size={16} className="ios-chevron ml-1" />
</div>
</button>
);
}
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 dacVolDirect = (ds?.dacVolumeDirect ?? 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 (
<div className="min-h-screen bg-black">
<div className="page-header">
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">DSP & DAC</h1>
<div className="w-8" />
</div>
<div className="px-4 pb-32">
{/* ── DSP 引擎 ── */}
<SectionHeader title="DSP 引擎" />
<div className="ios-list-group">
<ListRow label="DSP 处理" description="主数字信号处理器"
toggle checked={dspOn}
onToggle={(v) => updateSetting({ dsp_enable: v ? 0 : 1 })} />
<ListRow label="参数均衡器" description="5 段参数均衡器"
toggle checked={peqOn}
onToggle={(v) => updateSetting({ peqEnable: v ? 1 : 0 })} />
</div>
{/* ── DAC 配置 ── */}
<SectionHeader title="DAC 配置" />
<div className="ios-list-group">
<ListRow label="ARC 模式" description="音频回传通道模式"
value={DAC_ARC_OPTIONS[dacArcIdx]}
onClick={() => goSelect("ARC 模式", DAC_ARC_OPTIONS, dacArcIdx, "dacArc")} />
<ListRow label="DAC 增益" description="数模转换器增益"
value={DAC_GAIN_OPTIONS[dacGainIdx]}
onClick={() => goSelect("DAC 增益", DAC_GAIN_OPTIONS, dacGainIdx, "dacGain")} />
<ListRow label="DAC 阻抗" description="输出阻抗设置"
value={DAC_IMP_OPTIONS[dacImpIdx]}
onClick={() => goSelect("DAC 阻抗", DAC_IMP_OPTIONS, dacImpIdx, "dacImpedance")} />
<ListRow label="直通音量控制" description="DAC 直通音量"
toggle checked={dacVolDirect}
onToggle={(v) => updateSetting({ dacVolumeDirect: v ? 1 : 0 })} />
</div>
{/* ── 模拟级 ── */}
<SectionHeader title="模拟级" />
<div className="ios-list-group">
<ListRow label="模拟增益" description="模拟放大器增益级"
value={ANALOG_GAIN_OPTIONS[analogGainIdx]}
onClick={() => goSelect("模拟增益", ANALOG_GAIN_OPTIONS, analogGainIdx, "analogGain")} />
<ListRow label="XLR 极性" description="XLR 输出相位极性"
value={XLR_OPTIONS[xlrIdx]}
onClick={() => goSelect("XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} />
</div>
{/* ── 音频格式 ── */}
<SectionHeader title="当前音频格式" />
<div className="ios-list-group p-4">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full" style={{ background: "#00FFF6", boxShadow: "0 0 6px rgba(0,255,246,0.8)" }} />
<span className="text-[15px] font-semibold" style={{ color: "#00FFF6", fontFamily: "'JetBrains Mono', monospace" }}>
{ds?.audioFormat ?? "PCM 44.1kHz"}
</span>
</div>
</div>
</div>
<BottomNav />
</div>
);
}
+9 -8
View File
@@ -37,8 +37,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 {
FILTER_TYPES,
BAND_FREQ_MIN,
@@ -106,18 +107,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<
Record<"A" | "B", typeof DEFAULT_BANDS>
>({
@@ -1481,7 +1482,7 @@ export default function EQPage() {
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
HP-EQ
{eqPageTitle}
</h1>
<IOSToggle
checked={eqOn}
+8 -6
View File
@@ -17,8 +17,9 @@ import { navigateToSelect } from "./SelectPage";
import { parseFirmwareVersion, parseHearingData } from "@/lib/luxsinApi";
import { isFirmwareFeatureAvailable } from "@/config/firmwareFeatures";
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";
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
import { toast } from "sonner";
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
@@ -225,11 +226,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);
@@ -346,7 +348,7 @@ export default function EffectsPage() {
const [delayInputTarget, setDelayInputTarget] = useState<DelayInputTarget | null>(null);
const [delayInputValue, setDelayInputValue] = useState("");
const delayInputEnglish = deviceState?.language === 0;
const delayInputEnglish = resolveDeviceLanguage(deviceState?.language) === 0;
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
const openDelayInput = (target: DelayInputTarget) => {
@@ -423,7 +425,7 @@ export default function EffectsPage() {
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{effectText.pageTitle}</h1>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
<IOSToggle
checked={effectOn}
onChange={(v) => {
+6 -4
View File
@@ -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"]>;
@@ -212,8 +213,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]);
@@ -227,7 +227,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 ?? "输出";
+5 -20
View File
@@ -53,10 +53,9 @@ import {
} 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";
@@ -92,18 +91,6 @@ type LocaleRoot = {
const LOCALE_BY_LANG: Record<number, LocaleRoot> = {
0: localeEn as LocaleRoot,
1: localeZhHK as LocaleRoot,
2: localeZh as LocaleRoot,
};
/* ── Key → API field mapping ── */
const KEY_TO_SETTING: Record<string, string> = {
@@ -232,11 +219,9 @@ export default function SelectPage() {
const [passthroughConfirmIdx, setPassthroughConfirmIdx] = useState<number | null>(null);
const pageTitle = usePageTitleWithConnection(state?.title ?? "");
const langIdx = deviceState?.language ?? 2;
const dacVolumeDirectText = (LOCALE_BY_LANG[langIdx] ?? localeZh as LocaleRoot).dac?.dacVolumeDirect;
const dacVolumeDirectText = resolveLocalePack(deviceState?.language).dac?.dacVolumeDirect;
@@ -390,7 +375,7 @@ export default function SelectPage() {
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{title}</h1>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
<div className="w-8" />
+8 -12
View File
@@ -15,9 +15,9 @@ import { useEffect } from "react";
import BottomNav from "@/components/BottomNav";
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
import { navigateToSelect } from "./SelectPage";
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 (
@@ -96,12 +96,6 @@ type LocaleSettings = {
};
};
const LOCALE_BY_LANG: Record<number, LocaleSettings> = {
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);
@@ -137,10 +131,10 @@ 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 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, ["亮", "中等", "暗"]);
@@ -160,6 +154,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");
@@ -171,7 +167,7 @@ export default function SystemPage() {
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{settingsText.vu}</h1>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
<div className="w-8" />
</div>
+7 -6
View File
@@ -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 (
<div className="min-h-screen bg-black">
<div className="page-header">
@@ -48,7 +49,7 @@ export default function VUPage() {
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
{vuPageText.title ?? "VU 表库"}
{pageTitle}
</h1>
<div className="w-8" />
</div>