@leterhong/multichat 1.6.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/.agents/plugins/marketplace.json +32 -0
- package/.agents/plugins/plugins/developer-toolkit/.codex-plugin/plugin.json +24 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/code-review/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/code-review/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/security-review/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/security-review/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/.codex-plugin/plugin.json +24 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/agents/openai.yaml +3 -0
- package/.agents/skills/.gitkeep +1 -0
- package/.agents/skills/wanghong-handwritten-ppt/SKILL.md +172 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/HTML-PPT-LICENSE +21 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/NEAT-ANNOTATIONS-LICENSE +21 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/animations.css +138 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/base.css +150 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/deepseek-example.css +893 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/neat-annotations.css +152 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-cover.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-diagram.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-summary.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-table.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/runtime.js +960 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/template.css +198 -0
- package/.agents/skills/wanghong-handwritten-ppt/evals/evals.json +20 -0
- package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/index.html +414 -0
- package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/style.css +897 -0
- package/.agents/skills/wanghong-handwritten-ppt/references/prompts.md +170 -0
- package/.agents/skills/wanghong-handwritten-ppt/references/style-guide.md +133 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/check_deck.py +41 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/prepare_render_html.py +137 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/render.sh +110 -0
- package/.agents/skills/wanghong-handwritten-ppt/templates/deck.html +94 -0
- package/.agents/skills/wanghong-handwritten-ppt/tests/test_font_contract.py +73 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/backend/adapters/README.md +77 -0
- package/backend/adapters/anthropic.ts +115 -0
- package/backend/adapters/base.ts +47 -0
- package/backend/adapters/index.ts +41 -0
- package/backend/adapters/lmstudio.ts +7 -0
- package/backend/adapters/moonshot.ts +7 -0
- package/backend/adapters/ollama.ts +7 -0
- package/backend/adapters/openai-compatible.ts +51 -0
- package/backend/adapters/openai.ts +7 -0
- package/backend/adapters/qwen.ts +112 -0
- package/backend/adapters/wenxin.ts +136 -0
- package/backend/adapters/zhipu.ts +93 -0
- package/backend/dist/adapters/anthropic.js +130 -0
- package/backend/dist/adapters/anthropic.js.map +1 -0
- package/backend/dist/adapters/base.js +38 -0
- package/backend/dist/adapters/base.js.map +1 -0
- package/backend/dist/adapters/index.js +39 -0
- package/backend/dist/adapters/index.js.map +1 -0
- package/backend/dist/adapters/lmstudio.js +8 -0
- package/backend/dist/adapters/lmstudio.js.map +1 -0
- package/backend/dist/adapters/moonshot.js +8 -0
- package/backend/dist/adapters/moonshot.js.map +1 -0
- package/backend/dist/adapters/ollama.js +8 -0
- package/backend/dist/adapters/ollama.js.map +1 -0
- package/backend/dist/adapters/openai-compatible.js +47 -0
- package/backend/dist/adapters/openai-compatible.js.map +1 -0
- package/backend/dist/adapters/openai.js +8 -0
- package/backend/dist/adapters/openai.js.map +1 -0
- package/backend/dist/adapters/qwen.js +103 -0
- package/backend/dist/adapters/qwen.js.map +1 -0
- package/backend/dist/adapters/wenxin.js +114 -0
- package/backend/dist/adapters/wenxin.js.map +1 -0
- package/backend/dist/adapters/zhipu.js +86 -0
- package/backend/dist/adapters/zhipu.js.map +1 -0
- package/backend/dist/extensions/demo-mcp-server.js +75 -0
- package/backend/dist/extensions/demo-mcp-server.js.map +1 -0
- package/backend/dist/extensions/importer.js +828 -0
- package/backend/dist/extensions/importer.js.map +1 -0
- package/backend/dist/extensions/manager.js +1154 -0
- package/backend/dist/extensions/manager.js.map +1 -0
- package/backend/dist/lib/catalog.js +30 -0
- package/backend/dist/lib/catalog.js.map +1 -0
- package/backend/dist/lib/context.js +59 -0
- package/backend/dist/lib/context.js.map +1 -0
- package/backend/dist/lib/errors.js +75 -0
- package/backend/dist/lib/errors.js.map +1 -0
- package/backend/dist/lib/provider-store.js +116 -0
- package/backend/dist/lib/provider-store.js.map +1 -0
- package/backend/dist/lib/redact.js +11 -0
- package/backend/dist/lib/redact.js.map +1 -0
- package/backend/dist/lib/runtime-store.js +28 -0
- package/backend/dist/lib/runtime-store.js.map +1 -0
- package/backend/dist/lib/sqlite-store.js +218 -0
- package/backend/dist/lib/sqlite-store.js.map +1 -0
- package/backend/dist/lib/sse.js +46 -0
- package/backend/dist/lib/sse.js.map +1 -0
- package/backend/dist/lib/ssrf.js +131 -0
- package/backend/dist/lib/ssrf.js.map +1 -0
- package/backend/dist/lib/store.js +84 -0
- package/backend/dist/lib/store.js.map +1 -0
- package/backend/dist/lib/usage.js +219 -0
- package/backend/dist/lib/usage.js.map +1 -0
- package/backend/dist/lib/util.js +38 -0
- package/backend/dist/lib/util.js.map +1 -0
- package/backend/dist/lib/workspace-store.js +152 -0
- package/backend/dist/lib/workspace-store.js.map +1 -0
- package/backend/dist/mcp.js +429 -0
- package/backend/dist/mcp.js.map +1 -0
- package/backend/dist/routes/agents.js +51 -0
- package/backend/dist/routes/agents.js.map +1 -0
- package/backend/dist/routes/assistants.js +32 -0
- package/backend/dist/routes/assistants.js.map +1 -0
- package/backend/dist/routes/chat.js +697 -0
- package/backend/dist/routes/chat.js.map +1 -0
- package/backend/dist/routes/control-plane.js +200 -0
- package/backend/dist/routes/control-plane.js.map +1 -0
- package/backend/dist/routes/conversations.js +66 -0
- package/backend/dist/routes/conversations.js.map +1 -0
- package/backend/dist/routes/extensions-import.js +22 -0
- package/backend/dist/routes/extensions-import.js.map +1 -0
- package/backend/dist/routes/import.js +85 -0
- package/backend/dist/routes/import.js.map +1 -0
- package/backend/dist/routes/mcp-servers.js +56 -0
- package/backend/dist/routes/mcp-servers.js.map +1 -0
- package/backend/dist/routes/meta.js +37 -0
- package/backend/dist/routes/meta.js.map +1 -0
- package/backend/dist/routes/mock.js +28 -0
- package/backend/dist/routes/mock.js.map +1 -0
- package/backend/dist/routes/plugins.js +41 -0
- package/backend/dist/routes/plugins.js.map +1 -0
- package/backend/dist/routes/providers.js +197 -0
- package/backend/dist/routes/providers.js.map +1 -0
- package/backend/dist/routes/runs.js +57 -0
- package/backend/dist/routes/runs.js.map +1 -0
- package/backend/dist/routes/skills.js +73 -0
- package/backend/dist/routes/skills.js.map +1 -0
- package/backend/dist/routes/tools.js +40 -0
- package/backend/dist/routes/tools.js.map +1 -0
- package/backend/dist/routes/usage.js +14 -0
- package/backend/dist/routes/usage.js.map +1 -0
- package/backend/dist/routes/workspaces.js +158 -0
- package/backend/dist/routes/workspaces.js.map +1 -0
- package/backend/dist/runtime/agent.js +518 -0
- package/backend/dist/runtime/agent.js.map +1 -0
- package/backend/dist/server.js +103 -0
- package/backend/dist/server.js.map +1 -0
- package/backend/dist/types.js +3 -0
- package/backend/dist/types.js.map +1 -0
- package/backend/extensions/demo-mcp-server.js +70 -0
- package/backend/extensions/importer.ts +709 -0
- package/backend/extensions/manager.ts +1055 -0
- package/backend/lib/catalog.ts +28 -0
- package/backend/lib/context.ts +61 -0
- package/backend/lib/errors.ts +78 -0
- package/backend/lib/provider-store.ts +116 -0
- package/backend/lib/redact.ts +9 -0
- package/backend/lib/runtime-store.ts +38 -0
- package/backend/lib/sqlite-store.ts +220 -0
- package/backend/lib/sse.ts +42 -0
- package/backend/lib/ssrf.ts +120 -0
- package/backend/lib/store.ts +79 -0
- package/backend/lib/usage.ts +212 -0
- package/backend/lib/util.ts +31 -0
- package/backend/lib/workspace-store.ts +150 -0
- package/backend/mcp.ts +417 -0
- package/backend/package.json +34 -0
- package/backend/prompts.json +82 -0
- package/backend/routes/agents.ts +55 -0
- package/backend/routes/assistants.ts +30 -0
- package/backend/routes/chat.ts +660 -0
- package/backend/routes/control-plane.ts +165 -0
- package/backend/routes/conversations.ts +62 -0
- package/backend/routes/extensions-import.ts +15 -0
- package/backend/routes/import.ts +72 -0
- package/backend/routes/mcp-servers.ts +36 -0
- package/backend/routes/meta.ts +38 -0
- package/backend/routes/mock.ts +25 -0
- package/backend/routes/plugins.ts +27 -0
- package/backend/routes/providers.ts +166 -0
- package/backend/routes/runs.ts +55 -0
- package/backend/routes/skills.ts +52 -0
- package/backend/routes/tools.ts +40 -0
- package/backend/routes/usage.ts +13 -0
- package/backend/routes/workspaces.ts +124 -0
- package/backend/runtime/agent.ts +474 -0
- package/backend/server.ts +106 -0
- package/backend/types.ts +126 -0
- package/bin/multichat.mjs +535 -0
- package/frontend/dist/assets/agentPicker-C39VzmYa.js +13 -0
- package/frontend/dist/assets/compare-BpaGD32J.js +1 -0
- package/frontend/dist/assets/conversations-COuLmg99.js +7 -0
- package/frontend/dist/assets/data-ZH7Dux1K.js +1 -0
- package/frontend/dist/assets/extensionImport-CnTWKtWm.js +28 -0
- package/frontend/dist/assets/importExport-B3mB-9zs.js +70 -0
- package/frontend/dist/assets/index-CASVCOUO.js +34 -0
- package/frontend/dist/assets/index-DSvbjNLc.css +1 -0
- package/frontend/dist/assets/init-ZPK483vB.js +1 -0
- package/frontend/dist/assets/jsx-runtime-ZSsbr0oW.js +9 -0
- package/frontend/dist/assets/modal-CgHocv3n.js +2 -0
- package/frontend/dist/assets/modelPicker-EUmmVnbj.js +14 -0
- package/frontend/dist/assets/pluginsUI-B6jGUQkx.js +16 -0
- package/frontend/dist/assets/render-DgAg1K8A.js +1 -0
- package/frontend/dist/assets/send-BJhZ_5YA.js +14 -0
- package/frontend/dist/assets/settings-DdnYwZl-.js +2 -0
- package/frontend/dist/assets/shell-DFV2khQl.js +1 -0
- package/frontend/dist/assets/workbench-BY2J4vWC.js +41 -0
- package/frontend/dist/icon-maskable.svg +4 -0
- package/frontend/dist/icon.svg +4 -0
- package/frontend/dist/index.html +21 -0
- package/frontend/dist/manifest.webmanifest +26 -0
- package/package.json +58 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
/* html-ppt :: runtime.js
|
|
2
|
+
* Keyboard-driven deck runtime. Zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* ← → / space / PgUp PgDn / Home End navigation
|
|
6
|
+
* F fullscreen
|
|
7
|
+
* S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer)
|
|
8
|
+
* The original window stays as audience view, synced via BroadcastChannel.
|
|
9
|
+
* Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout.
|
|
10
|
+
* N quick notes overlay (bottom drawer)
|
|
11
|
+
* O slide overview grid
|
|
12
|
+
* T cycle themes (reads data-themes on <html> or <body>)
|
|
13
|
+
* A cycle demo animation on current slide
|
|
14
|
+
* URL hash #/N deep-link to slide N (1-based)
|
|
15
|
+
* Progress bar auto-managed
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in',
|
|
21
|
+
'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep',
|
|
22
|
+
'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt',
|
|
23
|
+
'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
|
|
24
|
+
'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
|
|
25
|
+
|
|
26
|
+
function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
|
|
27
|
+
|
|
28
|
+
/* ========== Parse URL for preview-only mode ==========
|
|
29
|
+
* When loaded as iframe.src = "index.html?preview=3", runtime enters a
|
|
30
|
+
* locked single-slide mode: only slide N is visible, no chrome, no keys,
|
|
31
|
+
* no hash updates. This is how the presenter window shows pixel-perfect
|
|
32
|
+
* previews — by loading the actual deck file in an iframe and telling it
|
|
33
|
+
* to display only a specific slide.
|
|
34
|
+
*/
|
|
35
|
+
function getPreviewIdx() {
|
|
36
|
+
const m = /[?&]preview=(\d+)/.exec(location.search || '');
|
|
37
|
+
return m ? parseInt(m[1], 10) - 1 : -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ready(function () {
|
|
41
|
+
const deck = document.querySelector('.deck');
|
|
42
|
+
if (!deck) return;
|
|
43
|
+
const slides = Array.from(deck.querySelectorAll('.slide'));
|
|
44
|
+
if (!slides.length) return;
|
|
45
|
+
|
|
46
|
+
const previewOnlyIdx = getPreviewIdx();
|
|
47
|
+
const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
|
|
48
|
+
|
|
49
|
+
/* ===== Preview-only mode: show one slide, hide everything else ===== */
|
|
50
|
+
if (isPreviewMode) {
|
|
51
|
+
function showSlide(i) {
|
|
52
|
+
slides.forEach((s, j) => {
|
|
53
|
+
const active = (j === i);
|
|
54
|
+
s.classList.toggle('is-active', active);
|
|
55
|
+
s.style.display = active ? '' : 'none';
|
|
56
|
+
if (active) {
|
|
57
|
+
s.style.opacity = '1';
|
|
58
|
+
s.style.transform = 'none';
|
|
59
|
+
s.style.pointerEvents = 'auto';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
showSlide(previewOnlyIdx);
|
|
64
|
+
/* Hide chrome that the presenter shouldn't see in preview */
|
|
65
|
+
const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes';
|
|
66
|
+
document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
|
|
67
|
+
document.documentElement.setAttribute('data-preview', '1');
|
|
68
|
+
document.body.setAttribute('data-preview', '1');
|
|
69
|
+
/* Auto-detect theme base path for theme switching in preview mode */
|
|
70
|
+
function getPreviewThemeBase() {
|
|
71
|
+
const base = document.documentElement.getAttribute('data-theme-base');
|
|
72
|
+
if (base) return base;
|
|
73
|
+
const tl = document.getElementById('theme-link');
|
|
74
|
+
if (tl) {
|
|
75
|
+
const raw = tl.getAttribute('href') || '';
|
|
76
|
+
const ls = raw.lastIndexOf('/');
|
|
77
|
+
if (ls >= 0) return raw.substring(0, ls + 1);
|
|
78
|
+
}
|
|
79
|
+
return 'assets/themes/';
|
|
80
|
+
}
|
|
81
|
+
const previewThemeBase = getPreviewThemeBase();
|
|
82
|
+
|
|
83
|
+
/* Listen for postMessage from parent presenter window:
|
|
84
|
+
* - preview-goto: switch visible slide WITHOUT reloading
|
|
85
|
+
* - preview-theme: switch theme CSS link to match audience window */
|
|
86
|
+
window.addEventListener('message', function(e) {
|
|
87
|
+
if (!e.data) return;
|
|
88
|
+
if (e.data.type === 'preview-goto') {
|
|
89
|
+
const n = parseInt(e.data.idx, 10);
|
|
90
|
+
if (n >= 0 && n < slides.length) showSlide(n);
|
|
91
|
+
} else if (e.data.type === 'preview-theme' && e.data.name) {
|
|
92
|
+
let link = document.getElementById('theme-link');
|
|
93
|
+
if (!link) {
|
|
94
|
+
link = document.createElement('link');
|
|
95
|
+
link.rel = 'stylesheet';
|
|
96
|
+
link.id = 'theme-link';
|
|
97
|
+
document.head.appendChild(link);
|
|
98
|
+
}
|
|
99
|
+
link.href = previewThemeBase + e.data.name + '.css';
|
|
100
|
+
document.documentElement.setAttribute('data-theme', e.data.name);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
/* Signal to parent that preview iframe is ready */
|
|
104
|
+
try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let idx = 0;
|
|
109
|
+
const total = slides.length;
|
|
110
|
+
|
|
111
|
+
/* ===== BroadcastChannel for presenter sync ===== */
|
|
112
|
+
const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname;
|
|
113
|
+
let bc;
|
|
114
|
+
try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; }
|
|
115
|
+
|
|
116
|
+
// Are we running inside the presenter popup? (legacy flag, now unused)
|
|
117
|
+
const isPresenterWindow = false;
|
|
118
|
+
|
|
119
|
+
/* ===== progress bar ===== */
|
|
120
|
+
let bar = document.querySelector('.progress-bar');
|
|
121
|
+
if (!bar) {
|
|
122
|
+
bar = document.createElement('div');
|
|
123
|
+
bar.className = 'progress-bar';
|
|
124
|
+
bar.innerHTML = '<span></span>';
|
|
125
|
+
document.body.appendChild(bar);
|
|
126
|
+
}
|
|
127
|
+
const barFill = bar.querySelector('span');
|
|
128
|
+
|
|
129
|
+
/* ===== notes overlay (N key) ===== */
|
|
130
|
+
let notes = document.querySelector('.notes-overlay');
|
|
131
|
+
if (!notes) {
|
|
132
|
+
notes = document.createElement('div');
|
|
133
|
+
notes.className = 'notes-overlay';
|
|
134
|
+
document.body.appendChild(notes);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ===== overview grid (O key) ===== */
|
|
138
|
+
let overview = document.querySelector('.overview');
|
|
139
|
+
if (!overview) {
|
|
140
|
+
overview = document.createElement('div');
|
|
141
|
+
overview.className = 'overview';
|
|
142
|
+
slides.forEach((s, i) => {
|
|
143
|
+
const t = document.createElement('div');
|
|
144
|
+
t.className = 'thumb';
|
|
145
|
+
// Force 16:9 aspect ratio robustly
|
|
146
|
+
t.style.padding = '0 0 56.25% 0';
|
|
147
|
+
t.style.height = '0';
|
|
148
|
+
t.style.position = 'relative';
|
|
149
|
+
t.style.overflow = 'hidden';
|
|
150
|
+
|
|
151
|
+
const title = s.getAttribute('data-title') ||
|
|
152
|
+
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1));
|
|
153
|
+
|
|
154
|
+
// Create a container for the mini-slide
|
|
155
|
+
const mini = document.createElement('div');
|
|
156
|
+
mini.className = 'mini-slide';
|
|
157
|
+
mini.style.position = 'absolute';
|
|
158
|
+
mini.style.top = '0';
|
|
159
|
+
mini.style.left = '0';
|
|
160
|
+
mini.style.width = '1920px';
|
|
161
|
+
mini.style.height = '1080px';
|
|
162
|
+
mini.style.transformOrigin = 'top left';
|
|
163
|
+
mini.style.pointerEvents = 'none';
|
|
164
|
+
mini.style.background = 'var(--bg)';
|
|
165
|
+
|
|
166
|
+
// Clone the slide content
|
|
167
|
+
const clone = s.cloneNode(true);
|
|
168
|
+
clone.className = 'slide is-active'; // force active styles
|
|
169
|
+
clone.style.position = 'absolute';
|
|
170
|
+
clone.style.inset = '0';
|
|
171
|
+
clone.style.transform = 'none';
|
|
172
|
+
clone.style.opacity = '1';
|
|
173
|
+
clone.style.padding = '72px 96px'; // ensure padding is kept
|
|
174
|
+
|
|
175
|
+
mini.appendChild(clone);
|
|
176
|
+
t.appendChild(mini);
|
|
177
|
+
|
|
178
|
+
// Add the number and title overlay
|
|
179
|
+
const overlay = document.createElement('div');
|
|
180
|
+
overlay.style.position = 'absolute';
|
|
181
|
+
overlay.style.inset = '0';
|
|
182
|
+
overlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.8) 100%)';
|
|
183
|
+
overlay.style.color = '#fff';
|
|
184
|
+
overlay.style.zIndex = '10';
|
|
185
|
+
overlay.style.pointerEvents = 'none';
|
|
186
|
+
|
|
187
|
+
const n = document.createElement('div');
|
|
188
|
+
n.className = 'n';
|
|
189
|
+
n.textContent = i + 1;
|
|
190
|
+
n.style.position = 'absolute';
|
|
191
|
+
n.style.top = '12px';
|
|
192
|
+
n.style.left = '16px';
|
|
193
|
+
n.style.fontWeight = '700';
|
|
194
|
+
n.style.fontSize = '16px';
|
|
195
|
+
n.style.color = '#fff';
|
|
196
|
+
n.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
|
|
197
|
+
|
|
198
|
+
const text = document.createElement('div');
|
|
199
|
+
text.className = 't';
|
|
200
|
+
text.textContent = title.trim().slice(0,80);
|
|
201
|
+
text.style.position = 'absolute';
|
|
202
|
+
text.style.bottom = '12px';
|
|
203
|
+
text.style.left = '16px';
|
|
204
|
+
text.style.right = '16px';
|
|
205
|
+
text.style.fontWeight = '600';
|
|
206
|
+
text.style.fontSize = '14px';
|
|
207
|
+
text.style.color = '#fff';
|
|
208
|
+
text.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)';
|
|
209
|
+
|
|
210
|
+
overlay.appendChild(n);
|
|
211
|
+
overlay.appendChild(text);
|
|
212
|
+
t.appendChild(overlay);
|
|
213
|
+
|
|
214
|
+
t.addEventListener('click', () => { go(i); toggleOverview(false); });
|
|
215
|
+
overview.appendChild(t);
|
|
216
|
+
});
|
|
217
|
+
document.body.appendChild(overview);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ===== navigation ===== */
|
|
221
|
+
function go(n, fromRemote){
|
|
222
|
+
n = Math.max(0, Math.min(total-1, n));
|
|
223
|
+
slides.forEach((s,i) => {
|
|
224
|
+
s.classList.toggle('is-active', i===n);
|
|
225
|
+
s.classList.toggle('is-prev', i<n);
|
|
226
|
+
});
|
|
227
|
+
idx = n;
|
|
228
|
+
barFill.style.width = ((n+1)/total*100)+'%';
|
|
229
|
+
const numEl = document.querySelector('.slide-number');
|
|
230
|
+
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
|
|
231
|
+
|
|
232
|
+
// notes (bottom overlay)
|
|
233
|
+
const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes');
|
|
234
|
+
notes.innerHTML = note ? note.innerHTML : '';
|
|
235
|
+
|
|
236
|
+
// hash
|
|
237
|
+
const hashTarget = '#/'+(n+1);
|
|
238
|
+
if (location.hash !== hashTarget && !isPresenterWindow) {
|
|
239
|
+
history.replaceState(null,'', hashTarget);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// re-trigger entry animations
|
|
243
|
+
slides[n].querySelectorAll('[data-anim]').forEach(el => {
|
|
244
|
+
const a = el.getAttribute('data-anim');
|
|
245
|
+
el.classList.remove('anim-'+a);
|
|
246
|
+
void el.offsetWidth;
|
|
247
|
+
el.classList.add('anim-'+a);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// counter-up
|
|
251
|
+
slides[n].querySelectorAll('.counter').forEach(el => {
|
|
252
|
+
const target = parseFloat(el.getAttribute('data-to')||el.textContent);
|
|
253
|
+
const dur = parseInt(el.getAttribute('data-dur')||'1200',10);
|
|
254
|
+
const start = performance.now();
|
|
255
|
+
const from = 0;
|
|
256
|
+
function tick(now){
|
|
257
|
+
const t = Math.min(1,(now-start)/dur);
|
|
258
|
+
const v = from + (target-from)*(1-Math.pow(1-t,3));
|
|
259
|
+
el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1);
|
|
260
|
+
if (t<1) requestAnimationFrame(tick);
|
|
261
|
+
}
|
|
262
|
+
requestAnimationFrame(tick);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Broadcast to other window (audience ↔ presenter)
|
|
266
|
+
if (!fromRemote && bc) {
|
|
267
|
+
bc.postMessage({ type: 'go', idx: n });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ===== listen for remote navigation / theme changes ===== */
|
|
272
|
+
if (bc) {
|
|
273
|
+
bc.onmessage = function(e) {
|
|
274
|
+
if (!e.data) return;
|
|
275
|
+
if (e.data.type === 'go' && typeof e.data.idx === 'number') {
|
|
276
|
+
go(e.data.idx, true);
|
|
277
|
+
} else if (e.data.type === 'theme' && e.data.name) {
|
|
278
|
+
/* Sync theme across windows */
|
|
279
|
+
const i = themes.indexOf(e.data.name);
|
|
280
|
+
if (i >= 0) themeIdx = i;
|
|
281
|
+
applyTheme(e.data.name);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); }
|
|
287
|
+
function toggleOverview(force){
|
|
288
|
+
const isOpen = force!==undefined ? force : !overview.classList.contains('open');
|
|
289
|
+
overview.classList.toggle('open', isOpen);
|
|
290
|
+
if (isOpen) {
|
|
291
|
+
requestAnimationFrame(() => {
|
|
292
|
+
const thumbs = overview.querySelectorAll('.thumb');
|
|
293
|
+
if (thumbs.length) {
|
|
294
|
+
const scale = thumbs[0].clientWidth / 1920;
|
|
295
|
+
overview.querySelectorAll('.mini-slide').forEach(m => {
|
|
296
|
+
m.style.transform = 'scale(' + scale + ')';
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ========== PRESENTER MODE — Magnetic-card popup window ========== */
|
|
304
|
+
/* Opens a new window with 4 draggable, resizable cards:
|
|
305
|
+
* CURRENT — iframe(?preview=N) pixel-perfect preview of current slide
|
|
306
|
+
* NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide
|
|
307
|
+
* SCRIPT — large speaker notes (逐字稿)
|
|
308
|
+
* TIMER — elapsed timer + page counter + controls
|
|
309
|
+
* Cards remember position/size in localStorage.
|
|
310
|
+
* Two windows sync via BroadcastChannel.
|
|
311
|
+
*/
|
|
312
|
+
let presenterWin = null;
|
|
313
|
+
|
|
314
|
+
function openPresenterWindow() {
|
|
315
|
+
if (presenterWin && !presenterWin.closed) {
|
|
316
|
+
presenterWin.focus();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Build absolute URL of THIS deck file (without hash/query)
|
|
321
|
+
const deckUrl = location.protocol + '//' + location.host + location.pathname;
|
|
322
|
+
|
|
323
|
+
// Collect slide titles + notes (HTML strings)
|
|
324
|
+
const slideMeta = slides.map((s, i) => {
|
|
325
|
+
const note = s.querySelector('.notes, aside.notes, .speaker-notes');
|
|
326
|
+
return {
|
|
327
|
+
title: s.getAttribute('data-title') ||
|
|
328
|
+
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
|
|
329
|
+
notes: note ? note.innerHTML : ''
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/* Capture current theme so presenter previews match the audience */
|
|
334
|
+
const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || '');
|
|
335
|
+
const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme);
|
|
336
|
+
|
|
337
|
+
presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no');
|
|
338
|
+
if (!presenterWin) {
|
|
339
|
+
alert('请允许弹出窗口以使用演讲者视图');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
presenterWin.document.open();
|
|
343
|
+
presenterWin.document.write(presenterHTML);
|
|
344
|
+
presenterWin.document.close();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
|
|
348
|
+
const metaJSON = JSON.stringify(slideMeta);
|
|
349
|
+
const deckUrlJSON = JSON.stringify(deckUrl);
|
|
350
|
+
const channelJSON = JSON.stringify(channelName);
|
|
351
|
+
const themeJSON = JSON.stringify(currentTheme || '');
|
|
352
|
+
const storageKey = 'html-ppt-presenter:' + location.pathname;
|
|
353
|
+
|
|
354
|
+
// Build the document as a single template string for clarity
|
|
355
|
+
return `<!DOCTYPE html>
|
|
356
|
+
<html lang="zh-CN">
|
|
357
|
+
<head>
|
|
358
|
+
<meta charset="utf-8">
|
|
359
|
+
<title>Presenter View</title>
|
|
360
|
+
<style>
|
|
361
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
362
|
+
html, body {
|
|
363
|
+
width: 100%; height: 100%; overflow: hidden;
|
|
364
|
+
background: #1a1d24;
|
|
365
|
+
background-image:
|
|
366
|
+
radial-gradient(circle at 20% 30%, rgba(88,166,255,.04), transparent 50%),
|
|
367
|
+
radial-gradient(circle at 80% 70%, rgba(188,140,255,.04), transparent 50%);
|
|
368
|
+
color: #e6edf3;
|
|
369
|
+
font-family: "HanziPen SC";
|
|
370
|
+
}
|
|
371
|
+
/* Stage: positioned area where cards live */
|
|
372
|
+
#stage { position: absolute; inset: 0; overflow: hidden; }
|
|
373
|
+
|
|
374
|
+
/* Magnetic card */
|
|
375
|
+
.pcard {
|
|
376
|
+
position: absolute;
|
|
377
|
+
background: #0d1117;
|
|
378
|
+
border: 1px solid rgba(255,255,255,.1);
|
|
379
|
+
border-radius: 12px;
|
|
380
|
+
box-shadow: 0 8px 32px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.02);
|
|
381
|
+
display: flex; flex-direction: column;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
min-width: 180px; min-height: 100px;
|
|
384
|
+
transition: box-shadow .2s, border-color .2s;
|
|
385
|
+
}
|
|
386
|
+
.pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
|
|
387
|
+
.pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
|
|
388
|
+
.pcard:hover { border-color: rgba(88,166,255,.3); }
|
|
389
|
+
|
|
390
|
+
/* Card header (drag handle) */
|
|
391
|
+
.pcard-head {
|
|
392
|
+
display: flex; align-items: center; gap: 10px;
|
|
393
|
+
padding: 8px 12px;
|
|
394
|
+
background: rgba(255,255,255,.04);
|
|
395
|
+
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
396
|
+
cursor: move;
|
|
397
|
+
user-select: none;
|
|
398
|
+
flex-shrink: 0;
|
|
399
|
+
}
|
|
400
|
+
.pcard-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-color, #58a6ff); flex-shrink: 0; }
|
|
401
|
+
.pcard-title {
|
|
402
|
+
font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
|
|
403
|
+
font-weight: 700; color: #8b949e; flex: 1;
|
|
404
|
+
}
|
|
405
|
+
.pcard-meta { font-size: 11px; color: #6e7681; }
|
|
406
|
+
|
|
407
|
+
/* Card body */
|
|
408
|
+
.pcard-body { flex: 1; position: relative; overflow: hidden; min-height: 0; }
|
|
409
|
+
|
|
410
|
+
/* Preview cards (CURRENT/NEXT) — iframe-based pixel-perfect render */
|
|
411
|
+
.pcard-preview .pcard-body { background: #000; }
|
|
412
|
+
.pcard-preview iframe {
|
|
413
|
+
position: absolute; top: 0; left: 0;
|
|
414
|
+
width: 1920px; height: 1080px;
|
|
415
|
+
border: none;
|
|
416
|
+
transform-origin: top left;
|
|
417
|
+
pointer-events: none;
|
|
418
|
+
background: transparent;
|
|
419
|
+
}
|
|
420
|
+
.pcard-preview .preview-end {
|
|
421
|
+
position: absolute; inset: 0;
|
|
422
|
+
display: flex; align-items: center; justify-content: center;
|
|
423
|
+
color: #484f58; font-size: 14px; letter-spacing: .12em;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* Notes card */
|
|
427
|
+
.pcard-notes .pcard-body {
|
|
428
|
+
padding: 14px 18px;
|
|
429
|
+
overflow-y: auto;
|
|
430
|
+
font-size: 18px; line-height: 1.75;
|
|
431
|
+
color: #d0d7de;
|
|
432
|
+
font-family: "HanziPen SC";
|
|
433
|
+
}
|
|
434
|
+
.pcard-notes .pcard-body p { margin: 0 0 .7em 0; }
|
|
435
|
+
.pcard-notes .pcard-body strong { color: #f0883e; }
|
|
436
|
+
.pcard-notes .pcard-body em { color: #58a6ff; font-style: normal; }
|
|
437
|
+
.pcard-notes .pcard-body code {
|
|
438
|
+
font-family: "HanziPen SC"; font-size: .9em;
|
|
439
|
+
background: rgba(255,255,255,.08); padding: 1px 6px; border-radius: 4px;
|
|
440
|
+
}
|
|
441
|
+
.pcard-notes .empty { color: #484f58; font-style: italic; }
|
|
442
|
+
|
|
443
|
+
/* Timer card */
|
|
444
|
+
.pcard-timer .pcard-body {
|
|
445
|
+
display: flex; flex-direction: column; gap: 14px;
|
|
446
|
+
padding: 18px 20px; justify-content: center;
|
|
447
|
+
}
|
|
448
|
+
.timer-display {
|
|
449
|
+
font-family: "HanziPen SC";
|
|
450
|
+
font-size: 42px; font-weight: 700;
|
|
451
|
+
color: #3fb950;
|
|
452
|
+
letter-spacing: .04em;
|
|
453
|
+
line-height: 1;
|
|
454
|
+
}
|
|
455
|
+
.timer-row {
|
|
456
|
+
display: flex; align-items: center; gap: 12px;
|
|
457
|
+
font-size: 14px; color: #8b949e;
|
|
458
|
+
}
|
|
459
|
+
.timer-row .label { font-size: 10px; letter-spacing: .15em; text-transform: uppercase; color: #6e7681; }
|
|
460
|
+
.timer-row .val { color: #e6edf3; font-weight: 600; font-family: "HanziPen SC"; }
|
|
461
|
+
.timer-controls { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
462
|
+
.timer-btn {
|
|
463
|
+
background: rgba(255,255,255,.06);
|
|
464
|
+
border: 1px solid rgba(255,255,255,.1);
|
|
465
|
+
color: #e6edf3;
|
|
466
|
+
padding: 6px 12px;
|
|
467
|
+
border-radius: 6px;
|
|
468
|
+
font-size: 12px;
|
|
469
|
+
cursor: pointer;
|
|
470
|
+
font-family: inherit;
|
|
471
|
+
}
|
|
472
|
+
.timer-btn:hover { background: rgba(88,166,255,.15); border-color: #58a6ff; }
|
|
473
|
+
.timer-btn:active { transform: translateY(1px); }
|
|
474
|
+
|
|
475
|
+
/* Resize handle */
|
|
476
|
+
.pcard-resize {
|
|
477
|
+
position: absolute; right: 0; bottom: 0;
|
|
478
|
+
width: 18px; height: 18px;
|
|
479
|
+
cursor: nwse-resize;
|
|
480
|
+
background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 60%, transparent 60%, transparent 70%, rgba(255,255,255,.25) 70%, rgba(255,255,255,.25) 80%, transparent 80%);
|
|
481
|
+
z-index: 5;
|
|
482
|
+
}
|
|
483
|
+
.pcard-resize:hover { background: linear-gradient(135deg, transparent 50%, #58a6ff 50%, #58a6ff 60%, transparent 60%, transparent 70%, #58a6ff 70%, #58a6ff 80%, transparent 80%); }
|
|
484
|
+
|
|
485
|
+
/* Bottom hint bar */
|
|
486
|
+
.hint-bar {
|
|
487
|
+
position: fixed; bottom: 0; left: 0; right: 0;
|
|
488
|
+
background: rgba(0,0,0,.6);
|
|
489
|
+
backdrop-filter: blur(10px);
|
|
490
|
+
border-top: 1px solid rgba(255,255,255,.08);
|
|
491
|
+
padding: 6px 16px;
|
|
492
|
+
font-size: 11px; color: #8b949e;
|
|
493
|
+
display: flex; gap: 18px; align-items: center;
|
|
494
|
+
z-index: 1000;
|
|
495
|
+
}
|
|
496
|
+
.hint-bar kbd {
|
|
497
|
+
background: rgba(255,255,255,.08);
|
|
498
|
+
padding: 1px 6px; border-radius: 3px;
|
|
499
|
+
font-family: "HanziPen SC";
|
|
500
|
+
font-size: 10px;
|
|
501
|
+
border: 1px solid rgba(255,255,255,.1);
|
|
502
|
+
color: #e6edf3;
|
|
503
|
+
}
|
|
504
|
+
.hint-bar .reset-layout {
|
|
505
|
+
margin-left: auto;
|
|
506
|
+
background: transparent; border: 1px solid rgba(255,255,255,.15);
|
|
507
|
+
color: #8b949e; padding: 3px 10px; border-radius: 4px;
|
|
508
|
+
font-size: 11px; cursor: pointer; font-family: inherit;
|
|
509
|
+
}
|
|
510
|
+
.hint-bar .reset-layout:hover { background: rgba(248,81,73,.15); border-color: #f85149; color: #f85149; }
|
|
511
|
+
|
|
512
|
+
body.is-dragging-card * { user-select: none !important; }
|
|
513
|
+
body.is-dragging-card iframe { pointer-events: none !important; }
|
|
514
|
+
</style>
|
|
515
|
+
</head>
|
|
516
|
+
<body>
|
|
517
|
+
|
|
518
|
+
<div id="stage">
|
|
519
|
+
<div class="pcard pcard-preview" id="card-cur" style="--dot-color:#58a6ff">
|
|
520
|
+
<div class="pcard-head" data-drag>
|
|
521
|
+
<span class="pcard-dot"></span>
|
|
522
|
+
<span class="pcard-title">CURRENT</span>
|
|
523
|
+
<span class="pcard-meta" id="cur-meta">—</span>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="pcard-body"><iframe id="iframe-cur"></iframe></div>
|
|
526
|
+
<div class="pcard-resize" data-resize></div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<div class="pcard pcard-preview" id="card-nxt" style="--dot-color:#bc8cff">
|
|
530
|
+
<div class="pcard-head" data-drag>
|
|
531
|
+
<span class="pcard-dot"></span>
|
|
532
|
+
<span class="pcard-title">NEXT</span>
|
|
533
|
+
<span class="pcard-meta" id="nxt-meta">—</span>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="pcard-body"><iframe id="iframe-nxt"></iframe></div>
|
|
536
|
+
<div class="pcard-resize" data-resize></div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<div class="pcard pcard-notes" id="card-notes" style="--dot-color:#f0883e">
|
|
540
|
+
<div class="pcard-head" data-drag>
|
|
541
|
+
<span class="pcard-dot"></span>
|
|
542
|
+
<span class="pcard-title">SPEAKER SCRIPT · 逐字稿</span>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="pcard-body" id="notes-body"></div>
|
|
545
|
+
<div class="pcard-resize" data-resize></div>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div class="pcard pcard-timer" id="card-timer" style="--dot-color:#3fb950">
|
|
549
|
+
<div class="pcard-head" data-drag>
|
|
550
|
+
<span class="pcard-dot"></span>
|
|
551
|
+
<span class="pcard-title">TIMER</span>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="pcard-body">
|
|
554
|
+
<div class="timer-display" id="timer-display">00:00</div>
|
|
555
|
+
<div class="timer-row">
|
|
556
|
+
<span class="label">Slide</span>
|
|
557
|
+
<span class="val" id="timer-count">1 / ${total}</span>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="timer-controls">
|
|
560
|
+
<button class="timer-btn" id="btn-prev">← Prev</button>
|
|
561
|
+
<button class="timer-btn" id="btn-next">Next →</button>
|
|
562
|
+
<button class="timer-btn" id="btn-reset">⏱ Reset</button>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="pcard-resize" data-resize></div>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div class="hint-bar">
|
|
570
|
+
<span><kbd>← →</kbd> 翻页</span>
|
|
571
|
+
<span><kbd>R</kbd> 重置计时</span>
|
|
572
|
+
<span><kbd>Esc</kbd> 关闭</span>
|
|
573
|
+
<span style="color:#6e7681">拖动卡片头部移动 · 拖动右下角调整大小</span>
|
|
574
|
+
<button class="reset-layout" id="reset-layout">重置布局</button>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<script>
|
|
578
|
+
(function(){
|
|
579
|
+
var slideMeta = ${metaJSON};
|
|
580
|
+
var total = ${total};
|
|
581
|
+
var idx = ${startIdx};
|
|
582
|
+
var deckUrl = ${deckUrlJSON};
|
|
583
|
+
var STORAGE_KEY = ${JSON.stringify(storageKey)};
|
|
584
|
+
var bc;
|
|
585
|
+
try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
|
|
586
|
+
|
|
587
|
+
var iframeCur = document.getElementById('iframe-cur');
|
|
588
|
+
var iframeNxt = document.getElementById('iframe-nxt');
|
|
589
|
+
var notesBody = document.getElementById('notes-body');
|
|
590
|
+
var curMeta = document.getElementById('cur-meta');
|
|
591
|
+
var nxtMeta = document.getElementById('nxt-meta');
|
|
592
|
+
var timerDisplay = document.getElementById('timer-display');
|
|
593
|
+
var timerCount = document.getElementById('timer-count');
|
|
594
|
+
|
|
595
|
+
/* ===== Default card layout ===== */
|
|
596
|
+
function defaultLayout() {
|
|
597
|
+
var w = window.innerWidth;
|
|
598
|
+
var h = window.innerHeight - 36; /* leave room for hint bar */
|
|
599
|
+
return {
|
|
600
|
+
'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
|
|
601
|
+
'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
|
|
602
|
+
'card-notes': { x: Math.round(w*0.55) + 8, y: Math.round(h*0.42) + 8, w: w - Math.round(w*0.55) - 24, h: h - Math.round(h*0.42) - 16 },
|
|
603
|
+
'card-timer': { x: 16, y: Math.round(h*0.62) + 8, w: Math.round(w*0.55) - 24, h: h - Math.round(h*0.62) - 16 }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ===== Apply / save / restore layout ===== */
|
|
608
|
+
function applyLayout(layout) {
|
|
609
|
+
Object.keys(layout).forEach(function(id){
|
|
610
|
+
var el = document.getElementById(id);
|
|
611
|
+
var l = layout[id];
|
|
612
|
+
if (el && l) {
|
|
613
|
+
el.style.left = l.x + 'px';
|
|
614
|
+
el.style.top = l.y + 'px';
|
|
615
|
+
el.style.width = l.w + 'px';
|
|
616
|
+
el.style.height = l.h + 'px';
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
rescaleAll();
|
|
620
|
+
}
|
|
621
|
+
function readLayout() {
|
|
622
|
+
try {
|
|
623
|
+
var saved = localStorage.getItem(STORAGE_KEY);
|
|
624
|
+
if (saved) return JSON.parse(saved);
|
|
625
|
+
} catch(e) {}
|
|
626
|
+
return defaultLayout();
|
|
627
|
+
}
|
|
628
|
+
function saveLayout() {
|
|
629
|
+
var layout = {};
|
|
630
|
+
['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){
|
|
631
|
+
var el = document.getElementById(id);
|
|
632
|
+
if (el) {
|
|
633
|
+
layout[id] = {
|
|
634
|
+
x: parseInt(el.style.left,10) || 0,
|
|
635
|
+
y: parseInt(el.style.top,10) || 0,
|
|
636
|
+
w: parseInt(el.style.width,10) || 300,
|
|
637
|
+
h: parseInt(el.style.height,10) || 200
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); } catch(e) {}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* ===== iframe rescale to fit card body ===== */
|
|
645
|
+
function rescaleIframe(iframe) {
|
|
646
|
+
if (!iframe || iframe.style.display === 'none') return;
|
|
647
|
+
var body = iframe.parentElement;
|
|
648
|
+
var cw = body.clientWidth, ch = body.clientHeight;
|
|
649
|
+
if (!cw || !ch) return;
|
|
650
|
+
var s = Math.min(cw / 1920, ch / 1080);
|
|
651
|
+
iframe.style.transform = 'scale(' + s + ')';
|
|
652
|
+
/* Center the scaled iframe in the body */
|
|
653
|
+
var sw = 1920 * s, sh = 1080 * s;
|
|
654
|
+
iframe.style.left = Math.max(0, (cw - sw) / 2) + 'px';
|
|
655
|
+
iframe.style.top = Math.max(0, (ch - sh) / 2) + 'px';
|
|
656
|
+
}
|
|
657
|
+
function rescaleAll() {
|
|
658
|
+
rescaleIframe(iframeCur);
|
|
659
|
+
rescaleIframe(iframeNxt);
|
|
660
|
+
}
|
|
661
|
+
window.addEventListener('resize', rescaleAll);
|
|
662
|
+
|
|
663
|
+
/* ===== Drag (move card by header) ===== */
|
|
664
|
+
document.querySelectorAll('[data-drag]').forEach(function(handle){
|
|
665
|
+
handle.addEventListener('mousedown', function(e){
|
|
666
|
+
if (e.button !== 0) return;
|
|
667
|
+
var card = handle.closest('.pcard');
|
|
668
|
+
if (!card) return;
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
card.classList.add('dragging');
|
|
671
|
+
document.body.classList.add('is-dragging-card');
|
|
672
|
+
var startX = e.clientX, startY = e.clientY;
|
|
673
|
+
var startL = parseInt(card.style.left,10) || 0;
|
|
674
|
+
var startT = parseInt(card.style.top,10) || 0;
|
|
675
|
+
function onMove(ev){
|
|
676
|
+
var nx = Math.max(0, Math.min(window.innerWidth - 100, startL + ev.clientX - startX));
|
|
677
|
+
var ny = Math.max(0, Math.min(window.innerHeight - 50, startT + ev.clientY - startY));
|
|
678
|
+
card.style.left = nx + 'px';
|
|
679
|
+
card.style.top = ny + 'px';
|
|
680
|
+
}
|
|
681
|
+
function onUp(){
|
|
682
|
+
card.classList.remove('dragging');
|
|
683
|
+
document.body.classList.remove('is-dragging-card');
|
|
684
|
+
document.removeEventListener('mousemove', onMove);
|
|
685
|
+
document.removeEventListener('mouseup', onUp);
|
|
686
|
+
saveLayout();
|
|
687
|
+
}
|
|
688
|
+
document.addEventListener('mousemove', onMove);
|
|
689
|
+
document.addEventListener('mouseup', onUp);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
/* ===== Resize (drag bottom-right corner) ===== */
|
|
694
|
+
document.querySelectorAll('[data-resize]').forEach(function(handle){
|
|
695
|
+
handle.addEventListener('mousedown', function(e){
|
|
696
|
+
if (e.button !== 0) return;
|
|
697
|
+
var card = handle.closest('.pcard');
|
|
698
|
+
if (!card) return;
|
|
699
|
+
e.preventDefault(); e.stopPropagation();
|
|
700
|
+
card.classList.add('resizing');
|
|
701
|
+
document.body.classList.add('is-dragging-card');
|
|
702
|
+
var startX = e.clientX, startY = e.clientY;
|
|
703
|
+
var startW = parseInt(card.style.width,10) || card.offsetWidth;
|
|
704
|
+
var startH = parseInt(card.style.height,10) || card.offsetHeight;
|
|
705
|
+
function onMove(ev){
|
|
706
|
+
var nw = Math.max(180, startW + ev.clientX - startX);
|
|
707
|
+
var nh = Math.max(100, startH + ev.clientY - startY);
|
|
708
|
+
card.style.width = nw + 'px';
|
|
709
|
+
card.style.height = nh + 'px';
|
|
710
|
+
if (card.querySelector('iframe')) rescaleIframe(card.querySelector('iframe'));
|
|
711
|
+
}
|
|
712
|
+
function onUp(){
|
|
713
|
+
card.classList.remove('resizing');
|
|
714
|
+
document.body.classList.remove('is-dragging-card');
|
|
715
|
+
document.removeEventListener('mousemove', onMove);
|
|
716
|
+
document.removeEventListener('mouseup', onUp);
|
|
717
|
+
rescaleAll();
|
|
718
|
+
saveLayout();
|
|
719
|
+
}
|
|
720
|
+
document.addEventListener('mousemove', onMove);
|
|
721
|
+
document.addEventListener('mouseup', onUp);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
/* ===== Preview iframe ready tracking =====
|
|
726
|
+
* Each iframe loads the deck ONCE with ?preview=1 on init. Subsequent
|
|
727
|
+
* slide changes are sent via postMessage('preview-goto') so the iframe
|
|
728
|
+
* just toggles visibility of a different .slide — no reload, no flicker.
|
|
729
|
+
*/
|
|
730
|
+
var iframeReady = { cur: false, nxt: false };
|
|
731
|
+
var currentTheme = ${themeJSON};
|
|
732
|
+
window.addEventListener('message', function(e) {
|
|
733
|
+
if (!e.data || e.data.type !== 'preview-ready') return;
|
|
734
|
+
var iframe = null;
|
|
735
|
+
if (e.source === iframeCur.contentWindow) {
|
|
736
|
+
iframeReady.cur = true;
|
|
737
|
+
iframe = iframeCur;
|
|
738
|
+
postPreviewGoto(iframeCur, idx);
|
|
739
|
+
} else if (e.source === iframeNxt.contentWindow) {
|
|
740
|
+
iframeReady.nxt = true;
|
|
741
|
+
iframe = iframeNxt;
|
|
742
|
+
postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx);
|
|
743
|
+
}
|
|
744
|
+
/* Sync current theme to the iframe */
|
|
745
|
+
if (iframe && currentTheme) {
|
|
746
|
+
try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {}
|
|
747
|
+
}
|
|
748
|
+
if (iframe) rescaleIframe(iframe);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
function postPreviewGoto(iframe, n) {
|
|
752
|
+
try {
|
|
753
|
+
iframe.contentWindow.postMessage({ type: 'preview-goto', idx: n }, '*');
|
|
754
|
+
} catch(e) {}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ===== Update content =====
|
|
758
|
+
* Smooth (no-reload) navigation: send postMessage to iframes instead of
|
|
759
|
+
* resetting src. Iframes stay loaded, just switch visible .slide.
|
|
760
|
+
*/
|
|
761
|
+
function update(n) {
|
|
762
|
+
n = Math.max(0, Math.min(total - 1, n));
|
|
763
|
+
idx = n;
|
|
764
|
+
|
|
765
|
+
/* Current preview — postMessage (smooth) */
|
|
766
|
+
if (iframeReady.cur) postPreviewGoto(iframeCur, n);
|
|
767
|
+
curMeta.textContent = (n + 1) + '/' + total;
|
|
768
|
+
|
|
769
|
+
/* Next preview */
|
|
770
|
+
if (n + 1 < total) {
|
|
771
|
+
iframeNxt.style.display = '';
|
|
772
|
+
var endEl = document.querySelector('#card-nxt .preview-end');
|
|
773
|
+
if (endEl) endEl.remove();
|
|
774
|
+
if (iframeReady.nxt) postPreviewGoto(iframeNxt, n + 1);
|
|
775
|
+
nxtMeta.textContent = (n + 2) + '/' + total;
|
|
776
|
+
} else {
|
|
777
|
+
iframeNxt.style.display = 'none';
|
|
778
|
+
var body = document.querySelector('#card-nxt .pcard-body');
|
|
779
|
+
if (body && !body.querySelector('.preview-end')) {
|
|
780
|
+
var end = document.createElement('div');
|
|
781
|
+
end.className = 'preview-end';
|
|
782
|
+
end.textContent = '— END OF DECK —';
|
|
783
|
+
body.appendChild(end);
|
|
784
|
+
}
|
|
785
|
+
nxtMeta.textContent = 'END';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* Notes */
|
|
789
|
+
var note = slideMeta[n].notes;
|
|
790
|
+
notesBody.innerHTML = note || '<span class="empty">(这一页还没有逐字稿)</span>';
|
|
791
|
+
|
|
792
|
+
/* Timer count */
|
|
793
|
+
timerCount.textContent = (n + 1) + ' / ' + total;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* ===== Timer ===== */
|
|
797
|
+
var tStart = Date.now();
|
|
798
|
+
setInterval(function(){
|
|
799
|
+
var s = Math.floor((Date.now() - tStart) / 1000);
|
|
800
|
+
var mm = String(Math.floor(s/60)).padStart(2,'0');
|
|
801
|
+
var ss = String(s%60).padStart(2,'0');
|
|
802
|
+
timerDisplay.textContent = mm + ':' + ss;
|
|
803
|
+
}, 1000);
|
|
804
|
+
function resetTimer(){ tStart = Date.now(); timerDisplay.textContent = '00:00'; }
|
|
805
|
+
|
|
806
|
+
/* ===== BroadcastChannel sync ===== */
|
|
807
|
+
if (bc) {
|
|
808
|
+
bc.onmessage = function(e){
|
|
809
|
+
if (!e.data) return;
|
|
810
|
+
if (e.data.type === 'go') update(e.data.idx);
|
|
811
|
+
else if (e.data.type === 'theme' && e.data.name) {
|
|
812
|
+
currentTheme = e.data.name;
|
|
813
|
+
/* Forward theme change to preview iframes */
|
|
814
|
+
[iframeCur, iframeNxt].forEach(function(iframe){
|
|
815
|
+
try {
|
|
816
|
+
iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*');
|
|
817
|
+
} catch(err) {}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function go(n) {
|
|
823
|
+
update(n);
|
|
824
|
+
if (bc) bc.postMessage({ type: 'go', idx: idx });
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/* ===== Buttons ===== */
|
|
828
|
+
document.getElementById('btn-prev').addEventListener('click', function(){ go(idx - 1); });
|
|
829
|
+
document.getElementById('btn-next').addEventListener('click', function(){ go(idx + 1); });
|
|
830
|
+
document.getElementById('btn-reset').addEventListener('click', resetTimer);
|
|
831
|
+
document.getElementById('reset-layout').addEventListener('click', function(){
|
|
832
|
+
if (confirm('恢复默认卡片布局?')) {
|
|
833
|
+
try { localStorage.removeItem(STORAGE_KEY); } catch(e){}
|
|
834
|
+
applyLayout(defaultLayout());
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
/* ===== Keyboard ===== */
|
|
839
|
+
document.addEventListener('keydown', function(e){
|
|
840
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
841
|
+
switch(e.key) {
|
|
842
|
+
case 'ArrowRight': case ' ': case 'PageDown': go(idx + 1); e.preventDefault(); break;
|
|
843
|
+
case 'ArrowLeft': case 'PageUp': go(idx - 1); e.preventDefault(); break;
|
|
844
|
+
case 'Home': go(0); break;
|
|
845
|
+
case 'End': go(total - 1); break;
|
|
846
|
+
case 'r': case 'R': resetTimer(); break;
|
|
847
|
+
case 'Escape': window.close(); break;
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
/* ===== Iframe load → rescale (catches initial size) ===== */
|
|
852
|
+
iframeCur.addEventListener('load', function(){ rescaleIframe(iframeCur); });
|
|
853
|
+
iframeNxt.addEventListener('load', function(){ rescaleIframe(iframeNxt); });
|
|
854
|
+
|
|
855
|
+
/* ===== Init =====
|
|
856
|
+
* Load each iframe ONCE with the deck file. After they post
|
|
857
|
+
* 'preview-ready', all subsequent navigation is via postMessage
|
|
858
|
+
* (smooth, no reload, no flicker).
|
|
859
|
+
*/
|
|
860
|
+
applyLayout(readLayout());
|
|
861
|
+
iframeCur.src = deckUrl + '?preview=' + (idx + 1);
|
|
862
|
+
if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
|
|
863
|
+
/* Initialize notes/timer/count without touching iframes */
|
|
864
|
+
notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>';
|
|
865
|
+
curMeta.textContent = (idx + 1) + '/' + total;
|
|
866
|
+
nxtMeta.textContent = (idx + 2) + '/' + total;
|
|
867
|
+
timerCount.textContent = (idx + 1) + ' / ' + total;
|
|
868
|
+
})();
|
|
869
|
+
</` + `script>
|
|
870
|
+
</body></html>`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function fullscreen(){ const el=document.documentElement;
|
|
874
|
+
if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen();
|
|
875
|
+
else document.exitFullscreen&&document.exitFullscreen();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// theme cycling
|
|
879
|
+
const root = document.documentElement;
|
|
880
|
+
const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes');
|
|
881
|
+
const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
882
|
+
let themeIdx = 0;
|
|
883
|
+
|
|
884
|
+
// Auto-detect theme base path from existing <link id="theme-link">
|
|
885
|
+
let themeBase = root.getAttribute('data-theme-base');
|
|
886
|
+
if (!themeBase) {
|
|
887
|
+
const existingLink = document.getElementById('theme-link');
|
|
888
|
+
if (existingLink) {
|
|
889
|
+
// el.getAttribute('href') gives the raw relative path written in HTML
|
|
890
|
+
const rawHref = existingLink.getAttribute('href') || '';
|
|
891
|
+
const lastSlash = rawHref.lastIndexOf('/');
|
|
892
|
+
themeBase = lastSlash >= 0 ? rawHref.substring(0, lastSlash + 1) : 'assets/themes/';
|
|
893
|
+
} else {
|
|
894
|
+
themeBase = 'assets/themes/';
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function applyTheme(name) {
|
|
899
|
+
let link = document.getElementById('theme-link');
|
|
900
|
+
if (!link) {
|
|
901
|
+
link = document.createElement('link');
|
|
902
|
+
link.rel = 'stylesheet';
|
|
903
|
+
link.id = 'theme-link';
|
|
904
|
+
document.head.appendChild(link);
|
|
905
|
+
}
|
|
906
|
+
link.href = themeBase + name + '.css';
|
|
907
|
+
root.setAttribute('data-theme', name);
|
|
908
|
+
const ind = document.querySelector('.theme-indicator');
|
|
909
|
+
if (ind) ind.textContent = name;
|
|
910
|
+
}
|
|
911
|
+
function cycleTheme(fromRemote){
|
|
912
|
+
if (!themes.length) return;
|
|
913
|
+
themeIdx = (themeIdx+1) % themes.length;
|
|
914
|
+
const name = themes[themeIdx];
|
|
915
|
+
applyTheme(name);
|
|
916
|
+
/* Broadcast to other window (audience ↔ presenter) */
|
|
917
|
+
if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name });
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// animation cycling on current slide
|
|
921
|
+
let animIdx = 0;
|
|
922
|
+
function cycleAnim(){
|
|
923
|
+
animIdx = (animIdx+1) % ANIMS.length;
|
|
924
|
+
const a = ANIMS[animIdx];
|
|
925
|
+
const target = slides[idx].querySelector('[data-anim-target]') || slides[idx];
|
|
926
|
+
ANIMS.forEach(x => target.classList.remove('anim-'+x));
|
|
927
|
+
void target.offsetWidth;
|
|
928
|
+
target.classList.add('anim-'+a);
|
|
929
|
+
target.setAttribute('data-anim', a);
|
|
930
|
+
const ind = document.querySelector('.anim-indicator');
|
|
931
|
+
if (ind) ind.textContent = a;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
document.addEventListener('keydown', function (e) {
|
|
935
|
+
if (e.metaKey||e.ctrlKey||e.altKey) return;
|
|
936
|
+
switch (e.key) {
|
|
937
|
+
case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break;
|
|
938
|
+
case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break;
|
|
939
|
+
case 'Home': go(0); break;
|
|
940
|
+
case 'End': go(total-1); break;
|
|
941
|
+
case 'f': case 'F': fullscreen(); break;
|
|
942
|
+
case 's': case 'S': openPresenterWindow(); break;
|
|
943
|
+
case 'n': case 'N': toggleNotes(); break;
|
|
944
|
+
case 'o': case 'O': toggleOverview(); break;
|
|
945
|
+
case 't': case 'T': cycleTheme(); break;
|
|
946
|
+
case 'a': case 'A': cycleAnim(); break;
|
|
947
|
+
case 'Escape': toggleOverview(false); toggleNotes(false); break;
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// hash deep-link
|
|
952
|
+
function fromHash(){
|
|
953
|
+
const m = /^#\/(\d+)/.exec(location.hash||'');
|
|
954
|
+
if (m) go(Math.max(0, parseInt(m[1],10)-1));
|
|
955
|
+
}
|
|
956
|
+
window.addEventListener('hashchange', fromHash);
|
|
957
|
+
fromHash();
|
|
958
|
+
go(idx);
|
|
959
|
+
});
|
|
960
|
+
})();
|