'use strict'; const Pager = (() => { const SCREEN_WIDTH = 480; const SCREEN_HEIGHT = 222; const FB_STRIDE = SCREEN_WIDTH * 4; const PAGER_WIDTH = 745; const MAX_QUEUED_KEYS = 24; const KEY_MAP = { 'LEFT.png': 'ArrowLeft', 'UP.png': 'ArrowUp', 'RIGHT.png': 'ArrowRight', 'DOWN.png': 'ArrowDown', 'A_Button.png': 'Enter', 'B_Button.png': 'Escape' }; let panel = null; let bodyEl = null; let table = null; let pager = null; let canvas = null; let screenerr = null; let keyws = null; let screenws = null; let wantOpen = false; let retryTimer = null; let retryMs = 400; const pendingKeys = []; function ensure() { if (table) return; panel = document.getElementById('pager-panel'); bodyEl = document.getElementById('pager-body'); table = document.getElementById('pager_ui'); pager = document.getElementById('pager'); canvas = document.getElementById('pager_canvas'); screenerr = document.getElementById('pager_error'); table.querySelectorAll('img.pager-btn').forEach((img) => { const src = img.getAttribute('src').split('/').pop(); const key = KEY_MAP[src]; if (!key) return; img.setAttribute('alt', key.replace('Arrow', '')); img.addEventListener('pointerdown', (event) => { event.preventDefault(); press(img, key); }); }); const retry = document.getElementById('screen_retry'); if (retry) retry.addEventListener('click', () => { retryMs = 400; connect(); }); } function press(el, key) { el.classList.add('pressed'); setTimeout(() => el.classList.remove('pressed'), 80); sendKey(key); } function queueKey(k) { pendingKeys.push(k); while (pendingKeys.length > MAX_QUEUED_KEYS) pendingKeys.shift(); } function flushKeys() { while (pendingKeys.length && keyws && keyws.readyState === WebSocket.OPEN) { try { keyws.send(pendingKeys.shift()); } catch (e) { break; } } } function sendKey(k) { if (keyws && keyws.readyState === WebSocket.OPEN) { try { keyws.send(k); return true; } catch (e) {} } queueKey(k); if (wantOpen) connectKeys(); return false; } function renderRGBAFrame(bytes) { if (bytes.length < FB_STRIDE * SCREEN_HEIGHT) { console.warn('pager frame too small for 480x222', bytes.length); return; } const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT); const dst = imageData.data; let di = 0; for (let y = 0; y < SCREEN_HEIGHT; y++) { const rowStart = y * FB_STRIDE; for (let x = 0; x < SCREEN_WIDTH; x++) { const si = rowStart + x * 4; dst[di] = bytes[si]; dst[di + 1] = bytes[si + 1]; dst[di + 2] = bytes[si + 2]; dst[di + 3] = bytes[si + 3]; di += 4; } } ctx.putImageData(imageData, 0, 0); pager.src = canvas.toDataURL('image/png'); } function showError(show) { if (screenerr) screenerr.hidden = !show; } function scheduleReconnect() { if (!wantOpen) return; clearTimeout(retryTimer); retryTimer = setTimeout(() => { if (!wantOpen) return; connectScreen(); connectKeys(); }, retryMs); retryMs = Math.min(5000, Math.max(400, retryMs * 2)); } function connectScreen() { if (!wantOpen) return; if (screenws && (screenws.readyState === WebSocket.OPEN || screenws.readyState === WebSocket.CONNECTING)) return; try { const sock = new WebSocket(App.pagerScreenWs); sock.binaryType = 'arraybuffer'; screenws = sock; sock.onopen = () => { retryMs = 400; if (keyws && keyws.readyState === WebSocket.OPEN) showError(false); }; sock.onmessage = (ev) => { showError(false); if (ev.data instanceof ArrayBuffer) renderRGBAFrame(new Uint8Array(ev.data)); else if (ev.data && ev.data.arrayBuffer) ev.data.arrayBuffer().then((b) => renderRGBAFrame(new Uint8Array(b))); }; sock.onerror = () => { showError(true); }; sock.onclose = () => { if (screenws === sock) screenws = null; showError(true); scheduleReconnect(); }; } catch (e) { showError(true); scheduleReconnect(); } } function connectKeys() { if (!wantOpen) return; if (keyws && (keyws.readyState === WebSocket.OPEN || keyws.readyState === WebSocket.CONNECTING)) return; try { const sock = new WebSocket(App.pagerKeysWs); keyws = sock; sock.onopen = () => { retryMs = 400; flushKeys(); if (screenws && screenws.readyState === WebSocket.OPEN) showError(false); }; sock.onclose = () => { if (keyws === sock) keyws = null; scheduleReconnect(); }; sock.onerror = () => { try { sock.close(); } catch (e2) {} }; } catch (e) { keyws = null; scheduleReconnect(); } } function connect() { connectScreen(); connectKeys(); } function disconnect() { wantOpen = false; clearTimeout(retryTimer); retryTimer = null; pendingKeys.length = 0; if (screenws) { try { screenws.close(); } catch (e) {} screenws = null; } if (keyws) { try { keyws.close(); } catch (e) {} keyws = null; } } function applyScale() { if (!bodyEl || !table) return; const availableWidth = bodyEl.clientWidth - 20; const scale = Math.min(1, availableWidth / PAGER_WIDTH); table.style.zoom = String(scale); table.style.marginBottom = ''; } function toggle() { ensure(); if (panel.classList.contains('hidden')) { panel.classList.remove('hidden'); document.getElementById('pager-btn').classList.add('active'); applyScale(); wantOpen = true; retryMs = 400; connect(); try { pager.focus(); } catch (e) {} } else { panel.classList.add('hidden'); document.getElementById('pager-btn').classList.remove('active'); disconnect(); } } function isOpen() { return !!panel && !panel.classList.contains('hidden'); } function close() { if (isOpen()) toggle(); } window.addEventListener('resize', () => { if (table && panel && !panel.classList.contains('hidden')) applyScale(); }); document.addEventListener('keydown', (e) => { if (!isOpen()) return; const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.tagName === 'BUTTON' || t.tagName === 'A' || t.isContentEditable)) return; if (e.ctrlKey || e.metaKey || e.altKey) return; const keys = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'Enter', 'Escape']; if (keys.indexOf(e.key) === -1) return; if (e.key.indexOf('Arrow') === 0) e.preventDefault(); if (!e.repeat) sendKey(e.key); }); return { toggle, close, isOpen }; })();