diff --git a/web/app.ts b/web/app.ts index c6e4607..4c09bce 100644 --- a/web/app.ts +++ b/web/app.ts @@ -133,6 +133,12 @@ function connectWS(): void { state.ws = null; micBtn.disabled = true; if (state.recording) stopRecording(); + // Clean up pending async start on disconnect + if (state.pendingStart) { + state.pendingStart = false; + state.startCancelled = true; + micBtn.classList.remove("recording"); + } setStatus("disconnected", "已断开"); scheduleReconnect(); }; @@ -357,40 +363,24 @@ function escapeHtml(s: string): string { } // ── Event bindings ── function bindMicButton(): void { - // Touch events (mobile primary) - micBtn.addEventListener( - "touchstart", - (e: TouchEvent) => { - e.preventDefault(); - startRecording(); - }, - { passive: false }, - ); - micBtn.addEventListener( - "touchend", - (e: TouchEvent) => { - e.preventDefault(); - stopRecording(); - }, - { passive: false }, - ); - micBtn.addEventListener( - "touchcancel", - (e: TouchEvent) => { - e.preventDefault(); - stopRecording(); - }, - { passive: false }, - ); - // Mouse fallback (desktop testing) - micBtn.addEventListener("mousedown", (e: MouseEvent) => { + // Pointer Events: unified touch + mouse, no double-trigger + micBtn.addEventListener("pointerdown", (e: PointerEvent) => { if (e.button !== 0) return; + e.preventDefault(); startRecording(); }); - micBtn.addEventListener("mouseup", () => stopRecording()); - micBtn.addEventListener("mouseleave", () => { - if (state.recording) stopRecording(); + micBtn.addEventListener("pointerup", (e: PointerEvent) => { + e.preventDefault(); + stopRecording(); }); + micBtn.addEventListener("pointerleave", () => { + if (state.recording || state.pendingStart) stopRecording(); + }); + micBtn.addEventListener("pointercancel", () => { + if (state.recording || state.pendingStart) stopRecording(); + }); + // Prevent context menu on long press + micBtn.addEventListener("contextmenu", (e) => e.preventDefault()); } // ── Init ── function init(): void {