112 lines
5.0 KiB
JavaScript
112 lines
5.0 KiB
JavaScript
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('<div class="spinner"></div>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;
|
||
|
|
}
|
||
|
|
}
|