Add resilient presenter mode and speaker notes
This commit is contained in:
1 parent
929c2ecb63
commit
006ba0b87a
6 files changed
+704
-23
No files matched your search
+222
-8
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user