import { PdfViewer } from './pdf-viewer.js'; const $ = (s) => document.querySelector(s); const sessionId = location.pathname.split('/').filter(Boolean).pop(); const token = new URLSearchParams(location.search).get('token') || ''; const socket = io(); let kind = null, built = false, mode = 'synced'; let viewer = null, videoEl = null, page = 1, numPages = 1; function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 2000); } function stageMsg(html) { $('#stage').innerHTML = `
${html}
`; } $('#qrPageBtn').href = `/qr/${sessionId}`; socket.on('connect', () => socket.emit('join', { sessionId, role: 'presenter', token })); socket.on('error:join', (m) => stageMsg(m)); socket.on('ended', () => showEnded()); socket.on('joined', ({ isPresenter }) => { if (!isPresenter) stageMsg('Your presenter link is invalid. Reopen it from the host console.'); }); socket.on('state', onState); async function onState(s) { if (!s.meta) return; $('#deckTitle').textContent = s.meta.title || 'Presenting'; $('#viewerCount').textContent = s.counts.viewers; mode = s.mode; syncModeButtons(); if (s.meta.status === 'processing') { stageMsg('
Still converting your file…'); return; } if (s.meta.status === 'error') { stageMsg('Conversion failed: ' + (s.meta.error || '')); return; } if (!built) { await build(s.meta); built = true; } if (kind === 'pdf') { page = s.currentPage || 1; viewer.render(page); updatePageInd(); } } async function build(meta) { kind = meta.kind; const stage = $('#stage'); stage.innerHTML = ''; $('#controls').style.display = 'flex'; const src = `/s/${meta.id}/${meta.viewerFile}`; if (kind === 'pdf') { viewer = new PdfViewer(stage); numPages = await viewer.load(src); $('#pdfControls').style.display = 'flex'; updatePageInd(); viewer.render(page); } else if (kind === 'video') { videoEl = document.createElement('video'); videoEl.src = src; videoEl.controls = true; videoEl.playsInline = true; videoEl.preload = 'auto'; stage.appendChild(videoEl); $('#videoHint').style.display = 'block'; wireVideo(); } else if (kind === 'image') { const img = document.createElement('img'); img.src = src; img.alt = meta.title || 'Image'; stage.appendChild(img); } } // --- PDF paging (with first / last) --------------------------------------- function updatePageInd() { $('#pageInd').textContent = `${page} / ${numPages}`; } function goto(n) { if (kind !== 'pdf') return; page = Math.max(1, Math.min(numPages, n)); viewer.render(page); updatePageInd(); socket.emit('control', { currentPage: page }); } $('#first').addEventListener('click', () => goto(1)); $('#prev').addEventListener('click', () => goto(page - 1)); $('#next').addEventListener('click', () => goto(page + 1)); $('#last').addEventListener('click', () => goto(numPages)); document.addEventListener('keydown', (e) => { if (kind !== 'pdf') return; if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') { e.preventDefault(); goto(page + 1); } if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); goto(page - 1); } if (e.key === 'Home') { e.preventDefault(); goto(1); } if (e.key === 'End') { e.preventDefault(); goto(numPages); } }); // --- video sync ------------------------------------------------------------ function pushVideo() { if (!videoEl) return; socket.emit('control', { video: { playing: !videoEl.paused, time: videoEl.currentTime, rate: videoEl.playbackRate } }); } function wireVideo() { ['play', 'pause', 'seeked', 'ratechange'].forEach((ev) => videoEl.addEventListener(ev, pushVideo)); setInterval(() => { if (videoEl && !videoEl.paused) pushVideo(); }, 4000); } // --- mode toggle ----------------------------------------------------------- $('#modeSeg').addEventListener('click', (e) => { const btn = e.target.closest('button'); if (!btn) return; mode = btn.dataset.mode; syncModeButtons(); socket.emit('control', { mode }); toast(mode === 'synced' ? 'Audience now follows you' : 'Audience can browse freely'); }); function syncModeButtons() { document.querySelectorAll('#modeSeg button').forEach((b) => b.classList.toggle('active', b.dataset.mode === mode)); } // --- end session (invalidate QR + kick everyone) --------------------------- let ended = false; $('#endBtn').addEventListener('click', async () => { if (ended) return; if (!confirm('End the presentation for everyone? The QR code will stop working and all viewers will be disconnected.')) return; $('#endBtn').disabled = true; try { const res = await fetch(`/api/sessions/${sessionId}/terminate`, { method: 'POST' }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Could not end the session.'); ended = true; showEnded(); } catch (e) { $('#endBtn').disabled = false; toast(e.message); } }); function showEnded() { ended = true; document.querySelector('.lamp')?.classList.remove('on'); $('#controls').style.display = 'none'; $('#endBtn').textContent = 'Session ended'; stageMsg('This presentation has ended. Its QR code is no longer valid.

Back to dashboard'); }