Files
Presentation-system/public/js/view.js
T

112 lines
5.0 KiB
JavaScript
Raw Normal View History

2026-09-13 20:23:05 +01:00
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;
}
}