126 lines
5.2 KiB
JavaScript
126 lines
5.2 KiB
JavaScript
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 = `<div class="msg">${html}</div>`; }
|
||
|
|
|
||
|
|
$('#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('<div class="spinner"></div>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.<br><br><a class="btn ghost small" href="/">Back to dashboard</a>');
|
||
|
|
}
|