feat: expand presenter mode toolkit
This commit is contained in:
1 parent
c192c855d0
commit
8cad17760d
6 files changed
+800
-139
No files matched your search
+328
-51
@@ -217,23 +217,28 @@
|
||||
.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-sync[data-state="connecting"]::before,.ppt-sync[data-state="frozen"]::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(300px,.75fr);gap:18px;min-height:0;padding:18px 24px}
|
||||
.ppt-preview-stack{display:grid;grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px);gap:16px;min-width:0;min-height:0}
|
||||
.ppt-stage-card,.ppt-notes-card{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden}
|
||||
.ppt-stage-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}
|
||||
.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-viewport{display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
|
||||
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
|
||||
.ppt-annotation-tools{display:flex;align-items:center;gap:5px;margin-left:auto}.ppt-annotation-tools .ppt-ui-btn{padding:6px 9px;font-size:12px}.ppt-annotation-tools .ppt-ui-btn.is-active{color:#111318;background:#ff5b63;border-color:#ff5b63}.ppt-annotation-tools .ppt-clear-btn{min-width:0}
|
||||
.ppt-frame-viewport{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
|
||||
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}
|
||||
.ppt-annotation-layer{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:0;height:0;pointer-events:none;touch-action:none}.ppt-annotation-layer.is-active{pointer-events:auto;cursor:crosshair}
|
||||
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
|
||||
.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}
|
||||
.ppt-saved{color:#72df99}.ppt-presenter-bottom{display:grid;grid-template-columns:minmax(280px,1fr) auto minmax(260px,1fr);align-items:center;gap:20px;padding:14px 24px;border-top:1px solid #2b2e35;background:#15181d}
|
||||
.ppt-timer{display:flex;align-items:center;gap:9px;min-width:0}.ppt-time-copy{min-width:142px}.ppt-clock{font:600 24px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-time-detail{margin-top:5px;color:#9fa6b1;font-size:12px;white-space:nowrap}.ppt-time-detail.is-over{color:#ff8a90}.ppt-rehearsal-on{color:#ff8a90}.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}.ppt-auto-state{display:inline-flex;align-items:center;gap:6px;margin-top:7px}.ppt-auto-state.is-on{color:#72df99}.ppt-auto-state.is-paused{color:#f0c766}
|
||||
#ppt-presenter[data-layout="slides"] .ppt-presenter-main{grid-template-columns:minmax(0,2.15fr) minmax(260px,.58fr)}#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.15fr) minmax(380px,1fr)}
|
||||
.ppt-dialog{position:fixed;inset:0;z-index:130;display:grid;place-items:center;padding:24px;background:rgba(4,6,9,.72);backdrop-filter:blur(10px)}.ppt-dialog[hidden]{display:none}.ppt-dialog-panel{width:min(720px,calc(100vw - 48px));max-height:min(760px,calc(100vh - 48px));display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border:1px solid #343943;border-radius:14px;background:#171a20;box-shadow:0 24px 80px rgba(0,0,0,.45)}.ppt-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid #2b2f37}.ppt-dialog-title{font-size:20px;font-weight:700}.ppt-dialog-body{overflow:auto;padding:18px;color:#d8dce2;font-size:14px;line-height:1.6}.ppt-dialog-section+.ppt-dialog-section{margin-top:22px}.ppt-dialog-section h3{margin:0 0 10px;color:#fff;font-size:15px}.ppt-dialog-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.ppt-dialog-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #282c33}.ppt-dialog-row:last-child{border-bottom:0}.ppt-field{display:grid;gap:6px}.ppt-field label{color:#aeb4be;font-size:12px}.ppt-field input{width:100%;border:1px solid #3a3e48;border-radius:8px;background:#101216;color:#fff;padding:9px 10px;font:600 14px Inter,sans-serif}.ppt-check{display:flex;align-items:flex-start;gap:10px}.ppt-check-dot{width:9px;height:9px;margin-top:7px;border-radius:50%;background:#ff6b72}.ppt-check.is-ok .ppt-check-dot{background:#72df99}.ppt-check.is-warn .ppt-check-dot{background:#f0c766}.ppt-summary{width:100%;border-collapse:collapse;font-size:13px}.ppt-summary th,.ppt-summary td{padding:9px 8px;border-bottom:1px solid #2b2f37;text-align:left}.ppt-summary th{color:#9fa6b1;font-weight:600}.ppt-summary td:last-child,.ppt-summary th:last-child{text-align:right}.ppt-muted{color:#8f96a1}.ppt-shortcuts{display:grid;grid-template-columns:auto 1fr;gap:8px 14px}.ppt-shortcuts kbd{min-width:44px;padding:4px 7px;border:1px solid #3a3e48;border-radius:6px;background:#101216;color:#fff;text-align:center;font:600 12px var(--mono,monospace)}
|
||||
#overview{z-index:110!important}
|
||||
@media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.55fr) minmax(280px,.72fr)}}
|
||||
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px}.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-presenter-bottom{gap:8px;padding:12px 16px}.ppt-controls{gap:5px}.ppt-controls .ppt-ui-btn{min-width:58px;padding:8px 10px}.ppt-page-state{display:none}}
|
||||
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px;flex-wrap:wrap}.ppt-presenter-top .ppt-presenter-meta{gap:6px}.ppt-presenter-top .ppt-ui-btn{padding:8px 9px}.ppt-presenter-main,#ppt-presenter[data-layout="slides"] .ppt-presenter-main,#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-presenter-bottom{grid-template-columns:1fr auto;gap:8px;padding:10px 16px}.ppt-time-copy{min-width:112px}.ppt-time-detail{white-space:normal}.ppt-timer .ppt-ui-btn:nth-of-type(2){display:none}.ppt-controls{gap:5px}.ppt-controls .ppt-ui-btn{min-width:58px;padding:8px 10px}.ppt-page-state{display:none}.ppt-dialog-grid{grid-template-columns:1fr}}
|
||||
@media(max-height:700px){.ppt-presenter-top{padding-top:11px;padding-bottom:11px}.ppt-presenter-main{padding-top:12px;padding-bottom:12px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(96px,18vh,126px);gap:12px}.ppt-stage-label{padding:8px 11px}.ppt-notes-head{padding:12px 14px}.ppt-presenter-bottom{padding-top:10px;padding-bottom:10px}}
|
||||
|
||||
/* ============================================================
|
||||
@@ -534,10 +539,12 @@
|
||||
<script>
|
||||
const SPEAKER_NOTES = [
|
||||
/* {
|
||||
id: 'stable-slide-id', title: '页面标题', minutes: 1,
|
||||
id: 'stable-slide-id', title: '页面标题', section: '主体', minutes: 1,
|
||||
purpose: '这一页要让观众理解什么',
|
||||
talk: ['只在台上说的补充信息', '例子、证据或语气提示', '不复述页面可见文字'],
|
||||
transition: '为什么下一页紧接着出现', cue: '可选:停顿 / Demo / 视频'
|
||||
transition: '为什么下一页紧接着出现', cue: '可选:停顿 / Demo / 视频',
|
||||
interaction: '可选:现场提问', delivery: '可选:语气 / 重音',
|
||||
advance: '可选:翻页时机', fallback: '可选:Demo 失败时的备用说法'
|
||||
} */
|
||||
];
|
||||
window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
|
||||
@@ -749,51 +756,169 @@ window.__pptPresenter=(()=>{
|
||||
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')}`;
|
||||
const storageKey=name=>`guizang-ppt-${name}:${location.pathname}`;
|
||||
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 channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0,lastPingAt=0;
|
||||
let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17),presenterFrameObserver=null;
|
||||
let elapsed=0,timerRunning=false,timerStartedAt=0,lowPowerBeforePresenter=window.__lowPowerMode;
|
||||
let elapsed=0,timerRunning=false,timerStartedAt=0,timedSlideIndex=idx,slideTimerStartedAt=0,slideElapsed=Array(total).fill(0);
|
||||
let rehearsalActive=false,lowPowerBeforePresenter=window.__lowPowerMode;
|
||||
let layoutMode=localStorage.getItem(storageKey('layout'))||'balanced';
|
||||
let autoEnabled=localStorage.getItem(storageKey('auto-enabled'))==='1';
|
||||
let autoGlobalSeconds=Math.max(0,Number(localStorage.getItem(storageKey('auto-seconds'))||0));
|
||||
let autoDeadline=0,autoRemainingMs=0;
|
||||
let screenMode='normal',audienceFrozen=false;
|
||||
let annotationTool='none',annotationPointerDown=false,annotationStart=null,draftCircle=null,circles=[],audienceCircles=[];
|
||||
let laserPoint=null,audienceLaser=null,laserHideAt=0,audienceLaserHideAt=0,lastLaserSent=0;
|
||||
const seenNonces=[],seenNonceSet=new Set();
|
||||
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:'待补充转场'};
|
||||
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,purpose:'待补充本页目的',talk:['待补充讲述要点'],transition:'待补充转场'};
|
||||
}
|
||||
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
|
||||
function escapeHTML(value){return String(value??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));}
|
||||
function valueLines(value){
|
||||
if(Array.isArray(value))return value.filter(Boolean).map(x=>`• ${x}`).join('\n');
|
||||
return value?String(value):'';
|
||||
}
|
||||
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}`:''}`;
|
||||
const parts=[];
|
||||
if(n.purpose)parts.push(`【本页目的】\n${n.purpose}`);
|
||||
if(Array.isArray(n.talk)&&n.talk.length)parts.push(`【讲述要点】\n${valueLines(n.talk)}`);
|
||||
if(n.transition)parts.push(`【转场】\n${n.transition}`);
|
||||
const optional=[['舞台提示',n.cue],['互动',n.interaction],['表达提示',n.delivery],['翻页时机',n.advance],['备用方案',n.fallback],['读音提醒',n.pronunciation]];
|
||||
optional.forEach(([label,value])=>{const text=valueLines(value);if(text)parts.push(`【${label}】\n${text}`);});
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
|
||||
function loadNoteValue(id,i,n){
|
||||
const saved=localStorage.getItem(noteStorageKey(id));
|
||||
if(saved!==null)return saved;
|
||||
const prefix=window.__PPT_LEGACY_NOTE_KEY__;
|
||||
if(prefix){
|
||||
const legacy=localStorage.getItem(`${prefix}:${i}`);
|
||||
if(legacy!==null){localStorage.setItem(noteStorageKey(id),legacy);return legacy;}
|
||||
}
|
||||
return generatedNote(n);
|
||||
}
|
||||
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 plannedMsAt(i){
|
||||
const minutes=Number(noteAt(i).minutes);
|
||||
return Number.isFinite(minutes)&&minutes>0?minutes*60000:null;
|
||||
}
|
||||
function totalPlannedMs(){
|
||||
const values=Array.from({length:total},(_,i)=>plannedMsAt(i));
|
||||
return values.length&&values.every(Number.isFinite)?values.reduce((sum,x)=>sum+x,0):null;
|
||||
}
|
||||
function sectionInfo(i){
|
||||
const name=String(noteAt(i).section||'').trim();
|
||||
if(!name)return null;
|
||||
let first=i,last=i;
|
||||
while(first>0&&String(noteAt(first-1).section||'').trim()===name)first--;
|
||||
while(last<total-1&&String(noteAt(last+1).section||'').trim()===name)last++;
|
||||
const targets=[];for(let x=first;x<=last;x++)targets.push(plannedMsAt(x));
|
||||
const planned=targets.every(Number.isFinite)?targets.reduce((sum,x)=>sum+x,0):null;
|
||||
const used=slideElapsed.slice(first,last+1).reduce((sum,x)=>sum+x,0)+(timerRunning&&timedSlideIndex>=first&&timedSlideIndex<=last?Date.now()-slideTimerStartedAt:0);
|
||||
return{name,position:i-first+1,count:last-first+1,planned,used};
|
||||
}
|
||||
function durationText(ms){
|
||||
const value=Math.max(0,Math.round(Number(ms)||0)),seconds=Math.floor(value/1000);
|
||||
const hours=Math.floor(seconds/3600),minutes=Math.floor((seconds%3600)/60),secs=seconds%60;
|
||||
return hours?`${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`:`${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`;
|
||||
}
|
||||
function signedDuration(ms){return `${ms>=0?'+':'−'}${durationText(Math.abs(ms))}`;}
|
||||
function clockTime(timestamp){return new Date(timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}
|
||||
|
||||
function fitFrame(frame){
|
||||
const viewport=frame?.closest('.ppt-frame-viewport');
|
||||
if(!viewport)return;
|
||||
const availableWidth=viewport.clientWidth,availableHeight=viewport.clientHeight;
|
||||
if(!availableWidth||!availableHeight)return;
|
||||
const width=Math.min(availableWidth,availableHeight*(16/9));
|
||||
frame.style.width=`${width}px`;
|
||||
frame.style.height=`${width*(9/16)}px`;
|
||||
const width=Math.min(availableWidth,availableHeight*(16/9)),height=width*(9/16);
|
||||
frame.style.width=`${width}px`;frame.style.height=`${height}px`;
|
||||
if(frame.id==='ppt-current-frame')sizeInk(document.getElementById('ppt-presenter-ink'),width,height);
|
||||
}
|
||||
function fitPresenterFrames(){
|
||||
fitFrame(document.getElementById('ppt-current-frame'));
|
||||
fitFrame(document.getElementById('ppt-next-frame'));
|
||||
}
|
||||
function sizeInk(canvas,width,height){
|
||||
if(!canvas||!width||!height)return;
|
||||
canvas.style.width=`${width}px`;canvas.style.height=`${height}px`;
|
||||
const dpr=Math.min(devicePixelRatio||1,2),pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr);
|
||||
if(canvas.width!==pixelWidth||canvas.height!==pixelHeight){canvas.width=pixelWidth;canvas.height=pixelHeight;}
|
||||
}
|
||||
function drawEllipse(ctx,circle,width,height,alpha=1){
|
||||
const x1=circle.x1*width,y1=circle.y1*height,x2=circle.x2*width,y2=circle.y2*height;
|
||||
ctx.save();ctx.globalAlpha=alpha;ctx.strokeStyle='#ff4f59';ctx.lineWidth=Math.max(3,width*.004);ctx.lineCap='round';
|
||||
ctx.beginPath();ctx.ellipse((x1+x2)/2,(y1+y2)/2,Math.abs(x2-x1)/2,Math.abs(y2-y1)/2,0,0,Math.PI*2);ctx.stroke();ctx.restore();
|
||||
}
|
||||
function drawInk(canvas,list,laser,draft){
|
||||
if(!canvas)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;
|
||||
sizeInk(canvas,width,height);const dpr=canvas.width/width,ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
|
||||
(list||[]).forEach(circle=>drawEllipse(ctx,circle,width,height));
|
||||
if(draft)drawEllipse(ctx,draft,width,height,.72);
|
||||
if(laser){
|
||||
const x=laser.x*width,y=laser.y*height,r=Math.max(6,width*.007);
|
||||
const glow=ctx.createRadialGradient(x,y,0,x,y,r*2.8);glow.addColorStop(0,'rgba(255,255,255,.98)');glow.addColorStop(.2,'rgba(255,64,72,.98)');glow.addColorStop(1,'rgba(255,64,72,0)');
|
||||
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*2.8,0,Math.PI*2);ctx.fill();
|
||||
}
|
||||
}
|
||||
function renderInks(){
|
||||
const now=Date.now();if(now>laserHideAt)laserPoint=null;if(now>audienceLaserHideAt)audienceLaser=null;
|
||||
drawInk(document.getElementById('ppt-presenter-ink'),circles,laserPoint,draftCircle);
|
||||
drawInk(document.getElementById('ppt-audience-ink'),audienceCircles,audienceLaser,null);
|
||||
}
|
||||
function setAnnotationTool(tool){
|
||||
annotationTool=annotationTool===tool?'none':tool;annotationPointerDown=false;draftCircle=null;
|
||||
const canvas=document.getElementById('ppt-presenter-ink');canvas?.classList.toggle('is-active',annotationTool!=='none');
|
||||
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.classList.toggle('is-active',button.dataset.pptTool===annotationTool));
|
||||
renderInks();
|
||||
}
|
||||
function annotationPoint(event){
|
||||
const canvas=event.currentTarget,rect=canvas.getBoundingClientRect();
|
||||
return{x:Math.max(0,Math.min(1,(event.clientX-rect.left)/rect.width)),y:Math.max(0,Math.min(1,(event.clientY-rect.top)/rect.height))};
|
||||
}
|
||||
function onAnnotationPointerDown(event){
|
||||
if(annotationTool!=='circle')return;event.preventDefault();annotationPointerDown=true;annotationStart=annotationPoint(event);draftCircle={x1:annotationStart.x,y1:annotationStart.y,x2:annotationStart.x,y2:annotationStart.y};event.currentTarget.setPointerCapture?.(event.pointerId);renderInks();
|
||||
}
|
||||
function onAnnotationPointerMove(event){
|
||||
if(annotationTool==='laser'){
|
||||
const point=annotationPoint(event);laserPoint=point;laserHideAt=Date.now()+650;
|
||||
if(Date.now()-lastLaserSent>32){lastLaserSent=Date.now();emit({type:'laser',index:idx,point},{persist:false});}renderInks();return;
|
||||
}
|
||||
if(annotationTool==='circle'&&annotationPointerDown&&draftCircle){const point=annotationPoint(event);draftCircle.x2=point.x;draftCircle.y2=point.y;renderInks();}
|
||||
}
|
||||
function onAnnotationPointerUp(event){
|
||||
if(annotationTool!=='circle'||!annotationPointerDown||!draftCircle)return;event.preventDefault();annotationPointerDown=false;
|
||||
const circle={...draftCircle};draftCircle=null;
|
||||
if(Math.abs(circle.x2-circle.x1)>.012&&Math.abs(circle.y2-circle.y1)>.012){circles.push(circle);emit({type:'circle',index:idx,circle});}
|
||||
renderInks();
|
||||
}
|
||||
function onAnnotationPointerLeave(){
|
||||
if(annotationTool==='laser'){laserPoint=null;emit({type:'laser',index:idx,point:null},{persist:false});renderInks();}
|
||||
}
|
||||
function clearAnnotations(broadcast=true){circles=[];draftCircle=null;laserPoint=null;if(broadcast)emit({type:'clear',index:idx});renderInks();}
|
||||
|
||||
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><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div></div>
|
||||
document.body.insertAdjacentHTML('beforeend',`<div id="ppt-presenter" data-layout="${layoutMode}" aria-label="演讲者视图">
|
||||
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span><span id="ppt-section" hidden></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-preflight" class="ppt-ui-btn" type="button">检查</button><button id="ppt-settings" class="ppt-ui-btn" type="button">设置</button><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 class="ppt-annotation-tools"><button class="ppt-ui-btn" data-ppt-tool="laser" type="button" title="激光笔(L)">激光</button><button class="ppt-ui-btn" data-ppt-tool="circle" type="button" title="圈选(C)">圈选</button><button id="ppt-ink-clear" class="ppt-ui-btn ppt-clear-btn" type="button" title="清除标记(X)">清除</button></span><span id="ppt-current-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe><canvas id="ppt-presenter-ink" class="ppt-annotation-layer" aria-label="演讲标注层"></canvas></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></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 class="ppt-presenter-bottom"><div class="ppt-timer"><div class="ppt-time-copy"><div id="ppt-clock" class="ppt-clock">00:00</div><div id="ppt-time-detail" class="ppt-time-detail">本页 00:00 / —</div></div><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置</button><button id="ppt-rehearsal" 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><br><button id="ppt-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button></div></div>
|
||||
<div id="ppt-dialog" class="ppt-dialog" hidden><div class="ppt-dialog-panel" role="dialog" aria-modal="true"><div class="ppt-dialog-head"><div id="ppt-dialog-title" class="ppt-dialog-title"></div><button id="ppt-dialog-close" class="ppt-ui-btn" type="button">关闭</button></div><div id="ppt-dialog-body" class="ppt-dialog-body"></div></div></div>
|
||||
</div>`);
|
||||
document.getElementById('ppt-reopen').onclick=reopenAudience;
|
||||
document.getElementById('ppt-exit').onclick=exitPresenter;
|
||||
document.getElementById('ppt-preflight').onclick=openPreflight;
|
||||
document.getElementById('ppt-settings').onclick=openSettings;
|
||||
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;
|
||||
@@ -801,13 +926,20 @@ window.__pptPresenter=(()=>{
|
||||
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-rehearsal').onclick=toggleRehearsal;
|
||||
document.getElementById('ppt-auto-toggle').onclick=toggleAutoAdvance;
|
||||
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);
|
||||
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.onclick=()=>setAnnotationTool(button.dataset.pptTool));
|
||||
document.getElementById('ppt-ink-clear').onclick=()=>clearAnnotations(true);
|
||||
const ink=document.getElementById('ppt-presenter-ink');
|
||||
ink.addEventListener('pointerdown',onAnnotationPointerDown);ink.addEventListener('pointermove',onAnnotationPointerMove);ink.addEventListener('pointerup',onAnnotationPointerUp);ink.addEventListener('pointercancel',onAnnotationPointerUp);ink.addEventListener('pointerleave',onAnnotationPointerLeave);
|
||||
document.getElementById('ppt-dialog-close').onclick=closeDialog;
|
||||
document.getElementById('ppt-dialog').onclick=event=>{if(event.target.id==='ppt-dialog')closeDialog();};
|
||||
if('ResizeObserver'in window){presenterFrameObserver=new ResizeObserver(()=>requestAnimationFrame(fitPresenterFrames));presenterFrameObserver.observe(document.querySelector('.ppt-preview-stack'));}
|
||||
else addEventListener('resize',fitPresenterFrames);
|
||||
setNoteSize(noteSize,false);
|
||||
requestAnimationFrame(fitPresenterFrames);
|
||||
setNoteSize(noteSize,false);applyLayout(layoutMode,false);requestAnimationFrame(fitPresenterFrames);
|
||||
}
|
||||
function updateShell(){
|
||||
if(mode!=='presenter')return;mountShell();
|
||||
@@ -818,28 +950,143 @@ window.__pptPresenter=(()=>{
|
||||
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}`;
|
||||
const target=plannedMsAt(idx);document.getElementById('ppt-note-meta').textContent=`建议 ${target?(target/60000).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='已保存';}
|
||||
requestAnimationFrame(fitPresenterFrames);
|
||||
updateSyncStatus();
|
||||
if(renderedNoteId!==id){renderedNoteId=id;editor.value=loadNoteValue(id,idx,n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';}
|
||||
requestAnimationFrame(fitPresenterFrames);renderTimer();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 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 currentSlideElapsed(i=idx){return slideElapsed[i]+(timerRunning&&timedSlideIndex===i?Date.now()-slideTimerStartedAt:0);}
|
||||
function commitRunningTime(now=Date.now()){
|
||||
if(!timerRunning)return;elapsed+=now-timerStartedAt;slideElapsed[timedSlideIndex]+=now-slideTimerStartedAt;timerStartedAt=now;slideTimerStartedAt=now;
|
||||
}
|
||||
function rollSlideTimer(nextIndex){if(timerRunning)commitRunningTime();timedSlideIndex=nextIndex;slideTimerStartedAt=Date.now();}
|
||||
function renderTimer(){
|
||||
const totalUsed=currentElapsed(),pageUsed=currentSlideElapsed(),target=plannedMsAt(idx),clock=document.getElementById('ppt-clock');
|
||||
if(clock)clock.textContent=durationText(totalUsed);
|
||||
const detail=document.getElementById('ppt-time-detail');
|
||||
if(detail){
|
||||
detail.classList.toggle('is-over',Boolean(target&&pageUsed>target));
|
||||
detail.textContent=target?(pageUsed<=target?`本页 ${durationText(pageUsed)} / ${durationText(target)} · 剩余 ${durationText(target-pageUsed)}`:`本页 ${durationText(pageUsed)} / ${durationText(target)} · 超时 ${durationText(pageUsed-target)}`):`本页 ${durationText(pageUsed)} / —`;
|
||||
}
|
||||
const totalPlan=totalPlannedMs(),plan=document.getElementById('ppt-plan');
|
||||
if(plan)plan.textContent=totalPlan?`计划 ${Number(totalPlan/60000).toFixed(1)} 分钟 · 预计 ${clockTime(Date.now()+Math.max(0,totalPlan-totalUsed))}`:'计划 —';
|
||||
const section=sectionInfo(idx),sectionEl=document.getElementById('ppt-section');
|
||||
if(sectionEl){sectionEl.hidden=!section;sectionEl.textContent=section?`${section.name} · ${section.position}/${section.count}${section.planned?` · 剩余 ${durationText(Math.max(0,section.planned-section.used))}`:''}`:'';}
|
||||
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续':'开始');
|
||||
const rehearsalButton=document.getElementById('ppt-rehearsal');if(rehearsalButton){rehearsalButton.textContent=rehearsalActive?'结束排练':'排练';rehearsalButton.classList.toggle('ppt-rehearsal-on',rehearsalActive);}
|
||||
renderAutoState();
|
||||
}
|
||||
function toggleTimer(){const now=Date.now();if(timerRunning){commitRunningTime(now);timerRunning=false;}else{timerStartedAt=now;slideTimerStartedAt=now;timedSlideIndex=idx;timerRunning=true;}renderTimer();}
|
||||
function resetTimer(){
|
||||
elapsed=0;slideElapsed=Array(total).fill(0);timedSlideIndex=idx;
|
||||
if(timerRunning){timerStartedAt=Date.now();slideTimerStartedAt=timerStartedAt;}else{timerStartedAt=0;slideTimerStartedAt=0;}
|
||||
renderTimer();
|
||||
}
|
||||
function rehearsalHistory(){try{return JSON.parse(localStorage.getItem(storageKey('rehearsals'))||'[]');}catch(e){return[];}}
|
||||
function toggleRehearsal(){
|
||||
if(rehearsalActive){finishRehearsal(true);return;}
|
||||
rehearsalActive=true;if(timerRunning){commitRunningTime();timerRunning=false;}resetTimer();toggleTimer();renderTimer();
|
||||
}
|
||||
function finishRehearsal(showSummary){
|
||||
if(!rehearsalActive)return;if(timerRunning){commitRunningTime();timerRunning=false;}
|
||||
const result={endedAt:Date.now(),totalMs:elapsed,slides:slideElapsed.map((ms,i)=>({index:i,id:slideId(i),title:noteAt(i).title||slideId(i),ms,targetMs:plannedMsAt(i)}))};
|
||||
const history=rehearsalHistory();history.unshift(result);try{localStorage.setItem(storageKey('rehearsals'),JSON.stringify(history.slice(0,5)));}catch(e){}
|
||||
rehearsalActive=false;renderTimer();if(showSummary)openRehearsalSummary(result,history.slice(0,5));
|
||||
}
|
||||
function openRehearsalSummary(result,history){
|
||||
const rows=result.slides.filter(item=>item.ms>0).map(item=>`<tr><td>${item.index+1}</td><td>${escapeHTML(item.title)}</td><td>${durationText(item.ms)}</td><td>${item.targetMs?signedDuration(item.ms-item.targetMs):'—'}</td></tr>`).join('');
|
||||
const recent=history.map(item=>`<div class="ppt-dialog-row"><span>${new Date(item.endedAt).toLocaleString()}</span><strong>${durationText(item.totalMs)}</strong></div>`).join('');
|
||||
openDialog('排练记录',`<div class="ppt-dialog-section"><div class="ppt-dialog-row"><span>本次总时长</span><strong>${durationText(result.totalMs)}</strong></div><table class="ppt-summary"><thead><tr><th>#</th><th>页面</th><th>实际</th><th>与计划相比</th></tr></thead><tbody>${rows||'<tr><td colspan="4">没有记录到页面时间</td></tr>'}</tbody></table></div><div class="ppt-dialog-section"><h3>最近排练</h3>${recent}</div>`);
|
||||
}
|
||||
|
||||
function autoDurationMs(i=idx){const explicit=Number(noteAt(i).autoAdvanceSeconds);const seconds=Number.isFinite(explicit)&&explicit>0?explicit:autoGlobalSeconds;return seconds>0?seconds*1000:null;}
|
||||
function armAutoAdvance(){autoDeadline=0;autoRemainingMs=autoDurationMs()||0;}
|
||||
function autoBlockedReason(){
|
||||
if(overviewOn)return'总览中';if(annotationTool!=='none')return'标注中';if(screenMode!=='normal')return'观众屏已暂停';if(document.hidden)return'窗口不可见';
|
||||
const dialog=document.getElementById('ppt-dialog');if(dialog&&!dialog.hidden)return'设置中';
|
||||
if(audienceOpenedAt&&syncState()!=='synced')return'等待观众屏同步';return'';
|
||||
}
|
||||
function updateAutoAdvance(){
|
||||
if(!autoEnabled)return;
|
||||
const duration=autoDurationMs();if(!duration||idx===total-1)return;
|
||||
const blocked=autoBlockedReason(),now=Date.now();
|
||||
if(blocked){if(autoDeadline){autoRemainingMs=Math.max(0,autoDeadline-now);autoDeadline=0;}return;}
|
||||
if(!autoDeadline)autoDeadline=now+(autoRemainingMs||duration);
|
||||
autoRemainingMs=Math.max(0,autoDeadline-now);
|
||||
if(now>=autoDeadline){autoDeadline=0;autoRemainingMs=0;go(idx+1,{force:true});}
|
||||
}
|
||||
function renderAutoState(){
|
||||
const button=document.getElementById('ppt-auto-toggle');if(!button)return;
|
||||
const duration=autoDurationMs(),blocked=autoEnabled?autoBlockedReason():'';
|
||||
button.classList.toggle('is-on',autoEnabled&&!blocked);button.classList.toggle('is-paused',autoEnabled&&Boolean(blocked));
|
||||
if(!autoEnabled)button.textContent='自动翻页 · 关闭';
|
||||
else if(!duration)button.textContent='自动翻页 · 等待时长';
|
||||
else if(idx===total-1)button.textContent='自动翻页 · 已结束';
|
||||
else if(blocked)button.textContent=`自动翻页 · ${blocked}`;
|
||||
else button.textContent=`自动翻页 · ${durationText(autoRemainingMs||duration)}`;
|
||||
}
|
||||
function toggleAutoAdvance(){
|
||||
if(!autoEnabled&&!autoDurationMs()){openSettings();return;}
|
||||
autoEnabled=!autoEnabled;localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');armAutoAdvance();renderAutoState();
|
||||
}
|
||||
|
||||
function openDialog(title,html){
|
||||
const dialog=document.getElementById('ppt-dialog');if(!dialog)return;
|
||||
document.getElementById('ppt-dialog-title').textContent=title;document.getElementById('ppt-dialog-body').innerHTML=html;dialog.hidden=false;renderAutoState();
|
||||
}
|
||||
function closeDialog(){const dialog=document.getElementById('ppt-dialog');if(dialog)dialog.hidden=true;renderAutoState();}
|
||||
function applyLayout(value,persist=true){
|
||||
layoutMode=['balanced','slides','notes'].includes(value)?value:'balanced';
|
||||
document.getElementById('ppt-presenter')?.setAttribute('data-layout',layoutMode);
|
||||
if(persist)localStorage.setItem(storageKey('layout'),layoutMode);requestAnimationFrame(fitPresenterFrames);
|
||||
}
|
||||
function applyAudienceScreen(value){
|
||||
const cover=document.getElementById('ppt-audience-cover');if(!cover)return;
|
||||
if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none';
|
||||
}
|
||||
function setScreenMode(value){
|
||||
const next=['normal','black','white','frozen'].includes(value)?value:'normal',wasFrozen=audienceFrozen;
|
||||
screenMode=next;audienceFrozen=next==='frozen';emit({type:'screen',mode:next});
|
||||
if(wasFrozen&&!audienceFrozen)sendGoto(true);updateSyncStatus();armAutoAdvance();renderAutoState();
|
||||
}
|
||||
function openSettings(){
|
||||
openDialog('演示设置',`<div class="ppt-dialog-section"><h3>自动翻页</h3><div class="ppt-dialog-grid"><label class="ppt-dialog-row"><span>启用</span><input id="ppt-auto-enabled-setting" type="checkbox" ${autoEnabled?'checked':''}></label><div class="ppt-field"><label for="ppt-auto-seconds-setting">全局间隔(秒)</label><input id="ppt-auto-seconds-setting" type="number" min="1" step="1" value="${autoGlobalSeconds||''}" placeholder="未设置"></div><button id="ppt-settings-save" class="ppt-ui-btn" type="button">保存自动翻页</button></div><p class="ppt-muted">页面里的 autoAdvanceSeconds 优先于全局间隔;建议讲述时间 minutes 不会自动触发翻页。</p></div><div class="ppt-dialog-section"><h3>演讲者布局</h3><div class="ppt-dialog-grid"><button class="ppt-ui-btn" data-layout-choice="balanced" type="button">均衡</button><button class="ppt-ui-btn" data-layout-choice="slides" type="button">页面优先</button><button class="ppt-ui-btn" data-layout-choice="notes" type="button">备注优先</button></div></div><div class="ppt-dialog-section"><h3>观众屏</h3><div class="ppt-dialog-grid"><button class="ppt-ui-btn" data-screen-choice="normal" type="button">正常显示</button><button class="ppt-ui-btn" data-screen-choice="black" type="button">黑屏 B</button><button class="ppt-ui-btn" data-screen-choice="white" type="button">白屏 W</button><button class="ppt-ui-btn" data-screen-choice="frozen" type="button">冻结 F</button></div></div><div class="ppt-dialog-section"><button id="ppt-open-shortcuts" class="ppt-ui-btn" type="button">查看快捷键</button></div>`);
|
||||
document.querySelectorAll('[data-layout-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.layoutChoice===layoutMode);button.onclick=()=>{applyLayout(button.dataset.layoutChoice);openSettings();};});
|
||||
document.querySelectorAll('[data-screen-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.screenChoice===screenMode);button.onclick=()=>{setScreenMode(button.dataset.screenChoice);openSettings();};});
|
||||
document.getElementById('ppt-settings-save').onclick=()=>{autoEnabled=document.getElementById('ppt-auto-enabled-setting').checked;autoGlobalSeconds=Math.max(0,Number(document.getElementById('ppt-auto-seconds-setting').value||0));localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');localStorage.setItem(storageKey('auto-seconds'),String(autoGlobalSeconds));armAutoAdvance();closeDialog();};
|
||||
document.getElementById('ppt-open-shortcuts').onclick=openShortcuts;
|
||||
}
|
||||
function openShortcuts(){
|
||||
const items=[['← / →','上一页 / 下一页'],['Home / End','首页 / 尾页'],['Esc','总览;标注工具激活时先退出工具'],['L','激光笔'],['C','圈选'],['X','清除标记'],['B / W','观众屏黑屏 / 白屏'],['F','冻结 / 恢复观众屏'],['A','开启 / 关闭自动翻页'],['R','开始 / 结束排练'],['?','快捷键说明']];
|
||||
openDialog('快捷键',`<div class="ppt-shortcuts">${items.map(([key,label])=>`<kbd>${key}</kbd><span>${label}</span>`).join('')}</div>`);
|
||||
}
|
||||
function preflightChecks(){
|
||||
const images=[...document.images],videos=[...document.querySelectorAll('video')],frame=document.getElementById('ppt-current-frame'),rect=frame?.getBoundingClientRect();
|
||||
return[
|
||||
{label:'观众屏连接',ok:['synced','frozen'].includes(syncState()),detail:`当前状态:${document.getElementById('ppt-sync')?.textContent||'未连接'}`},
|
||||
{label:'弹窗权限',ok:!popupBlocked,detail:popupBlocked?'浏览器阻止了观众窗口':'未检测到弹窗拦截'},
|
||||
{label:'全屏状态',ok:Boolean(document.fullscreenElement),warn:true,detail:document.fullscreenElement?'已进入全屏':'尚未全屏,可在演讲前进入'},
|
||||
{label:'字体加载',ok:!document.fonts||document.fonts.status==='loaded',detail:document.fonts?`状态:${document.fonts.status}`:'浏览器不提供字体状态'},
|
||||
{label:'图片资源',ok:images.every(image=>image.complete&&image.naturalWidth>0),detail:`${images.filter(image=>image.complete&&image.naturalWidth>0).length} / ${images.length} 已加载`},
|
||||
{label:'视频资源',ok:videos.every(video=>video.readyState>=2),detail:videos.length?`${videos.filter(video=>video.readyState>=2).length} / ${videos.length} 可播放`:'没有视频'},
|
||||
{label:'当前页 16:9',ok:Boolean(rect&&Math.abs(rect.width/rect.height-16/9)<.003),detail:rect?`${Math.round(rect.width)} × ${Math.round(rect.height)}`:'尚未渲染'}
|
||||
];
|
||||
}
|
||||
function openPreflight(){
|
||||
const checks=preflightChecks(),html=checks.map(item=>`<div class="ppt-dialog-row"><div class="ppt-check ${item.ok?'is-ok':(item.warn?'is-warn':'')}"><span class="ppt-check-dot"></span><span><strong>${item.label}</strong><br><span class="ppt-muted">${item.detail}</span></span></div><strong>${item.ok?'正常':(item.warn?'建议':'异常')}</strong></div>`).join('');
|
||||
openDialog('演前检查',`<div class="ppt-dialog-section">${html}</div><div class="ppt-dialog-section"><button id="ppt-preflight-refresh" class="ppt-ui-btn" type="button">重新检查</button> <button id="ppt-enter-fullscreen" class="ppt-ui-btn" type="button">进入全屏</button></div><p class="ppt-muted">这里只能检查浏览器和观众页面的软件状态,不能确认 HDMI、转接器或投影仪线缆是否物理连接。</p>`);
|
||||
document.getElementById('ppt-preflight-refresh').onclick=openPreflight;
|
||||
document.getElementById('ppt-enter-fullscreen').onclick=async()=>{try{await document.documentElement.requestFullscreen();}catch(e){}openPreflight();};
|
||||
}
|
||||
|
||||
function initTransport(){
|
||||
if(transportReady)return;transportReady=true;
|
||||
@@ -847,62 +1094,89 @@ window.__pptPresenter=(()=>{
|
||||
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){
|
||||
function emit(payload,{persist=true}={}){
|
||||
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){}
|
||||
if(persist)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(message.nonce){if(seenNonceSet.has(message.nonce))return;seenNonceSet.add(message.nonce);seenNonces.push(message.nonce);if(seenNonces.length>256)seenNonceSet.delete(seenNonces.shift());}
|
||||
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});
|
||||
if(mode!=='audience')return;
|
||||
if(message.type==='screen'){applyAudienceScreen(message.mode);return;}
|
||||
if(message.type==='laser'&&Number(message.index)===idx){audienceLaser=message.point||null;audienceLaserHideAt=Date.now()+650;renderInks();return;}
|
||||
if(message.type==='circle'&&Number(message.index)===idx&&message.circle){audienceCircles.push(message.circle);renderInks();return;}
|
||||
if(message.type==='clear'&&Number(message.index)===idx){audienceCircles=[];renderInks();return;}
|
||||
if(message.type==='goto'||message.type==='ping'){
|
||||
if(message.type==='goto'){audienceCircles=Array.isArray(message.circles)?message.circles:[];applyAudienceScreen(message.screenMode);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 sendGoto(force=false){
|
||||
if(mode!=='presenter'||audienceFrozen)return;if(force||lastAckIndex!==idx)seq++;
|
||||
emit({type:'goto',index:idx,seq,screenMode,circles});updateSyncStatus();
|
||||
}
|
||||
function syncState(){
|
||||
if(popupBlocked)return'blocked';
|
||||
if(audienceWindow&&audienceWindow.closed)return'disconnected';
|
||||
if(popupBlocked)return'blocked';if(audienceWindow&&audienceWindow.closed)return'disconnected';if(audienceFrozen)return'frozen';
|
||||
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';
|
||||
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} 页`};
|
||||
const labels={blocked:'弹窗被拦截',disconnected:'未连接',connecting:'连接中',unsynced:'未同步',frozen:'已冻结',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;
|
||||
if(audienceFrozen){audienceFrozen=false;screenMode='normal';}
|
||||
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();
|
||||
window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience();
|
||||
}
|
||||
function exitPresenter(){
|
||||
emit({type:'bye'});mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
|
||||
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}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}`;
|
||||
document.body.insertAdjacentHTML('beforeend','<canvas id="ppt-audience-ink" aria-hidden="true"></canvas><div id="ppt-audience-cover" aria-hidden="true"></div>');
|
||||
const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize();
|
||||
initTransport();emit({type:'ready',index:idx});
|
||||
}
|
||||
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 onSlideChange(nextIndex){
|
||||
if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();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;}
|
||||
const key=e.key?.toLowerCase();
|
||||
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
|
||||
const dialog=document.getElementById('ppt-dialog');
|
||||
if(!dialog?.hidden){if(e.key==='Escape'){e.preventDefault();closeDialog();}return true;}
|
||||
if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else toggleOverview();return true;}
|
||||
if(overviewOn)return true;
|
||||
if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;}
|
||||
if(key==='c'){e.preventDefault();setAnnotationTool('circle');return true;}
|
||||
if(key==='x'){e.preventDefault();clearAnnotations(true);return true;}
|
||||
if(key==='b'){e.preventDefault();setScreenMode(screenMode==='black'?'normal':'black');return true;}
|
||||
if(key==='w'){e.preventDefault();setScreenMode(screenMode==='white'?'normal':'white');return true;}
|
||||
if(key==='f'){e.preventDefault();setScreenMode(audienceFrozen?'normal':'frozen');return true;}
|
||||
if(key==='a'){e.preventDefault();toggleAutoAdvance();return true;}
|
||||
if(key==='r'){e.preventDefault();toggleRehearsal();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;}
|
||||
@@ -915,8 +1189,11 @@ window.__pptPresenter=(()=>{
|
||||
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};
|
||||
setInterval(()=>{
|
||||
renderTimer();renderInks();updateAutoAdvance();
|
||||
if(mode==='presenter'&&Date.now()-lastPingAt>900){lastPingAt=Date.now();if(audienceWindow?.closed)updateSyncStatus();emit({type:'ping',index:idx,seq});updateSyncStatus();}
|
||||
},250);
|
||||
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})};
|
||||
})();
|
||||
|
||||
addEventListener('keydown',e=>{
|
||||
|
||||
Reference in new issue
Block a user