diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 08d29a8..f46052e 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -89,6 +89,7 @@ export interface DeviceState { dacGain: number; dacArc: number; dacImpedance: number; + dreMode: number; dacVolumeDirect: number; analogGain: number; effect_enable: number; @@ -213,7 +214,9 @@ export class LuxsinAPI { const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`); const text = await response.text(); const decoded = decodeCustomBase64(text.trim()); - return JSON.parse(decoded) as DeviceState; + const state = JSON.parse(decoded) as DeviceState; + console.log("[syncData]", state); + return state; } catch (error) { this.maybeRedirectForHttpsCert(error); throw error; @@ -333,6 +336,15 @@ export function parseFirmwareVersion(version: unknown): number { return Number(parts[parts.length - 1]); } +/** UI display: build number 26 → `1.0.0.26`. Already-prefixed values are unchanged. */ +export function formatFirmwareVersionDisplay(version: unknown): string { + if (version === null || version === undefined) return "—"; + const raw = String(version).trim(); + if (!raw) return "—"; + if (/^1\.0\.0\./i.test(raw)) return raw; + return `1.0.0.${raw}`; +} + /** Max `bootSound` index available for the current firmware. */ export function getBootSoundMaxIndex(firmwareVersion: number): number { return firmwareVersion >= 26 ? 10 : 6; @@ -387,6 +399,7 @@ export const MOCK_DEVICE_STATE: DeviceState = { dacGain: 0, dacArc: 0, dacImpedance: 0, + dreMode: 0, dacVolumeDirect: 0, analogGain: 0, effect_enable: 0, diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 69773a5..ac0c5d2 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -24,7 +24,9 @@ "volumeMaxConfirmTitle": "Set volume to maximum?", "volumeMaxConfirmDesc": "Volume will be set to the highest level. Continue?", "volumeMaxConfirmOk": "Set to maximum", - "powerOffToastDemo": "Demo mode: disconnected from the device.", + "volumePassthroughLockedToast": "Volume is in passthrough mode and cannot be adjusted.", + "volumePassthroughLockedHint": "Volume is in passthrough mode and cannot be adjusted.", + "powerOffToastDemo": "Demo mode: shutdown command sent.", "powerOffToastSent": "Shutdown command sent.", "doubleClickHint": "Double-click to enter", "backToLegacy": "Legacy version" @@ -382,6 +384,36 @@ } ] }, + "autoImpedance": { + "label": "Auto Impedance Detection", + "options": [ + { "index": 0, "label": "Off" }, + { "index": 1, "label": "On" } + ] + }, + "dreMode": { + "label": "Dynamic Range Enhancement", + "options": [ + { "index": 0, "label": "Off" }, + { "index": 1, "label": "On" } + ] + }, + "dacVolumeDirect": { + "label": "Pre-out Volume passthrough", + "options": [ + { "index": 0, "label": "Off" }, + { "index": 1, "label": "0dB" }, + { "index": 2, "label": "-12dB" } + ] + }, + "dacVolumeDirectConfirm": { + "title0db": "Enable 0 dB passthrough?", + "desc0db": "Passthrough mode sets volume to maximum and disables volume adjustment.", + "title12db": "Enable -12 dB passthrough?", + "desc12db": "Passthrough mode sets volume to -12 dB and disables volume adjustment.", + "confirm": "Confirm", + "cancel": "Cancel" + }, "volumeStep": "Volume step", "bootVolume": { "label": "Boot volume", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index d5bae9f..ad50abd 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -24,7 +24,9 @@ "volumeMaxConfirmTitle": "確認將音量設為最大?", "volumeMaxConfirmDesc": "音量將調至最大,是否繼續?", "volumeMaxConfirmOk": "設為最大", - "powerOffToastDemo": "演示模式:已中斷與裝置的連線", + "volumePassthroughLockedToast": "目前為音量直通模式,無法調節音量", + "volumePassthroughLockedHint": "目前為音量直通模式,無法調節音量", + "powerOffToastDemo": "演示模式:已傳送關機指令", "powerOffToastSent": "已傳送關機指令", "doubleClickHint": "按兩下進入", "backToLegacy": "返回舊版" @@ -291,6 +293,36 @@ { "index": 2, "label": "高" } ] }, + "autoImpedance": { + "label": "自動檢測耳機阻抗", + "options": [ + { "index": 0, "label": "關閉" }, + { "index": 1, "label": "打開" } + ] + }, + "dreMode": { + "label": "動態範圍增強", + "options": [ + { "index": 0, "label": "關閉" }, + { "index": 1, "label": "打開" } + ] + }, + "dacVolumeDirect": { + "label": "前級輸出音量直通模式", + "options": [ + { "index": 0, "label": "關閉" }, + { "index": 1, "label": "0dB" }, + { "index": 2, "label": "-12dB" } + ] + }, + "dacVolumeDirectConfirm": { + "title0db": "開啟 0dB 直通模式?", + "desc0db": "直通模式會將音量調到最大,並且不可調節。", + "title12db": "開啟 -12dB 直通模式?", + "desc12db": "直通模式會將音量調到 -12dB,並且不可調節。", + "confirm": "確認", + "cancel": "取消" + }, "volumeStep": "音量級距", "bootVolume": { "label": "開機音量", diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index d807580..85afefd 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -24,7 +24,9 @@ "volumeMaxConfirmTitle": "确认将音量设为最大?", "volumeMaxConfirmDesc": "音量将调至最大,是否继续?", "volumeMaxConfirmOk": "设为最大", - "powerOffToastDemo": "演示模式:已断开与设备的连接", + "volumePassthroughLockedToast": "当前为音量直通模式,无法调节音量", + "volumePassthroughLockedHint": "当前为音量直通模式,无法调节音量", + "powerOffToastDemo": "演示模式:已发送关机指令", "powerOffToastSent": "已发送关机指令", "doubleClickHint": "双击进入", "backToLegacy": "返回旧版" @@ -291,6 +293,36 @@ { "index": 2, "label": "高" } ] }, + "autoImpedance": { + "label": "自动检测耳机阻抗", + "options": [ + { "index": 0, "label": "关闭" }, + { "index": 1, "label": "打开" } + ] + }, + "dreMode": { + "label": "动态范围增强", + "options": [ + { "index": 0, "label": "关闭" }, + { "index": 1, "label": "打开" } + ] + }, + "dacVolumeDirect": { + "label": "前级输出音量直通模式", + "options": [ + { "index": 0, "label": "关闭" }, + { "index": 1, "label": "0dB" }, + { "index": 2, "label": "-12dB" } + ] + }, + "dacVolumeDirectConfirm": { + "title0db": "开启 0dB 直通模式?", + "desc0db": "直通模式会将音量调到最大,并且不可调节。", + "title12db": "开启 -12dB 直通模式?", + "desc12db": "直通模式会将音量调到 -12dB,并且不可调节。", + "confirm": "确认", + "cancel": "取消" + }, "volumeStep": "音量幅度", "bootVolume": { "label": "开机音量", diff --git a/client/src/pages/AudioPage.tsx b/client/src/pages/AudioPage.tsx index b23cc41..c64cedd 100644 --- a/client/src/pages/AudioPage.tsx +++ b/client/src/pages/AudioPage.tsx @@ -70,6 +70,15 @@ function SectionHeader({ title }: { title: string }) { ); } +function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { + return ( + + ); +} + function ListRow({ label, value, onClick }: { label: string; value: string; onClick?: () => void }) { return ( @@ -211,16 +246,20 @@ export default function EffectsPage() { min={0} max={100} step={1} + disabled={!widthOn} onChange={(v) => { + if (!widthOn) return; isDragging.current = true; setLocalWidth(Math.round(v)); }} onRelease={(v) => { + if (!widthOn) return; isDragging.current = false; const w = Math.round(v); setLocalWidth(w); updateSetting({ width_value: w }); - }} /> + }} + /> {/* ── 交叉反馈 ── */} @@ -230,8 +269,17 @@ export default function EffectsPage() { updateSetting({ crossfeed_enable: v ? 1 : 0 })} /> diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index f084df6..ac9b3e2 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -63,6 +63,8 @@ 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; +/** 与 IOPage 输出选项 index 2(耳机)一致 */ +const HEADPHONE_OUTPUT_INDEX = 2; /** 旋律主音在 20 段 VU 上的位置(约 5s) */ const EGG_MELODY_LEAD = [ @@ -164,7 +166,7 @@ function ListRow({ } export default function Home() { - const { isConnected, deviceState, setVolume, updateSetting, api, disconnect, isDemoMode, ip } = useDevice(); + const { isConnected, deviceState, setVolume, updateSetting, api, isDemoMode, ip } = useDevice(); const [, setLocation] = useLocation(); const powerActionRef = useRef(false); const hpEqLastTapRef = useRef(null); @@ -233,23 +235,21 @@ export default function Home() { powerActionRef.current = true; try { if (isDemoMode) { - toast.info(homeText.powerOffToastDemo ?? "演示模式:已断开与设备的连接"); - disconnect(); + toast.info(homeText.powerOffToastDemo ?? "演示模式:已发送关机指令"); return; } if (api) { try { await api.powerOff(); } catch { - // 设备可能已掉线或未返回响应,仍断开本地会话 + // 设备可能已关机或未返回响应,仍保留当前页面与会话状态 } } - disconnect(); toast.success(homeText.powerOffToastSent ?? "已发送关机指令"); } finally { powerActionRef.current = false; } - }, [api, disconnect, isDemoMode, homeText]); + }, [api, isDemoMode, homeText]); /** HP-EQ 圆钮:单击在短延迟后切换 peqEnable(避免与双击抢);280ms 内第二次点击则取消开关并进入 /eq */ const HP_EQ_DOUBLE_CLICK_MS = 280; @@ -393,6 +393,24 @@ export default function Home() { // 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6 const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1; + const volumePassthroughActive = + (ds.dacVolumeDirect ?? 0) === 1 || (ds.dacVolumeDirect ?? 0) === 2; + const volumeControlsLocked = + volumePassthroughActive && ds.output !== HEADPHONE_OUTPUT_INDEX; + + const volumePassthroughHint = + homeText.volumePassthroughLockedHint ?? + homeText.volumePassthroughLockedToast ?? + "当前为音量直通模式,无法调节音量"; + + const guardVolumeChange = useCallback( + (action: () => void) => { + if (volumeControlsLocked) return; + action(); + }, + [volumeControlsLocked], + ); + // Slider fill % (0-200 → 0-100%) const fillPct = (localVol / 200) * 100; // Knob angle (map 0..200 -> -135..+135 degrees) @@ -413,6 +431,7 @@ export default function Home() { }; const beginKnobDrag = (e: React.PointerEvent) => { + if (volumeControlsLocked) return; const el = e.currentTarget; const rect = el.getBoundingClientRect(); knobDraggingRef.current = true; @@ -442,6 +461,7 @@ export default function Home() { knobDraggingRef.current = false; knobActivePointerIdRef.current = null; isDragging.current = false; + if (volumeControlsLocked) return; setVolume(knobLastVolRef.current); try { e.currentTarget.releasePointerCapture(e.pointerId); @@ -551,7 +571,10 @@ export default function Home() { {/* Right knob */}
+ {volumeControlsLocked && ( +

+ {volumePassthroughHint} +

+ )}
-
+
{/* Track fill - 根据按钮位置调整 */}
+ style={{ + width: `${fillPct}%`, + background: volumeControlsLocked ? "rgba(148,163,184,0.7)" : "#00FFF6", + boxShadow: volumeControlsLocked ? "none" : "0 0 8px rgba(0,255,246,0.5)", + }} + /> { + if (volumeControlsLocked) return; const v = Number(e.target.value); setLocalVol(v); }} onPointerDown={() => { + if (volumeControlsLocked) return; isDragging.current = true; }} onPointerUp={(e) => { isDragging.current = false; + if (volumeControlsLocked) return; setVolume(Number(e.currentTarget.value)); }} onPointerCancel={(e) => { isDragging.current = false; + if (volumeControlsLocked) return; setVolume(Number(e.currentTarget.value)); }} onBlur={(e) => { isDragging.current = false; + if (volumeControlsLocked) { + return; + } setVolume(Number(e.currentTarget.value)); }} onKeyUp={(e) => { + if (volumeControlsLocked) return; const k = e.key; if ( k === "ArrowLeft" || @@ -646,8 +699,12 @@ export default function Home() {
+ + { + if (!open) setVolumeDirectConfirmIdx(null); + }} + > + + + {volumeDirectDialogCopy?.title} + + {volumeDirectDialogCopy?.desc} + + + + + {volumeDirectConfirmText.cancel ?? "取消"} + + { + const idx = volumeDirectConfirmIdx; + setVolumeDirectConfirmIdx(null); + if (idx !== null) void commitSelect(idx); + }} + > + {volumeDirectConfirmText.confirm ?? "确认"} + + + +
); } diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx index 5a033cf..00cd4f3 100644 --- a/client/src/pages/SystemPage.tsx +++ b/client/src/pages/SystemPage.tsx @@ -14,6 +14,7 @@ import { useLocation } from "wouter"; import { useEffect } from "react"; import BottomNav from "@/components/BottomNav"; import { navigateToSelect } from "./SelectPage"; +import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi"; import localeZh from "@/locales/data-zh.json"; import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; @@ -215,7 +216,7 @@ export default function SystemPage() {
{ui.firmwareVersion} - {ds?.version ?? "—"} + {formatFirmwareVersionDisplay(ds?.version)}
{ui.macAddress}