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:
@@ -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,22 +1,114 @@
|
||||
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" }}
|
||||
>
|
||||
<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 (
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* Effect */}
|
||||
@@ -586,7 +591,12 @@ export default function Home() {
|
||||
>
|
||||
<Activity size={22} className={(!bypassOn && effectOn) ? "text-[#00FFF6]" : "text-white/60"} />
|
||||
</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>
|
||||
|
||||
{/* 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