Update interactive presentation and presenter console

This commit is contained in:
Codex
2026-09-20 19:43:33 +02:00
parent 348f4a9738
commit 5217c574e9
21 changed files with 1318 additions and 112 deletions
+76
View File
@@ -0,0 +1,76 @@
/* Opt-in, same-browser synchronization. The regular deck has no channel or session. */
(() => {
const params = new URLSearchParams(location.search);
const session = params.get('session');
const role = params.get('view');
if (!session || !['preview', 'audience'].includes(role)) return;
const channel = new BroadcastChannel(`aritmolab-presenter:${session}`);
const sender = crypto.randomUUID();
const storageKey = `aritmolab-state:${session}`;
let applying = false;
let pending = false;
const send = (type, extra = {}) => channel.postMessage({ type, role, sender, ...extra });
const api = window.PresentationControls;
const valid = state => state && Number.isInteger(state.index) && state.index >= 0 && state.index < Reveal.getTotalSlides();
function save(state) { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} }
function publish() {
if (applying || pending) return;
pending = true;
queueMicrotask(() => {
pending = false;
const state = api.snapshot();
save(state);
send('state', { state });
});
}
function apply(state) {
if (!valid(state)) return;
applying = true;
Reveal.slide(state.index);
api.closePopup();
if (state.popup) api.openPopup(state.popup);
applying = false;
save(api.snapshot());
}
function command(message) {
if (message.action === 'next') Reveal.next();
if (message.action === 'previous') Reveal.prev();
if (message.action === 'slide' && valid({ index: message.index })) Reveal.slide(message.index);
if (message.action === 'popup' && message.popup) api.openPopup(message.popup);
if (message.action === 'close') api.closePopup();
publish();
}
function ready() {
Reveal.configure({ overview: false, pause: false });
if (role === 'preview') {
// Remove only the presenter's footer and its 34px, preserving the slide body geometry.
document.documentElement.classList.add('presenter-preview');
Reveal.configure({ height: 686 });
}
try { apply(JSON.parse(localStorage.getItem(storageKey))); } catch {}
addEventListener('presentationchange', publish);
Reveal.on('slidechanged', publish);
channel.onmessage = ({ data }) => {
if (!data || data.sender === sender) return;
if (data.type === 'state') apply(data.state);
if (data.type === 'command' && role === 'preview') command(data);
if (data.type === 'request' && role === 'preview') publish();
};
if (role === 'preview') publish();
else send('request');
send('presence');
setInterval(() => send('presence'), 1000);
addEventListener('pagehide', () => send('departed'));
if (role === 'audience') {
document.title = 'AritmoLab | Audience';
const button = document.createElement('button');
button.textContent = 'Full screen';
button.setAttribute('aria-label', 'Enter audience full screen');
Object.assign(button.style, { position: 'fixed', right: '16px', top: '16px', zIndex: '200', padding: '10px 18px', borderRadius: '6px', border: '1px solid #b9afb1', background: '#faf7f7', color: '#482d34', cursor: 'pointer', font: '15px system-ui' });
button.onclick = () => document.documentElement.requestFullscreen().catch(() => { button.textContent = 'Use the browser full-screen command'; });
document.addEventListener('fullscreenchange', () => { button.hidden = Boolean(document.fullscreenElement); });
document.body.append(button);
}
}
if (Reveal.isReady()) ready(); else Reveal.on('ready', ready);
})();