Update interactive presentation and presenter console
This commit is contained in:
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user