/** * Debug dashboard, manual overrides, and bindings for the controls panel. */ import { OVERRIDE_MODES } from './state-manager.js'; const STATE_COLORS = Object.freeze({ idle: '#888', lobby: '#4CAF50', playing: '#f0ad4e', ended: '#d9534f', disconnected: '#d9534f', }); const STORAGE_API_URL = 'jackbox-api-url'; const STORAGE_API_KEY = 'jackbox-api-key'; /** * @param {import('./state-manager.js').OverlayManager} manager * @param {import('./websocket-client.js').WebSocketClient} wsClient * @param {{ roomCode?: unknown, audio?: unknown, playerList?: unknown }} components */ export function initControls(manager, wsClient, components) { const stateEl = document.getElementById('manager-state'); const roomCodeEl = document.getElementById('manager-room-code'); const sessionIdEl = document.getElementById('manager-session-id'); const gameTitleEl = document.getElementById('manager-game-title'); const playerCountEl = document.getElementById('manager-player-count'); const eventLogEl = document.getElementById('manager-event-log'); const componentNames = Object.keys(components); for (const name of componentNames) { const select = document.getElementById(`override-${name}`); if (!select) continue; if (select.options.length === 0) { for (const mode of Object.values(OVERRIDE_MODES)) { const opt = document.createElement('option'); opt.value = mode; opt.textContent = mode.replace(/_/g, ' '); select.appendChild(opt); } } select.value = manager.getOverride(name); select.addEventListener('change', () => { manager.setOverride(name, select.value); }); } function formatGameTitleLine(ctx) { const g = ctx.game; if (!g || typeof g !== 'object') return '—'; const rec = /** @type {Record} */ (g); const title = (typeof rec.title === 'string' && rec.title) || (typeof rec.name === 'string' && rec.name) || ''; const pack = (typeof rec.pack_name === 'string' && rec.pack_name) || (typeof rec.packName === 'string' && rec.packName) || (typeof rec.pack === 'string' && rec.pack) || ''; if (!title) return '—'; return pack ? `${title} (${pack})` : title; } function formatStatusRow(name, info) { const s = info?.status; if (!s || typeof s !== 'object') return '—'; if (name === 'roomCode') { const rc = /** @type {{ active?: boolean, roomCode?: string, meterFill?: number }} */ (s); if (!rc.active) return 'Inactive'; const meter = rc.meterFill != null ? ` | Meter: ${rc.meterFill}%` : ''; return `Active (${rc.roomCode ?? ''})${meter}`; } if (name === 'audio') { const a = /** @type {{ active?: boolean, playing?: boolean }} */ (s); if (!a.active) return 'Inactive'; return a.playing ? 'Playing' : 'Active (muted)'; } if (name === 'playerList') { const pl = /** @type {{ active?: boolean, playerCount?: number, maxPlayers?: number }} */ ( s ); if (!pl.active) return 'Inactive'; const n = pl.playerCount ?? 0; const m = pl.maxPlayers ?? '?'; return `Active (${n}/${m})`; } return /** @type {{ active?: boolean }} */ (s).active ? 'Active' : 'Inactive'; } function updateDashboard() { const state = manager.getState(); const ctx = manager.getContext(); if (stateEl) { stateEl.textContent = state.toUpperCase(); stateEl.style.backgroundColor = STATE_COLORS[state] ?? '#888'; } if (roomCodeEl) { roomCodeEl.textContent = ctx.roomCode != null ? String(ctx.roomCode) : '—'; } if (sessionIdEl) { sessionIdEl.textContent = ctx.sessionId != null ? String(ctx.sessionId) : '—'; } if (gameTitleEl) { gameTitleEl.textContent = formatGameTitleLine(ctx); } if (playerCountEl) { const n = ctx.playerCount ?? 0; const m = ctx.maxPlayers != null ? String(ctx.maxPlayers) : '?'; playerCountEl.textContent = `${n}/${m}`; } const statuses = manager.getComponentStatuses(); for (const name of componentNames) { const row = document.getElementById(`status-${name}`); if (!row) continue; const info = statuses[name]; row.textContent = formatStatusRow(name, info); } for (const name of componentNames) { const sel = document.getElementById(`override-${name}`); if (!sel) continue; const want = manager.getOverride(name); if (sel.value !== want) sel.value = want; } if (eventLogEl) { const log = manager.getEventLog(); const last = log.slice(-20); eventLogEl.replaceChildren(); for (const e of last) { const row = document.createElement('div'); row.className = 'event-log-entry'; const time = document.createElement('span'); time.className = 'event-time'; time.textContent = new Date(e.at).toLocaleTimeString(); const typ = document.createElement('span'); typ.className = 'event-type'; typ.textContent = e.type; row.appendChild(time); row.appendChild(document.createTextNode(' ')); row.appendChild(typ); eventLogEl.appendChild(row); } eventLogEl.scrollTop = eventLogEl.scrollHeight; } syncToggleDisplayLabel(); } const toggleDisplayBtn = document.getElementById('toggle-display-btn'); function syncToggleDisplayLabel() { if (!toggleDisplayBtn) return; const s = manager.getState(); toggleDisplayBtn.textContent = s === 'lobby' ? 'Hide Display' : 'Show Display'; } manager.onChange(() => { updateDashboard(); }); updateDashboard(); const connectBtn = document.getElementById('ws-connect-btn'); const disconnectBtn = document.getElementById('ws-disconnect-btn'); const apiUrlInput = document.getElementById('api-url-input'); const apiKeyInput = document.getElementById('api-key-input'); if (connectBtn && apiUrlInput && apiKeyInput) { connectBtn.addEventListener('click', () => { const url = apiUrlInput.value.trim(); const key = apiKeyInput.value.trim(); if (url) localStorage.setItem(STORAGE_API_URL, url); if (key) localStorage.setItem(STORAGE_API_KEY, key); void wsClient.connect(url, key); }); } if (disconnectBtn) { disconnectBtn.addEventListener('click', () => { wsClient.disconnect(); manager.handleEvent('session.ended', { session: {} }); }); } apiUrlInput?.addEventListener('change', () => { const url = apiUrlInput.value.trim(); if (url) localStorage.setItem(STORAGE_API_URL, url); }); apiKeyInput?.addEventListener('change', () => { localStorage.setItem(STORAGE_API_KEY, apiKeyInput.value.trim()); }); const volumeSlider = document.getElementById('volume-slider'); const volumeValue = document.getElementById('volume-value'); const themeSound = /** @type {HTMLAudioElement | null} */ ( document.getElementById('theme-sound') ); if (volumeSlider && volumeValue) { volumeSlider.addEventListener('input', () => { const v = Number(volumeSlider.value); volumeValue.textContent = `${Math.round(v * 100)}%`; if (themeSound) themeSound.volume = v; }); } const testSoundBtn = document.getElementById('test-sound-btn'); const soundUrlInput = document.getElementById('sound-url-input'); if (testSoundBtn && themeSound && soundUrlInput && volumeSlider) { testSoundBtn.addEventListener('click', () => { themeSound.src = soundUrlInput.value; themeSound.volume = Number(volumeSlider.value); themeSound.currentTime = 0; themeSound.loop = false; themeSound.play().catch((err) => { const msg = err instanceof Error ? err.message : String(err); console.log('[Test] Playback failed:', msg); window.alert( 'Audio playback failed. Check the URL or browser autoplay permissions.', ); }); window.setTimeout(() => { themeSound.pause(); themeSound.loop = true; }, 5000); }); } function synthesizeGameAddedFromInputs() { const code1 = document.getElementById('code1-input')?.value ?? ''; const code2 = document.getElementById('code2-input')?.value ?? ''; const roomCode = `${code1}${code2}`.trim(); if (!roomCode) return; manager.handleEvent('game.added', { session: {}, game: { room_code: roomCode, title: 'Manual', max_players: 8, }, }); } const updateBtn = document.getElementById('update-btn'); const previewBtn = document.getElementById('preview-btn'); updateBtn?.addEventListener('click', () => { synthesizeGameAddedFromInputs(); }); previewBtn?.addEventListener('click', () => { synthesizeGameAddedFromInputs(); }); if (toggleDisplayBtn) { toggleDisplayBtn.addEventListener('click', () => { const s = manager.getState(); if (s === 'lobby') { manager.handleEvent('session.ended', { session: {} }); } else if (s === 'idle') { const code1 = document.getElementById('code1-input')?.value ?? ''; const code2 = document.getElementById('code2-input')?.value ?? ''; const roomCode = `${code1}${code2}`.trim(); if (roomCode) { manager.handleEvent('game.added', { session: {}, game: { room_code: roomCode, title: 'Display', max_players: 8, }, }); } } syncToggleDisplayLabel(); }); } const showControlsBtn = document.getElementById('show-controls-btn'); const controls = document.getElementById('controls'); if (showControlsBtn && controls) { showControlsBtn.addEventListener('click', () => { if (controls.style.display === 'block') { controls.style.display = 'none'; showControlsBtn.textContent = 'Show Controls'; } else { controls.style.display = 'block'; showControlsBtn.textContent = 'Hide Controls'; } }); } document.querySelectorAll('.section-header').forEach((hdr) => { hdr.addEventListener('click', () => { const content = hdr.nextElementSibling; if (!(content instanceof HTMLElement)) return; const indicator = hdr.querySelector('.toggle-indicator'); if (content.style.display === 'block') { content.style.display = 'none'; indicator?.classList.remove('active'); hdr.classList.remove('active'); } else { content.style.display = 'block'; indicator?.classList.add('active'); hdr.classList.add('active'); } }); }); const positionToggle = document.getElementById('position-toggle'); if (positionToggle && controls) { positionToggle.addEventListener('change', () => { controls.classList.toggle('bottom-position', positionToggle.checked); }); } let inactivityTimer = 0; function resetInactivityTimer() { window.clearTimeout(inactivityTimer); inactivityTimer = window.setTimeout(() => { if (controls) controls.style.display = 'none'; if (showControlsBtn) showControlsBtn.textContent = 'Show Controls'; }, 20000); } document.addEventListener('mousemove', resetInactivityTimer); document.addEventListener('keypress', resetInactivityTimer); document.addEventListener('click', resetInactivityTimer); resetInactivityTimer(); } /** * @returns {(state: string, message?: string) => void} */ export function initConnectionStatusHandler() { const wsStatusDot = document.getElementById('ws-status-dot'); const wsStatusText = document.getElementById('ws-status-text'); const wsConnectBtn = document.getElementById('ws-connect-btn'); const wsDisconnectRow = document.getElementById('ws-disconnect-row'); const apiUrlInput = document.getElementById('api-url-input'); const apiKeyInput = document.getElementById('api-key-input'); return (state, message) => { if (wsStatusDot) wsStatusDot.className = `status-dot ${state}`; if (wsStatusText) wsStatusText.textContent = message ?? String(state); if (state === 'connected') { if (wsConnectBtn) wsConnectBtn.style.display = 'none'; if (wsDisconnectRow) wsDisconnectRow.style.display = 'flex'; if (apiUrlInput) apiUrlInput.disabled = true; if (apiKeyInput) apiKeyInput.disabled = true; } else { if (wsConnectBtn) wsConnectBtn.style.display = 'block'; if (wsDisconnectRow) wsDisconnectRow.style.display = 'none'; if (apiUrlInput) apiUrlInput.disabled = false; if (apiKeyInput) apiKeyInput.disabled = false; } }; }