+
+
{body}
@@ -1019,7 +1375,7 @@ EPISODE_HTML = """
const player = document.getElementById('player');
const missing = document.getElementById('audio_missing');
- // If the audio element fails to load, hide it and show a notice.
+ // Hide audio + show notice if the file is unavailable on this server
player.addEventListener('error', () => {{
player.style.display = 'none';
missing.style.display = '';
@@ -1035,42 +1391,80 @@ EPISODE_HTML = """
}} catch (e) {{}}
}}
- // Click handler for any element with a data-seek attribute.
+ // Click handler for any element with data-seek
document.body.addEventListener('click', (ev) => {{
const el = ev.target.closest('[data-seek]');
if (!el) return;
- // Only intercept if it's a # anchor or button/click — let normal navigation work otherwise.
const tag = el.tagName.toLowerCase();
if (tag === 'a' && el.getAttribute('href') && !el.getAttribute('href').startsWith('#')) return;
ev.preventDefault();
- const t = el.getAttribute('data-seek');
- seek(t);
+ seek(el.getAttribute('data-seek'));
const href = el.getAttribute('href') || '';
if (tag === 'a' && (href.startsWith('#qa-') || href.startsWith('#intro-'))) {{
- // also scroll the anchor into view
const target = document.getElementById(href.slice(1));
if (target) target.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
}}
}});
- // On page load, if the URL contains #qa-
, seek the audio to that Q&A's start.
+ // On hash load, jump + seek
function handleHash() {{
const h = window.location.hash;
if (!h) return;
const target = document.getElementById(h.slice(1));
if (!target) return;
target.scrollIntoView({{ block: 'start' }});
- // find nearest data-seek descendant for the start time
const seekEl = target.querySelector('[data-seek]');
if (seekEl) {{
const t = seekEl.getAttribute('data-seek');
- // wait for metadata before seeking so currentTime sticks
if (player.readyState >= 1) seek(t);
else player.addEventListener('loadedmetadata', () => seek(t), {{ once: true }});
}}
}}
handleHash();
window.addEventListener('hashchange', handleHash);
+
+ // Active-Q&A highlighting following audio playhead.
+ // Build a sorted index of {{ start, end, el, indexLi }} once.
+ const qaBlocks = Array.from(document.querySelectorAll('.qa[id^="qa-"]')).map(el => {{
+ const startEl = el.querySelector('[data-seek]');
+ const start = startEl ? parseFloat(startEl.getAttribute('data-seek')) : 0;
+ // The corresponding aside list item
+ const id = el.id;
+ const indexLi = document.querySelector(`#qa_index a[href="#${{id}}"]`);
+ return {{ el, start, indexLi: indexLi ? indexLi.parentElement : null }};
+ }}).sort((a, b) => a.start - b.start);
+
+ // Compute end as next start (capped at +180s for safety).
+ for (let i = 0; i < qaBlocks.length; i++) {{
+ const next = qaBlocks[i+1];
+ qaBlocks[i].end = next ? Math.min(next.start, qaBlocks[i].start + 180) : qaBlocks[i].start + 180;
+ }}
+
+ let currentActive = null;
+ function updateActive() {{
+ const t = player.currentTime;
+ if (player.paused || isNaN(t) || t === 0) {{
+ if (currentActive) {{
+ currentActive.el.classList.remove('active');
+ if (currentActive.indexLi) currentActive.indexLi.classList.remove('active');
+ currentActive = null;
+ }}
+ return;
+ }}
+ const found = qaBlocks.find(b => t >= b.start && t < b.end);
+ if (found === currentActive) return;
+ if (currentActive) {{
+ currentActive.el.classList.remove('active');
+ if (currentActive.indexLi) currentActive.indexLi.classList.remove('active');
+ }}
+ if (found) {{
+ found.el.classList.add('active');
+ if (found.indexLi) found.indexLi.classList.add('active');
+ }}
+ currentActive = found || null;
+ }}
+ player.addEventListener('timeupdate', updateActive);
+ player.addEventListener('pause', updateActive);
}})();