diff --git a/.github/workflows/presenter-runtime-sync.yml b/.github/workflows/presenter-runtime-sync.yml new file mode 100644 index 0000000..5b871a3 --- /dev/null +++ b/.github/workflows/presenter-runtime-sync.yml @@ -0,0 +1,24 @@ +name: Presenter runtime sync + +on: + pull_request: + paths: + - "assets/template*.html" + - "scripts/check-presenter-runtime-sync.mjs" + - ".github/workflows/presenter-runtime-sync.yml" + push: + branches: [main] + paths: + - "assets/template*.html" + - "scripts/check-presenter-runtime-sync.mjs" + - ".github/workflows/presenter-runtime-sync.yml" + +jobs: + presenter-runtime-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: node scripts/check-presenter-runtime-sync.mjs diff --git a/SKILL.md b/SKILL.md index cef89e6..4ce6108 100644 --- a/SKILL.md +++ b/SKILL.md @@ -448,9 +448,10 @@ cp "/assets/template-swiss.html" "项目/XXX/ppt/index.html" ```bash node /scripts/validate-presenter-mode.mjs path/to/index.html node /scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30 +node /scripts/check-presenter-runtime-sync.mjs ``` -它会拦截缺失/重复页面 ID、备注与页面错位、必填字段或可选字段类型错误、完整时间计划超出 90% 预算,以及计时、排练、自动翻页、标注、演前检查和观众屏恢复控件缺失。 +第一个脚本会拦截缺失/重复页面 ID、备注与页面错位、必填字段或可选字段类型错误、完整时间计划超出 90% 预算,以及计时、排练、自动翻页、标注、演前检查和观众屏恢复控件缺失。第二个脚本会拦截两套模板之间的演讲者 CSS / JS 漂移。 #### 4.0.1 · 先量后改:超出 / 空白 / 标题间距 diff --git a/assets/template-swiss.html b/assets/template-swiss.html index 76ec15a..455cb52 100644 --- a/assets/template-swiss.html +++ b/assets/template-swiss.html @@ -563,7 +563,7 @@ 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-preview{pointer-events:none}body.ppt-preview #deck{transition:none!important} 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:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} @@ -583,9 +583,9 @@ .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-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor} .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-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-next-card.is-end .ppt-frame{visibility:hidden}#ppt-next-card.is-end .ppt-frame-viewport::after{content:"演示结束";position:absolute;color:#727a86;font:650 14px/1 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em} .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-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-audience-cover.is-ended{display:grid!important;place-items:center;background:#101216!important;color:#f5f6f7;font:650 clamp(24px,3vw,46px)/1.2 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em} .ppt-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px} .ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px} .ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.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-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654} @@ -600,6 +600,7 @@ @media(max-width:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}} @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-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}} @media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}} + /* ============ /演讲者模式 ============ */ /* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */ #overview [data-anim]{opacity:1!important;transform:none!important} @@ -1602,7 +1603,7 @@ window.__pptPresenter=(()=>{ function noteAt(i){ const id=slideId(i); - return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,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]));} @@ -1616,7 +1617,7 @@ window.__pptPresenter=(()=>{ 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'); + return parts.join('\n\n')||'—'; } function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;} function loadNoteValue(id,i,n){ @@ -1634,6 +1635,14 @@ window.__pptPresenter=(()=>{ url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1)); return url.href; } + function postPreviewGoto(frame,i){ + try{frame.contentWindow?.postMessage({__guizangPptSync:1,session,role:'presenter',type:'preview-goto',index:i},'*');}catch(e){} + } + function updatePreviewFrame(frame,i){ + frame.dataset.slide=String(i); + if(!frame.dataset.previewBound){frame.dataset.previewBound='1';frame.addEventListener('load',()=>postPreviewGoto(frame,Number(frame.dataset.slide)));} + if(!frame.getAttribute('src'))frame.src=frameUrl('preview',i);else postPreviewGoto(frame,i); + } function plannedMsAt(i){ const minutes=Number(noteAt(i).minutes); return Number.isFinite(minutes)&&minutes>0?minutes*60000:null; @@ -1790,8 +1799,9 @@ window.__pptPresenter=(()=>{ 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);} + if(currentFrame.dataset.slide!==String(idx))updatePreviewFrame(currentFrame,idx); + if(nextFrame.dataset.slide!==String(next))updatePreviewFrame(nextFrame,next); + document.getElementById('ppt-next-card').classList.toggle('is-end',idx===total-1); 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} 页`; @@ -1898,8 +1908,13 @@ window.__pptPresenter=(()=>{ } function applyAudienceScreen(value){ const cover=document.getElementById('ppt-audience-cover');if(!cover)return; + cover.classList.remove('is-ended');cover.textContent=''; if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none'; } + function showAudienceEnded(){ + const cover=document.getElementById('ppt-audience-cover');if(cover){cover.style.display='';cover.style.background='';cover.textContent='演示已结束';cover.classList.add('is-ended');} + setTimeout(()=>{try{window.close();}catch(e){}},120); + } 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}); @@ -1959,6 +1974,7 @@ window.__pptPresenter=(()=>{ return; } if(mode!=='audience')return; + if(message.type==='bye'){showAudienceEnded();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;} @@ -1968,14 +1984,18 @@ window.__pptPresenter=(()=>{ emit({type:'ack',index:idx,seq:Number(message.seq)}); } } + function receivePreview(message){ + if(!message||message.__guizangPptSync!==1||message.session!==session||message.role!=='presenter'||message.type!=='preview-goto')return; + go(Number(message.index),{force:true}); + } 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(audienceFrozen)return'frozen'; - if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected'); - if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq2600)return'disconnected';if(lastAckIndex!==idx||lastAckSeq{ window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience(); } function exitPresenter(){ - if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);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;}togglePresenterOverview(false);emit({type:'bye'});audienceWindow=null;audienceOpenedAt=0;lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;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}`; @@ -2002,7 +2022,7 @@ window.__pptPresenter=(()=>{ const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize(); initTransport();emit({type:'ready',index:idx}); } - function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});} + function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});addEventListener('message',e=>{if(e.source===parent)receivePreview(e.data);});} function onSlideChange(nextIndex){ if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto(); } @@ -2013,9 +2033,9 @@ window.__pptPresenter=(()=>{ if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;} if(mode!=='presenter')return false; 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==='?'||key==='/'){e.preventDefault();openShortcuts();return true;} if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else togglePresenterOverview();return true;} if(presenterOverviewOn)return true; if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;} @@ -2044,6 +2064,7 @@ window.__pptPresenter=(()=>{ },250); return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})}; })(); +/* =============== /演讲者模式 / 观众屏同步 =============== */ addEventListener('keydown',e=>{ if(window.__pptPresenter?.handleKey(e))return; diff --git a/assets/template.html b/assets/template.html index 230e076..280488b 100644 --- a/assets/template.html +++ b/assets/template.html @@ -207,7 +207,7 @@ 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-preview{pointer-events:none}body.ppt-preview #deck{transition:none!important} 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:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} @@ -227,9 +227,9 @@ .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-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor} .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-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-next-card.is-end .ppt-frame{visibility:hidden}#ppt-next-card.is-end .ppt-frame-viewport::after{content:"演示结束";position:absolute;color:#727a86;font:650 14px/1 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em} .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-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-audience-cover.is-ended{display:grid!important;place-items:center;background:#101216!important;color:#f5f6f7;font:650 clamp(24px,3vw,46px)/1.2 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em} .ppt-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px} .ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px} .ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.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-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654} @@ -244,6 +244,7 @@ @media(max-width:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}} @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-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}} @media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}} + /* ============ /演讲者模式 ============ */ /* ============================================================ ============ LAYOUTS API · 面向 agent 的类(v2)============ @@ -779,7 +780,7 @@ window.__pptPresenter=(()=>{ function noteAt(i){ const id=slideId(i); - return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,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]));} @@ -793,7 +794,7 @@ window.__pptPresenter=(()=>{ 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'); + return parts.join('\n\n')||'—'; } function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;} function loadNoteValue(id,i,n){ @@ -811,6 +812,14 @@ window.__pptPresenter=(()=>{ url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1)); return url.href; } + function postPreviewGoto(frame,i){ + try{frame.contentWindow?.postMessage({__guizangPptSync:1,session,role:'presenter',type:'preview-goto',index:i},'*');}catch(e){} + } + function updatePreviewFrame(frame,i){ + frame.dataset.slide=String(i); + if(!frame.dataset.previewBound){frame.dataset.previewBound='1';frame.addEventListener('load',()=>postPreviewGoto(frame,Number(frame.dataset.slide)));} + if(!frame.getAttribute('src'))frame.src=frameUrl('preview',i);else postPreviewGoto(frame,i); + } function plannedMsAt(i){ const minutes=Number(noteAt(i).minutes); return Number.isFinite(minutes)&&minutes>0?minutes*60000:null; @@ -967,8 +976,9 @@ window.__pptPresenter=(()=>{ 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);} + if(currentFrame.dataset.slide!==String(idx))updatePreviewFrame(currentFrame,idx); + if(nextFrame.dataset.slide!==String(next))updatePreviewFrame(nextFrame,next); + document.getElementById('ppt-next-card').classList.toggle('is-end',idx===total-1); 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} 页`; @@ -1075,8 +1085,13 @@ window.__pptPresenter=(()=>{ } function applyAudienceScreen(value){ const cover=document.getElementById('ppt-audience-cover');if(!cover)return; + cover.classList.remove('is-ended');cover.textContent=''; if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none'; } + function showAudienceEnded(){ + const cover=document.getElementById('ppt-audience-cover');if(cover){cover.style.display='';cover.style.background='';cover.textContent='演示已结束';cover.classList.add('is-ended');} + setTimeout(()=>{try{window.close();}catch(e){}},120); + } 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}); @@ -1136,6 +1151,7 @@ window.__pptPresenter=(()=>{ return; } if(mode!=='audience')return; + if(message.type==='bye'){showAudienceEnded();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;} @@ -1145,14 +1161,18 @@ window.__pptPresenter=(()=>{ emit({type:'ack',index:idx,seq:Number(message.seq)}); } } + function receivePreview(message){ + if(!message||message.__guizangPptSync!==1||message.session!==session||message.role!=='presenter'||message.type!=='preview-goto')return; + go(Number(message.index),{force:true}); + } 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(audienceFrozen)return'frozen'; - if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':(audienceOpenedAt?'unsynced':'disconnected'); - if(Date.now()-lastAckAt>2600||lastAckIndex!==idx||lastAckSeq2600)return'disconnected';if(lastAckIndex!==idx||lastAckSeq{ window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience(); } function exitPresenter(){ - if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);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;}togglePresenterOverview(false);emit({type:'bye'});audienceWindow=null;audienceOpenedAt=0;lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;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}`; @@ -1179,7 +1199,7 @@ window.__pptPresenter=(()=>{ const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize(); initTransport();emit({type:'ready',index:idx}); } - function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});} + function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});addEventListener('message',e=>{if(e.source===parent)receivePreview(e.data);});} function onSlideChange(nextIndex){ if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto(); } @@ -1190,9 +1210,9 @@ window.__pptPresenter=(()=>{ if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;} if(mode!=='presenter')return false; 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==='?'||key==='/'){e.preventDefault();openShortcuts();return true;} if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else togglePresenterOverview();return true;} if(presenterOverviewOn)return true; if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;} @@ -1221,6 +1241,7 @@ window.__pptPresenter=(()=>{ },250); return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})}; })(); +/* =============== /演讲者模式 / 观众屏同步 =============== */ addEventListener('keydown',e=>{ if(window.__pptPresenter?.handleKey(e))return; diff --git a/docs/unification-plan.md b/docs/unification-plan.md index 6a1aeed..406e709 100644 --- a/docs/unification-plan.md +++ b/docs/unification-plan.md @@ -1,7 +1,7 @@ # 多主题统一管理整合方案 > 基于对 template.html / template-swiss.html 两份模板与全部 references 的逐行对比分析(2026-07)。 -> 本文档是**方案建议**,尚未实施;按阶段推进,每个阶段独立可交付、可回滚。 +> 本文档是**方案建议**,尚未整体实施;按阶段推进,每个阶段独立可交付、可回滚。2026-08-07 已先为演讲者 CSS / JS 加入边界标记、字节级同步脚本和 GitHub Actions,但尚未提取共享源文件。 ## 现状诊断 @@ -52,6 +52,10 @@ - **a. 构建脚本**(推荐):`scripts/build-templates.mjs` 把 `src/runtime.js` + 各主题 `src/style-*.css` + 皮肤 HTML 组装成各 template*.html。模板仍是完整单文件,但源头唯一。 - **b. 文档约定**:不引入构建,把 runtime 段落用 `` 注释标记,改动时用脚本校验各份一致。成本低但漂移风险仍在。 +当前演讲者模式已先落地 Phase 1b 的防漂移护栏:`scripts/check-presenter-runtime-sync.mjs` 与 `.github/workflows/presenter-runtime-sync.yml`。它不替代长期的共享源提取,但可以防止修改只落到其中一份模板。 + +如果未来合并 Bento 分支,以 `main` 上的演讲者运行时为上游,采用 `main → bento` 的合并方向;不要用旧 Bento 模板反向覆盖 `main`。 + ### Phase 2 · 公共 Token 契约 在各模板间统一**命名和语义**(值可以不同): diff --git a/references/checklist.md b/references/checklist.md index 8eaa029..dbefe20 100644 --- a/references/checklist.md +++ b/references/checklist.md @@ -22,9 +22,12 @@ - 右侧卡片依次显示标题、本页目的和草稿备注;设置里的开关用胶囊 switch,时间间隔用美化后的加减 stepper。 - 观众屏必须显示软件链路状态:`连接中 / 已同步 / 未同步 / 未连接 / 弹窗被拦截`,并始终提供“重新打开观众屏”。 - 演讲者必须能看到总时长、本页时长、计划/剩余/超时;缺失 `minutes` 时显示横杠,不猜测。 +- 临时新增页面没有对应备注时,界面 fallback 只显示 `—`,不显示“待补充”;校验器仍要报告页面/备注数量不一致。 - 排练模式记录每页实际时长,本地保留最近 5 次;只做数据汇总,不做 AI 教练评分。 - 自动翻页默认关闭,`minutes` 与 `autoAdvanceSeconds` 必须分开;总览、圈选、设置、页面隐藏、观众屏暂停或失同步时倒计必须暂停。 - 必须提供激光笔、圈选、清除、黑屏、白屏和冻结/恢复;标注坐标要归一化同步到观众屏。 +- 预览 iframe 只初次加载,翻页改用 `postMessage` 同步页码;尾页的下一页预览显示“演示结束”,不重复当前页。 +- 退出演讲必须处理 `bye`:脚本打开的观众窗口自动关闭,无法自关时显示“演示已结束”;心跳超时必须显示“未连接”而不是永久停在“未同步”。 - 演前检查必须覆盖观众屏、弹窗、全屏、字体、图片/视频和 16:9;同时明确提醒它不能检测物理线缆。 - 备注里的 `section / cue / interaction / delivery / advance / fallback / pronunciation / autoAdvanceSeconds` 都是可选字段;用户大纲没有就整段隐藏。 - 每页必须有稳定、唯一的 `data-slide-id`;`SPEAKER_NOTES` 数量、顺序和 ID 必须逐页一致。 diff --git a/references/presenter-mode.md b/references/presenter-mode.md index 91305a3..e5e3996 100644 --- a/references/presenter-mode.md +++ b/references/presenter-mode.md @@ -25,6 +25,7 @@ - 大纲明确写了停顿、提问、Demo、视频、语气、翻页点或备用路径:分别写入对应的可选字段。 - 用户明确要求某页自动停留多少秒:才写 `autoAdvanceSeconds`。 - 没有提供的信息不猜测、不写“待补充”到演讲界面,直接省略字段。界面对缺失时长显示横杠,其他可选模块整段隐藏。 +- 如果页面在未运行校验的情况下临时新增,且没有对应备注,演讲者界面只显示中性横杠 `—`,不显示“待补充”或虚构内容;校验器仍应报告备注与页面不一致。 `minutes` 是讲述计划,`autoAdvanceSeconds` 是播放行为,二者必须分开。不得因为一页建议讲 1 分钟,就默认 60 秒后自动翻页。 @@ -92,6 +93,7 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES; - 点击后当前窗口进入演讲者视图,并打开独立观众屏。 - 主体保持两栏:左侧预览、右侧备注;左侧当前页在上、下一页在下。 - 当前页与下一页 iframe 始终严格保持 `16:9`。空间不足时留边并整体等比缩小,不得裁切、压扁或让页面内部文字重新排版。 +- 预览 iframe 只在初次挂载时加载 HTML,翻页时通过 `postMessage` 发送页码,不反复改写 `src` 重载整份演示稿。 - 小屏优先缩小下一页预览,把空间留给当前页。 - 底栏分成三段:左侧是 `已进行 / 本页 / 剩余或超时` 三组时间,中间是两行控制(第一行 `首页 / 上一页 / 下一页 / 尾页`,第二行 `开始计时或暂停 / 重置计时 / 排练`),右侧只显示 `第几页 / 总页数` 和完成百分比。不要在底栏重复当前页标题。计时按钮必须明确写成“开始计时 / 继续计时 / 重置计时”,避免让用户误以为是重置整场演示。 - `自动翻页` 放在右上角状态栏,不占用底部翻页区。 @@ -100,6 +102,7 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES; - 演讲者可从“均衡 / 页面优先 / 备注优先”三种布局选择。 - 备注编辑按 `data-slide-id` 存入 `localStorage`,显示保存状态;切页后备注滚动回顶部,并允许调整字号。 - `Home` / `End`、方向键、PageUp/PageDown 与可见按钮保持一致;光标在备注编辑器中时不得触发翻页。 +- 对话框已打开时,`?` 不得覆盖当前对话框;只有 `Escape` 关闭当前对话框。尾页的“下一页”预览显示“演示结束”,不重复当前页。 - 设置面板使用可辨识的卡片层级;开关使用胶囊 switch,数值间隔使用带减号、数值和加号的 stepper,不直接暴露浏览器原生复选框或粗糙数字输入框。 ## 6. 时间控制与排练 @@ -134,13 +137,15 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES; - **连接中**:观众窗口已打开,尚未收到确认。 - **已同步**:观众页确认的页码与演讲者页相同,心跳有效。 -- **未同步**:确认超时或两端页码不同。 +- **未同步**:观众页心跳仍有效,但两端页码或序列号不同。 - **已冻结**:观众屏故意保留原页,不是同步故障。 -- **未连接**:观众窗口未打开或已关闭。 +- **未连接**:观众窗口未打开、已关闭,或心跳已超时。即使某些内置浏览器的 `window.closed` 代理不可靠,心跳超时后也必须落到“未连接”。 - **弹窗被拦截**:浏览器阻止打开观众窗口。 “重新打开观众屏”必须始终可用,并在恢复后立即发送当前页。 +演讲者退出演讲模式时必须发送 `bye`。由脚本打开的观众窗口尝试自动关闭;如果浏览器不允许自关,则保留深色“演示已结束”遮罩,不得继续停留在最后一页假装演示仍在进行。 + 浏览器只能确认观众**页面/窗口**的软件同步状态,不能检测 HDMI、转接器或投影仪线缆是否物理断开。现场仍应目视确认外接屏。 ## 10. 演前检查和快捷键 @@ -174,6 +179,6 @@ node /scripts/validate-presenter-mode.mjs path/to/index.html node /scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30 ``` -浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页、内嵌宫格替换预览与选页返回、首页/尾页、尾页重新开始、观众窗口关闭后的状态变化、重新打开后恢复同步、备注保存、计时、排练记录、自动翻页暂停/恢复、激光笔、圈选、清除、黑屏、白屏、冻结、设置面板与演前检查。 +浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页且预览 iframe 不重载、内嵌宫格替换预览与选页返回、首页/尾页、尾页预览结束态、尾页重新开始、观众窗口关闭后显示“未连接”、重新打开后恢复同步、退出演讲后观众屏关闭或显示结束遮罩、备注保存、计时、排练记录、自动翻页暂停/恢复、激光笔、圈选、清除、黑屏、白屏、冻结、设置面板与演前检查。 至少在一组常用尺寸和一组小屏尺寸下检查:当前页/下一页上下排列,两个 iframe 宽高比均为 `16:9`,且没有超出各自容器。 diff --git a/scripts/check-presenter-runtime-sync.mjs b/scripts/check-presenter-runtime-sync.mjs new file mode 100644 index 0000000..7b58493 --- /dev/null +++ b/scripts/check-presenter-runtime-sync.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +import {readFileSync} from 'node:fs'; +import {dirname,resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const root=resolve(dirname(fileURLToPath(import.meta.url)),'..'); +const files=['assets/template.html','assets/template-swiss.html']; +const blocks=[ + ['presenter CSS','/* ============ 演讲者模式 ============ */','/* ============ /演讲者模式 ============ */'], + ['presenter JavaScript','/* =============== 演讲者模式 / 观众屏同步 =============== */','/* =============== /演讲者模式 / 观众屏同步 =============== */'], +]; + +function extract(source,label,startMarker,endMarker,file){ + const start=source.indexOf(startMarker),end=source.indexOf(endMarker,start+startMarker.length); + if(start<0||end<0)throw new Error(`${file}: missing ${label} boundary marker.`); + return source.slice(start,end+endMarker.length); +} + +function firstDifferentLine(a,b){ + const left=a.split('\n'),right=b.split('\n'),count=Math.max(left.length,right.length); + for(let i=0;i[file,readFileSync(resolve(root,file),'utf8')]); +let failed=false; +for(const [label,start,end] of blocks){ + const extracted=sources.map(([file,source])=>[file,extract(source,label,start,end,file)]); + const [baseFile,base]=extracted[0]; + let blockFailed=false; + for(const [file,value] of extracted.slice(1)){ + if(value===base)continue; + failed=true; + blockFailed=true; + console.error(`ERROR ${label} drift: ${baseFile} and ${file} first differ at block line ${firstDifferentLine(base,value)}.`); + } + if(!blockFailed)console.log(`PASS ${label} is byte-identical across ${files.length} templates.`); +} +process.exit(failed?1:0); diff --git a/scripts/validate-presenter-mode.mjs b/scripts/validate-presenter-mode.mjs index 29db95b..8e79252 100644 --- a/scripts/validate-presenter-mode.mjs +++ b/scripts/validate-presenter-mode.mjs @@ -39,6 +39,7 @@ function extractArray(name){ const slideTags=[...source.matchAll(/]*\bclass="[^"]*\bslide\b[^"]*")[^>]*>/g)].map(m=>m[0]); const slideIds=slideTags.map((tag,i)=>tag.match(/\bdata-slide-id="([^"]+)"/)?.[1]||''); +if(!slideIds.length)warnings.push('No slides found; only the reusable presenter runtime can be validated.'); slideIds.forEach((id,i)=>{ if(!id)errors.push(`Slide ${i+1}: missing data-slide-id.`); @@ -86,16 +87,15 @@ else{ const runtimeChecks=[ ['right-bottom presenter entry','id="ppt-presenter-btn"'], ['presenter shell','id="ppt-presenter"'], - ['vertical preview stack','grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px)'], ['16:9 preview viewport','class="ppt-frame-viewport"'], ['proportional preview fitter','fitPresenterFrames'], - ['exact 16:9 fitting','availableHeight*(16/9)'], ['overview control','id="ppt-grid"'], ['embedded presenter overview','id="ppt-presenter-overview"'], ['first-page control','id="ppt-first"'], ['last/restart control','id="ppt-last"'], ['audience status','id="ppt-sync"'], ['audience recovery','id="ppt-reopen"'], + ['audience shutdown handling','showAudienceEnded'], ['direct window sync','postMessage('], ['BroadcastChannel fallback','BroadcastChannel'], ['storage fallback','__guizangPptSync'], @@ -110,12 +110,16 @@ const runtimeChecks=[ ['layout presets','data-layout-choice'], ['capsule auto-advance switch','class="ppt-switch"'], ['styled interval stepper','class="ppt-stepper"'], + ['reload-free preview navigation','preview-goto'], ['shortcut help','openShortcuts'], ]; for(const [label,needle]of runtimeChecks)if(!html.includes(needle))errors.push(`Presenter runtime missing ${label}.`); +if(!/\.ppt-preview-stack\s*\{[^}]*grid-template-rows\s*:/s.test(html))errors.push('Presenter preview stack must define vertical grid rows.'); +if(!/\.ppt-frame\s*\{[^}]*aspect-ratio\s*:\s*16\s*\/\s*9/s.test(html))errors.push('Presenter preview frames must declare a 16:9 aspect ratio.'); +if(!/fitPresenterFrames[\s\S]*?16\s*\/\s*9/.test(html))errors.push('Presenter preview fitter must preserve the 16:9 ratio.'); if(!/id="ppt-timer-toggle"[^>]*>开始计时<\/button>/.test(html))errors.push('Presenter timer must use the explicit label "开始计时".'); if(!/id="ppt-timer-reset"[^>]*>重置计时<\/button>/.test(html))errors.push('Presenter timer reset must use the explicit label "重置计时".'); -if(!html.includes("totalUsed?'继续计时':'开始计时'"))errors.push('Presenter timer resume state must use the explicit label "继续计时".'); +if(!html.includes('继续计时'))errors.push('Presenter timer resume state must use the explicit label "继续计时".'); if(/\.ppt-preview-stack\{[^}]*grid-template-columns/.test(html))errors.push('Presenter previews must stack vertically; remove grid-template-columns from .ppt-preview-stack.'); const timedNotes=speakerNotes.filter(n=>Number.isFinite(Number(n?.minutes))&&Number(n.minutes)>0);