dsh-ppt 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0(2026-09-27)
4
+
5
+ - 包含新的演讲者预览、缩略图和计时控件。浏览器不支持或拒绝全屏时显示操作提示,不再无声失败。
6
+
7
+ - **演讲者视图升级**:独立窗口内显示真实当前页与下一页预览(复用幻灯片渲染)、全片缩略图条(点击跳页)、wall clock + 计时(P 暂停 / T 重置)、备注字号 +/-、黑屏 / 白屏(B / W)、结束放映一键退出。
8
+ - **通信更稳**:演讲者窗口改用 Blob URL + postMessage 同步,不再依赖同源 document 直连,file:// 打开的 deck 也能正常联动;窗口被直接关闭后 0.8 秒内自动恢复普通模式。
9
+ - **观众端**:黑屏 / 白屏时隐藏 HUD;观众端备注与 HUD 默认隐藏逻辑保持不变。
10
+ - 播放器保留 0.4.5 的全部修复,70 项测试通过。
3
11
  ## 0.4.5(2026-09-23)
4
12
 
5
13
  - **播放器交互修复(由合成用户复测驱动)**:备注面板不再遮挡页脚 HUD,新增关闭按钮并支持 Esc;无备注页显示「本页无备注」占位;`G` 改为缩略图网格总览(当前页高亮、页码同步、点击跳页、Esc 退出、页码角标);`?` 打开快捷键帮助;表格页补页面标题、数字列右对齐、表格边界与内容列对齐、表头对比度达标;窄屏字号下限 14px、HUD 按钮 44px;新增环绕提示、ArrowUp/Down 翻页、数字 1-9 跳页、focus-visible 与 reduced-motion 处理。
package/README.en.md CHANGED
@@ -1,4 +1,10 @@
1
1
  # dsh-ppt
2
+
3
+ ## 0.5.0 update (2026-09-27)
4
+
5
+ Includes presenter previews, thumbnails and timer controls. Unsupported or rejected fullscreen requests now show an actionable message instead of silently failing.
6
+
7
+ Validation host: Harness 0.1.7-rc.2 built from official sources, retaining the local tool-scheduler fix. Build and automated checks pass; interactive coverage and external-service limits are recorded in this release round.
2
8
 
