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
+9 -8
View File
@@ -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) {
+33 -1
View File
@@ -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 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": {
"label": "Input/Output",
"input": {
@@ -226,6 +234,11 @@
"eqUi": {
"batchEditTitle": "Batch edit",
"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",
"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": {
+33 -1
View File
@@ -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,第 211 行為 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": {
+33 -1
View File
@@ -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,第 211 行为 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": {
+55 -15
View File
@@ -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 端口极性"}
+96 -4
View File
@@ -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
View File
@@ -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>
+10
View File
@@ -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) */}
+39 -15
View File
@@ -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>