Add resilient presenter mode and speaker notes

This commit is contained in:
郭浩 committed 2026-08-06 21:42:31 +08:00
1 parent 929c2ecb63
commit 006ba0b87a
6 files changed
+704 -23

No files matched your search

+222 -8
View File
@@ -195,10 +195,43 @@
body.light-bg #nav{background:rgba(255,255,255,.25)}
body.light-bg #nav .dot{background:rgba(var(--ink-rgb),.25)}
body.light-bg #nav .dot.active{background:rgba(var(--ink-rgb),.9)}
#hint{position:fixed;bottom:3vh;right:3vw;z-index:30;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;opacity:.4;mix-blend-mode:difference;color:#aaa}
#hint{position:fixed;bottom:3vh;right:3vw;z-index:30;display:flex;align-items:center;gap:.65em;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;opacity:.4;mix-blend-mode:difference;color:#aaa}
#hint button{all:unset;display:inline-flex;align-items:center;gap:.45em;cursor:pointer;color:inherit;white-space:nowrap}
#hint button:hover,#hint button:focus-visible{opacity:1;text-decoration:underline;text-underline-offset:.35em}
#hint .ppt-key{padding:.16em .38em;border:1px solid currentColor;border-radius:3px;letter-spacing:.08em}
#hint .ppt-sep{opacity:.55}
body.low-power #hint{opacity:.72;color:var(--paper);mix-blend-mode:normal}
body.light-bg.low-power #hint{color:var(--ink)}
/* ============ 演讲者模式 ============ */
body.ppt-audience #hint,body.ppt-audience #nav,
body.ppt-preview #hint,body.ppt-preview #nav{display:none!important}
body.ppt-audience{cursor:none}
body.ppt-preview{pointer-events:none}
body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint,
body.ppt-presenter>.bg{visibility:hidden}
#ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:1000px;min-height:680px;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif}
body.ppt-presenter #ppt-presenter{display:grid;grid-template-rows:auto 1fr auto}
.ppt-presenter-top{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px 24px;border-bottom:1px solid #2b2e35;background:#15181d}
.ppt-presenter-brand{font-weight:650;letter-spacing:.02em}.ppt-presenter-meta{display:flex;align-items:center;gap:12px;color:#aeb4be;font-size:14px}
.ppt-sync{display:inline-flex;align-items:center;gap:8px;padding:7px 11px;border:1px solid #363a43;border-radius:999px;color:#d2d6dc;background:#1b1e24}
.ppt-sync::before{content:"";width:8px;height:8px;border-radius:50%;background:#818894}
.ppt-sync[data-state="synced"]::before{background:#72df99;box-shadow:0 0 0 4px rgba(114,223,153,.12)}
.ppt-sync[data-state="connecting"]::before{background:#f0c766}.ppt-sync[data-state="unsynced"]::before,.ppt-sync[data-state="blocked"]::before{background:#ff9e64}.ppt-sync[data-state="disconnected"]::before{background:#ff6b72}
.ppt-ui-btn{appearance:none;border:1px solid #3a3e48;background:#20242a;color:#f6f7f8;border-radius:8px;padding:9px 13px;font:600 14px/1 Inter,"Noto Sans SC",sans-serif;cursor:pointer}
.ppt-ui-btn:hover{background:#2a2f37;border-color:#525864}.ppt-ui-btn:focus-visible{outline:2px solid #84a8ff;outline-offset:2px}.ppt-ui-btn:disabled{opacity:.28;cursor:not-allowed}
.ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(320px,.75fr);gap:18px;min-height:0;padding:18px 24px}
.ppt-preview-stack{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,.42fr);gap:16px;min-width:0;align-items:start}
.ppt-stage-card,.ppt-notes-card{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden}
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
.ppt-frame{display:block;width:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}
.ppt-notes-card{display:grid;grid-template-rows:auto 1fr;min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-title{font-size:20px;font-weight:650;margin-bottom:7px}.ppt-notes-sub{display:flex;align-items:center;justify-content:space-between;gap:10px;color:#9fa6b1;font-size:13px}
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif}
.ppt-saved{color:#72df99}.ppt-presenter-bottom{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;padding:16px 24px;border-top:1px solid #2b2e35;background:#15181d}
.ppt-timer{display:flex;align-items:center;gap:9px}.ppt-clock{min-width:72px;font:600 24px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-controls{display:flex;align-items:center;gap:8px}.ppt-controls .ppt-ui-btn{min-width:72px}.ppt-controls .ppt-primary{background:#f2f4f6;color:#111318;border-color:#f2f4f6}.ppt-controls .ppt-primary:hover{background:#fff}.ppt-page-state{text-align:right;color:#aeb4be;font-size:14px}.ppt-page-state strong{display:block;color:#fff;font-size:18px;margin-bottom:3px}
#overview{z-index:110!important}
@media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(300px,.75fr)}.ppt-preview-stack{grid-template-columns:1fr}.ppt-preview-stack .ppt-stage-card:last-child{display:none}}
/* ============================================================
============ LAYOUTS API · 面向 agent 的类(v2)============
所有 layouts.md 中的骨架都基于下面这套命名。
@@ -451,8 +484,8 @@
const stored = localStorage.getItem(KEY);
window.__lowPowerMode = stored === '1' || (stored === null && reduced);
function updateHint(){
const hint = document.getElementById('hint');
if(hint) hint.textContent = `← → 翻页 · B ${window.__lowPowerMode ? '动态' : '静态'} · ESC 索引`;
const label = document.querySelector('.ppt-low-power-label');
if(label) label.textContent = window.__lowPowerMode ? '动态' : '静态';
}
window.__setLowPowerMode = function(on, opts={}){
window.__lowPowerMode = !!on;
@@ -472,7 +505,12 @@
<canvas id="bg-dark" class="bg"></canvas>
<canvas id="bg-light" class="bg"></canvas>
<div id="hint">← → 翻页 · B 静态 · ESC 索引</div>
<div id="hint" role="toolbar" aria-label="快捷控制">
<span>← → 翻页</span><span class="ppt-sep">·</span>
<button id="ppt-low-power-btn" type="button"><span class="ppt-key">B</span><span class="ppt-low-power-label">静态</span></button><span class="ppt-sep">·</span>
<button id="ppt-overview-btn" type="button"><span class="ppt-key">ESC</span>总览</button><span class="ppt-sep">·</span>
<button id="ppt-presenter-btn" type="button"><span class="ppt-key">P</span>演讲模式</button>
</div>
<div id="deck">
@@ -488,6 +526,19 @@
<div id="nav"></div>
<!-- 演讲备注:每页一条,id 必须与 section[data-slide-id] 一致。见 references/presenter-mode.md -->
<script>
const SPEAKER_NOTES = [
/* {
id: 'stable-slide-id', title: '页面标题', minutes: 1,
purpose: '这一页要让观众理解什么',
talk: ['只在台上说的补充信息', '例子、证据或语气提示', '不复述页面可见文字'],
transition: '为什么下一页紧接着出现', cue: '可选:停顿 / Demo / 视频'
} */
];
window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
</script>
<script>
/* =============== WebGL 双背景 ===============
深色页:Holographic Dispersion(全息色散 · 钛金暗流)—— 彩虹微扰、鼠标径向涟漪
@@ -633,8 +684,8 @@ slides.forEach((s,i)=>{
nav.appendChild(b);
});
function go(n){
if(lock)return;
function go(n,opts={}){
if(lock&&!opts.force)return;
idx=Math.max(0,Math.min(total-1,n));
window.__currentSlideIndex = idx;
deck.style.transform=`translateX(${-idx*100}vw)`;
@@ -645,7 +696,8 @@ function go(n){
document.body.classList.toggle('light-bg',th==='light');
/* 动效:翻页过渡中段触发当前页的入场动画(由 motion-boot 注册) */
if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
lock=true;setTimeout(()=>lock=false,700);
window.__pptPresenter?.onSlideChange(idx);
if(!opts.force){lock=true;setTimeout(()=>lock=false,700);}
}
/* =============== ESC 索引视图 =============== */
@@ -671,7 +723,7 @@ function buildOverview(){
wrap.appendChild(clone);
const label=document.createElement('div');
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper);opacity:.7';
label.textContent=(i+1)+' / '+total;
label.textContent=window.__pptPresenter?.slideLabel(i) || ((i+1)+' / '+total);
card.appendChild(wrap);
card.appendChild(label);
card.onclick=()=>{toggleOverview();go(i)};
@@ -686,7 +738,168 @@ function toggleOverview(){
else{ov.style.display='none';}
}
/* =============== 演讲者模式 / 观众屏同步 =============== */
window.__pptPresenter=(()=>{
const query=new URLSearchParams(location.search);
const requestedMode=query.get('mode')||'deck';
const originalTitle=document.title;
const notes=()=>Array.isArray(window.__SPEAKER_NOTES__)?window.__SPEAKER_NOTES__:[];
const slideId=i=>slides[i]?.dataset.slideId||`slide-${String(i+1).padStart(2,'0')}`;
let mode='deck',session=query.get('session')||Math.random().toString(36).slice(2),audienceWindow=null;
let channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0;
let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17);
let elapsed=0,timerRunning=false,timerStartedAt=0,lowPowerBeforePresenter=window.__lowPowerMode;
const syncKey=()=>`guizang-ppt-sync:${location.pathname}:${session}`;
const channelName=()=>`guizang-ppt:${location.pathname}:${session}`;
function noteAt(i){
const id=slideId(i);
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,minutes:0,purpose:'待补充本页目的',talk:['待补充讲述要点'],transition:'待补充转场'};
}
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
function generatedNote(n){
const talk=(n.talk||[]).map(x=>`• ${x}`).join('\n');
return `【本页目的】\n${n.purpose||''}\n\n【讲述要点】\n${talk}\n\n【转场】\n${n.transition||''}${n.cue?`\n\n【舞台提示】\n${n.cue}`:''}`;
}
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
function frameUrl(targetMode,i){
const url=new URL(location.href);url.hash='';
url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1));
return url.href;
}
function mountShell(){
if(document.getElementById('ppt-presenter'))return;
document.body.insertAdjacentHTML('beforeend',`<div id="ppt-presenter" aria-label="演讲者视图">
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-reopen" class="ppt-ui-btn" type="button">重新打开观众屏</button><button id="ppt-exit" class="ppt-ui-btn" type="button">退出</button></div></div>
<div class="ppt-presenter-main"><div class="ppt-preview-stack"><div class="ppt-stage-card"><div class="ppt-stage-label"><span>当前页</span><span id="ppt-current-label"></span></div><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div>
<div class="ppt-notes-card"><div class="ppt-notes-head"><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-notes-sub"><span id="ppt-note-meta"></span><span class="ppt-notes-tools"><span id="ppt-saved" class="ppt-saved">已保存</span><button id="ppt-note-smaller" class="ppt-ui-btn" type="button" title="缩小备注字号">A−</button><button id="ppt-note-larger" class="ppt-ui-btn" type="button" title="放大备注字号">A+</button></span></div></div><textarea id="ppt-note-editor" class="ppt-note-editor" spellcheck="false" aria-label="本页演讲备注"></textarea></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-timer"><span id="ppt-clock" class="ppt-clock">00:00</span><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始计时</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置</button></div><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-grid" class="ppt-ui-btn ppt-primary" type="button">总览</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-page-state"><strong id="ppt-page-title"></strong><span id="ppt-page-count"></span></div></div>
</div>`);
document.getElementById('ppt-reopen').onclick=reopenAudience;
document.getElementById('ppt-exit').onclick=exitPresenter;
document.getElementById('ppt-first').onclick=()=>go(0,{force:true});
document.getElementById('ppt-prev').onclick=()=>go(idx-1,{force:true});
document.getElementById('ppt-grid').onclick=toggleOverview;
document.getElementById('ppt-next').onclick=()=>go(idx+1,{force:true});
document.getElementById('ppt-last').onclick=()=>go(idx===total-1?0:total-1,{force:true});
document.getElementById('ppt-timer-toggle').onclick=toggleTimer;
document.getElementById('ppt-timer-reset').onclick=resetTimer;
document.getElementById('ppt-note-smaller').onclick=()=>setNoteSize(noteSize-1);
document.getElementById('ppt-note-larger').onclick=()=>setNoteSize(noteSize+1);
document.getElementById('ppt-note-editor').addEventListener('input',saveNote);
setNoteSize(noteSize,false);
}
function updateShell(){
if(mode!=='presenter')return;mountShell();
const n=noteAt(idx),next=Math.min(idx+1,total-1),id=slideId(idx),editor=document.getElementById('ppt-note-editor');
const currentFrame=document.getElementById('ppt-current-frame'),nextFrame=document.getElementById('ppt-next-frame');
if(currentFrame.dataset.slide!==String(idx)){currentFrame.src=frameUrl('preview',idx);currentFrame.dataset.slide=String(idx);}
if(nextFrame.dataset.slide!==String(next)){nextFrame.src=frameUrl('preview',next);nextFrame.dataset.slide=String(next);}
document.getElementById('ppt-current-label').textContent=`${idx+1} / ${total}`;
document.getElementById('ppt-next-label').textContent=idx===total-1?'已到尾页':`${next+1} / ${total}`;
document.getElementById('ppt-notes-title').textContent=n.title||`第 ${idx+1} 页`;
document.getElementById('ppt-note-meta').textContent=`建议 ${Number(n.minutes||0).toFixed(1)} 分钟 · ${id}`;
document.getElementById('ppt-page-title').textContent=n.title||`第 ${idx+1} 页`;
document.getElementById('ppt-page-count').textContent=`第 ${idx+1} / ${total} 页 · ${Math.round(((idx+1)/total)*100)}%`;
document.getElementById('ppt-plan').textContent=`计划 ${notes().reduce((s,x)=>s+Number(x.minutes||0),0).toFixed(1)} 分钟`;
document.getElementById('ppt-first').disabled=idx===0;document.getElementById('ppt-prev').disabled=idx===0;document.getElementById('ppt-next').disabled=idx===total-1;
document.getElementById('ppt-last').textContent=idx===total-1?'重新开始':'尾页';
if(renderedNoteId!==id){renderedNoteId=id;editor.value=localStorage.getItem(noteStorageKey(id))??generatedNote(n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';}
updateSyncStatus();
}
function saveNote(){
const saved=document.getElementById('ppt-saved');saved.textContent='保存中…';saved.classList.remove('ppt-saved');clearTimeout(saveTimer);
saveTimer=setTimeout(()=>{localStorage.setItem(noteStorageKey(renderedNoteId),document.getElementById('ppt-note-editor').value);saved.textContent='已保存';saved.classList.add('ppt-saved');},300);
}
function setNoteSize(value,persist=true){
noteSize=Math.max(14,Math.min(26,value));document.documentElement.style.setProperty('--ppt-note-size',`${noteSize}px`);
if(persist)localStorage.setItem('guizang-ppt-note-size',String(noteSize));
}
function currentElapsed(){return elapsed+(timerRunning?Date.now()-timerStartedAt:0);}
function renderTimer(){const sec=Math.floor(currentElapsed()/1000),clock=document.getElementById('ppt-clock');if(clock)clock.textContent=`${String(Math.floor(sec/60)).padStart(2,'0')}:${String(sec%60).padStart(2,'0')}`;}
function toggleTimer(){if(timerRunning){elapsed=currentElapsed();timerRunning=false;}else{timerStartedAt=Date.now();timerRunning=true;}document.getElementById('ppt-timer-toggle').textContent=timerRunning?'暂停':'继续';renderTimer();}
function resetTimer(){elapsed=0;timerRunning=false;timerStartedAt=0;document.getElementById('ppt-timer-toggle').textContent='开始计时';renderTimer();}
function initTransport(){
if(transportReady)return;transportReady=true;
if('BroadcastChannel'in window){try{channel=new BroadcastChannel(channelName());channel.onmessage=e=>receive(e.data);}catch(e){}}
addEventListener('message',e=>receive(e.data));
addEventListener('storage',e=>{if(e.key===syncKey()&&e.newValue){try{receive(JSON.parse(e.newValue));}catch(err){}}});
}
function emit(payload){
const message={__guizangPptSync:1,session,role:mode,...payload,at:Date.now(),nonce:Math.random().toString(36).slice(2)};
try{channel?.postMessage(message);}catch(e){}
try{localStorage.setItem(syncKey(),JSON.stringify(message));}catch(e){}
try{if(mode==='presenter'&&audienceWindow&&!audienceWindow.closed)audienceWindow.postMessage(message,'*');}catch(e){}
try{if(mode==='audience'&&window.opener&&!window.opener.closed)window.opener.postMessage(message,'*');}catch(e){}
}
function receive(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role===mode)return;
if(mode==='presenter'){
if(message.type==='ready'){popupBlocked=false;sendGoto(true);}
if(message.type==='ack'){lastAckAt=Date.now();lastAckIndex=Number(message.index);lastAckSeq=Number(message.seq);updateSyncStatus();}
return;
}
if(mode==='audience'&&(message.type==='goto'||message.type==='ping')){
if(message.type==='goto')go(Number(message.index),{force:true});
emit({type:'ack',index:idx,seq:Number(message.seq)});
}
}
function sendGoto(force=false){if(mode!=='presenter')return;if(force||lastAckIndex!==idx)seq++;emit({type:'goto',index:idx,seq});updateSyncStatus();}
function syncState(){
if(popupBlocked)return'blocked';
if(audienceWindow&&audienceWindow.closed)return'disconnected';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected');
if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';
return'synced';
}
function updateSyncStatus(){
const el=document.getElementById('ppt-sync');if(!el)return;const state=syncState();
const labels={blocked:'弹窗被拦截',disconnected:'未连接',connecting:'连接中',unsynced:'未同步',synced:`已同步 · 第 ${idx+1} 页`};
el.dataset.state=state;el.textContent=`观众屏 · ${labels[state]}`;
}
function reopenAudience(){
initTransport();popupBlocked=false;audienceOpenedAt=Date.now();lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;
audienceWindow=window.open(frameUrl('audience',idx),`guizang-ppt-audience-${session}`,'popup=yes,width=1440,height=900');
if(!audienceWindow)popupBlocked=true;updateSyncStatus();
}
function enterPresenter(openAudience=true){
if(mode==='presenter')return;lowPowerBeforePresenter=window.__lowPowerMode;mode='presenter';initTransport();mountShell();
document.body.classList.remove('ppt-audience','ppt-preview');document.body.classList.add('ppt-presenter');document.title=`演讲者视图 · ${originalTitle}`;
window.__setLowPowerMode?.(true,{persist:false});updateShell();if(openAudience)reopenAudience();
}
function exitPresenter(){
emit({type:'bye'});mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
}
function initAudience(){mode='audience';document.body.classList.add('ppt-audience');document.title=`观众屏 · ${originalTitle}`;initTransport();emit({type:'ready',index:idx});}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});}
function onSlideChange(){if(mode==='presenter'){updateShell();sendGoto();}}
function handleKey(e){
const editable=e.target?.matches?.('input,textarea,[contenteditable="true"]');
if(editable){if(e.key==='Escape')e.target.blur();return true;}
if(mode==='audience'||mode==='preview'){e.preventDefault();return true;}
if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;}
if(mode!=='presenter')return false;
if(e.key==='Escape'){e.preventDefault();toggleOverview();return true;}
if(overviewOn)return true;
if(['ArrowRight','PageDown',' ','ArrowDown'].includes(e.key)){e.preventDefault();go(idx+1,{force:true});return true;}
if(['ArrowLeft','PageUp','ArrowUp'].includes(e.key)){e.preventDefault();go(idx-1,{force:true});return true;}
if(e.key==='Home'){e.preventDefault();go(0,{force:true});return true;}
if(e.key==='End'){e.preventDefault();go(total-1,{force:true});return true;}
return false;
}
function initMode(){
document.getElementById('ppt-low-power-btn')?.addEventListener('click',()=>window.__setLowPowerMode(!window.__lowPowerMode));
document.getElementById('ppt-overview-btn')?.addEventListener('click',toggleOverview);
document.getElementById('ppt-presenter-btn')?.addEventListener('click',()=>enterPresenter(true));
if(requestedMode==='audience')initAudience();else if(requestedMode==='preview')initPreview();else if(requestedMode==='presenter')enterPresenter(false);
}
setInterval(()=>{renderTimer();if(mode==='presenter'){if(audienceWindow?.closed)updateSyncStatus();emit({type:'ping',index:idx,seq});updateSyncStatus();}},1000);
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience};
})();
addEventListener('keydown',e=>{
if(window.__pptPresenter?.handleKey(e))return;
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
if(e.key && e.key.toLowerCase()==='b' && !e.metaKey && !e.ctrlKey && !e.altKey){
e.preventDefault();
@@ -732,6 +945,7 @@ addEventListener('touchend',e=>{
const initialSlideParam = new URLSearchParams(location.search).get('slide');
const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0;
go(Number.isFinite(initialSlide) ? initialSlide : 0);
window.__pptPresenter?.initMode();
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>