Refactor BottomNav and CommunityPage to enhance community forum integration; update navigation logic for mobile and desktop views. Add new localized strings for community page and system settings, including device information and user prompts.

This commit is contained in:
yangy
2026-05-13 17:42:26 +08:00
parent 5285b886ea
commit b3d3f0edcb
9 changed files with 372 additions and 92 deletions
+16 -15
View File
@@ -2,7 +2,7 @@
BOTTOM NAV — Luxsin X9 Controller (Mobile) BOTTOM NAV — Luxsin X9 Controller (Mobile)
Design: iOS/Android App-style bottom tab bar Design: iOS/Android App-style bottom tab bar
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页) 5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
- 社区:仅桌面宽屏(lg ≥1024px)显示,手机等小屏隐藏 - 社区:移动端进入 /community 引导复制链接;桌面(lg+)点击仍 window.open 论坛
- Active tab: cyan icon + top indicator bar - Active tab: cyan icon + top indicator bar
- Inactive: muted icon + label - Inactive: muted icon + label
============================================================ */ ============================================================ */
@@ -54,12 +54,6 @@ export default function BottomNav() {
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale; return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
}, [deviceState?.language]); }, [deviceState?.language]);
/** 社区入口仅在桌面级视口显示(与 useIsLgUp / Tailwind lg 一致) */
const visibleNavItems = useMemo(
() => NAV_ITEMS.filter((item) => item.path !== "/community" || isLgUp),
[isLgUp],
);
return ( return (
<nav <nav
className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2" className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2"
@@ -73,7 +67,7 @@ export default function BottomNav() {
}} }}
> >
<div className="flex items-stretch justify-around"> <div className="flex items-stretch justify-around">
{visibleNavItems.map((item) => { {NAV_ITEMS.map((item) => {
const isActive = const isActive =
item.path === "/ai" item.path === "/ai"
? location === "/ai" || (isLgUp && aiDrawerOpen) ? location === "/ai" || (isLgUp && aiDrawerOpen)
@@ -132,15 +126,22 @@ export default function BottomNav() {
); );
if (item.path === "/community") { if (item.path === "/community") {
if (isLgUp) {
return (
<button
key={item.path}
type="button"
className="cursor-pointer border-0 bg-transparent p-0 text-inherit outline-none focus-visible:ring-2 focus-visible:ring-[#00FFF6]/40 focus-visible:ring-offset-2 focus-visible:ring-offset-[rgba(10,10,12,0.96)]"
onClick={() => window.open(FORUM_URL, "_blank", "noopener,noreferrer")}
>
{inner}
</button>
);
}
return ( return (
<button <Link key={item.path} href={item.path}>
key={item.path}
type="button"
className="cursor-pointer border-0 bg-transparent p-0 text-inherit outline-none focus-visible:ring-2 focus-visible:ring-[#00FFF6]/40 focus-visible:ring-offset-2 focus-visible:ring-offset-[rgba(10,10,12,0.96)]"
onClick={() => window.open(FORUM_URL, "_blank", "noopener,noreferrer")}
>
{inner} {inner}
</button> </Link>
); );
} }
+33 -1
View File
@@ -11,7 +11,8 @@
"powerOffConfirmOk": "Turn off", "powerOffConfirmOk": "Turn off",
"powerOffConfirmCancel": "Cancel", "powerOffConfirmCancel": "Cancel",
"powerOffToastDemo": "Demo mode: disconnected from the device.", "powerOffToastDemo": "Demo mode: disconnected from the device.",
"powerOffToastSent": "Shutdown command sent." "powerOffToastSent": "Shutdown command sent.",
"doubleClickHint": "Double-click to enter"
}, },
"vuPage": { "vuPage": {
"title": "VU meter gallery" "title": "VU meter gallery"
@@ -23,6 +24,13 @@
"effects": "Effects", "effects": "Effects",
"myAi": "My AI" "myAi": "My AI"
}, },
"communityPage": {
"title": "Community",
"intro": "The forum opens best in your system browser. Copy the full link below, paste it into your mobile browsers address bar, and open it.",
"copyLink": "Copy link",
"toastCopied": "Link copied",
"toastCopyFailed": "Could not copy. Long-press the link to copy it manually."
},
"source": { "source": {
"label": "Input/Output", "label": "Input/Output",
"input": { "input": {
@@ -226,6 +234,11 @@
"eqUi": { "eqUi": {
"batchEditTitle": "Batch edit", "batchEditTitle": "Batch edit",
"batchEditHint": "11 lines total: line 1 is Preamp, lines 211 are Filter 110. Saving overwrites the current headphone preset.", "batchEditHint": "11 lines total: line 1 is Preamp, lines 211 are Filter 110. Saving overwrites the current headphone preset.",
"addPresetTitle": "New preset",
"addPresetNameEmpty": "Name cannot be empty",
"addPresetNameExists": "That name already exists. Choose another.",
"toastAddPresetOk": "Preset added",
"toastAddPresetFail": "Failed to add preset",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"tabBrands": "Brands", "tabBrands": "Brands",
@@ -275,6 +288,9 @@
} }
}, },
"dac": { "dac": {
"sectionBalanceSensitivity": "Balance & sensitivity",
"sectionOutput": "Output characteristics",
"sectionSystemInterface": "System & interface",
"balance": "Left-right balance", "balance": "Left-right balance",
"sensitivity": "VU Meter Sensitivity", "sensitivity": "VU Meter Sensitivity",
"filters": { "filters": {
@@ -528,6 +544,22 @@
"label": "60 seconds of inactivity" "label": "60 seconds of inactivity"
} }
] ]
},
"systemPage": {
"sectionDisplay": "Display",
"sectionGeneral": "General",
"sectionAbout": "About",
"screenBrightnessDesc": "Adjust overall screen brightness",
"turnOffScreenDesc": "Time before the screen turns off after no activity",
"knobBrightnessDesc": "Brightness of the knob indicator LEDs",
"buttonLightDesc": "Breathing light effect on the knob when the screen is locked",
"languageDesc": "System display language",
"automaticDesc": "Return to the home screen automatically after idle time",
"sleepDesc": "When the device enters sleep or low-power mode",
"deviceName": "Device name",
"firmwareVersion": "Firmware version",
"macAddress": "MAC address",
"disconnect": "Disconnect"
} }
}, },
"prompt": { "prompt": {
+33 -1
View File
@@ -11,7 +11,8 @@
"powerOffConfirmOk": "關閉電源", "powerOffConfirmOk": "關閉電源",
"powerOffConfirmCancel": "取消", "powerOffConfirmCancel": "取消",
"powerOffToastDemo": "演示模式:已中斷與裝置的連線", "powerOffToastDemo": "演示模式:已中斷與裝置的連線",
"powerOffToastSent": "已傳送關機指令" "powerOffToastSent": "已傳送關機指令",
"doubleClickHint": "按兩下進入"
}, },
"vuPage": { "vuPage": {
"title": "VU 表庫" "title": "VU 表庫"
@@ -23,6 +24,13 @@
"effects": "Effect", "effects": "Effect",
"myAi": "我的AI" "myAi": "我的AI"
}, },
"communityPage": {
"title": "社區",
"intro": "論壇需在系統瀏覽器中開啟。請複製下方完整連結,在手機瀏覽器網址列貼上並前往。",
"copyLink": "複製連結",
"toastCopied": "連結已複製",
"toastCopyFailed": "複製失敗,請長按連結手動複製"
},
"source": { "source": {
"label": "輸入/輸出", "label": "輸入/輸出",
"input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" }, "input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" },
@@ -162,6 +170,11 @@
"eqUi": { "eqUi": {
"batchEditTitle": "批次編輯", "batchEditTitle": "批次編輯",
"batchEditHint": "共 11 行:第 1 行為 Preamp,第 211 行為 Filter 1–10。儲存後將覆寫目前耳機的參數。", "batchEditHint": "共 11 行:第 1 行為 Preamp,第 211 行為 Filter 1–10。儲存後將覆寫目前耳機的參數。",
"addPresetTitle": "新增預設",
"addPresetNameEmpty": "名稱不能為空",
"addPresetNameExists": "名稱已存在,請修改後再儲存",
"toastAddPresetOk": "已新增預設",
"toastAddPresetFail": "新增預設失敗",
"cancel": "取消", "cancel": "取消",
"save": "儲存", "save": "儲存",
"tabBrands": "品牌", "tabBrands": "品牌",
@@ -211,6 +224,9 @@
} }
}, },
"dac": { "dac": {
"sectionBalanceSensitivity": "平衡與靈敏度",
"sectionOutput": "輸出特性",
"sectionSystemInterface": "系統與介面",
"balance": "左右平衡", "balance": "左右平衡",
"sensitivity": "VU表靈敏度", "sensitivity": "VU表靈敏度",
"filters": { "filters": {
@@ -344,6 +360,22 @@
{ "index": 2, "label": "無操作40秒" }, { "index": 2, "label": "無操作40秒" },
{ "index": 3, "label": "無操作60秒" } { "index": 3, "label": "無操作60秒" }
] ]
},
"systemPage": {
"sectionDisplay": "顯示",
"sectionGeneral": "一般",
"sectionAbout": "關於",
"screenBrightnessDesc": "調節螢幕的整體亮度",
"turnOffScreenDesc": "裝置無操作後自動關閉螢幕的時間",
"knobBrightnessDesc": "調節旋鈕指示燈的亮度",
"buttonLightDesc": "鎖屏後旋鈕是否顯示呼吸燈效果",
"languageDesc": "設定系統顯示語言",
"automaticDesc": "閒置一段時間後自動回到主畫面",
"sleepDesc": "設定裝置進入低功耗模式的時間",
"deviceName": "裝置名稱",
"firmwareVersion": "韌體版本",
"macAddress": "MAC 位址",
"disconnect": "中斷連線"
} }
}, },
"prompt": { "prompt": {
+33 -1
View File
@@ -11,7 +11,8 @@
"powerOffConfirmOk": "关闭电源", "powerOffConfirmOk": "关闭电源",
"powerOffConfirmCancel": "取消", "powerOffConfirmCancel": "取消",
"powerOffToastDemo": "演示模式:已断开与设备的连接", "powerOffToastDemo": "演示模式:已断开与设备的连接",
"powerOffToastSent": "已发送关机指令" "powerOffToastSent": "已发送关机指令",
"doubleClickHint": "双击进入"
}, },
"vuPage": { "vuPage": {
"title": "VU 表库" "title": "VU 表库"
@@ -23,6 +24,13 @@
"effects": "Effect", "effects": "Effect",
"myAi": "我的AI" "myAi": "我的AI"
}, },
"communityPage": {
"title": "社区",
"intro": "论坛需在系统浏览器中打开。请复制下方完整链接,在手机浏览器地址栏粘贴并访问。",
"copyLink": "复制链接",
"toastCopied": "链接已复制",
"toastCopyFailed": "复制失败,请长按链接手动复制"
},
"source": { "source": {
"label": "输入输出", "label": "输入输出",
"input": { "coaxial": "同轴", "optical": "光钎", "bluetooth": "蓝牙", "rca": "模拟RCA" }, "input": { "coaxial": "同轴", "optical": "光钎", "bluetooth": "蓝牙", "rca": "模拟RCA" },
@@ -162,6 +170,11 @@
"eqUi": { "eqUi": {
"batchEditTitle": "批量编辑", "batchEditTitle": "批量编辑",
"batchEditHint": "共 11 行:第 1 行为 Preamp,第 211 行为 Filter 1–10。保存后将覆盖当前耳机的参数。", "batchEditHint": "共 11 行:第 1 行为 Preamp,第 211 行为 Filter 1–10。保存后将覆盖当前耳机的参数。",
"addPresetTitle": "新增预设",
"addPresetNameEmpty": "名称不能为空",
"addPresetNameExists": "名称已存在,请修改后再保存",
"toastAddPresetOk": "已新增预设",
"toastAddPresetFail": "新增预设失败",
"cancel": "取消", "cancel": "取消",
"save": "保存", "save": "保存",
"tabBrands": "品牌", "tabBrands": "品牌",
@@ -211,6 +224,9 @@
} }
}, },
"dac": { "dac": {
"sectionBalanceSensitivity": "平衡与灵敏度",
"sectionOutput": "输出特性",
"sectionSystemInterface": "系统与接口",
"balance": "左右平衡", "balance": "左右平衡",
"sensitivity": "vu表灵敏度", "sensitivity": "vu表灵敏度",
"filters": { "filters": {
@@ -345,6 +361,22 @@
{ "index": 2, "label": "无操作40秒" }, { "index": 2, "label": "无操作40秒" },
{ "index": 3, "label": "无操作60秒" } { "index": 3, "label": "无操作60秒" }
] ]
},
"systemPage": {
"sectionDisplay": "显示",
"sectionGeneral": "通用",
"sectionAbout": "关于",
"screenBrightnessDesc": "调节屏幕的整体亮度",
"turnOffScreenDesc": "设备无操作后自动关闭屏幕的时间",
"knobBrightnessDesc": "调节旋钮指示灯的亮度",
"buttonLightDesc": "锁屏后旋钮是否显示呼吸灯效果",
"languageDesc": "设置系统显示语言",
"automaticDesc": "闲置一段时间后自动回到主界面",
"sleepDesc": "设置设备进入低功耗模式的时间",
"deviceName": "设备名称",
"firmwareVersion": "固件版本",
"macAddress": "MAC 地址",
"disconnect": "断开连接"
} }
}, },
"prompt": { "prompt": {
+55 -15
View File
@@ -17,17 +17,40 @@ import localeZh from "@/locales/data-zh.json";
import localeZhHK from "@/locales/data-zh-HK.json"; import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json"; import localeEn from "@/locales/data-en.json";
function CyanSlider({ value, min, max, step = 1, onChange }: { function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: {
value: number; min: number; max: number; step?: number; onChange: (v: number) => void; value: number;
min: number;
max: number;
step?: number;
onChange: (v: number) => void;
/** Commit to device once when the user releases the thumb (pointer / keyboard). */
onRelease?: (v: number) => void;
}) { }) {
const fillPct = ((value - min) / (max - min)) * 100; const fillPct = ((value - min) / (max - min)) * 100;
const emitRelease = (el: EventTarget | null) => {
if (!(el instanceof HTMLInputElement)) return;
onRelease?.(Number(el.value));
};
return ( return (
<div className="relative flex items-center w-full mt-2"> <div className="relative flex items-center w-full mt-2">
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none" <div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
style={{ width: `${fillPct}%`, background: "#00FFF6", boxShadow: "0 0 6px rgba(0,255,246,0.5)" }} /> style={{ width: `${fillPct}%`, background: "#00FFF6", boxShadow: "0 0 6px rgba(0,255,246,0.5)" }} />
<input type="range" min={min} max={max} step={step} value={value} <input
type="range"
min={min}
max={max}
step={step}
value={value}
className="cyan-slider relative z-10" className="cyan-slider relative z-10"
onChange={(e) => onChange(Number(e.target.value))} /> onChange={(e) => onChange(Number(e.target.value))}
onPointerUp={(e) => emitRelease(e.currentTarget)}
onPointerCancel={(e) => emitRelease(e.currentTarget)}
onKeyUp={(e) => {
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(e.key)) {
emitRelease(e.currentTarget);
}
}}
/>
</div> </div>
); );
} }
@@ -52,6 +75,9 @@ function ListRow({ label, value, onClick }: { label: string; value: string; onCl
type LocaleDac = { type LocaleDac = {
dac?: { dac?: {
sectionBalanceSensitivity?: string;
sectionOutput?: string;
sectionSystemInterface?: string;
balance?: string; balance?: string;
sensitivity?: string; sensitivity?: string;
filters?: { label?: string; options?: Array<{ index: number; label: string }> }; filters?: { label?: string; options?: Array<{ index: number; label: string }> };
@@ -137,7 +163,7 @@ export default function AudioPage() {
<div className="px-4 pb-32"> <div className="px-4 pb-32">
{/* ── 平衡与灵敏度 ── */} {/* ── 平衡与灵敏度 ── */}
<SectionHeader title="平衡与灵敏度" /> <SectionHeader title={dacText.sectionBalanceSensitivity ?? "平衡与灵敏度"} />
<div className="ios-list-group px-4 py-3 space-y-4"> <div className="ios-list-group px-4 py-3 space-y-4">
<div> <div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -146,14 +172,21 @@ export default function AudioPage() {
{localBalance >= 0 ? `+${localBalance.toFixed(1)}` : localBalance.toFixed(1)} dB {localBalance >= 0 ? `+${localBalance.toFixed(1)}` : localBalance.toFixed(1)} dB
</span> </span>
</div> </div>
<CyanSlider value={localBalance} min={-15} max={15} step={0.1} <CyanSlider
value={localBalance}
min={-15}
max={15}
step={0.1}
onChange={(v) => { onChange={(v) => {
isBalanceDragging.current = true; isBalanceDragging.current = true;
setLocalBalance(v); setLocalBalance(v);
// 实时调用接口(例如:-13.0 → -130) }}
onRelease={(v) => {
isBalanceDragging.current = false;
setLocalBalance(v);
updateSetting({ balance: Math.round(v * 10) }); updateSetting({ balance: Math.round(v * 10) });
setTimeout(() => { isBalanceDragging.current = false; }, 500); }}
}} /> />
</div> </div>
<div className="border-t border-white/[0.06]" /> <div className="border-t border-white/[0.06]" />
<div> <div>
@@ -163,19 +196,26 @@ export default function AudioPage() {
{localVuSens >= 0 ? `+${localVuSens}` : localVuSens} dB {localVuSens >= 0 ? `+${localVuSens}` : localVuSens} dB
</span> </span>
</div> </div>
<CyanSlider value={localVuSens} min={-20} max={20} <CyanSlider
value={localVuSens}
min={-20}
max={20}
step={1}
onChange={(v) => { onChange={(v) => {
isVuDragging.current = true; isVuDragging.current = true;
setLocalVuSens(v); setLocalVuSens(v);
// 接口参数需要将显示值乘以 2(例如:10dB -> vuSensor=20 }}
onRelease={(v) => {
isVuDragging.current = false;
setLocalVuSens(v);
updateSetting({ vuSensor: Math.round(v * 2) }); updateSetting({ vuSensor: Math.round(v * 2) });
setTimeout(() => { isVuDragging.current = false; }, 500); }}
}} /> />
</div> </div>
</div> </div>
{/* ── 输出特性 ── */} {/* ── 输出特性 ── */}
<SectionHeader title="输出特性" /> <SectionHeader title={dacText.sectionOutput ?? "输出特性"} />
<div className="ios-list-group"> <div className="ios-list-group">
<ListRow <ListRow
label={dacText.filters?.label ?? "滤波特性"} label={dacText.filters?.label ?? "滤波特性"}
@@ -195,7 +235,7 @@ export default function AudioPage() {
</div> </div>
{/* ── 系统与接口 ── */} {/* ── 系统与接口 ── */}
<SectionHeader title="系统与接口" /> <SectionHeader title={dacText.sectionSystemInterface ?? "系统与接口"} />
<div className="ios-list-group"> <div className="ios-list-group">
<ListRow <ListRow
label={dacText.xlr?.label ?? "XLR 端口极性"} label={dacText.xlr?.label ?? "XLR 端口极性"}
+106 -14
View File
@@ -1,22 +1,114 @@
import BottomNav from "@/components/BottomNav"; 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 { ChevronLeft, Copy } from "lucide-react";
import { useMemo, useCallback } from "react";
import { useLocation } from "wouter";
import { toast } from "sonner";
const FORUM_URL = "https://forum.zidoo.tv"; const FORUM_URL = "https://forum.zidoo.tv/index.php";
type CommunityPageLocale = NonNullable<(typeof localeZh)["communityPage"]>;
export default function CommunityPage() { export default function CommunityPage() {
const isLgUp = useIsLgUp();
const [, setLocation] = useLocation();
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;
}, [deviceState?.language]);
const copyForumUrl = useCallback(async () => {
try {
await navigator.clipboard.writeText(FORUM_URL);
toast.success(text.toastCopied);
} catch {
try {
const ta = document.createElement("textarea");
ta.value = FORUM_URL;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
if (ok) toast.success(text.toastCopied);
else toast.error(text.toastCopyFailed);
} catch {
toast.error(text.toastCopyFailed);
}
}
}, [text.toastCopied, text.toastCopyFailed]);
if (isLgUp) {
return (
<div
className="h-[100dvh] flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]"
style={{ background: "#04060d" }}
>
<iframe
className="flex-1 w-full min-h-0 border-0 bg-white"
src={FORUM_URL}
title="Zidoo forum"
allow="fullscreen; clipboard-read; clipboard-write"
referrerPolicy="strict-origin-when-cross-origin"
/>
<BottomNav />
</div>
);
}
return ( return (
<div <div className="min-h-[100dvh] bg-black flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]">
className="h-[100dvh] flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]" <div className="page-header">
style={{ <button
background: "#04060d", type="button"
}} onClick={() => setLocation("/")}
> className="mr-4 text-white/60 active:text-white transition-colors"
<iframe aria-label="Back"
className="flex-1 w-full min-h-0 border-0 bg-white" >
src={FORUM_URL} <ChevronLeft size={24} />
title="Zidoo forum" </button>
allow="fullscreen; clipboard-read; clipboard-write" <h1 className="flex-1 text-center text-[17px] font-semibold text-white">{text.title}</h1>
referrerPolicy="strict-origin-when-cross-origin" <div className="w-8" />
/> </div>
<div className="flex-1 px-4 pt-4">
<div
className="ios-list-group p-4 space-y-4"
style={{
background: "linear-gradient(180deg, rgba(34,36,40,0.95) 0%, rgba(24,26,30,0.95) 100%)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 14,
boxShadow: "0 8px 32px rgba(0,0,0,0.35)",
}}
>
<p className="text-[15px] leading-relaxed text-white/75">{text.intro}</p>
<div
className="rounded-[10px] border border-white/12 bg-[#15171b] px-3 py-3 font-mono text-[12px] leading-snug break-all text-[#00FFF6]/90 select-all"
>
{FORUM_URL}
</div>
<button
type="button"
onClick={copyForumUrl}
className="flex w-full items-center justify-center gap-2 rounded-full py-3 text-[15px] font-semibold text-black transition-all active:scale-[0.98] bg-[#00FFF6] hover:brightness-95"
>
<Copy size={18} strokeWidth={2.25} />
{text.copyLink}
</button>
</div>
</div>
<BottomNav /> <BottomNav />
</div> </div>
); );
+45 -28
View File
@@ -6,7 +6,7 @@
2. Two action cards: 批量编辑 / 添加耳机型号 2. Two action cards: 批量编辑 / 添加耳机型号
3. Headphone model row: [Name ▼] [] [+] 3. Headphone model row: [Name ▼] [] [+]
4. Freq response chart: legend + A/B + 复制到B + DIFF + SVG curve 4. Freq response chart: legend + A/B + 复制到B + DIFF + SVG curve
5. Band grid: 10 pills (2 rows × 5) 5. Band grid: below lg breakpoint 2×5 pills; lg+ single row ×10
6. Band detail card: 滤波器 / FREQ / GAIN / Q值 6. Band detail card: 滤波器 / FREQ / GAIN / Q值
7. Total gain card: 总增益 value + AUTO toggle + slider 7. Total gain card: 总增益 value + AUTO toggle + slider
============================================================ */ ============================================================ */
@@ -998,11 +998,11 @@ export default function EQPage() {
const handleSaveAddPreset = async () => { const handleSaveAddPreset = async () => {
const nextName = addPresetMode === "copy" ? copyPresetName.trim() : flatPresetName.trim(); const nextName = addPresetMode === "copy" ? copyPresetName.trim() : flatPresetName.trim();
if (!nextName) { if (!nextName) {
toast.error("名称不能为空"); toast.error(eqUi.addPresetNameEmpty);
return; return;
} }
if (headphoneModels.includes(nextName)) { if (headphoneModels.includes(nextName)) {
toast.error("名称已存在,请修改后再保存"); toast.error(eqUi.addPresetNameExists);
return; return;
} }
@@ -1056,9 +1056,9 @@ export default function EQPage() {
setHeadphoneIdx(headphoneModels.length); setHeadphoneIdx(headphoneModels.length);
} }
setIsAddPresetDialogOpen(false); setIsAddPresetDialogOpen(false);
toast.success("已新增预设"); toast.success(eqUi.toastAddPresetOk);
} catch { } catch {
toast.error("新增预设失败"); toast.error(eqUi.toastAddPresetFail);
} }
}; };
@@ -1545,13 +1545,13 @@ export default function EQPage() {
eqUi={eqUi} eqUi={eqUi}
/> />
{/* ── Band grid (2 rows × 5) ── */} {/* ── Band grid: 2×5 below lg, 1×10 on lg+ (typical desktop) ── */}
<div className="ios-list-group p-3"> <div className="ios-list-group p-3">
<div className="grid grid-cols-5 gap-1.5"> <div className="grid grid-cols-5 gap-1.5 lg:grid-cols-10 lg:gap-1">
{bands.map((b, i) => ( {bands.map((b, i) => (
<button key={i} <button key={i}
className={cn( className={cn(
"flex flex-col items-center py-2.5 px-1 rounded-[10px] transition-all duration-150 active:scale-95", "flex flex-col items-center rounded-[10px] transition-all duration-150 active:scale-95 py-2.5 px-1 lg:py-2 lg:px-0.5",
selectedBand === i ? "text-black" : "text-white/55" selectedBand === i ? "text-black" : "text-white/55"
)} )}
style={selectedBand === i ? { style={selectedBand === i ? {
@@ -1561,8 +1561,8 @@ export default function EQPage() {
background: "rgba(44,44,46,0.65)", background: "rgba(44,44,46,0.65)",
}} }}
onClick={() => setSelectedBand(i)}> onClick={() => setSelectedBand(i)}>
<span className="text-[12px] font-bold leading-tight">{freqLabel(b.freq)}</span> <span className="text-[12px] font-bold leading-tight lg:text-[11px]">{freqLabel(b.freq)}</span>
<span className="text-[9px] mt-0.5 opacity-70 leading-tight">{getFilterShortName(b.type)}</span> <span className="text-[9px] mt-0.5 opacity-70 leading-tight lg:text-[8px] lg:mt-0">{getFilterShortName(b.type)}</span>
</button> </button>
))} ))}
</div> </div>
@@ -1697,69 +1697,86 @@ export default function EQPage() {
</div> </div>
{isAddPresetDialogOpen && ( {isAddPresetDialogOpen && (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/60 px-4"> <div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4">
<div <div
className="w-full max-w-[420px] rounded-[14px] p-5" className="w-full max-w-[420px] rounded-[14px] p-5"
style={{ style={{
background: "rgba(236,236,238,0.97)", background: "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
border: "1px solid rgba(255,255,255,0.35)", border: "1px solid rgba(255,255,255,0.12)",
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
}} }}
onClick={(e) => e.stopPropagation()}
> >
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-4">
<h3 className="text-[31px] text-black/75 font-semibold"></h3> <h3 className="text-[18px] font-semibold leading-tight text-white/90">{eqUi.addPresetTitle}</h3>
<button <button
type="button" type="button"
className="text-black/45 hover:text-black/70 transition-colors" className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
onClick={() => setIsAddPresetDialogOpen(false)} onClick={() => setIsAddPresetDialogOpen(false)}
aria-label={eqUi.closeDrawer}
> >
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
<div className="space-y-4"> <div className="space-y-3">
<label className="flex items-center gap-3"> <label
className={cn(
"flex cursor-pointer items-center gap-3 rounded-[12px] border border-white/10 p-3 transition-colors",
addPresetMode === "copy" ? "bg-white/[0.06]" : "bg-white/[0.02] hover:bg-white/[0.04]",
)}
>
<input <input
type="radio" type="radio"
name="add-preset-mode"
checked={addPresetMode === "copy"} checked={addPresetMode === "copy"}
onChange={() => setAddPresetMode("copy")} onChange={() => setAddPresetMode("copy")}
className="w-4 h-4" className="h-4 w-4 shrink-0 accent-[#00FFF6]"
/> />
<input <input
value={copyPresetName} value={copyPresetName}
onChange={(e) => setCopyPresetName(e.target.value)} onChange={(e) => setCopyPresetName(e.target.value)}
className="flex-1 h-10 px-3 rounded-[5px] text-[15px] text-black/70 bg-[#f0f0f2] border border-black/15 outline-none" onClick={(e) => e.stopPropagation()}
className="h-10 min-w-0 flex-1 rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[15px] text-white/90 outline-none ring-0 ring-offset-0 placeholder:text-white/30 focus:border-white/15 focus:ring-0 focus-visible:ring-0"
/> />
</label> </label>
<label className="flex items-center gap-3"> <label
className={cn(
"flex cursor-pointer items-center gap-3 rounded-[12px] border border-white/10 p-3 transition-colors",
addPresetMode === "flat" ? "bg-white/[0.06]" : "bg-white/[0.02] hover:bg-white/[0.04]",
)}
>
<input <input
type="radio" type="radio"
name="add-preset-mode"
checked={addPresetMode === "flat"} checked={addPresetMode === "flat"}
onChange={() => setAddPresetMode("flat")} onChange={() => setAddPresetMode("flat")}
className="w-4 h-4" className="h-4 w-4 shrink-0 accent-[#00FFF6]"
/> />
<input <input
value={flatPresetName} value={flatPresetName}
onChange={(e) => setFlatPresetName(e.target.value)} onChange={(e) => setFlatPresetName(e.target.value)}
className="flex-1 h-10 px-3 rounded-[5px] text-[15px] text-black/70 bg-[#f0f0f2] border border-black/15 outline-none" onClick={(e) => e.stopPropagation()}
className="h-10 min-w-0 flex-1 rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[15px] text-white/90 outline-none ring-0 ring-offset-0 placeholder:text-white/30 focus:border-white/15 focus:ring-0 focus-visible:ring-0"
/> />
</label> </label>
</div> </div>
<div className="mt-6 flex items-center justify-center gap-16"> <div className="mt-6 flex items-center justify-center gap-4 sm:gap-8">
<button <button
type="button" type="button"
className="px-5 py-2 rounded-full text-[26px] text-black/80 bg-[#d2d2d5] hover:bg-[#c7c7ca] transition-colors" className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-medium text-white/85 transition-colors bg-[#3f4349] hover:bg-[#4a4f56] active:scale-[0.98]"
onClick={() => setIsAddPresetDialogOpen(false)} onClick={() => setIsAddPresetDialogOpen(false)}
> >
{eqUi.cancel}
</button> </button>
<button <button
type="button" type="button"
className="px-5 py-2 rounded-full text-[26px] text-black bg-[#00FFF6] hover:brightness-95 transition-all" className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-semibold text-black transition-all bg-[#00FFF6] hover:brightness-95 active:scale-[0.98]"
onClick={handleSaveAddPreset} onClick={handleSaveAddPreset}
> >
{eqUi.save}
</button> </button>
</div> </div>
</div> </div>
+12 -2
View File
@@ -574,7 +574,12 @@ export default function Home() {
> >
<Sliders size={22} className={(!bypassOn && ds.peqEnable === 1) ? "text-[#00FFF6]" : "text-white/60"} /> <Sliders size={22} className={(!bypassOn && ds.peqEnable === 1) ? "text-[#00FFF6]" : "text-white/60"} />
</button> </button>
<span className={cn("text-[11px]", (!bypassOn && ds.peqEnable === 1) ? "text-[#00FFF6]" : "text-white/35")}>HP-EQ</span> <div className="flex flex-col items-center gap-0.5">
<span className={cn("text-[11px]", (!bypassOn && ds.peqEnable === 1) ? "text-[#00FFF6]" : "text-white/35")}>HP-EQ</span>
<span className="text-[9px] text-white/25 text-center leading-snug max-w-[76px]">
{homeText.doubleClickHint ?? "Double-click to enter"}
</span>
</div>
</div> </div>
{/* Effect */} {/* Effect */}
@@ -586,7 +591,12 @@ export default function Home() {
> >
<Activity size={22} className={(!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/60"} /> <Activity size={22} className={(!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/60"} />
</button> </button>
<span className={cn("text-[11px]", (!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/35")}>Effect</span> <div className="flex flex-col items-center gap-0.5">
<span className={cn("text-[11px]", (!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/35")}>Effect</span>
<span className="text-[9px] text-white/25 text-center leading-snug max-w-[76px]">
{homeText.doubleClickHint ?? "Double-click to enter"}
</span>
</div>
</div> </div>
{/* Bypass (DSP) */} {/* Bypass (DSP) */}
+39 -15
View File
@@ -67,6 +67,7 @@ function ListRow({ label, description, value, onClick, toggle, checked, onToggle
type LocaleSettings = { type LocaleSettings = {
settings: { settings: {
vu: string; vu: string;
vuMeter?: string;
screenBrightness: { label: string; options: Array<{ index: number; label: string }> }; screenBrightness: { label: string; options: Array<{ index: number; label: string }> };
turnOffScreen: { label: string; options: Array<{ index: number; label: string }> }; turnOffScreen: { label: string; options: Array<{ index: number; label: string }> };
knobBrightness: { label: string; options: Array<{ index: number; label: string }> }; knobBrightness: { label: string; options: Array<{ index: number; label: string }> };
@@ -75,6 +76,22 @@ type LocaleSettings = {
Automatic: { label: string; options: Array<{ index: number; label: string }> }; Automatic: { label: string; options: Array<{ index: number; label: string }> };
sleepTime?: { label: string; options: Array<{ index: number; label: string }> }; sleepTime?: { label: string; options: Array<{ index: number; label: string }> };
sleep?: { label: string; options: Array<{ index: number; label: string }> }; sleep?: { label: string; options: Array<{ index: number; label: string }> };
systemPage: {
sectionDisplay: string;
sectionGeneral: string;
sectionAbout: string;
screenBrightnessDesc: string;
turnOffScreenDesc: string;
knobBrightnessDesc: string;
buttonLightDesc: string;
languageDesc: string;
automaticDesc: string;
sleepDesc: string;
deviceName: string;
firmwareVersion: string;
macAddress: string;
disconnect: string;
};
}; };
}; };
@@ -111,6 +128,7 @@ export default function SystemPage() {
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0; const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh; const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
const settingsText = localeData.settings; const settingsText = localeData.settings;
const ui = settingsText.systemPage;
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]); const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
const KNOB_LIGHT_OPTIONS = optionLabels(settingsText.knobBrightness?.options, ["关闭", "亮", "中等", "暗"]); const KNOB_LIGHT_OPTIONS = optionLabels(settingsText.knobBrightness?.options, ["关闭", "亮", "中等", "暗"]);
const SCREEN_OFF_OPTIONS = optionLabels(settingsText.turnOffScreen?.options, ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]); const SCREEN_OFF_OPTIONS = optionLabels(settingsText.turnOffScreen?.options, ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]);
@@ -118,6 +136,12 @@ export default function SystemPage() {
const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]); const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]);
const LANG_OPTIONS = optionLabels(settingsText.language?.options, ["English", "繁體中文", "简体中文"]); const LANG_OPTIONS = optionLabels(settingsText.language?.options, ["English", "繁體中文", "简体中文"]);
const sleepRowLabel =
settingsText.sleepTime?.label ??
settingsText.sleep?.label ??
(localeEn as LocaleSettings).settings.sleep?.label ??
"Sleep";
const goSelect = (title: string, options: string[], selected: number, key: string) => { const goSelect = (title: string, options: string[], selected: number, key: string) => {
navigateToSelect(title, options, selected, "/system", key); navigateToSelect(title, options, selected, "/system", key);
setLocation("/select"); setLocation("/select");
@@ -135,49 +159,49 @@ export default function SystemPage() {
<div className="px-4 pb-32"> <div className="px-4 pb-32">
{/* ── 显示 ── */} {/* ── 显示 ── */}
<SectionHeader title="显示" /> <SectionHeader title={ui.sectionDisplay} />
<div className="ios-list-group"> <div className="ios-list-group">
<ListRow label={settingsText.screenBrightness.label} description="调节屏幕的整体亮度" <ListRow label={settingsText.screenBrightness.label} description={ui.screenBrightnessDesc}
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]} value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
onClick={() => goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} /> onClick={() => goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
<ListRow label={settingsText.turnOffScreen.label} description="设备无操作后自动关闭屏幕的时间" <ListRow label={settingsText.turnOffScreen.label} description={ui.turnOffScreenDesc}
value={SCREEN_OFF_OPTIONS[screenOffIdx]} value={SCREEN_OFF_OPTIONS[screenOffIdx]}
onClick={() => goSelect(settingsText.turnOffScreen.label, SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} /> onClick={() => goSelect(settingsText.turnOffScreen.label, SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />
<ListRow label={settingsText.knobBrightness.label} description="调节旋钮指示灯的亮度" <ListRow label={settingsText.knobBrightness.label} description={ui.knobBrightnessDesc}
value={KNOB_LIGHT_OPTIONS[buttonLightIdx]} value={KNOB_LIGHT_OPTIONS[buttonLightIdx]}
onClick={() => goSelect(settingsText.knobBrightness.label, KNOB_LIGHT_OPTIONS, buttonLightIdx, "buttonLight")} /> onClick={() => goSelect(settingsText.knobBrightness.label, KNOB_LIGHT_OPTIONS, buttonLightIdx, "buttonLight")} />
<ListRow label={settingsText.buttonLight.label} description="锁屏后旋钮是否显示呼吸灯效果" <ListRow label={settingsText.buttonLight.label} description={ui.buttonLightDesc}
toggle checked={knobBreathLightOn} toggle checked={knobBreathLightOn}
onToggle={(v) => updateSetting({ knob_breathlight: v ? 0 : 1 })} /> onToggle={(v) => updateSetting({ knob_breathlight: v ? 0 : 1 })} />
</div> </div>
{/* ── 通用 ── */} {/* ── 通用 ── */}
<SectionHeader title="通用" /> <SectionHeader title={ui.sectionGeneral} />
<div className="ios-list-group"> <div className="ios-list-group">
<ListRow label={settingsText.language.label} description="设置系统显示语言" <ListRow label={settingsText.language.label} description={ui.languageDesc}
value={LANG_OPTIONS[langIdx]} value={LANG_OPTIONS[langIdx]}
onClick={() => goSelect(settingsText.language.label, LANG_OPTIONS, langIdx, "language")} /> onClick={() => goSelect(settingsText.language.label, LANG_OPTIONS, langIdx, "language")} />
<ListRow label={settingsText.Automatic.label} description="闲置一段时间后自动回到主界面" <ListRow label={settingsText.Automatic.label} description={ui.automaticDesc}
value={AUTO_HOME_OPTIONS[autoHomeIdx]} value={AUTO_HOME_OPTIONS[autoHomeIdx]}
onClick={() => goSelect(settingsText.Automatic.label, AUTO_HOME_OPTIONS, autoHomeIdx, "autoHome")} /> onClick={() => goSelect(settingsText.Automatic.label, AUTO_HOME_OPTIONS, autoHomeIdx, "autoHome")} />
<ListRow label={settingsText.sleepTime?.label ?? settingsText.sleep?.label ?? "休眠"} description="设置设备进入低功耗模式的时间" <ListRow label={sleepRowLabel} description={ui.sleepDesc}
value={SLEEP_OPTIONS[sleepIdx]} value={SLEEP_OPTIONS[sleepIdx]}
onClick={() => goSelect(settingsText.sleepTime?.label ?? settingsText.sleep?.label ?? "休眠", SLEEP_OPTIONS, sleepIdx, "sleep")} /> onClick={() => goSelect(sleepRowLabel, SLEEP_OPTIONS, sleepIdx, "sleep")} />
</div> </div>
{/* ── 关于 ── */} {/* ── 关于 ── */}
<SectionHeader title="关于" /> <SectionHeader title={ui.sectionAbout} />
<div className="ios-list-group"> <div className="ios-list-group">
<div className="ios-list-row"> <div className="ios-list-row">
<span className="flex-1 text-[16px] text-white"></span> <span className="flex-1 text-[16px] text-white">{ui.deviceName}</span>
<span className="ios-row-value">{ds?.device ?? "Luxsin X9"}</span> <span className="ios-row-value">{ds?.device ?? "Luxsin X9"}</span>
</div> </div>
<div className="ios-list-row"> <div className="ios-list-row">
<span className="flex-1 text-[16px] text-white"></span> <span className="flex-1 text-[16px] text-white">{ui.firmwareVersion}</span>
<span className="ios-row-value">{ds?.version ?? "—"}</span> <span className="ios-row-value">{ds?.version ?? "—"}</span>
</div> </div>
<div className="ios-list-row"> <div className="ios-list-row">
<span className="flex-1 text-[16px] text-white">MAC </span> <span className="flex-1 text-[16px] text-white">{ui.macAddress}</span>
<span className="ios-row-value text-[12px]">{ds?.mac ?? "—"}</span> <span className="ios-row-value text-[12px]">{ds?.mac ?? "—"}</span>
</div> </div>
</div> </div>
@@ -188,7 +212,7 @@ export default function SystemPage() {
className="w-full py-4 rounded-[14px] text-[16px] font-medium transition-all duration-150 active:scale-[0.98]" className="w-full py-4 rounded-[14px] text-[16px] font-medium transition-all duration-150 active:scale-[0.98]"
style={{ background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.2)", color: "#ef4444" }} style={{ background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.2)", color: "#ef4444" }}
onClick={() => { disconnect(); setLocation("/"); }}> onClick={() => { disconnect(); setLocation("/"); }}>
{ui.disconnect}
</button> </button>
</div> </div>
</div> </div>