修复bug
This commit is contained in:
@@ -171,6 +171,9 @@ export interface PeqApplyPayload {
|
|||||||
export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"];
|
export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"];
|
||||||
export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"];
|
export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"];
|
||||||
|
|
||||||
|
/** Device `input` index for Bluetooth source. */
|
||||||
|
export const INPUT_BLUETOOTH_INDEX = 4;
|
||||||
|
|
||||||
/** Device `output` index for headphone / headset. */
|
/** Device `output` index for headphone / headset. */
|
||||||
export const OUTPUT_HEADSET_INDEX = 2;
|
export const OUTPUT_HEADSET_INDEX = 2;
|
||||||
export const LANGUAGE_LABELS = ["English", "繁體中文", "简体中文"];
|
export const LANGUAGE_LABELS = ["English", "繁體中文", "简体中文"];
|
||||||
@@ -345,6 +348,19 @@ export function parseFirmwareVersion(version: unknown): number {
|
|||||||
return Number(parts[parts.length - 1]);
|
return Number(parts[parts.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Display firmware version as X.Y.0.Z (e.g. 2005 → 2.0.0.5). */
|
||||||
|
export function formatFirmwareVersionDisplay(version: unknown): string {
|
||||||
|
if (version === null || version === undefined || String(version).trim() === "") {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const n = parseFirmwareVersion(version);
|
||||||
|
const major = Math.floor(n / 1000);
|
||||||
|
const minor = Math.floor((n % 1000) / 100);
|
||||||
|
const patch = 0;
|
||||||
|
const build = n % 10;
|
||||||
|
return `${major}.${minor}.${patch}.${build}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Pre-out volume passthrough mode selected (0dB or -12dB). */
|
/** Pre-out volume passthrough mode selected (0dB or -12dB). */
|
||||||
export function isVolumePassthroughActive(dacVolumeDirect: number | undefined): boolean {
|
export function isVolumePassthroughActive(dacVolumeDirect: number | undefined): boolean {
|
||||||
return dacVolumeDirect === 1 || dacVolumeDirect === 2;
|
return dacVolumeDirect === 1 || dacVolumeDirect === 2;
|
||||||
|
|||||||
@@ -404,11 +404,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"index": 4,
|
"index": 4,
|
||||||
"label": "De-emphasis filter"
|
"label": "Super Slow Roll-off"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"index": 5,
|
"index": 5,
|
||||||
"label": "Non-oversampling"
|
"label": "Low Dispersion Short Delay"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -326,8 +326,8 @@
|
|||||||
{ "index": 1, "label": "慢速衰減" },
|
{ "index": 1, "label": "慢速衰減" },
|
||||||
{ "index": 2, "label": "短延遲快速衰減" },
|
{ "index": 2, "label": "短延遲快速衰減" },
|
||||||
{ "index": 3, "label": "短延遲慢速衰減" },
|
{ "index": 3, "label": "短延遲慢速衰減" },
|
||||||
{ "index": 4, "label": "去重強調" },
|
{ "index": 4, "label": "超慢速滾降" },
|
||||||
{ "index": 5, "label": "非過采樣(NOS)" }
|
{ "index": 5, "label": "低離散短延遲" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dacGain": {
|
"dacGain": {
|
||||||
|
|||||||
@@ -326,8 +326,8 @@
|
|||||||
{ "index": 1, "label": "慢速滚降" },
|
{ "index": 1, "label": "慢速滚降" },
|
||||||
{ "index": 2, "label": "短延迟快速滚降" },
|
{ "index": 2, "label": "短延迟快速滚降" },
|
||||||
{ "index": 3, "label": "短延迟慢速滚降" },
|
{ "index": 3, "label": "短延迟慢速滚降" },
|
||||||
{ "index": 4, "label": "去重强调" },
|
{ "index": 4, "label": "超慢速滚降" },
|
||||||
{ "index": 5, "label": "非过采样(NOS)" }
|
{ "index": 5, "label": "低离散短延迟" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dacGain": {
|
"dacGain": {
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export default function AudioPage() {
|
|||||||
const bootSoundSelectIdx = Math.min(bootSoundVal, bootSoundMaxIdx);
|
const bootSoundSelectIdx = Math.min(bootSoundVal, bootSoundMaxIdx);
|
||||||
const BOOT_VOLUME_OPTIONS = bootVolumeOptions(dacText, bootSoundMaxIdx);
|
const BOOT_VOLUME_OPTIONS = bootVolumeOptions(dacText, bootSoundMaxIdx);
|
||||||
|
|
||||||
const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "去重强调", "非过采样(NOS)"]);
|
const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "超慢速滚降", "低离散短延迟"]);
|
||||||
const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]);
|
const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]);
|
||||||
const DAC_VOLUME_DIRECT_OPTIONS = optionLabels(dacText.dacVolumeDirect?.options, ["关闭", "0dB", "-12dB"]);
|
const DAC_VOLUME_DIRECT_OPTIONS = optionLabels(dacText.dacVolumeDirect?.options, ["关闭", "0dB", "-12dB"]);
|
||||||
const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]);
|
const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]);
|
||||||
|
|||||||
@@ -144,27 +144,31 @@ function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: bool
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CyanSlider({ value, min, max, step = 0.1, onChange, onRelease }: {
|
function CyanSlider({ value, min, max, step = 0.1, disabled = false, onChange, onRelease }: {
|
||||||
value: number;
|
value: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
disabled?: boolean;
|
||||||
onChange: (v: number) => void;
|
onChange: (v: number) => void;
|
||||||
/** Fires once when the user releases the thumb (pointer/mouse/touch). Read value from DOM to avoid stale React state. */
|
/** Fires once when the user releases the thumb (pointer/mouse/touch). Read value from DOM to avoid stale React state. */
|
||||||
onRelease?: (v: number) => void;
|
onRelease?: (v: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const fillPct = ((value - min) / (max - min)) * 100;
|
const fillPct = ((value - min) / (max - min)) * 100;
|
||||||
const emitRelease = (el: EventTarget | null) => {
|
const emitRelease = (el: EventTarget | null) => {
|
||||||
if (!(el instanceof HTMLInputElement)) return;
|
if (disabled || !(el instanceof HTMLInputElement)) return;
|
||||||
onRelease?.(Number(el.value));
|
onRelease?.(Number(el.value));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center w-full mt-3">
|
<div className={`relative flex items-center w-full mt-3 ${disabled ? "opacity-50 pointer-events-none" : ""}`}>
|
||||||
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
<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)" }} />
|
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} disabled={disabled}
|
||||||
className="cyan-slider relative z-10"
|
className="cyan-slider relative z-10"
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => {
|
||||||
|
if (disabled) return;
|
||||||
|
onChange(Number(e.target.value));
|
||||||
|
}}
|
||||||
onPointerUp={(e) => emitRelease(e.currentTarget)}
|
onPointerUp={(e) => emitRelease(e.currentTarget)}
|
||||||
onPointerCancel={(e) => emitRelease(e.currentTarget)}
|
onPointerCancel={(e) => emitRelease(e.currentTarget)}
|
||||||
onKeyUp={(e) => {
|
onKeyUp={(e) => {
|
||||||
@@ -359,8 +363,16 @@ export default function EffectsPage() {
|
|||||||
<IOSToggle checked={sceneOn} onChange={(v) => updateSetting({ effect_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={sceneOn} onChange={(v) => updateSetting({ effect_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
type="button"
|
||||||
onClick={() => goSelect(effectText.selectStyleTitle, sceneLabels, sceneVal, "effect_value")}>
|
disabled={!sceneOn}
|
||||||
|
className={`flex items-center justify-between w-full transition-opacity ${
|
||||||
|
sceneOn ? "active:opacity-70" : "cursor-default opacity-50"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!sceneOn) return;
|
||||||
|
goSelect(effectText.selectStyleTitle, sceneLabels, sceneVal, "effect_value");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="text-[15px] text-white/55">{sceneDisplay}</span>
|
<span className="text-[15px] text-white/55">{sceneDisplay}</span>
|
||||||
<ChevronRight size={16} className="ios-chevron" />
|
<ChevronRight size={16} className="ios-chevron" />
|
||||||
</button>
|
</button>
|
||||||
@@ -380,16 +392,20 @@ export default function EffectsPage() {
|
|||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
step={1}
|
step={1}
|
||||||
|
disabled={!widthOn}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
if (!widthOn) return;
|
||||||
isDragging.current = true;
|
isDragging.current = true;
|
||||||
setLocalWidth(Math.round(v));
|
setLocalWidth(Math.round(v));
|
||||||
}}
|
}}
|
||||||
onRelease={(v) => {
|
onRelease={(v) => {
|
||||||
|
if (!widthOn) return;
|
||||||
isDragging.current = false;
|
isDragging.current = false;
|
||||||
const w = Math.round(v);
|
const w = Math.round(v);
|
||||||
setLocalWidth(w);
|
setLocalWidth(w);
|
||||||
updateSetting({ width_value: w });
|
updateSetting({ width_value: w });
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 交叉反馈 ── */}
|
{/* ── 交叉反馈 ── */}
|
||||||
@@ -399,8 +415,16 @@ export default function EffectsPage() {
|
|||||||
<IOSToggle checked={crossfeedOn} onChange={(v) => updateSetting({ crossfeed_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={crossfeedOn} onChange={(v) => updateSetting({ crossfeed_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
type="button"
|
||||||
onClick={() => goSelect(effectText.selectCrossfeedTitle, crossfeedLabels, crossfeedVal, "crossfeed_value")}>
|
disabled={!crossfeedOn}
|
||||||
|
className={`flex items-center justify-between w-full transition-opacity ${
|
||||||
|
crossfeedOn ? "active:opacity-70" : "cursor-default opacity-50"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!crossfeedOn) return;
|
||||||
|
goSelect(effectText.selectCrossfeedTitle, crossfeedLabels, crossfeedVal, "crossfeed_value");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="text-[14px] text-white/45">{crossfeedDisplay}</span>
|
<span className="text-[14px] text-white/45">{crossfeedDisplay}</span>
|
||||||
<ChevronRight size={16} className="ios-chevron" />
|
<ChevronRight size={16} className="ios-chevron" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+12
-13
@@ -22,7 +22,12 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { INPUT_LABELS, isHomeVolumeLockedByPassthrough, OUTPUT_LABELS } from "@/lib/luxsinApi";
|
import {
|
||||||
|
INPUT_BLUETOOTH_INDEX,
|
||||||
|
INPUT_LABELS,
|
||||||
|
isHomeVolumeLockedByPassthrough,
|
||||||
|
OUTPUT_LABELS,
|
||||||
|
} from "@/lib/luxsinApi";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -164,7 +169,7 @@ function ListRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
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 [, setLocation] = useLocation();
|
||||||
const powerActionRef = useRef(false);
|
const powerActionRef = useRef(false);
|
||||||
const hpEqLastTapRef = useRef<number | null>(null);
|
const hpEqLastTapRef = useRef<number | null>(null);
|
||||||
@@ -232,24 +237,18 @@ export default function Home() {
|
|||||||
if (powerActionRef.current) return;
|
if (powerActionRef.current) return;
|
||||||
powerActionRef.current = true;
|
powerActionRef.current = true;
|
||||||
try {
|
try {
|
||||||
if (isDemoMode) {
|
if (!isDemoMode && api) {
|
||||||
toast.info(homeText.powerOffToastDemo ?? "演示模式:已断开与设备的连接");
|
|
||||||
disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (api) {
|
|
||||||
try {
|
try {
|
||||||
await api.powerOff();
|
await api.powerOff();
|
||||||
} catch {
|
} catch {
|
||||||
// 设备可能已掉线或未返回响应,仍断开本地会话
|
// 设备关机后可能已无响应,仍保留当前页面与连接状态
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disconnect();
|
|
||||||
toast.success(homeText.powerOffToastSent ?? "已发送关机指令");
|
toast.success(homeText.powerOffToastSent ?? "已发送关机指令");
|
||||||
} finally {
|
} finally {
|
||||||
powerActionRef.current = false;
|
powerActionRef.current = false;
|
||||||
}
|
}
|
||||||
}, [api, disconnect, isDemoMode, homeText]);
|
}, [api, isDemoMode, homeText]);
|
||||||
|
|
||||||
/** HP-EQ 圆钮:单击在短延迟后切换 peqEnable(避免与双击抢);280ms 内第二次点击则取消开关并进入 /eq */
|
/** HP-EQ 圆钮:单击在短延迟后切换 peqEnable(避免与双击抢);280ms 内第二次点击则取消开关并进入 /eq */
|
||||||
const HP_EQ_DOUBLE_CLICK_MS = 280;
|
const HP_EQ_DOUBLE_CLICK_MS = 280;
|
||||||
@@ -779,8 +778,8 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bluetooth info */}
|
{/* Bluetooth info — only when input source is Bluetooth */}
|
||||||
{ds.bt_status !== 0 && (
|
{ds.input === INPUT_BLUETOOTH_INDEX && ds.bt_status !== 0 && (
|
||||||
<div className="mt-3 px-3 py-3 text-center">
|
<div className="mt-3 px-3 py-3 text-center">
|
||||||
<div className="flex items-center justify-center gap-1.5 mb-1">
|
<div className="flex items-center justify-center gap-1.5 mb-1">
|
||||||
<Bluetooth size={14} className="text-[#00FFF6]" />
|
<Bluetooth size={14} className="text-[#00FFF6]" />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
|
|||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
|
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
|
||||||
import { navigateToSelect } from "./SelectPage";
|
import { navigateToSelect } from "./SelectPage";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||||
@@ -215,7 +216,7 @@ export default function SystemPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ios-list-row">
|
<div className="ios-list-row">
|
||||||
<span className="flex-1 text-[16px] text-white">{ui.firmwareVersion}</span>
|
<span className="flex-1 text-[16px] text-white">{ui.firmwareVersion}</span>
|
||||||
<span className="ios-row-value">{ds?.version ?? "—"}</span>
|
<span className="ios-row-value">{formatFirmwareVersionDisplay(ds?.version)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ios-list-row">
|
<div className="ios-list-row">
|
||||||
<span className="flex-1 text-[16px] text-white">{ui.macAddress}</span>
|
<span className="flex-1 text-[16px] text-white">{ui.macAddress}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user