From 36ecd2cf307a842edb1aaa7a4d02e4ab06e6c6c3 Mon Sep 17 00:00:00 2001 From: lewis Date: Fri, 17 Apr 2026 23:34:09 +0800 Subject: [PATCH] fix(presenter): sync theme across audience + presenter iframes (T key) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: pressing T in audience window only swapped the host page's theme CSS link. Preview iframes in the presenter window stayed on the theme that was active when the popup was opened (or the HTML default). Fix: 3-hop theme propagation via message types 1. audience cycleTheme() → BroadcastChannel 'theme' message 2. presenter window receives BC msg → postMessage 'preview-theme' to both iframes 3. iframe preview mode listens for 'preview-theme' → swaps its own href Also: - Audience window listens for 'theme' BC events → applies theme (so pressing T in one window cycles theme in BOTH) - Presenter window captures audience's current theme at open time (data-theme attr) and forwards it to each iframe on 'preview-ready' so previews match audience from frame 1, even if audience had already cycled theme before opening presenter - preview mode auto-detects theme base path from existing theme-link href (same logic as audience cycleTheme) Verified in headless Chrome: BC msg {type:theme, name:dracula} → both audience and preview iframes show data-theme=dracula with matching colors. --- assets/runtime.js | 90 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/assets/runtime.js b/assets/runtime.js index c6675e3..b5d4939 100644 --- a/assets/runtime.js +++ b/assets/runtime.js @@ -66,12 +66,39 @@ document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; }); document.documentElement.setAttribute('data-preview', '1'); document.body.setAttribute('data-preview', '1'); - /* Listen for postMessage from parent presenter window to switch slides - * WITHOUT reloading — this eliminates flicker during navigation. */ + /* Auto-detect theme base path for theme switching in preview mode */ + function getPreviewThemeBase() { + const base = document.documentElement.getAttribute('data-theme-base'); + if (base) return base; + const tl = document.getElementById('theme-link'); + if (tl) { + const raw = tl.getAttribute('href') || ''; + const ls = raw.lastIndexOf('/'); + if (ls >= 0) return raw.substring(0, ls + 1); + } + return 'assets/themes/'; + } + const previewThemeBase = getPreviewThemeBase(); + + /* Listen for postMessage from parent presenter window: + * - preview-goto: switch visible slide WITHOUT reloading + * - preview-theme: switch theme CSS link to match audience window */ window.addEventListener('message', function(e) { - if (!e.data || e.data.type !== 'preview-goto') return; - const n = parseInt(e.data.idx, 10); - if (n >= 0 && n < slides.length) showSlide(n); + if (!e.data) return; + if (e.data.type === 'preview-goto') { + const n = parseInt(e.data.idx, 10); + if (n >= 0 && n < slides.length) showSlide(n); + } else if (e.data.type === 'preview-theme' && e.data.name) { + let link = document.getElementById('theme-link'); + if (!link) { + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.id = 'theme-link'; + document.head.appendChild(link); + } + link.href = previewThemeBase + e.data.name + '.css'; + document.documentElement.setAttribute('data-theme', e.data.name); + } }); /* Signal to parent that preview iframe is ready */ try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {} @@ -175,11 +202,17 @@ } } - /* ===== listen for remote navigation ===== */ + /* ===== listen for remote navigation / theme changes ===== */ if (bc) { bc.onmessage = function(e) { - if (e.data && e.data.type === 'go' && typeof e.data.idx === 'number') { + if (!e.data) return; + if (e.data.type === 'go' && typeof e.data.idx === 'number') { go(e.data.idx, true); + } else if (e.data.type === 'theme' && e.data.name) { + /* Sync theme across windows */ + const i = themes.indexOf(e.data.name); + if (i >= 0) themeIdx = i; + applyTheme(e.data.name); } }; } @@ -217,7 +250,9 @@ }; }); - const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME); + /* Capture current theme so presenter previews match the audience */ + const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || ''); + const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme); presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no'); if (!presenterWin) { @@ -229,10 +264,11 @@ presenterWin.document.close(); } - function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName) { + function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) { const metaJSON = JSON.stringify(slideMeta); const deckUrlJSON = JSON.stringify(deckUrl); const channelJSON = JSON.stringify(channelName); + const themeJSON = JSON.stringify(currentTheme || ''); const storageKey = 'html-ppt-presenter:' + location.pathname; // Build the document as a single template string for clarity @@ -612,17 +648,24 @@ * just toggles visibility of a different .slide — no reload, no flicker. */ var iframeReady = { cur: false, nxt: false }; + var currentTheme = ${themeJSON}; window.addEventListener('message', function(e) { if (!e.data || e.data.type !== 'preview-ready') return; + var iframe = null; if (e.source === iframeCur.contentWindow) { iframeReady.cur = true; + iframe = iframeCur; postPreviewGoto(iframeCur, idx); - rescaleIframe(iframeCur); } else if (e.source === iframeNxt.contentWindow) { iframeReady.nxt = true; + iframe = iframeNxt; postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx); - rescaleIframe(iframeNxt); } + /* Sync current theme to the iframe */ + if (iframe && currentTheme) { + try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {} + } + if (iframe) rescaleIframe(iframe); }); function postPreviewGoto(iframe, n) { @@ -683,7 +726,17 @@ /* ===== BroadcastChannel sync ===== */ if (bc) { bc.onmessage = function(e){ - if (e.data && e.data.type === 'go') update(e.data.idx); + if (!e.data) return; + if (e.data.type === 'go') update(e.data.idx); + else if (e.data.type === 'theme' && e.data.name) { + currentTheme = e.data.name; + /* Forward theme change to preview iframes */ + [iframeCur, iframeNxt].forEach(function(iframe){ + try { + iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*'); + } catch(err) {} + }); + } }; } function go(n) { @@ -761,10 +814,7 @@ } } - function cycleTheme(){ - if (!themes.length) return; - themeIdx = (themeIdx+1) % themes.length; - const name = themes[themeIdx]; + function applyTheme(name) { let link = document.getElementById('theme-link'); if (!link) { link = document.createElement('link'); @@ -777,6 +827,14 @@ const ind = document.querySelector('.theme-indicator'); if (ind) ind.textContent = name; } + function cycleTheme(fromRemote){ + if (!themes.length) return; + themeIdx = (themeIdx+1) % themes.length; + const name = themes[themeIdx]; + applyTheme(name); + /* Broadcast to other window (audience ↔ presenter) */ + if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name }); + } // animation cycling on current slide let animIdx = 0;