修复bug

This commit is contained in:
yangy
2026-05-27 15:10:04 +08:00
parent fe2a0f6b3d
commit b59a7bb583
10 changed files with 412 additions and 49 deletions
+91 -26
View File
@@ -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);