修复bug
This commit is contained in:
+91
-26
@@ -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<number | null>(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<HTMLDivElement>) => {
|
||||
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 */}
|
||||
<div className="pr-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center touch-none select-none"
|
||||
className={cn(
|
||||
"w-12 h-12 rounded-full flex items-center justify-center touch-none select-none",
|
||||
volumeControlsLocked && "opacity-45 cursor-not-allowed",
|
||||
)}
|
||||
style={{
|
||||
background: "radial-gradient(circle at 35% 35%, #4a4a4e, #1a1a1c)",
|
||||
border: "2px solid rgba(255,255,255,0.12)",
|
||||
@@ -584,11 +607,23 @@ export default function Home() {
|
||||
|
||||
{/* ── Volume slider ── */}
|
||||
<div className="px-4 mb-5">
|
||||
{volumeControlsLocked && (
|
||||
<p
|
||||
className="mb-2.5 rounded-[10px] border border-amber-400/25 bg-amber-500/10 px-3 py-2 text-center text-[12px] leading-relaxed text-amber-100/90"
|
||||
role="status"
|
||||
>
|
||||
{volumePassthroughHint}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVolumeConfirm("min")}
|
||||
className="text-white/50 active:text-white transition-colors flex-shrink-0"
|
||||
disabled={volumeControlsLocked}
|
||||
onClick={() => guardVolumeChange(() => setVolumeConfirm("min"))}
|
||||
className={cn(
|
||||
"text-white/50 active:text-white transition-colors flex-shrink-0",
|
||||
volumeControlsLocked && "opacity-40 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
@@ -596,37 +631,55 @@ export default function Home() {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex-1 relative flex items-center touch-pan-y">
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 relative flex items-center touch-pan-y",
|
||||
volumeControlsLocked && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{/* Track fill - 根据按钮位置调整 */}
|
||||
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
||||
style={{
|
||||
width: `${fillPct}%`,
|
||||
background: "#00FFF6",
|
||||
boxShadow: "0 0 8px rgba(0,255,246,0.5)"
|
||||
}} />
|
||||
style={{
|
||||
width: `${fillPct}%`,
|
||||
background: volumeControlsLocked ? "rgba(148,163,184,0.7)" : "#00FFF6",
|
||||
boxShadow: volumeControlsLocked ? "none" : "0 0 8px rgba(0,255,246,0.5)",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="range" min={0} max={200} value={localVol}
|
||||
type="range"
|
||||
min={0}
|
||||
max={200}
|
||||
value={localVol}
|
||||
disabled={volumeControlsLocked}
|
||||
className="cyan-slider relative z-10"
|
||||
onChange={(e) => {
|
||||
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() {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVolumeConfirm("max")}
|
||||
className="text-white/50 active:text-white transition-colors flex-shrink-0"
|
||||
disabled={volumeControlsLocked}
|
||||
onClick={() => guardVolumeChange(() => setVolumeConfirm("max"))}
|
||||
className={cn(
|
||||
"text-white/50 active:text-white transition-colors flex-shrink-0",
|
||||
volumeControlsLocked && "opacity-40 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
@@ -661,7 +718,7 @@ export default function Home() {
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Decrease volume"
|
||||
disabled={localVol <= 0}
|
||||
disabled={volumeControlsLocked || localVol <= 0}
|
||||
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)",
|
||||
@@ -669,9 +726,11 @@ export default function Home() {
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
}}
|
||||
onClick={() => {
|
||||
const next = Math.max(0, Math.min(200, localVol - fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
setVolume(next);
|
||||
guardVolumeChange(() => {
|
||||
const next = Math.max(0, Math.min(200, localVol - fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
setVolume(next);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="text-[20px] leading-none">-</span>
|
||||
@@ -684,7 +743,7 @@ export default function Home() {
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Increase volume"
|
||||
disabled={localVol >= 200}
|
||||
disabled={volumeControlsLocked || 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)",
|
||||
@@ -692,9 +751,11 @@ export default function Home() {
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
}}
|
||||
onClick={() => {
|
||||
const next = Math.max(0, Math.min(200, localVol + fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
setVolume(next);
|
||||
guardVolumeChange(() => {
|
||||
const next = Math.max(0, Math.min(200, localVol + fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
setVolume(next);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="text-[20px] leading-none">+</span>
|
||||
@@ -875,6 +936,10 @@ export default function Home() {
|
||||
<AlertDialogAction
|
||||
className="bg-[#00FFF6] text-black hover:bg-[#00FFF6]/90 focus-visible:ring-[#00FFF6]"
|
||||
onClick={() => {
|
||||
if (volumeControlsLocked) {
|
||||
setVolumeConfirm(null);
|
||||
return;
|
||||
}
|
||||
if (volumeConfirm === "min") {
|
||||
setLocalVol(0);
|
||||
setVolume(0);
|
||||
|
||||
Reference in New Issue
Block a user