import { PdfViewer } from './pdf-viewer.js'; const $ = (s) => document.querySelector(s); const sessionId = location.pathname.split('/').filter(Boolean).pop(); const accessCode = new URLSearchParams(location.search).get('k') || ''; const socket = io(); let kind = null, built = false, mode = 'synced', following = true; let viewer = null, videoEl = null, joinedAudio = false; let localPage = 1, numPages = 1, lastVideo = null; 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').querySelector('.msg')?.remove(); const d = document.createElement('div'); d.className = 'msg'; d.innerHTML = html; $('#stage').prepend(d); } function clearMsg() { $('#stage').querySelector('.msg')?.remove(); } function setStatus(text, live) { $('#statusText').textContent = text; $('#lamp').classList.toggle('on', !!live); } socket.on('connect', () => { setStatus('Connecting…', false); socket.emit('join', { sessionId, role: 'viewer', k: accessCode }); }); socket.on('disconnect', () => setStatus('Reconnecting…', false)); socket.on('error:join', (m) => stageMsg(m)); socket.on('ended', () => { stageMsg('The presenter ended this session.'); setStatus('Ended', false); }); socket.on('state', onState); async function onState(s) { if (!s.meta) return; $('#deckTitle').textContent = s.meta.title || 'Presentation'; if (s.meta.status === 'processing') { stageMsg('
The presenter is preparing this. Hang tight…'); setStatus('Preparing', false); return; } if (s.meta.status === 'error') { stageMsg('This presentation could not be prepared.'); setStatus('Unavailable', false); return; } if (!built) { await build(s.meta); built = true; } mode = s.mode; setStatus(mode === 'free' ? 'Browse freely' : (following ? 'Live · in sync' : 'Live · you moved away'), true); if (kind === 'pdf') { updateFollowBar(); if (mode === 'synced' && following) { localPage = s.currentPage || 1; viewer.render(localPage); updatePageInd(); } $('#controls').style.display = 'flex'; } else if (kind === 'video') { lastVideo = s.video; applyVideo(s.video); } } async function build(meta) { kind = meta.kind; clearMsg(); const src = `/s/${meta.id}/${meta.viewerFile}?k=${encodeURIComponent(accessCode)}`; if (kind === 'pdf') { viewer = new PdfViewer($('#stage')); numPages = await viewer.load(src); updatePageInd(); } else if (kind === 'video') { videoEl = document.createElement('video'); videoEl.src = src; videoEl.playsInline = true; videoEl.preload = 'auto'; videoEl.muted = true; videoEl.setAttribute('webkit-playsinline', ''); $('#stage').appendChild(videoEl); $('#tapjoin').classList.add('show'); } else if (kind === 'image') { const img = document.createElement('img'); img.src = src; img.alt = meta.title || 'Image'; $('#stage').appendChild(img); } } // --- PDF: audience self-navigation (with first / last) -------------------- function updatePageInd() { $('#pageInd').textContent = `${localPage} / ${numPages}`; } function updateFollowBar() { $('#followbar').classList.toggle('show', mode === 'synced' && !following); } function localGoto(n) { if (kind !== 'pdf') return; localPage = Math.max(1, Math.min(numPages, n)); viewer.render(localPage); updatePageInd(); if (mode === 'synced') { following = false; updateFollowBar(); setStatus('Live · you moved away', true); } } $('#first').addEventListener('click', () => localGoto(1)); $('#prev').addEventListener('click', () => localGoto(localPage - 1)); $('#next').addEventListener('click', () => localGoto(localPage + 1)); $('#last').addEventListener('click', () => localGoto(numPages)); $('#followBtn').addEventListener('click', () => { following = true; updateFollowBar(); socket.emit('join', { sessionId, role: 'viewer', k: accessCode }); setStatus('Live · in sync', true); }); document.addEventListener('keydown', (e) => { if (kind !== 'pdf') return; if (e.key === 'ArrowRight') localGoto(localPage + 1); if (e.key === 'ArrowLeft') localGoto(localPage - 1); if (e.key === 'Home') localGoto(1); if (e.key === 'End') localGoto(numPages); }); // --- video following ------------------------------------------------------- $('#tapBtn').addEventListener('click', () => { joinedAudio = true; if (videoEl) videoEl.muted = false; $('#tapjoin').classList.remove('show'); if (lastVideo) applyVideo(lastVideo); }); function applyVideo(v) { if (!videoEl || !v) return; const rate = v.rate || 1; if (v.playing) { const elapsed = (Date.now() - (v.updatedAt || Date.now())) / 1000; const expected = (v.time || 0) + elapsed * rate; if (Math.abs(videoEl.currentTime - expected) > 0.8) videoEl.currentTime = expected; videoEl.playbackRate = rate; videoEl.play().catch(() => { if (!joinedAudio) $('#tapjoin').classList.add('show'); }); } else { videoEl.pause(); if (typeof v.time === 'number' && Math.abs(videoEl.currentTime - v.time) > 0.4) videoEl.currentTime = v.time; } }