|
@@ -61,6 +61,53 @@ export const useAuthStore = defineStore("auth", () => {
|
|
|
let reconnectTimer: number | null = null;
|
|
let reconnectTimer: number | null = null;
|
|
|
const WS_BASE_URL = "ws://localhost:8000";
|
|
const WS_BASE_URL = "ws://localhost:8000";
|
|
|
|
|
|
|
|
|
|
+ // ── Audio ──────────────────────────────────────────────────────────────────
|
|
|
|
|
+ // Single shared AudioContext, created lazily on first user gesture.
|
|
|
|
|
+ let sharedAudioCtx: AudioContext | null = null;
|
|
|
|
|
+
|
|
|
|
|
+ function getAudioCtx(): AudioContext | null {
|
|
|
|
|
+ const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
|
|
|
|
+ if (!AudioCtx) return null;
|
|
|
|
|
+ if (!sharedAudioCtx) sharedAudioCtx = new AudioCtx();
|
|
|
|
|
+ return sharedAudioCtx;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Unlock AudioContext as soon as the user interacts with the page
|
|
|
|
|
+ function unlockAudio() {
|
|
|
|
|
+ const ctx = getAudioCtx();
|
|
|
|
|
+ if (ctx && ctx.state === 'suspended') ctx.resume().catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+ window.addEventListener('click', unlockAudio, { passive: true });
|
|
|
|
|
+ window.addEventListener('keydown', unlockAudio, { passive: true });
|
|
|
|
|
+ window.addEventListener('touchstart', unlockAudio, { passive: true });
|
|
|
|
|
+
|
|
|
|
|
+ async function playUpdateSound() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ctx = getAudioCtx();
|
|
|
|
|
+ if (!ctx) return;
|
|
|
|
|
+ if (ctx.state === 'suspended') await ctx.resume();
|
|
|
|
|
+
|
|
|
|
|
+ const osc = ctx.createOscillator();
|
|
|
|
|
+ const gainNode = ctx.createGain();
|
|
|
|
|
+
|
|
|
|
|
+ osc.type = 'sine';
|
|
|
|
|
+ osc.frequency.setValueAtTime(180, ctx.currentTime);
|
|
|
|
|
+ osc.frequency.exponentialRampToValueAtTime(140, ctx.currentTime + 0.3);
|
|
|
|
|
+
|
|
|
|
|
+ gainNode.gain.setValueAtTime(0, ctx.currentTime);
|
|
|
|
|
+ gainNode.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.05);
|
|
|
|
|
+ gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.35);
|
|
|
|
|
+
|
|
|
|
|
+ osc.connect(gainNode);
|
|
|
|
|
+ gainNode.connect(ctx.destination);
|
|
|
|
|
+ osc.start();
|
|
|
|
|
+ osc.stop(ctx.currentTime + 0.35);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn("Audio update sound error", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
function startPing() {
|
|
function startPing() {
|
|
|
stopPing(); // Ensure fresh start
|
|
stopPing(); // Ensure fresh start
|
|
|
const token = localStorage.getItem("token");
|
|
const token = localStorage.getItem("token");
|
|
@@ -109,30 +156,6 @@ export const useAuthStore = defineStore("auth", () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- function playUpdateSound() {
|
|
|
|
|
- try {
|
|
|
|
|
- const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
|
|
|
|
- if (!AudioCtx) return;
|
|
|
|
|
- const ctx = new AudioCtx();
|
|
|
|
|
- const osc = ctx.createOscillator();
|
|
|
|
|
- const gainNode = ctx.createGain();
|
|
|
|
|
-
|
|
|
|
|
- osc.type = 'sine';
|
|
|
|
|
- osc.frequency.setValueAtTime(180, ctx.currentTime);
|
|
|
|
|
- osc.frequency.exponentialRampToValueAtTime(140, ctx.currentTime + 0.3);
|
|
|
|
|
-
|
|
|
|
|
- gainNode.gain.setValueAtTime(0, ctx.currentTime);
|
|
|
|
|
- gainNode.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 0.05);
|
|
|
|
|
- gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.35);
|
|
|
|
|
-
|
|
|
|
|
- osc.connect(gainNode);
|
|
|
|
|
- gainNode.connect(ctx.destination);
|
|
|
|
|
- osc.start();
|
|
|
|
|
- osc.stop(ctx.currentTime + 0.35);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.warn("Audio disabled", e);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
globalWs.onclose = (event) => {
|
|
globalWs.onclose = (event) => {
|
|
|
if (pingInterval) clearInterval(pingInterval);
|
|
if (pingInterval) clearInterval(pingInterval);
|