/* ============================================================
HOME — Luxsin X9 Controller (Main Dashboard)
Design: Reference luxsin_x8_首页.png
Layout (top to bottom):
1. Device area
2. Device status card (运行中/待机/充电 + 电量/模式/环保参数)
3. Device render image (hardware photo)
4. Volume slider with dB display
5. 4 quick-action circle buttons (电源/HP-EQ/Effect/Bypass;HP-EQ/Effect 支持双击开关)
6. iOS list rows (输入源/输出端口/音频设置/系统设置/VU表)
============================================================ */
import { useDevice } from "@/contexts/DeviceContext";
import ConnectionPlaceholder from "@/components/ConnectionPlaceholder";
import BottomNav from "@/components/BottomNav";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
INPUT_BLUETOOTH_INDEX,
INPUT_LABELS,
isHomeVolumeLockedByPassthrough,
OUTPUT_LABELS,
} from "@/lib/luxsinApi";
import { cn } from "@/lib/utils";
import {
ChevronRight,
Power,
Settings,
Sliders,
Activity,
Gauge,
Bluetooth,
SkipBack,
SkipForward,
Play,
Pause,
SquareArrowRightEnter,
SquareArrowRightExit,
FileMusic,
Minus,
Plus,
} from "lucide-react";
import { useLocation } from "wouter";
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
import { toast } from "sonner";
import localeZh from "@/locales/data-zh.json";
import localeZhHK from "@/locales/data-zh-HK.json";
import localeEn from "@/locales/data-en.json";
type HomeLocale = NonNullable<(typeof localeZh)["home"]>;
// ── Volume in dB (0-200 → -100dB to 0dB) ──
function volToDB(v: number) {
if (v === 0) return "-100dB";
const db = (v - 200) / 2;
return `${db >= 0 ? "+" : ""}${db.toFixed(1)}dB`;
}
/** 耳机插孔彩蛋:大孔 → 上小孔 → 下小孔 */
const JACK_EGG_SEQUENCE = ["lg", "smTop", "smBot"] as const;
type JackId = (typeof JACK_EGG_SEQUENCE)[number];
const VU_BAR_COUNT = 20;
const EGG_DANCE_DURATION_MS = 5000;
const EGG_WAVE_DURATION_MS = 5000;
const EGG_WAVE_HALF_MS = EGG_WAVE_DURATION_MS / 2;
const EGG_BEAT_MS = 400;
const JACK_SEQUENCE_TIMEOUT_MS = 4000;
/** 旋律主音在 20 段 VU 上的位置(约 5s) */
const EGG_MELODY_LEAD = [
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 19, 17, 15, 13, 11, 9, 7, 5, 3, 1,
0, 4, 8, 12, 16, 19, 16, 12, 8, 4, 0, 2, 5, 9, 13, 17, 19, 14, 10, 6, 2,
0, 3, 7, 11, 15, 18, 15, 11, 7, 3,
];
function idleVuBarHeights(): number[] {
return Array.from({ length: VU_BAR_COUNT }, (_, i) =>
Math.max(0.12, Math.sin((i / VU_BAR_COUNT) * Math.PI) * 0.65 + 0.2),
);
}
/** 彩蛋律动:主峰随节拍移动,每拍起落,全柱叠加波纹 */
function vuHeightsForDance(elapsedMs: number): number[] {
const t = elapsedMs / 1000;
const beatIdx = Math.floor(elapsedMs / EGG_BEAT_MS) % EGG_MELODY_LEAD.length;
const lead = EGG_MELODY_LEAD[beatIdx];
const beatPhase = (elapsedMs % EGG_BEAT_MS) / EGG_BEAT_MS;
const kick = Math.sin(beatPhase * Math.PI);
return Array.from({ length: VU_BAR_COUNT }, (_, i) => {
const dist = Math.abs(i - lead);
let peak = 0.06;
if (dist === 0) peak = 1;
else if (dist === 1) peak = 0.82;
else if (dist === 2) peak = 0.62;
else if (dist === 3) peak = 0.42;
else if (dist === 4) peak = 0.26;
const ripple = 0.35 * Math.max(0, Math.sin(t * 2.75 + i * 0.62));
const sway = 0.1 * Math.sin(t * 1.4 + i * 0.35);
const h = 0.05 + peak * kick + ripple * (0.55 + kick * 0.45) + sway;
return Math.min(1, Math.max(0.05, h));
});
}
/** 彩蛋律动二:海浪形波从左→右,再右→左 */
function vuHeightsForWave(phaseElapsedMs: number): number[] {
const half = EGG_WAVE_HALF_MS;
const ltr = phaseElapsedMs < half;
const localMs = ltr ? phaseElapsedMs : phaseElapsedMs - half;
const t = localMs / 1000;
const direction = ltr ? 1 : -1;
const speed = 0.75;
return Array.from({ length: VU_BAR_COUNT }, (_, i) => {
const phase = t * speed * Math.PI * 2 - direction * i * 0.72;
const wave = (Math.sin(phase) + 1) / 2;
const foam = 0.06 + 0.05 * Math.sin(i * 0.28 + t * 0.5);
return Math.min(1, Math.max(0.06, foam + wave * 0.9));
});
}
// ── iOS-style Toggle ──
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
);
}
// ── List row ──
function ListRow({
icon, label, value, onClick, toggle, checked, onToggle, thumbnail,
}: {
icon: React.ReactNode;
label: string;
value?: string;
onClick?: () => void;
toggle?: boolean;
checked?: boolean;
onToggle?: (v: boolean) => void;
thumbnail?: string;
}) {
return (
{icon}
{label}
{toggle ? (
{})} />
) : (
{thumbnail && (

)}
{value &&
{value}}
)}
);
}
export default function Home() {
const { isConnected, deviceState, setVolume, updateSetting, api, isDemoMode, ip } = useDevice();
const [, setLocation] = useLocation();
const powerActionRef = useRef(false);
const hpEqLastTapRef = useRef(null);
const hpEqNavigateTimerRef = useRef | null>(null);
const effectLastTapRef = useRef(null);
const effectNavigateTimerRef = useRef | null>(null);
const [powerConfirmOpen, setPowerConfirmOpen] = useState(false);
const [volumeConfirm, setVolumeConfirm] = useState(null);
const [easterEggActive, setEasterEggActive] = useState(false);
const [vuHeights, setVuHeights] = useState(idleVuBarHeights);
const jackProgressRef = useRef(0);
const jackResetTimerRef = useRef | null>(null);
const eggRafRef = useRef(null);
/** 下次三连击成功时播放的律动:1=旋律,2=海浪(各需单独三连击触发) */
const jackEggNextRef = useRef<1 | 2>(1);
const homeText = useMemo((): HomeLocale => {
if (!deviceState) return {} as HomeLocale;
const loc =
deviceState.language === 0 ? localeEn : deviceState.language === 1 ? localeZhHK : localeZh;
return (loc.home ?? {}) as HomeLocale;
}, [deviceState]);
const volumeConfirmCopy = useMemo(() => {
if (volumeConfirm === "min") {
return {
title: homeText.volumeMinConfirmTitle ?? "确认将音量设为最小?",
desc:
homeText.volumeMinConfirmDesc ??
"耳机音量将调至最低(静音),是否继续?",
ok: homeText.volumeMinConfirmOk ?? "设为最小",
};
}
if (volumeConfirm === "max") {
return {
title: homeText.volumeMaxConfirmTitle ?? "确认将音量设为最大?",
desc:
homeText.volumeMaxConfirmDesc ??
"耳机音量将调至最大,是否继续?",
ok: homeText.volumeMaxConfirmOk ?? "设为最大",
};
}
return null;
}, [volumeConfirm, homeText]);
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
const isDragging = useRef(false);
const volumeSliderRef = useRef(null);
const volumeSliderDragAllowedRef = useRef(false);
const knobDraggingRef = useRef(false);
const knobActivePointerIdRef = useRef(null);
const knobStartAngleRef = useRef(0);
const knobStartVolRef = useRef(0);
const knobLastVolRef = useRef(localVol);
useEffect(() => {
if (!isDragging.current && deviceState?.volume !== undefined) {
setLocalVol(deviceState.volume);
}
}, [deviceState?.volume]);
useEffect(() => {
knobLastVolRef.current = localVol;
}, [localVol]);
const isPointerOnVolumeSliderThumb = useCallback((clientX: number) => {
const el = volumeSliderRef.current;
if (!el) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0) return false;
const thumbCenterX = rect.left + (localVol / 200) * rect.width;
const hitRadius = Math.max(28, rect.width * 0.05);
return Math.abs(clientX - thumbCenterX) <= hitRadius;
}, [localVol]);
const handlePowerOff = useCallback(async () => {
if (powerActionRef.current) return;
powerActionRef.current = true;
try {
if (!isDemoMode && api) {
try {
await api.powerOff();
} catch {
// 设备关机后可能已无响应,仍保留当前页面与连接状态
}
}
toast.success(homeText.powerOffToastSent ?? "已发送关机指令");
} finally {
powerActionRef.current = false;
}
}, [api, isDemoMode, homeText]);
/** HP-EQ 圆钮:单击在短延迟后切换 peqEnable(避免与双击抢);280ms 内第二次点击则取消开关并进入 /eq */
const HP_EQ_DOUBLE_CLICK_MS = 280;
const handleHpEqCircleClick = useCallback(() => {
const now = Date.now();
const last = hpEqLastTapRef.current;
if (last !== null && now - last < HP_EQ_DOUBLE_CLICK_MS) {
hpEqLastTapRef.current = null;
if (hpEqNavigateTimerRef.current) {
clearTimeout(hpEqNavigateTimerRef.current);
hpEqNavigateTimerRef.current = null;
}
setLocation("/eq");
return;
}
hpEqLastTapRef.current = now;
if (hpEqNavigateTimerRef.current) clearTimeout(hpEqNavigateTimerRef.current);
hpEqNavigateTimerRef.current = setTimeout(() => {
hpEqNavigateTimerRef.current = null;
hpEqLastTapRef.current = null;
const bypassOn = (deviceState?.dsp_enable ?? 0) === 0;
if (bypassOn) {
void updateSetting({ peqEnable: 1, dsp_enable: 1 });
} else {
const on = (deviceState?.peqEnable ?? 0) === 1;
void updateSetting({ peqEnable: on ? 0 : 1 });
}
}, HP_EQ_DOUBLE_CLICK_MS);
}, [deviceState?.peqEnable, deviceState?.dsp_enable, updateSetting, setLocation]);
/** Effect 圆钮:单击短延迟后切换 audio_enable;280ms 内第二次点击则进入 /effects */
const handleEffectCircleClick = useCallback(() => {
const now = Date.now();
const last = effectLastTapRef.current;
if (last !== null && now - last < HP_EQ_DOUBLE_CLICK_MS) {
effectLastTapRef.current = null;
if (effectNavigateTimerRef.current) {
clearTimeout(effectNavigateTimerRef.current);
effectNavigateTimerRef.current = null;
}
setLocation("/effects");
return;
}
effectLastTapRef.current = now;
if (effectNavigateTimerRef.current) clearTimeout(effectNavigateTimerRef.current);
effectNavigateTimerRef.current = setTimeout(() => {
effectNavigateTimerRef.current = null;
effectLastTapRef.current = null;
const bypassOn = (deviceState?.dsp_enable ?? 0) === 0;
if (bypassOn) {
void updateSetting({ audio_enable: 1, dsp_enable: 1 });
} else {
const on = (deviceState?.audio_enable ?? 0) === 1;
void updateSetting({ audio_enable: on ? 0 : 1 });
}
}, HP_EQ_DOUBLE_CLICK_MS);
}, [deviceState?.audio_enable, deviceState?.dsp_enable, updateSetting, setLocation]);
useEffect(() => {
return () => {
if (hpEqNavigateTimerRef.current) clearTimeout(hpEqNavigateTimerRef.current);
if (effectNavigateTimerRef.current) clearTimeout(effectNavigateTimerRef.current);
};
}, []);
const runJackEggAnimation = useCallback(
(mode: "dance" | "wave") => {
setEasterEggActive(true);
setVuHeights(mode === "dance" ? vuHeightsForDance(0) : vuHeightsForWave(0));
jackProgressRef.current = 0;
const duration = mode === "dance" ? EGG_DANCE_DURATION_MS : EGG_WAVE_DURATION_MS;
const start = performance.now();
const tick = (now: number) => {
const elapsed = now - start;
if (elapsed >= duration) {
setEasterEggActive(false);
setVuHeights(idleVuBarHeights());
eggRafRef.current = null;
return;
}
setVuHeights(
mode === "dance" ? vuHeightsForDance(elapsed) : vuHeightsForWave(elapsed),
);
eggRafRef.current = requestAnimationFrame(tick);
};
if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
eggRafRef.current = requestAnimationFrame(tick);
},
[],
);
const onJackClick = useCallback(
(jack: JackId) => {
if (easterEggActive) return;
const expected = JACK_EGG_SEQUENCE[jackProgressRef.current];
if (jack === expected) {
jackProgressRef.current += 1;
if (jackProgressRef.current >= JACK_EGG_SEQUENCE.length) {
if (jackEggNextRef.current === 1) {
runJackEggAnimation("dance");
jackEggNextRef.current = 2;
} else {
runJackEggAnimation("wave");
jackEggNextRef.current = 1;
}
}
} else {
jackProgressRef.current = jack === JACK_EGG_SEQUENCE[0] ? 1 : 0;
}
if (jackResetTimerRef.current) clearTimeout(jackResetTimerRef.current);
jackResetTimerRef.current = setTimeout(() => {
jackProgressRef.current = 0;
}, JACK_SEQUENCE_TIMEOUT_MS);
},
[easterEggActive, runJackEggAnimation],
);
useEffect(() => {
return () => {
if (jackResetTimerRef.current) clearTimeout(jackResetTimerRef.current);
if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
};
}, []);
const volumePassthroughActive = isHomeVolumeLockedByPassthrough(
deviceState?.dacVolumeDirect,
deviceState?.output,
);
const volumePassthroughModeLabel =
deviceState?.dacVolumeDirect === 1
? (homeText.volumePassthroughMode0dB ?? "0dB")
: deviceState?.dacVolumeDirect === 2
? (homeText.volumePassthroughMode12dB ?? "-12dB")
: "";
const notifyVolumePassthroughBlocked = useCallback(() => {
const template =
homeText.volumePassthroughBlocked ??
"当前为音量直通模式({mode}),无法调节耳机音量。请在音频设置中关闭直通或改为「关闭」。";
toast.info(template.replace("{mode}", volumePassthroughModeLabel));
}, [homeText, volumePassthroughModeLabel]);
const blockVolumeAdjust = useCallback(() => {
if (!volumePassthroughActive) return false;
notifyVolumePassthroughBlocked();
return true;
}, [volumePassthroughActive, notifyVolumePassthroughBlocked]);
const applyVolume = useCallback(
(v: number) => {
if (blockVolumeAdjust()) return;
setVolume(v);
},
[blockVolumeAdjust, setVolume],
);
if (!isConnected) return ;
const ds = deviceState!;
const inputLabel = INPUT_LABELS[ds.input] ?? "USB-C";
const outputLabel = OUTPUT_LABELS[ds.output] ?? "Headset";
const effectOn = ds.audio_enable === 1;
const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`;
const bypassOn = (ds.dsp_enable ?? 0) === 0;
// Home 的 volume 取值为 0..200,对应 dB = (v - 200) / 2
// 因此 1 个 volume 单位 = 0.5dB
// AudioPage 的 soundStep:0->0.5dB, 1->1dB, 2->2dB, 3->3dB
// 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6
const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1;
// Slider fill % (0-200 → 0-100%)
const fillPct = (localVol / 200) * 100;
// Knob angle (map 0..200 -> -135..+135 degrees)
const knobAngle = -135 + (localVol / 200) * 270;
const pointAngle = (clientX: number, clientY: number, rect: DOMRect) => {
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
return Math.atan2(clientY - cy, clientX - cx);
};
const normalizeDelta = (delta: number) => {
// Wrap to [-PI, PI] to avoid jumps across the -PI/PI boundary.
const pi2 = Math.PI * 2;
let d = ((delta + Math.PI) % pi2) - Math.PI;
if (d < -Math.PI) d += pi2;
return d;
};
const beginKnobDrag = (e: React.PointerEvent) => {
if (blockVolumeAdjust()) return;
const el = e.currentTarget;
const rect = el.getBoundingClientRect();
knobDraggingRef.current = true;
knobActivePointerIdRef.current = e.pointerId;
knobStartAngleRef.current = pointAngle(e.clientX, e.clientY, rect);
knobStartVolRef.current = knobLastVolRef.current;
isDragging.current = true;
el.setPointerCapture(e.pointerId);
};
/** 拖动中只更新本地 UI,不调用 setVolume(避免来回旋时频繁打接口) */
const moveKnobDrag = (e: React.PointerEvent) => {
if (!knobDraggingRef.current || knobActivePointerIdRef.current !== e.pointerId) return;
const el = e.currentTarget;
const rect = el.getBoundingClientRect();
const a = pointAngle(e.clientX, e.clientY, rect);
const delta = normalizeDelta(a - knobStartAngleRef.current);
// 270deg travel -> full scale; 1 rad maps to ~200/(1.5*pi) volume steps
const stepsPerRad = 200 / (Math.PI * 1.5);
const next = Math.max(0, Math.min(200, Math.round(knobStartVolRef.current + delta * stepsPerRad)));
setLocalVol(next);
knobLastVolRef.current = next;
};
const commitKnobDragEnd = (e: React.PointerEvent) => {
if (!knobDraggingRef.current || knobActivePointerIdRef.current !== e.pointerId) return;
knobDraggingRef.current = false;
knobActivePointerIdRef.current = null;
isDragging.current = false;
applyVolume(knobLastVolRef.current);
try {
e.currentTarget.releasePointerCapture(e.pointerId);
} catch {
// ignore
}
};
const endKnobDrag = (e: React.PointerEvent) => {
commitKnobDragEnd(e);
};
/** 异常丢失 capture 时仍提交一次,避免只松手未触发 pointerup 时漏提交 */
const lostKnobPointerCapture = (e: React.PointerEvent) => {
if (!knobDraggingRef.current || knobActivePointerIdRef.current !== e.pointerId) return;
commitKnobDragEnd(e);
};
return (
{/* ── Device render image ── */}
{/* Stylized device illustration */}
{/* Device body */}
{/* Left ports */}
{/* Big hole (left) — 彩蛋第 1 步 */}
{/* Two small holes (right, stacked) */}
{/* VU display area */}
{ds.audioFormat}
{volToDB(localVol)}
{/* Mini VU bars */}
{vuHeights.map((h, i) => {
const isHot = i > 16;
return (
13 ? "#f59e0b" : "#00FFF6",
opacity: easterEggActive ? 0.95 : 0.7,
transition: easterEggActive
? "none"
: "height 200ms ease-out, opacity 200ms ease-out",
}}
/>
);
})}
{/* Right knob */}
{/* Brand label */}
{deviceState?.device ?? "LUXSIN"}
{/* ── Volume slider ── */}
{
if (volumePassthroughActive) {
e.preventDefault();
e.stopPropagation();
notifyVolumePassthroughBlocked();
}
}}
>
{volumePassthroughActive && (
{homeText.volumePassthroughHint ?? "当前为音量直通模式,音量不可调节"}
{volumePassthroughModeLabel ? ` (${volumePassthroughModeLabel})` : ""}
)}
{
if (blockVolumeAdjust()) return;
setVolumeConfirm("min");
}}
className="text-white/50 active:text-white transition-colors flex-shrink-0 disabled:opacity-40"
>
{/* Track fill - 根据按钮位置调整 */}
{
if (volumePassthroughActive) return;
if (!volumeSliderDragAllowedRef.current) {
e.target.value = String(localVol);
return;
}
const v = Number(e.target.value);
setLocalVol(v);
}}
onPointerDown={(e) => {
if (volumePassthroughActive) return;
const onThumb = isPointerOnVolumeSliderThumb(e.clientX);
volumeSliderDragAllowedRef.current = onThumb;
if (!onThumb) {
e.preventDefault();
return;
}
isDragging.current = true;
}}
onPointerUp={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!allowed || volumePassthroughActive) return;
applyVolume(Number(e.currentTarget.value));
}}
onPointerCancel={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onBlur={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onKeyUp={(e) => {
const k = e.key;
if (
k === "ArrowLeft" ||
k === "ArrowRight" ||
k === "ArrowUp" ||
k === "ArrowDown" ||
k === "Home" ||
k === "End" ||
k === "PageUp" ||
k === "PageDown"
) {
applyVolume(Number(e.currentTarget.value));
}
}}
/>
{
if (blockVolumeAdjust()) return;
setVolumeConfirm("max");
}}
className="text-white/50 active:text-white transition-colors flex-shrink-0 disabled:opacity-40"
>
{/* dB value */}
{
if (blockVolumeAdjust()) return;
const next = Math.max(0, Math.min(200, localVol - fineVolumeStep));
setLocalVol(next);
applyVolume(next);
}}
>
{volToDB(localVol)}
= 200}
className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed"
style={{
background: "rgba(44,44,46,0.6)",
border: "1px solid rgba(255,255,255,0.08)",
color: "rgba(255,255,255,0.55)",
}}
onClick={() => {
if (blockVolumeAdjust()) return;
const next = Math.max(0, Math.min(200, localVol + fineVolumeStep));
setLocalVol(next);
applyVolume(next);
}}
>
{/* Bluetooth info — only when input source is Bluetooth */}
{ds.input === INPUT_BLUETOOTH_INDEX && ds.bt_status !== 0 && (
{ds.bt_srcname}
{ds.bt_title && (
{ds.bt_title}
{ds.bt_artist && - {ds.bt_artist}}
)}
{/* Playback controls */}
updateSetting({ bt_next: 0 })}
className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-95"
style={{ background: "rgba(0,255,246,0.15)", border: "1px solid rgba(0,255,246,0.3)" }}>
updateSetting({ bt_play: 1 })}
className="w-12 h-12 rounded-full flex items-center justify-center transition-all active:scale-95"
style={{ background: "transparent", border: "1px solid rgba(0,255,246,0.5)", boxShadow: "0 0 12px rgba(0,255,246,0.4)" }}>
{ds.bt_status === 1 ? : }
updateSetting({ bt_next: 1 })}
className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-95"
style={{ background: "rgba(0,255,246,0.15)", border: "1px solid rgba(0,255,246,0.3)" }}>
)}
{/* ── 4 quick-action circle buttons ── */}
{/* Power */}
setPowerConfirmOpen(true)}
>
{homeText.power ?? "电源"}
{/* EQ */}
HP-EQ
{homeText.doubleClickHint ?? "Double-click to enter"}
{/* Effect */}
Effect
{homeText.doubleClickHint ?? "Double-click to enter"}
{/* Bypass (DSP) */}
updateSetting({ dsp_enable: bypassOn ? 1 : 0 })}
>
Bypass
{/* ── iOS list rows ── */}
}
label={homeText.input ?? "输入源"}
value={inputLabel}
onClick={() => setLocation("/io")}
/>
}
label={homeText.output ?? "输出端口"}
value={outputLabel}
onClick={() => setLocation("/io")}
/>
}
label={homeText.audioSet ?? "音频设置"}
onClick={() => setLocation("/audio")}
/>
}
label={homeText.systemSet ?? "系统设置"}
onClick={() => setLocation("/system")}
/>
}
label={homeText.vu ?? "VU表"}
value={vuLabel}
thumbnail={`${import.meta.env.BASE_URL}images/vu/vu${(ds.vu ?? 0) + 1}.png`}
onClick={() => setLocation("/vu")}
/>
{
if (!open) setVolumeConfirm(null);
}}
>
{volumeConfirmCopy?.title}
{volumeConfirmCopy?.desc}
{homeText.powerOffConfirmCancel ?? "取消"}
{
if (volumeConfirm === "min") {
setLocalVol(0);
applyVolume(0);
} else if (volumeConfirm === "max") {
setLocalVol(200);
applyVolume(200);
}
setVolumeConfirm(null);
}}
>
{volumeConfirmCopy?.ok}
{homeText.powerOffConfirmTitle ?? "确认关闭电源?"}
{homeText.powerOffConfirmDesc ??
"设备将关机并断开与本应用的连接,是否继续?"}
{homeText.powerOffConfirmCancel ?? "取消"}
void handlePowerOff()}
>
{homeText.powerOffConfirmOk ?? "关闭电源"}
{/* Bottom nav */}
);
}