3
9
  ![npm](https://img.shields.io/npm/v/dsh-ppt) ![downloads](https://img.shields.io/npm/dm/dsh-ppt) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-ppt) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-ppt?style=social)
4
10
 
@@ -85,7 +91,7 @@ Existing artifacts are not overwritten by default: if any member of the trio alr
85
91
 
86
92
  | File | Purpose |
87
93
  | --- | --- |
88
- | `*.html` | Standalone slideshow: arrow keys, V presenter view (separate window with notes/next/timer/progress), S notes, G thumbnail overview, F fullscreen, P print, ? shortcuts, Esc close |
94
+ | `*.html` | Standalone slideshow: arrow keys, V presenter view (separate window with real current/next previews, thumbnail strip, notes, timer, progress, pause, font size, blank screen), S notes, G thumbnail overview, F fullscreen, P print, ? shortcuts, Esc close |
89
95
  | `*.pptx` | Editable 16:9 presentation (hand-written OOXML, zip via `node:zlib`, no third-party deps) |
90
96
  | `*.json` | Structured manifest (version, theme, language, slides) |
91
97
 
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  [English](README.en.md)
2
2
 
3
3
  # dsh-ppt
4
+
5
+ ## 0.5.0 更新(2026-09-27)
6
+
7
+ 包含新的演讲者预览、缩略图和计时控件。浏览器不支持或拒绝全屏时显示操作提示,不再无声失败。
8
+
9
+ 验证宿主:官方源码构建的 Harness 0.1.7-rc.2(保留本地工具调度器修复)。构建与自动测试通过;实际操作和外部服务限制见本轮验收记录。
4
10
 
5
11
  > **一句话 / 一篇文档 → 完整演示文稿**:HTML 网页放映 + PPTX 导出,5 套视觉主题,页间转场 + 要点入场动画,中英双语。
6
12
 
@@ -78,7 +84,7 @@ node <skill-dir>/scripts/build-deck.mjs \
78
84
 
79
85
  | 文件 | 用途 |
80
86
  | --- | --- |
81
- | `*.html` | 独立网页放映:← → 翻页,V 演讲者视图(独立窗口:备注/下一页/计时/进度),S 备注,G 缩略图总览,F 全屏,P 打印,? 快捷键,Esc 关闭 |
87
+ | `*.html` | 独立网页放映:← → 翻页,V 演讲者视图(独立窗口:真实当前页/下一页预览、缩略图跳页、备注、计时、进度、暂停、字号、黑/白屏),S 备注,G 缩略图总览,F 全屏,P 打印,? 快捷键,Esc 关闭 |
82
88
  | `*.pptx` | 16:9 可编辑演示文稿(OOXML 由插件手写,zip 用 node:zlib,无第三方依赖) |
83
89
  | `*.json` | 结构化 manifest(版本、主题、语言、每页内容) |
84
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ppt",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "DSH 演示文稿插件:Markdown 生成网页放映与可编辑 PPTX,长表格自动分页,内置 5 套主题、备注与转场动画。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -171,6 +171,7 @@ export const LANGUAGES = {
171
171
  presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示,观众看不到',
172
172
  wrapHint: '已环绕到',
173
173
  popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
174
+ fullscreenUnavailable: '当前窗口无法进入全屏,请在浏览器中独立打开演示文件后重试。',
174
175
  notesCloseLabel: '关闭备注',
175
176
  overviewHint: '点击缩略图跳页,方向键切换,Enter 进入,Esc 退出'
176
177
  },
@@ -206,6 +207,7 @@ export const LANGUAGES = {
206
207
  presenterHint: 'Notes: S shows them on top until a presenter window is open; afterwards only in the presenter window.',
207
208
  wrapHint: 'Wrapped to',
208
209
  popupBlocked: 'The presenter window was blocked. Allow pop-ups and press V to retry.',
210
+ fullscreenUnavailable: 'Fullscreen is unavailable here. Open the presentation in a browser tab and try again.',
209
211
  notesCloseLabel: 'Close notes',
210
212
  overviewHint: 'Click a thumbnail to jump; arrows move, Enter opens, Esc exits'
211
213
  },
@@ -241,6 +243,7 @@ export const LANGUAGES = {
241
243
  presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示 · Notes: S shows on top until a presenter window is open',
242
244
  wrapHint: '已环绕到 · Wrapped to',
243
245
  popupBlocked: '浏览器拦截了演讲者窗口 · Presenter window blocked; allow pop-ups and press V',
246
+ fullscreenUnavailable: '当前窗口无法全屏,请独立打开演示文件 · Open this presentation in a browser tab for fullscreen.',
244
247
  notesCloseLabel: '关闭备注 · Close notes',
245
248
  overviewHint: '点击缩略图跳页 · Click to jump, arrows move, Enter opens, Esc exits'
246
249
  },
@@ -1157,6 +1160,10 @@ body.notes-open #notes-panel{display:block}
1157
1160
  padding:9px 18px;font-size:13px;
1158
1161
  }
1159
1162
  #toast[hidden]{display:none}
1163
+ #blank{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
1164
+ body.blank-black #blank{display:block;background:#000}
1165
+ body.blank-white #blank{display:block;background:#fff}
1166
+ body.blank-black #hud,body.blank-white #hud{visibility:hidden;pointer-events:none}
1160
1167
  :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
1161
1168
  body.overview #stage{
1162
1169
  display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;
@@ -1239,11 +1246,12 @@ ${slides}
1239
1246
  </div>
1240
1247
  <div id="help-panel" hidden>
1241
1248
  <div class="notes-label">${escapeHtml(ui.helpTitle)}</div>
1242
- <p><kbd>←</kbd> <kbd>→</kbd> / <kbd>Space</kbd> 翻页 · <kbd>Home</kbd> / <kbd>End</kbd> 首末页 · <kbd>S</kbd> ${escapeHtml(ui.notesToggle)} · <kbd>V</kbd> ${escapeHtml(ui.presenterToggle)} · <kbd>G</kbd> ${escapeHtml(ui.overviewToggle)} · <kbd>F</kbd> 全屏 · <kbd>P</kbd> 打印 · <kbd>Esc</kbd> ${escapeHtml(ui.closeLabel)} · <kbd>?</kbd> ${escapeHtml(ui.helpTitle)}</p>
1249
+ <p><kbd>←</kbd> <kbd>→</kbd> / <kbd>Space</kbd> 翻页 · <kbd>Home</kbd> / <kbd>End</kbd> 首末页 · <kbd>S</kbd> ${escapeHtml(ui.notesToggle)} · <kbd>V</kbd> ${escapeHtml(ui.presenterToggle)} · <kbd>G</kbd> ${escapeHtml(ui.overviewToggle)} · <kbd>F</kbd> 全屏 · <kbd>P</kbd> 打印 · <kbd>B</kbd> 黑屏 · <kbd>W</kbd> 白屏 · <kbd>Esc</kbd> ${escapeHtml(ui.closeLabel)} · <kbd>?</kbd> ${escapeHtml(ui.helpTitle)}</p>
1243
1250
  <p>${escapeHtml(ui.presenterHint)}</p>
