Files
Labyricorn 797be4d878
Deploy production / deploy (push) Successful in 5s
Add optional narration players
2026-08-12 11:45:40 -07:00

88 lines
2.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});