/* 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); })();