Files
Presentation-system/public/js/present.js
T
2026-09-13 20:23:05 +01:00

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>');
}