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:
@@ -2,7 +2,7 @@
|
||||
BOTTOM NAV — Luxsin X9 Controller (Mobile)
|
||||
Design: iOS/Android App-style bottom tab bar
|
||||
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
|
||||
- 社区:仅桌面宽屏(lg ≥1024px)显示,手机等小屏隐藏
|
||||
- 社区:移动端进入 /community 引导复制链接;桌面(lg+)点击仍 window.open 论坛
|
||||
- Active tab: cyan icon + top indicator bar
|
||||
- Inactive: muted icon + label
|
||||
============================================================ */
|
||||
@@ -54,12 +54,6 @@ export default function BottomNav() {
|
||||
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
|
||||
}, [deviceState?.language]);
|
||||
|
||||
/** 社区入口仅在桌面级视口显示(与 useIsLgUp / Tailwind lg 一致) */
|
||||
const visibleNavItems = useMemo(
|
||||
() => NAV_ITEMS.filter((item) => item.path !== "/community" || isLgUp),
|
||||
[isLgUp],
|
||||
);
|
||||
|
||||
return (
|
||||
<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"
|
||||
@@ -73,7 +67,7 @@ export default function BottomNav() {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-stretch justify-around">
|
||||
{visibleNavItems.map((item) => {
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive =
|
||||
item.path === "/ai"
|
||||
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
||||
@@ -132,6 +126,7 @@ export default function BottomNav() {
|
||||
);
|
||||
|
||||
if (item.path === "/community") {
|
||||
if (isLgUp) {
|
||||
return (
|
||||
<button
|
||||
key={item.path}
|
||||
@@ -143,6 +138,12 @@ export default function BottomNav() {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link key={item.path} href={item.path}>
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.path === "/ai") {
|
||||
if (!isLgUp) {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"powerOffConfirmOk": "Turn off",
|
||||
"powerOffConfirmCancel": "Cancel",
|
||||
"powerOffToastDemo": "Demo mode: disconnected from the device.",
|
||||
"powerOffToastSent": "Shutdown command sent."
|
||||
"powerOffToastSent": "Shutdown command sent.",
|
||||
"doubleClickHint": "Double-click to enter"
|
||||
},
|
||||
"vuPage": {
|
||||
"title": "VU meter gallery"
|
||||
@@ -23,6 +24,13 @@
|
||||
"effects": "Effects",
|
||||
"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 browser’s address bar, and open it.",
|
||||
"copyLink": "Copy link",
|
||||
"toastCopied": "Link copied",
|
||||
"toastCopyFailed": "Could not copy. Long-press the link to copy it manually."
|
||||
},
|
||||
"source": {
|
||||
"label": "Input/Output",
|
||||
"input": {
|
||||
@@ -226,6 +234,11 @@
|
||||
"eqUi": {
|
||||
"batchEditTitle": "Batch edit",
|
||||
"batchEditHint": "11 lines total: line 1 is Preamp, lines 2–11 are Filter 1–10. 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",
|
||||
"save": "Save",
|
||||
"tabBrands": "Brands",
|
||||
@@ -275,6 +288,9 @@
|
||||
}
|
||||
},
|
||||
"dac": {
|
||||
"sectionBalanceSensitivity": "Balance & sensitivity",
|
||||
"sectionOutput": "Output characteristics",
|
||||
"sectionSystemInterface": "System & interface",
|
||||
"balance": "Left-right balance",
|
||||
"sensitivity": "VU Meter Sensitivity",
|
||||
"filters": {
|
||||
@@ -528,6 +544,22 @@
|
||||
"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": {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"powerOffConfirmOk": "關閉電源",
|
||||
"powerOffConfirmCancel": "取消",
|
||||
"powerOffToastDemo": "演示模式:已中斷與裝置的連線",
|
||||
"powerOffToastSent": "已傳送關機指令"
|
||||
"powerOffToastSent": "已傳送關機指令",
|
||||
"doubleClickHint": "按兩下進入"
|
||||
},
|
||||
"vuPage": {
|
||||
"title": "VU 表庫"
|
||||
@@ -23,6 +24,13 @@
|
||||
"effects": "Effect",
|
||||
"myAi": "我的AI"
|
||||
},
|
||||
"communityPage": {
|
||||
"title": "社區",
|
||||
"intro": "論壇需在系統瀏覽器中開啟。請複製下方完整連結,在手機瀏覽器網址列貼上並前往。",
|
||||
"copyLink": "複製連結",
|
||||
"toastCopied": "連結已複製",
|
||||
"toastCopyFailed": "複製失敗,請長按連結手動複製"
|
||||
},
|
||||
"source": {
|
||||
"label": "輸入/輸出",
|
||||
"input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" },
|
||||
@@ -162,6 +170,11 @@
|
||||
"eqUi": {
|
||||
"batchEditTitle": "批次編輯",
|
||||
"batchEditHint": "共 11 行:第 1 行為 Preamp,第 2–11 行為 Filter 1–10。儲存後將覆寫目前耳機的參數。",
|
||||
"addPresetTitle": "新增預設",
|
||||
"addPresetNameEmpty": "名稱不能為空",
|
||||
"addPresetNameExists": "名稱已存在,請修改後再儲存",
|
||||
"toastAddPresetOk": "已新增預設",
|
||||
"toastAddPresetFail": "新增預設失敗",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"tabBrands": "品牌",
|
||||
@@ -211,6 +224,9 @@
|
||||
}
|
||||
},
|
||||
"dac": {
|
||||
"sectionBalanceSensitivity": "平衡與靈敏度",
|
||||
"sectionOutput": "輸出特性",
|
||||
"sectionSystemInterface": "系統與介面",
|
||||
"balance": "左右平衡",
|
||||
"sensitivity": "VU表靈敏度",
|
||||
"filters": {
|
||||
@@ -344,6 +360,22 @@
|
||||
{ "index": 2, "label": "無操作40秒" },
|
||||
{ "index": 3, "label": "無操作60秒" }
|
||||
]
|
||||
},
|
||||
"systemPage": {
|
||||
"sectionDisplay": "顯示",
|
||||
"sectionGeneral": "一般",
|
||||
"sectionAbout": "關於",
|
||||
"screenBrightnessDesc": "調節螢幕的整體亮度",
|
||||
"turnOffScreenDesc": "裝置無操作後自動關閉螢幕的時間",
|
||||
"knobBrightnessDesc": "調節旋鈕指示燈的亮度",
|
||||
"buttonLightDesc": "鎖屏後旋鈕是否顯示呼吸燈效果",
|
||||
"languageDesc": "設定系統顯示語言",
|
||||
"automaticDesc": "閒置一段時間後自動回到主畫面",
|
||||
"sleepDesc": "設定裝置進入低功耗模式的時間",
|
||||
"deviceName": "裝置名稱",
|
||||
"firmwareVersion": "韌體版本",
|
||||
"macAddress": "MAC 位址",
|
||||
"disconnect": "中斷連線"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"powerOffConfirmOk": "关闭电源",
|
||||
"powerOffConfirmCancel": "取消",
|
||||
"powerOffToastDemo": "演示模式:已断开与设备的连接",
|
||||
"powerOffToastSent": "已发送关机指令"
|
||||
"powerOffToastSent": "已发送关机指令",
|
||||
"doubleClickHint": "双击进入"
|
||||
},
|
||||
"vuPage": {
|
||||
"title": "VU 表库"
|
||||
@@ -23,6 +24,13 @@
|
||||
"effects": "Effect",
|
||||
"myAi": "我的AI"
|
||||
},
|
||||
"communityPage": {
|
||||
"title": "社区",
|
||||
"intro": "论坛需在系统浏览器中打开。请复制下方完整链接,在手机浏览器地址栏粘贴并访问。",
|
||||
"copyLink": "复制链接",
|
||||
"toastCopied": "链接已复制",
|
||||
"toastCopyFailed": "复制失败,请长按链接手动复制"
|
||||
},
|
||||
"source": {
|
||||
"label": "输入输出",
|
||||
"input": { "coaxial": "同轴", "optical": "光钎", "bluetooth": "蓝牙", "rca": "模拟RCA" },
|
||||
@@ -162,6 +170,11 @@
|
||||
"eqUi": {
|
||||
"batchEditTitle": "批量编辑",
|
||||
"batchEditHint": "共 11 行:第 1 行为 Preamp,第 2–11 行为 Filter 1–10。保存后将覆盖当前耳机的参数。",
|
||||
"addPresetTitle": "新增预设",
|
||||
"addPresetNameEmpty": "名称不能为空",
|
||||
"addPresetNameExists": "名称已存在,请修改后再保存",
|
||||
"toastAddPresetOk": "已新增预设",
|
||||
"toastAddPresetFail": "新增预设失败",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"tabBrands": "品牌",
|
||||
@@ -211,6 +224,9 @@
|
||||
}
|
||||
},
|
||||
"dac": {
|
||||
"sectionBalanceSensitivity": "平衡与灵敏度",
|
||||
"sectionOutput": "输出特性",
|
||||
"sectionSystemInterface": "系统与接口",
|
||||
"balance": "左右平衡",
|
||||
"sensitivity": "vu表灵敏度",
|
||||
"filters": {
|
||||
@@ -345,6 +361,22 @@
|
||||
{ "index": 2, "label": "无操作40秒" },
|
||||
{ "index": 3, "label": "无操作60秒" }
|
||||
]
|
||||
},
|
||||
"systemPage": {
|
||||
"sectionDisplay": "显示",
|
||||
"sectionGeneral": "通用",
|
||||
"sectionAbout": "关于",
|
||||
"screenBrightnessDesc": "调节屏幕的整体亮度",
|
||||
"turnOffScreenDesc": "设备无操作后自动关闭屏幕的时间",
|
||||
"knobBrightnessDesc": "调节旋钮指示灯的亮度",
|
||||
"buttonLightDesc": "锁屏后旋钮是否显示呼吸灯效果",
|
||||
"languageDesc": "设置系统显示语言",
|
||||
"automaticDesc": "闲置一段时间后自动回到主界面",
|
||||
"sleepDesc": "设置设备进入低功耗模式的时间",
|
||||
"deviceName": "设备名称",
|
||||
"firmwareVersion": "固件版本",
|
||||
"macAddress": "MAC 地址",
|
||||
"disconnect": "断开连接"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -17,17 +17,40 @@ import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
|
||||
function CyanSlider({ value, min, max, step = 1, onChange }: {
|
||||
value: number; min: number; max: number; step?: number; onChange: (v: number) => void;
|
||||
function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: {
|
||||
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 emitRelease = (el: EventTarget | null) => {
|
||||
if (!(el instanceof HTMLInputElement)) return;
|
||||
onRelease?.(Number(el.value));
|
||||
};
|
||||
return (
|
||||
<div className="relative flex items-center w-full mt-2">
|
||||
<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)" }} />
|
||||
<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"
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +75,9 @@ function ListRow({ label, value, onClick }: { label: string; value: string; onCl
|
||||
|
||||
type LocaleDac = {
|
||||
dac?: {
|
||||
sectionBalanceSensitivity?: string;
|
||||
sectionOutput?: string;
|
||||
sectionSystemInterface?: string;
|
||||
balance?: string;
|
||||
sensitivity?: string;
|
||||
filters?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
@@ -137,7 +163,7 @@ export default function AudioPage() {
|
||||
|
||||
<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>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -146,14 +172,21 @@ export default function AudioPage() {
|
||||
{localBalance >= 0 ? `+${localBalance.toFixed(1)}` : localBalance.toFixed(1)} dB
|
||||
</span>
|
||||
</div>
|
||||
<CyanSlider value={localBalance} min={-15} max={15} step={0.1}
|
||||
<CyanSlider
|
||||
value={localBalance}
|
||||
min={-15}
|
||||
max={15}
|
||||
step={0.1}
|
||||
onChange={(v) => {
|
||||
isBalanceDragging.current = true;
|
||||
setLocalBalance(v);
|
||||
// 实时调用接口(例如:-13.0 → -130)
|
||||
}}
|
||||
onRelease={(v) => {
|
||||
isBalanceDragging.current = false;
|
||||
setLocalBalance(v);
|
||||
updateSetting({ balance: Math.round(v * 10) });
|
||||
setTimeout(() => { isBalanceDragging.current = false; }, 500);
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-white/[0.06]" />
|
||||
<div>
|
||||
@@ -163,19 +196,26 @@ export default function AudioPage() {
|
||||
{localVuSens >= 0 ? `+${localVuSens}` : localVuSens} dB
|
||||
</span>
|
||||
</div>
|
||||
<CyanSlider value={localVuSens} min={-20} max={20}
|
||||
<CyanSlider
|
||||
value={localVuSens}
|
||||
min={-20}
|
||||
max={20}
|
||||
step={1}
|
||||
onChange={(v) => {
|
||||
isVuDragging.current = true;
|
||||
setLocalVuSens(v);
|
||||
// 接口参数需要将显示值乘以 2(例如:10dB -> vuSensor=20)
|
||||
}}
|
||||
onRelease={(v) => {
|
||||
isVuDragging.current = false;
|
||||
setLocalVuSens(v);
|
||||
updateSetting({ vuSensor: Math.round(v * 2) });
|
||||
setTimeout(() => { isVuDragging.current = false; }, 500);
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 输出特性 ── */}
|
||||
<SectionHeader title="输出特性" />
|
||||
<SectionHeader title={dacText.sectionOutput ?? "输出特性"} />
|
||||
<div className="ios-list-group">
|
||||
<ListRow
|
||||
label={dacText.filters?.label ?? "滤波特性"}
|
||||
@@ -195,7 +235,7 @@ export default function AudioPage() {
|
||||
</div>
|
||||
|
||||
{/* ── 系统与接口 ── */}
|
||||
<SectionHeader title="系统与接口" />
|
||||
<SectionHeader title={dacText.sectionSystemInterface ?? "系统与接口"} />
|
||||
<div className="ios-list-group">
|
||||
<ListRow
|
||||
label={dacText.xlr?.label ?? "XLR 端口极性"}
|
||||
|
||||
@@ -1,14 +1,57 @@
|
||||
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() {
|
||||
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",
|
||||
}}
|
||||
style={{ background: "#04060d" }}
|
||||
>
|
||||
<iframe
|
||||
className="flex-1 w-full min-h-0 border-0 bg-white"
|
||||
@@ -17,6 +60,55 @@ export default function CommunityPage() {
|
||||
allow="fullscreen; clipboard-read; clipboard-write"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
/>
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-[100dvh] bg-black flex flex-col pb-[calc(env(safe-area-inset-bottom,0px)+56px)]">
|
||||
<div className="page-header">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocation("/")}
|
||||
className="mr-4 text-white/60 active:text-white transition-colors"
|
||||
aria-label="Back"
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</button>
|
||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{text.title}</h1>
|
||||
<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 />
|
||||
</div>
|
||||
);
|
||||
|
||||
+45
-28
@@ -6,7 +6,7 @@
|
||||
2. Two action cards: 批量编辑 / 添加耳机型号
|
||||
3. Headphone model row: [Name ▼] [−] [+]
|
||||
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值
|
||||
7. Total gain card: 总增益 value + AUTO toggle + slider
|
||||
============================================================ */
|
||||
@@ -998,11 +998,11 @@ export default function EQPage() {
|
||||
const handleSaveAddPreset = async () => {
|
||||
const nextName = addPresetMode === "copy" ? copyPresetName.trim() : flatPresetName.trim();
|
||||
if (!nextName) {
|
||||
toast.error("名称不能为空");
|
||||
toast.error(eqUi.addPresetNameEmpty);
|
||||
return;
|
||||
}
|
||||
if (headphoneModels.includes(nextName)) {
|
||||
toast.error("名称已存在,请修改后再保存");
|
||||
toast.error(eqUi.addPresetNameExists);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1056,9 +1056,9 @@ export default function EQPage() {
|
||||
setHeadphoneIdx(headphoneModels.length);
|
||||
}
|
||||
setIsAddPresetDialogOpen(false);
|
||||
toast.success("已新增预设");
|
||||
toast.success(eqUi.toastAddPresetOk);
|
||||
} catch {
|
||||
toast.error("新增预设失败");
|
||||
toast.error(eqUi.toastAddPresetFail);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1545,13 +1545,13 @@ export default function EQPage() {
|
||||
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="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) => (
|
||||
<button key={i}
|
||||
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"
|
||||
)}
|
||||
style={selectedBand === i ? {
|
||||
@@ -1561,8 +1561,8 @@ export default function EQPage() {
|
||||
background: "rgba(44,44,46,0.65)",
|
||||
}}
|
||||
onClick={() => setSelectedBand(i)}>
|
||||
<span className="text-[12px] font-bold leading-tight">{freqLabel(b.freq)}</span>
|
||||
<span className="text-[9px] mt-0.5 opacity-70 leading-tight">{getFilterShortName(b.type)}</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 lg:text-[8px] lg:mt-0">{getFilterShortName(b.type)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1697,69 +1697,86 @@ export default function EQPage() {
|
||||
</div>
|
||||
|
||||
{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
|
||||
className="w-full max-w-[420px] rounded-[14px] p-5"
|
||||
style={{
|
||||
background: "rgba(236,236,238,0.97)",
|
||||
border: "1px solid rgba(255,255,255,0.35)",
|
||||
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.12)",
|
||||
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h3 className="text-[31px] text-black/75 font-semibold">新增预设</h3>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] font-semibold leading-tight text-white/90">{eqUi.addPresetTitle}</h3>
|
||||
<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)}
|
||||
aria-label={eqUi.closeDrawer}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-3">
|
||||
<div className="space-y-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
|
||||
type="radio"
|
||||
name="add-preset-mode"
|
||||
checked={addPresetMode === "copy"}
|
||||
onChange={() => setAddPresetMode("copy")}
|
||||
className="w-4 h-4"
|
||||
className="h-4 w-4 shrink-0 accent-[#00FFF6]"
|
||||
/>
|
||||
<input
|
||||
value={copyPresetName}
|
||||
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 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
|
||||
type="radio"
|
||||
name="add-preset-mode"
|
||||
checked={addPresetMode === "flat"}
|
||||
onChange={() => setAddPresetMode("flat")}
|
||||
className="w-4 h-4"
|
||||
className="h-4 w-4 shrink-0 accent-[#00FFF6]"
|
||||
/>
|
||||
<input
|
||||
value={flatPresetName}
|
||||
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>
|
||||
</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
|
||||
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)}
|
||||
>
|
||||
取消
|
||||
{eqUi.cancel}
|
||||
</button>
|
||||
<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}
|
||||
>
|
||||
保存
|
||||
{eqUi.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -574,7 +574,12 @@ export default function Home() {
|
||||
>
|
||||
<Sliders size={22} className={(!bypassOn && ds.peqEnable === 1) ? "text-[#00FFF6]" : "text-white/60"} />
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{/* Effect */}
|
||||
@@ -586,7 +591,12 @@ export default function Home() {
|
||||
>
|
||||
<Activity size={22} className={(!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/60"} />
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{/* Bypass (DSP) */}
|
||||
|
||||
@@ -67,6 +67,7 @@ function ListRow({ label, description, value, onClick, toggle, checked, onToggle
|
||||
type LocaleSettings = {
|
||||
settings: {
|
||||
vu: string;
|
||||
vuMeter?: string;
|
||||
screenBrightness: { 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 }> };
|
||||
@@ -75,6 +76,22 @@ type LocaleSettings = {
|
||||
Automatic: { 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 }> };
|
||||
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 localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
|
||||
const settingsText = localeData.settings;
|
||||
const ui = settingsText.systemPage;
|
||||
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
|
||||
const KNOB_LIGHT_OPTIONS = optionLabels(settingsText.knobBrightness?.options, ["关闭", "亮", "中等", "暗"]);
|
||||
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 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) => {
|
||||
navigateToSelect(title, options, selected, "/system", key);
|
||||
setLocation("/select");
|
||||
@@ -135,49 +159,49 @@ export default function SystemPage() {
|
||||
|
||||
<div className="px-4 pb-32">
|
||||
{/* ── 显示 ── */}
|
||||
<SectionHeader title="显示" />
|
||||
<SectionHeader title={ui.sectionDisplay} />
|
||||
<div className="ios-list-group">
|
||||
<ListRow label={settingsText.screenBrightness.label} description="调节屏幕的整体亮度"
|
||||
<ListRow label={settingsText.screenBrightness.label} description={ui.screenBrightnessDesc}
|
||||
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
|
||||
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]}
|
||||
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]}
|
||||
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}
|
||||
onToggle={(v) => updateSetting({ knob_breathlight: v ? 0 : 1 })} />
|
||||
</div>
|
||||
|
||||
{/* ── 通用 ── */}
|
||||
<SectionHeader title="通用" />
|
||||
<SectionHeader title={ui.sectionGeneral} />
|
||||
<div className="ios-list-group">
|
||||
<ListRow label={settingsText.language.label} description="设置系统显示语言"
|
||||
<ListRow label={settingsText.language.label} description={ui.languageDesc}
|
||||
value={LANG_OPTIONS[langIdx]}
|
||||
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]}
|
||||
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]}
|
||||
onClick={() => goSelect(settingsText.sleepTime?.label ?? settingsText.sleep?.label ?? "休眠", SLEEP_OPTIONS, sleepIdx, "sleep")} />
|
||||
onClick={() => goSelect(sleepRowLabel, SLEEP_OPTIONS, sleepIdx, "sleep")} />
|
||||
</div>
|
||||
|
||||
{/* ── 关于 ── */}
|
||||
<SectionHeader title="关于" />
|
||||
<SectionHeader title={ui.sectionAbout} />
|
||||
<div className="ios-list-group">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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]"
|
||||
style={{ background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.2)", color: "#ef4444" }}
|
||||
onClick={() => { disconnect(); setLocation("/"); }}>
|
||||
断开连接
|
||||
{ui.disconnect}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user