document.querySelectorAll("[data-narration-player]").forEach((player) => { const audio = player.querySelector("audio"); const controls = player.querySelector("[data-narration-controls]"); const toggle = player.querySelector("[data-narration-toggle]"); const toggleIcon = player.querySelector("[data-narration-toggle-icon]"); const progress = player.querySelector("[data-narration-progress]"); const currentTime = player.querySelector("[data-narration-current]"); const duration = player.querySelector("[data-narration-duration]"); const speed = player.querySelector("[data-narration-speed]"); let pendingSeek = null; if (!audio || !controls || !toggle || !toggleIcon || !progress || !currentTime || !duration || !speed) return; const formatTime = (seconds) => { if (!Number.isFinite(seconds)) return "–:––"; const wholeSeconds = Math.max(0, Math.floor(seconds)); const minutes = Math.floor(wholeSeconds / 60); return `${minutes}:${String(wholeSeconds % 60).padStart(2, "0")}`; }; const updateTimeline = () => { const total = Number.isFinite(audio.duration) ? audio.duration : 0; const position = pendingSeek ?? audio.currentTime; progress.max = String(total); progress.value = String(position); currentTime.textContent = formatTime(position); duration.textContent = formatTime(audio.duration); progress.setAttribute( "aria-valuetext", `${formatTime(position)} of ${formatTime(audio.duration)}` ); }; const updateToggle = () => { const isPlaying = !audio.paused && !audio.ended; toggle.setAttribute("aria-label", isPlaying ? "Pause narration" : "Play narration"); toggleIcon.textContent = isPlaying ? "Ⅱ" : "▶"; }; audio.controls = false; controls.hidden = false; player.classList.add("is-enhanced"); toggle.addEventListener("click", async () => { if (audio.paused || audio.ended) { try { await audio.play(); } catch { updateToggle(); } } else { audio.pause(); } }); progress.addEventListener("input", () => { const seekTime = Number.parseFloat(progress.value); if (!Number.isFinite(seekTime)) return; pendingSeek = seekTime; audio.currentTime = pendingSeek; updateTimeline(); }); audio.addEventListener("loadedmetadata", updateTimeline); audio.addEventListener("durationchange", updateTimeline); audio.addEventListener("timeupdate", updateTimeline); audio.addEventListener("seeked", () => { pendingSeek = null; updateTimeline(); }); audio.addEventListener("play", updateToggle); audio.addEventListener("pause", updateToggle); audio.addEventListener("ended", updateToggle); speed.addEventListener("change", () => { const playbackRate = Number.parseFloat(speed.value); if (!Number.isFinite(playbackRate)) return; audio.defaultPlaybackRate = playbackRate; audio.playbackRate = playbackRate; }); updateTimeline(); updateToggle(); });