当设备未连接时,各页面默认显示英文语言的文本,并在标题追加 (Disconnect)
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ?? "输出";
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user