77 lines
3.5 KiB
JavaScript
77 lines
3.5 KiB
JavaScript
/* 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);
|
|
})();
|