/* ============================================================ HOME — Luxsin X9 Controller (Main Dashboard) Design: Reference luxsin_x8_首页.png Layout (top to bottom): 1. TopBar: device name + settings icon 2. Device status card (运行中/待机/充电 + 电量/模式/环保参数) 3. Device render image (hardware photo) 4. Volume slider with dB display 5. 4 quick-action circle buttons (电源/EQ/HP-EQ/Bypass) 6. iOS list rows (输入源/输出端口/Effect/音频设置/VU表) ============================================================ */ import { useDevice } from "@/contexts/DeviceContext"; import ConnectScreen from "@/components/ConnectScreen"; import BottomNav from "@/components/BottomNav"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { INPUT_LABELS, OUTPUT_LABELS } from "@/lib/luxsinApi"; import { cn } from "@/lib/utils"; import { ChevronRight, Power, Settings, Sliders, Activity, Gauge, ChevronDown, Zap, Wifi, Bluetooth, } 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 "-∞"; const db = (v - 200) / 2; return `${db >= 0 ? "+" : ""}${db.toFixed(1)}dB`; } // ── 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, }: { icon: React.ReactNode; label: string; value?: string; onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void; }) { return (
{icon}
{label}
{toggle ? ( {})} /> ) : (
{value && {value}}
)}
); } export default function Home() { const { isConnected, deviceState, setVolume, updateSetting, api, disconnect, isDemoMode } = useDevice(); const [, setLocation] = useLocation(); const powerActionRef = useRef(false); const [powerConfirmOpen, setPowerConfirmOpen] = useState(false); 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 [localVol, setLocalVol] = useState(deviceState?.volume ?? 100); const isDragging = useRef(false); const knobDraggingRef = useRef(false); 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 handleVolChange = useCallback((e: React.ChangeEvent) => { const v = Number(e.target.value); setLocalVol(v); setVolume(v); }, [setVolume]); const handlePowerOff = useCallback(async () => { if (powerActionRef.current) return; powerActionRef.current = true; try { if (isDemoMode) { toast.info(homeText.powerOffToastDemo ?? "演示模式:已断开与设备的连接"); disconnect(); return; } if (api) { try { await api.powerOff(); } catch { // 设备可能已掉线或未返回响应,仍断开本地会话 } } disconnect(); toast.success(homeText.powerOffToastSent ?? "已发送关机指令"); } finally { powerActionRef.current = false; } }, [api, disconnect, isDemoMode, homeText]); 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 audioOn = ds.audio_enable === 1; const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`; const bypassOn = (ds.dsp_enable ?? 0) === 0; // 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) => { const el = e.currentTarget; const rect = el.getBoundingClientRect(); knobDraggingRef.current = true; knobStartAngleRef.current = pointAngle(e.clientX, e.clientY, rect); knobStartVolRef.current = knobLastVolRef.current; isDragging.current = true; el.setPointerCapture(e.pointerId); }; const moveKnobDrag = (e: React.PointerEvent) => { if (!knobDraggingRef.current) 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); setVolume(next); }; const endKnobDrag = (e: React.PointerEvent) => { if (!knobDraggingRef.current) return; knobDraggingRef.current = false; isDragging.current = false; try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { // ignore } }; return (
{/* ── Top bar ── */}
{/* ── Device render image ── */}
{/* Stylized device illustration */}
{/* Device body */}
{/* Left ports */}
{/* Big hole (left) */}
{/* Two small holes (right, stacked) */}
{/* VU display area */}
{ds.audioFormat} {volToDB(localVol)}
{/* Mini VU bars */}
{Array.from({ length: 20 }).map((_, i) => { const h = Math.max(0.1, Math.sin((i / 20) * Math.PI) * 0.8 + Math.random() * 0.2); const isHot = i > 16; return (
13 ? "#f59e0b" : "#00FFF6", opacity: 0.7, }} /> ); })}
{/* Right knob */}
{/* Brand label */}
LUXSIN
{/* ── Volume slider ── */}
{/* Track fill - 根据按钮位置调整 */}
{ isDragging.current = true; }} onMouseUp={() => { isDragging.current = false; }} onTouchStart={() => { isDragging.current = true; }} onTouchEnd={() => { isDragging.current = false; }} onChange={handleVolChange} />
{/* dB value */}
{volToDB(localVol)}
{/* Bluetooth info */} {ds.bt_status !== 0 && (
{ds.bt_srcname}
{ds.bt_title && (

{ds.bt_title} {ds.bt_artist && - {ds.bt_artist}}

)} {/* Playback controls */}
)}
{/* ── 4 quick-action circle buttons ── */}
{/* Power */}
{homeText.power ?? "电源"}
{/* EQ */}
HP-EQ
{/* Effect */}
Effect
{/* Bypass (DSP) */}
Bypass
{/* ── iOS list rows ── */}
} label={homeText.input ?? "输入源"} value={inputLabel} onClick={() => setLocation("/io")} /> } label={homeText.output ?? "输出端口"} value={outputLabel} onClick={() => setLocation("/io")} /> } label="Effect" value={effectOn ? "已开启" : undefined} toggle checked={effectOn} onToggle={(v) => updateSetting({ audio_enable: v ? 1 : 0 })} /> } label={homeText.audioSet ?? "音频设置"} onClick={() => setLocation("/audio")} /> } label={homeText.vu ?? "VU表"} value={vuLabel} onClick={() => setLocation("/vu")} />
{homeText.powerOffConfirmTitle ?? "确认关闭电源?"} {homeText.powerOffConfirmDesc ?? "设备将关机并断开与本应用的连接,是否继续?"} {homeText.powerOffConfirmCancel ?? "取消"} void handlePowerOff()} > {homeText.powerOffConfirmOk ?? "关闭电源"} {/* Bottom nav */}
); }