88 lines
2.9 KiB
JavaScript
88 lines
2.9 KiB
JavaScript
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();
|
||
});
|