1244
1251
  <p>${escapeHtml(ui.overviewHint)}</p>
1245
1252
  </div>
1246
1253
  <div id="toast" role="status" hidden></div>
1254
+ <div id="blank" aria-hidden="true"></div>
1247
1255
  <script>
1248
1256
  (() => {
1249
1257
  const frames = Array.from(document.querySelectorAll('.slide-frame'));
@@ -1262,6 +1270,7 @@ ${slides}
1262
1270
  const toastEl = document.getElementById('toast');
1263
1271
  let index = 0;
1264
1272
  let presenterWin = null;
1273
+ let presenterInitSent = false;
1265
1274
  let toastTimer = 0;
1266
1275
  const total = slides.length;
1267
1276
  const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
@@ -1292,13 +1301,40 @@ ${slides}
1292
1301
  updatePresenter();
1293
1302
  }
1294
1303
 
1295
- function updatePresenter() {
1296
- if (presenterOpen() && typeof presenterWin.__deckPresenterUpdate === 'function') {
1297
- try { presenterWin.__deckPresenterUpdate(state()); } catch (_) { /* window closing */ }
1298
- }
1299
- }
1300
-
1304
+ function sendInit() {
1305
+ if (!presenterOpen()) return;
1306
+ try {
1307
+ presenterWin.postMessage({
1308
+ __dshPpt: 'init',
1309
+ title: (document.title.split(' · ')[1] || document.title),
1310
+ css: document.querySelector('style') ? document.querySelector('style').textContent : '',
1311
+ total: total,
1312
+ slides: slides.map(function (slide) { return slide.outerHTML; }),
1313
+ }, '*');
1314
+ } catch (_) { /* popup closing */ }
1315
+ }
1316
+
1317
+ function sendState() {
1318
+ if (!presenterOpen()) return;
1319
+ const next = (index + 1) % total;
1320
+ try {
1321
+ presenterWin.postMessage({
1322
+ __dshPpt: 'state',
1323
+ index: index,
1324
+ total: total,
1325
+ title: titleOf(index),
1326
+ nextTitle: titleOf(next),
1327
+ notes: notesOf(index),
1328
+ nextNotes: notesOf(next),
1329
+ blank: document.body.classList.contains('blank-black') ? 'black' : (document.body.classList.contains('blank-white') ? 'white' : null),
1330
+ }, '*');
1331
+ } catch (_) { /* popup closing */ }
1332
+ }
1333
+
1334
+ function updatePresenter() { sendState(); }
1335
+
1301
1336
  function go(next) {
1337
+ if (document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) setBlank(null);
1302
1338
  const wrapped = next < 0 || next >= total;
1303
1339
  index = (next + total) % total;
1304
1340
  slides.forEach(function (slide, i) {
@@ -1342,71 +1378,167 @@ ${slides}
1342
1378
  }
1343
1379
  }
1344
1380
 
1345
- function presenterHtml() {
1346
- const cs = getComputedStyle(document.documentElement);
1347
- const val = function (name, fallback) { const v = cs.getPropertyValue(name).trim(); return v || fallback; };
1348
- const css = ':root{--bg:' + val('--bg', '#070B14') + ';--panel:' + val('--panel', '#0D1424') + ';--fg:' + val('--fg', '#E8F1FF') + ';--muted:' + val('--muted', '#7E8BA8') + ';--accent:' + val('--accent', '#7C3AED') + ';--accent2:' + val('--accent2', '#06B6D4') + ';--font-heading:' + val('--font-heading', 'sans-serif') + ';--font-body:' + val('--font-body', 'sans-serif') + '}'
1349
- + 'body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-body);height:100vh;display:flex;flex-direction:column}'
1350
- + 'header{display:flex;justify-content:space-between;gap:16px;padding:14px 20px;border-bottom:1px solid color-mix(in srgb, var(--muted) 40%, transparent);font-size:14px;color:var(--muted)}'
1351
- + 'header #ptitle{color:var(--fg);font-weight:700}'
1352
- + 'main{flex:1;display:grid;grid-template-columns:1.05fr 1fr;gap:18px;padding:18px 20px;min-height:0}'
1353
- + 'section{background:var(--panel);border:1px solid color-mix(in srgb, var(--muted) 30%, transparent);border-radius:14px;padding:16px 18px;min-height:0;display:flex;flex-direction:column;gap:10px}'
1354
- + '.lbl{color:var(--accent);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}'
1355
- + '#pcur{font-family:var(--font-heading);font-size:30px;font-weight:800;line-height:1.2}'
1356
- + '#pnext{font-size:20px;line-height:1.35}'
1357
- + '#pnotes{font-size:19px;line-height:1.65;white-space:pre-wrap;overflow:auto;flex:1}'
1358
- + '.hint{color:var(--muted);font-size:12px;margin-top:auto}'
1359
- + 'footer{display:flex;align-items:center;gap:16px;padding:12px 20px;border-top:1px solid color-mix(in srgb, var(--muted) 40%, transparent)}'
1360
- + '#ptimer{font-variant-numeric:tabular-nums;font-size:22px;font-weight:700;color:var(--accent)}'
1361
- + '.bar{flex:1;height:8px;background:color-mix(in srgb, var(--muted) 28%, transparent);border-radius:99px;overflow:hidden}'
1362
- + '#pprog{height:100%;width:0;background:var(--accent);transition:width .2s}'
1363
- + 'body.big #pnotes{font-size:26px}'
1364
- + '@media (max-width:760px){main{grid-template-columns:1fr}}';
1365
- const js = 'var started=Date.now(),elapsed=0,paused=false,big=false;'
1366
- + 'function fmt(ms){var s=Math.max(0,Math.floor(ms/1000));var m=Math.floor(s/60);s=s%60;return (m<10?"0":"")+m+":"+(s<10?"0":"")+s}'
1367
- + 'function tick(){if(!paused)elapsed=Date.now()-started;document.getElementById("ptimer").textContent=fmt(elapsed);requestAnimationFrame(tick)}'
1368
- + 'window.__deckPresenterUpdate=function(st){document.getElementById("pcur").textContent=st.title||("Slide "+(st.index+1));document.getElementById("pnext").textContent=st.nextTitle||"\u2014";document.getElementById("pnotes").textContent=st.notes||"' + ui.noNotes.replace(/"/g, '\\"') + '";document.getElementById("pcounter").textContent=(st.index+1)+" / "+st.total;document.getElementById("pprog").style.width=((st.index+1)/st.total*100)+"%"}'
1369
- + ';document.addEventListener("keydown",function(e){var k=e.key;'
1370
- + 'if(k==="ArrowRight"||k===" "){e.preventDefault();try{opener.__dshPpt.go(1)}catch(_){}}'
1371
- + 'else if(k==="ArrowLeft"){e.preventDefault();try{opener.__dshPpt.go(-1)}catch(_){}}'
1372
- + 'else if(k==="Escape"){window.close()}'
1373
- + 'else if(k.toLowerCase()==="t"){started=Date.now();elapsed=0;paused=false}'
1374
- + 'else if(k.toLowerCase()==="p"){paused=!paused;if(!paused)started=Date.now()-elapsed}'
1375
- + 'else if(k==="+"||k==="="){big=true;document.body.classList.add("big")}'
1376
- + 'else if(k==="-"){big=false;document.body.classList.remove("big")}});'
1377
- + 'setInterval(function(){try{if(opener&&!opener.closed&&opener.__dshPpt)window.__deckPresenterUpdate(opener.__dshPpt.state())}catch(_){}},400);'
1378
- + 'tick();';
1379
- return '<!DOCTYPE html><html lang="' + ${JSON.stringify(lang.attr)} + '"><head><meta charset="UTF-8"><title>' + ${JSON.stringify(manifest.title + ' · ' + ui.presenterLabel).replace(/</g, '\\u003c')} + '</title><style>' + css + '</style></head><body>'
1380
- + '<header><div id="ptitle">' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')} + '</div><div id="pcounter"></div></header>'
1381
- + '<main><section><div class="lbl">' + ${JSON.stringify(ui.currentLabel).replace(/</g, '\\u003c')} + '</div><div id="pcur"></div><div class="lbl">' + ${JSON.stringify(ui.nextLabel).replace(/</g, '\\u003c')} + '</div><div id="pnext"></div></section>'
1382
- + '<section><div class="lbl">' + ${JSON.stringify(ui.notesLabel).replace(/</g, '\\u003c')} + '</div><div id="pnotes"></div><div class="hint">+ / - 字号 · T 重置计时 · P 暂停 · ← → 翻页 · Esc 关闭</div></section></main>'
1383
- + '<footer><div id="ptimer">00:00</div><div class="bar"><div id="pprog"></div></div></footer>'
1384
- + '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
1385
- }
1386
-
1387
- function openPresenter() {
1388
- if (presenterOpen()) { presenterWin.focus(); return; }
1389
- presenterWin = window.open('', 'dsh-ppt-presenter', 'width=1180,height=760');
1390
- if (!presenterWin) { toast(ui.popupBlocked); return; }
1381
+ function presenterHtml() {
1382
+ const labelCurrent = ${JSON.stringify(ui.currentLabel)};
1383
+ const labelNext = ${JSON.stringify(ui.nextLabel)};
1384
+ const labelNotes = ${JSON.stringify(ui.notesLabel)};
1385
+ const noNotes = ${JSON.stringify(ui.noNotes)};
1386
+ const hintText = ${JSON.stringify('← → 翻页 · P 暂停 · T 重置 · +/- 字号 · B/W 黑/白屏 · Esc 结束')};
1387
+ const deckTitle = ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
1388
+ const langAttr = ${JSON.stringify(lang.attr)};
1389
+ const cs = getComputedStyle(document.documentElement);
1390
+ const val = function (name, fallback) { const v = cs.getPropertyValue(name).trim(); return v || fallback; };
1391
+ const css = [
1392
+ ':root{--bg:' + val('--bg', '#070B14') + ';--panel:' + val('--panel', '#0D1424') + ';--fg:' + val('--fg', '#E8F1FF') + ';--muted:' + val('--muted', '#7E8BA8') + ';--accent:' + val('--accent', '#7C3AED') + ';--accent2:' + val('--accent2', '#06B6D4') + ';--font-heading:' + val('--font-heading', 'sans-serif') + ';--font-body:' + val('--font-body', 'sans-serif') + '}',
1393
+ '*{box-sizing:border-box}',
1394
+ 'body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-body);height:100vh;display:flex;flex-direction:column;overflow:hidden}',
1395
+ 'header{display:flex;align-items:center;gap:14px;padding:9px 16px;border-bottom:1px solid color-mix(in srgb, var(--muted) 40%, transparent);font-size:13px;color:var(--muted)}',
1396
+ 'header #ptitle{color:var(--fg);font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1397
+ 'header #pclock{font-variant-numeric:tabular-nums}',
1398
+ 'main{flex:1;display:grid;grid-template-columns:1.12fr .88fr;gap:12px;padding:12px 16px;min-height:0}',
1399
+ '.col{display:flex;flex-direction:column;gap:10px;min-height:0}',
1400
+ '.card{background:var(--panel);border:1px solid color-mix(in srgb, var(--muted) 30%, transparent);border-radius:12px;padding:10px 12px;min-height:0;display:flex;flex-direction:column;gap:6px}',
1401
+ '.card-notes{flex:1}',
1402
+ '.lbl{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}',
1403
+ '.pv{position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:8px;background:var(--bg);border:1px solid color-mix(in srgb, var(--muted) 25%, transparent);--pv-scale:.3}',
1404
+ '.pv-slide{position:absolute;left:0;top:0;transform:translate(var(--pv-x,0px),var(--pv-y,0px)) scale(var(--pv-scale,.25));transform-origin:top left}',
1405
+ '.pv-slide .slide{display:flex !important;position:absolute;left:0;top:0;width:100vw;height:100vh;animation:none !important}',
1406
+ '.pv-slide .bullets li{opacity:1 !important;animation:none !important}',
1407
+ '#thumbs{display:flex;gap:8px;overflow-x:auto;padding-bottom:2px}',
1408
+ '#thumbs .thumb{position:relative;flex:0 0 128px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);cursor:pointer;background:var(--bg);--pv-scale:.09}',
1409
+ '#thumbs .thumb.is-current{outline:2px solid var(--accent);outline-offset:1px}',
1410
+ '#pnotes{font-size:var(--notes-size,19px);line-height:1.6;white-space:pre-wrap;overflow:auto;flex:1}',
1411
+ 'footer{display:flex;align-items:center;gap:8px;padding:9px 16px;border-top:1px solid color-mix(in srgb, var(--muted) 40%, transparent);flex-wrap:wrap}',
1412
+ 'button{background:color-mix(in srgb, var(--panel) 90%, transparent);color:var(--fg);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;padding:6px 12px;min-height:34px;font:inherit;font-size:13px;cursor:pointer}',
1413
+ 'button:hover{border-color:var(--accent);color:var(--accent)}',
1414
+ 'button[aria-pressed="true"]{background:var(--accent);color:var(--bg);border-color:var(--accent)}',
1415
+ '#ptimer{font-variant-numeric:tabular-nums;font-size:20px;font-weight:700;color:var(--accent);margin-left:6px}',
1416
+ '.bar{flex:1;min-width:100px;height:8px;background:color-mix(in srgb, var(--muted) 28%, transparent);border-radius:99px;overflow:hidden}',
1417
+ '#pprog{height:100%;width:0;background:var(--accent);transition:width .2s}',
1418
+ '.hint{color:var(--muted);font-size:11px;width:100%}',
1419
+ ].join('');
1420
+ const js = [
1421
+ 'var total=0,index=0,elapsed=0,started=Date.now(),paused=false,notesSize=19,slides=[],cssText="",curBlank=null,initialized=false,readyTries=0;',
1422
+ 'function q(id){return document.getElementById(id)}',
1423
+ 'function post(m){try{if(window.opener)window.opener.postMessage(m,"*")}catch(e){}}',
1424
+ 'function fmt(ms){var s=Math.max(0,Math.floor(ms/1000)),m=Math.floor(s/60);s=s%60;var h=Math.floor(m/60);m=m%60;return (h>0?(h<10?"0":"")+h+":":"")+(m<10?"0":"")+m+":"+(s<10?"0":"")+s}',
1425
+ 'function clock(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}',
1426
+ 'function tick(){if(!paused)elapsed=Date.now()-started;q("ptimer").textContent=fmt(elapsed);q("pclock").textContent=clock();if(window.requestAnimationFrame)requestAnimationFrame(tick);else setTimeout(tick,250)}',
1427
+ 'function fitBox(box){var rw=box.clientWidth||320,rh=box.clientHeight||180;var s=Math.min(rw/window.innerWidth,rh/window.innerHeight);var ox=(rw-window.innerWidth*s)/2,oy=(rh-window.innerHeight*s)/2;box.style.setProperty("--pv-scale",String(s));box.style.setProperty("--pv-x",ox+"px");box.style.setProperty("--pv-y",oy+"px")}',
1428
+ 'function fit(){fitBox(q("pv-cur"));fitBox(q("pv-next"));var th=q("thumbs").children;for(var i=0;i<th.length;i++){fitBox(th[i])}}',
1429
+ 'function setSlide(box,html){box.innerHTML="";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;box.appendChild(f)}',
1430
+ 'function buildThumbs(){var box=q("thumbs");box.innerHTML="";slides.forEach(function(html,i){var t=document.createElement("div");t.className="thumb";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;t.appendChild(f);t.addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",index:i})});box.appendChild(t)});fit()}',
1431
+ 'function renderState(st){index=st.index;total=st.total||total;q("pcounter").textContent=(index+1)+" / "+total;setSlide(q("pv-cur"),slides[index]||"");setSlide(q("pv-next"),slides[(index+1)%total]||"");q("pnotes").textContent=st.notes||' + JSON.stringify(ui.noNotes) + ';q("pprog").style.width=((index+1)/total*100)+"%";curBlank=st.blank||null;q("pv-black").setAttribute("aria-pressed",curBlank==="black"?"true":"false");q("pv-white").setAttribute("aria-pressed",curBlank==="white"?"true":"false");q("pv-black").textContent=curBlank==="black"?"取消黑屏":"黑屏";q("pv-white").textContent=curBlank==="white"?"取消白屏":"白屏";var th=q("thumbs").children;for(var i=0;i<th.length;i++){th[i].classList.toggle("is-current",i===index)}fit()}',
1432
+ 'function togglePause(){paused=!paused;if(!paused)started=Date.now()-elapsed;var b=q("pause");b.setAttribute("aria-pressed",paused?"true":"false");b.textContent=paused?"继续":"暂停"}',
1433
+ 'function resetTimer(){started=Date.now();elapsed=0}',
1434
+ 'function applyNotesSize(){document.documentElement.style.setProperty("--notes-size",notesSize+"px")}',
1435
+ 'window.addEventListener("message",function(e){var d=e.data||{};if(d.__dshPpt==="init"&&!initialized){initialized=true;slides=d.slides||[];total=d.total||slides.length;cssText=d.css||"";var st=document.createElement("style");st.textContent=cssText;document.head.appendChild(st);q("ptitle").textContent=d.title||"";buildThumbs()}else if(d.__dshPpt==="state"){renderState(d)}else if(d.__dshPpt==="close"){window.close()}});',
1436
+ 'window.addEventListener("resize",fit);',
1437
+ 'window.addEventListener("beforeunload",function(){post({__dshPpt:"cmd",kind:"closed"})});',
1438
+ 'document.addEventListener("keydown",function(e){var k=e.key;if(k==="ArrowRight"||k===" "){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:1})}else if(k==="ArrowLeft"){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:-1})}else if(k==="Escape"){post({__dshPpt:"cmd",kind:"end"});window.close()}else if(k.toLowerCase()==="p"){togglePause()}else if(k.toLowerCase()==="t"){resetTimer()}else if(k==="+"||k==="="){notesSize=Math.min(34,notesSize+2);applyNotesSize()}else if(k==="-"){notesSize=Math.max(12,notesSize-2);applyNotesSize()}else if(k.toLowerCase()==="b"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})}else if(k.toLowerCase()==="w"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})}});',
1439
+ 'q("prev").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:-1})});',
1440
+ 'q("next").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:1})});',
1441
+ 'q("pause").addEventListener("click",togglePause);',
1442
+ 'q("reset").addEventListener("click",resetTimer);',
1443
+ 'q("font-plus").addEventListener("click",function(){notesSize=Math.min(34,notesSize+2);applyNotesSize()});',
1444
+ 'q("font-minus").addEventListener("click",function(){notesSize=Math.max(12,notesSize-2);applyNotesSize()});',
1445
+ 'q("pv-black").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})});',
1446
+ 'q("pv-white").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})});',
1447
+ 'q("end").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"end"});window.close()});',
1448
+ 'applyNotesSize();tick();function ready(){if(initialized)return;post({__dshPpt:"ready"});readyTries++;if(readyTries<5)setTimeout(ready,700)}setTimeout(ready,250);',
1449
+ ].join('');
1450
+ return '<!DOCTYPE html><html lang="' + langAttr + '"><head><meta charset="UTF-8"><title>' + deckTitle + ' · 演讲者视图</title><style>' + css + '</style></head><body>'
1451
+ + '<header><div id="ptitle"></div><div id="pcounter"></div><div id="pclock" title="本地时间"></div></header>'
1452
+ + '<main><div class="col">'
1453
+ + '<div class="card"><div class="lbl">' + labelCurrent + '</div><div class="pv" id="pv-cur"></div></div>'
1454
+ + '<div class="card"><div class="lbl">' + labelNext + '</div><div class="pv" id="pv-next"></div></div>'
1455
+ + '<div class="card"><div class="lbl">SLIDES</div><div id="thumbs"></div></div>'
1456
+ + '</div><div class="col"><div class="card card-notes"><div class="lbl">' + labelNotes + '</div><div id="pnotes"></div></div></div></main>'
1457
+ + '<footer><button id="prev" title="上一页 (←)">◀</button><button id="next" title="下一页 (→)">▶</button><button id="pause" title="暂停 (P)" aria-pressed="false">暂停</button><button id="reset" title="重置计时 (T)">重置计时</button><button id="font-minus" title="备注字号减小 (-)">A−</button><button id="font-plus" title="备注字号增大 (+)">A+</button><button id="pv-black" title="黑屏 (B)">黑屏</button><button id="pv-white" title="白屏 (W)">白屏</button><button id="end" title="结束放映 (Esc)">结束放映</button><div id="ptimer">00:00</div><div class="bar"><div id="pprog"></div></div><div class="hint">' + hintText + '</div></footer>'
1458
+ + '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
1459
+ }
1460
+
1461
+ async function toggleFullscreen() {
1391
1462
  try {
1392
- presenterWin.document.open();
1393
- presenterWin.document.write(presenterHtml());
1394
- presenterWin.document.close();
1395
- } catch (_) { toast(ui.popupBlocked); return; }
1396
- document.body.classList.add('presenting');
1397
- if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
1398
- setTimeout(updatePresenter, 150);
1399
- }
1400
-
1401
- function closePresenter() {
1402
- if (presenterOpen()) { try { presenterWin.close(); } catch (_) { /* already gone */ } }
1403
- document.body.classList.remove('presenting');
1463
+ if (document.fullscreenElement) { await document.exitFullscreen?.(); return; }
1464
+ if (!document.documentElement.requestFullscreen || document.fullscreenEnabled === false) {
1465
+ toast(ui.fullscreenUnavailable);
1466
+ return;
1467
+ }
1468
+ await document.documentElement.requestFullscreen();
1469
+ if (!document.fullscreenElement) toast(ui.fullscreenUnavailable);
1470
+ } catch (_) { toast(ui.fullscreenUnavailable); }
1404
1471
  }
1405
1472
 
1406
- setInterval(function () {
1407
- if (!presenterOpen() && document.body.classList.contains('presenting')) document.body.classList.remove('presenting');
1408
- }, 800);
1409
-
1473
+ function openPresenter() {
1474
+ if (presenterOpen()) { try { presenterWin.focus(); } catch (_) { /* gone */ } return; }
1475
+ let url = '';
1476
+ try {
1477
+ url = URL.createObjectURL(new Blob([presenterHtml()], { type: 'text/html' }));
1478
+ } catch (_) { toast(ui.popupBlocked); return; }
1479
+ presenterInitSent = false;
1480
+ presenterWin = window.open(url, 'dsh-ppt-presenter', 'width=1180,height=760');
1481
+ if (!presenterWin) {
1482
+ try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ }
1483
+ toast(ui.popupBlocked);
1484
+ return;
1485
+ }
1486
+ document.body.classList.add('presenting');
1487
+ if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
1488
+ setTimeout(function () { try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ } }, 5000);
1489
+ }
1490
+
1491
+ function closePresenter() {
1492
+ if (presenterOpen()) {
1493
+ try { presenterWin.postMessage({ __dshPpt: 'close' }, '*'); presenterWin.close(); } catch (_) { /* gone */ }
1494
+ }
1495
+ document.body.classList.remove('presenting');
1496
+ presenterInitSent = false;
1497
+ if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
1498
+ setBlank(null);
1499
+ }
1500
+
1501
+ function setBlank(color) {
1502
+ document.body.classList.remove('blank-black', 'blank-white');
1503
+ if (color === 'black') document.body.classList.add('blank-black');
1504
+ else if (color === 'white') document.body.classList.add('blank-white');
1505
+ sendState();
1506
+ }
1507
+
1508
+ setInterval(function () {
1509
+ if (!presenterOpen() && document.body.classList.contains('presenting')) {
1510
+ document.body.classList.remove('presenting');
1511
+ if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
1512
+ setBlank(null);
1513
+ }
1514
+ }, 800);
1515
+
1516
+ window.addEventListener('message', function (event) {
1517
+ if (event.source !== presenterWin) return;
1518
+ const data = event.data || {};
1519
+ if (data.__dshPpt === 'cmd' && (data.kind === 'end' || data.kind === 'closed')) { closePresenter(); return; }
1520
+ if (!presenterOpen()) return;
1521
+ if (data.__dshPpt === 'ready') {
1522
+ if (!presenterInitSent) { presenterInitSent = true; sendInit(); }
1523
+ sendState();
1524
+ return;
1525
+ }
1526
+ if (data.__dshPpt !== 'cmd') return;
1527
+ if (data.kind === 'go') {
1528
+ setBlank(null);
1529
+ if (typeof data.index === 'number') go(data.index);
1530
+ else go(index + (data.delta || 0));
1531
+ sendState();
1532
+ } else if (data.kind === 'blank') {
1533
+ setBlank(data.color || null);
1534
+ } else if (data.kind === 'end') {
1535
+ closePresenter();
1536
+ } else if (data.kind === 'closed') {
1537
+ document.body.classList.remove('presenting');
1538
+ setBlank(null);
1539
+ }
1540
+ });
1541
+
1410
1542
  document.addEventListener('keydown', (event) => {
1411
1543
  const key = event.key;
1412
1544
  const lower = key.toLowerCase();
@@ -1417,6 +1549,7 @@ ${slides}
1417
1549
  if (!helpPanel.hidden) { helpPanel.hidden = true; return; }
1418
1550
  if (document.body.classList.contains('overview')) { setOverview(false); return; }
1419
1551
  if (notesOpen()) { setNotes(false); return; }
1552
+ if (document.body.classList.contains('presenting') || document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) { closePresenter(); return; }
1420
1553
  if (document.fullscreenElement) { document.exitFullscreen?.(); return; }
1421
1554
  return;
1422
1555
  }
@@ -1435,8 +1568,7 @@ ${slides}
1435
1568
  } else if (key === 'Enter' && inOverview) {
1436
1569
  event.preventDefault(); setOverview(false);
1437
1570
  } else if (lower === 'f') {
1438
- if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
1439
- else document.exitFullscreen?.();
1571
+ void toggleFullscreen();
1440
1572
  } else if (lower === 'g') {
1441
1573
  setOverview(!inOverview);
1442
1574
  } else if (lower === 's') {
@@ -1445,6 +1577,10 @@ ${slides}
1445
1577
  } else if (lower === 'v') {
1446
1578
  if (presenterOpen()) closePresenter();
1447
1579
  else openPresenter();
1580
+ } else if (lower === 'b') {
1581
+ setBlank(document.body.classList.contains('blank-black') ? null : 'black');
1582
+ } else if (lower === 'w') {
1583
+ setBlank(document.body.classList.contains('blank-white') ? null : 'white');
1448
1584
  } else if (lower === 'p') {
1449
1585
  window.print();
1450
1586
  } else if (key === '?') {
@@ -1467,10 +1603,7 @@ ${slides}
1467
1603
  if (Math.abs(delta) > 48 && !document.body.classList.contains('overview')) go(index + (delta < 0 ? 1 : -1));
1468
1604
  }, { passive: true });
1469
1605
 
1470
- fullscreenBtn.addEventListener('click', () => {
1471
- if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
1472
- else document.exitFullscreen?.();
1473
- });
1606
+ fullscreenBtn.addEventListener('click', () => { void toggleFullscreen(); });
1474
1607
  notesBtn.addEventListener('click', () => {
1475
1608
  if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); return; }
1476
1609
  setNotes(!notesOpen());