player
This commit is contained in:
1 parent
86547731c8
commit
ce5eae7bce
3 files changed
+298
-8
No files matched your search
@@ -52,6 +52,11 @@
|
||||
"modal.transcript": "Transcript",
|
||||
"modal.sources": "Sources",
|
||||
"modal.download": "Download audio",
|
||||
"modal.downloading": "Downloading…",
|
||||
"modal.downloaded": "Saved",
|
||||
"player.play": "Play",
|
||||
"player.pause": "Pause",
|
||||
"player.seek": "Seek in episode",
|
||||
"footer.text": "Daily Spanish B1 news podcast · Delivered via Telegram.",
|
||||
},
|
||||
de: {
|
||||
@@ -80,6 +85,11 @@
|
||||
"modal.transcript": "Transkript",
|
||||
"modal.sources": "Quellen",
|
||||
"modal.download": "Audio herunterladen",
|
||||
"modal.downloading": "Wird geladen…",
|
||||
"modal.downloaded": "Gespeichert",
|
||||
"player.play": "Abspielen",
|
||||
"player.pause": "Pausieren",
|
||||
"player.seek": "Im Podcast spulen",
|
||||
"footer.text": "Täglicher Spanisch-B1-Nachrichten-Podcast · Geliefert über Telegram.",
|
||||
},
|
||||
es: {
|
||||
@@ -108,6 +118,11 @@
|
||||
"modal.transcript": "Transcripción",
|
||||
"modal.sources": "Fuentes",
|
||||
"modal.download": "Descargar audio",
|
||||
"modal.downloading": "Descargando…",
|
||||
"modal.downloaded": "Guardado",
|
||||
"player.play": "Reproducir",
|
||||
"player.pause": "Pausar",
|
||||
"player.seek": "Buscar en el episodio",
|
||||
"footer.text": "Podcast diario de noticias en español B1 · Enviado por Telegram.",
|
||||
},
|
||||
};
|
||||
@@ -160,6 +175,12 @@
|
||||
modalTitle: document.getElementById("modalTitle"),
|
||||
modalBody: document.getElementById("modalBody"),
|
||||
modalDownload: document.getElementById("modalDownload"),
|
||||
audio: document.getElementById("audio"),
|
||||
player: document.getElementById("player"),
|
||||
playerToggle: document.getElementById("playerToggle"),
|
||||
playerTrack: document.getElementById("playerTrack"),
|
||||
playerFill: document.getElementById("playerFill"),
|
||||
playerTime: document.getElementById("playerTime"),
|
||||
langButtons: Array.from(document.querySelectorAll(".lang-switch button")),
|
||||
};
|
||||
|
||||
@@ -283,6 +304,11 @@
|
||||
el.langButtons.forEach(function (btn) {
|
||||
btn.classList.toggle("is-active", btn.getAttribute("data-lang") === lang);
|
||||
});
|
||||
|
||||
if (el.playerToggle) {
|
||||
el.playerToggle.setAttribute("aria-label", el.audio.paused ? t("player.play") : t("player.pause"));
|
||||
}
|
||||
if (el.playerTrack) el.playerTrack.setAttribute("aria-label", t("player.seek"));
|
||||
}
|
||||
|
||||
function setLang(next) {
|
||||
@@ -433,8 +459,8 @@
|
||||
a.setAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
el.modalDownload.href = audioUrl(ep.date);
|
||||
el.modalDownload.setAttribute("download", "learn-and-prep-" + (ep.date || "episode") + ".mp3");
|
||||
el.modalDownload.dataset.url = audioUrl(ep.date);
|
||||
el.modalDownload.dataset.filename = "learn-and-prep-" + (ep.date || "episode") + ".mp3";
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
@@ -447,6 +473,7 @@
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
el.audio.pause();
|
||||
el.modal.hidden = true;
|
||||
document.body.style.overflow = "";
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
@@ -463,7 +490,10 @@
|
||||
el.modalDate.textContent = formatDate(date);
|
||||
el.modalTitle.textContent = "";
|
||||
el.modalBody.innerHTML = '<div class="modal__loading">' + escapeHtml(t("modal.loading")) + "</div>";
|
||||
el.modalDownload.href = audioUrl(date);
|
||||
setDownloadState("idle");
|
||||
el.modalDownload.dataset.url = audioUrl(date);
|
||||
el.modalDownload.dataset.filename = "learn-and-prep-" + date + ".mp3";
|
||||
setPlayerSource(audioUrl(date));
|
||||
|
||||
try {
|
||||
const payload = await fetchJson(detailUrl(date));
|
||||
@@ -482,6 +512,177 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
Audio player
|
||||
--------------------------------------------------------- */
|
||||
function formatTime(seconds) {
|
||||
if (!isFinite(seconds) || seconds < 0) seconds = 0;
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return m + ":" + (s < 10 ? "0" + s : String(s));
|
||||
}
|
||||
|
||||
function updatePlayerUI() {
|
||||
const duration = el.audio.duration;
|
||||
const current = el.audio.currentTime;
|
||||
const pct = isFinite(duration) && duration > 0 ? (current / duration) * 100 : 0;
|
||||
el.playerFill.style.width = pct + "%";
|
||||
el.playerTrack.setAttribute("aria-valuenow", String(Math.round(pct)));
|
||||
el.playerTime.textContent = formatTime(current) + " / " + formatTime(duration);
|
||||
}
|
||||
|
||||
function setPlayerSource(url) {
|
||||
el.audio.pause();
|
||||
el.player.classList.remove("is-playing");
|
||||
el.playerFill.style.width = "0%";
|
||||
el.playerTime.textContent = "0:00 / 0:00";
|
||||
el.playerToggle.setAttribute("aria-label", t("player.play"));
|
||||
if (url) {
|
||||
el.audio.src = url;
|
||||
} else {
|
||||
el.audio.removeAttribute("src");
|
||||
}
|
||||
el.audio.load();
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (!el.audio.getAttribute("src")) return;
|
||||
if (el.audio.paused) {
|
||||
const promise = el.audio.play();
|
||||
if (promise && typeof promise.catch === "function") promise.catch(function () {});
|
||||
} else {
|
||||
el.audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function seekToRatio(ratio) {
|
||||
const duration = el.audio.duration;
|
||||
if (!isFinite(duration) || duration <= 0) return;
|
||||
el.audio.currentTime = Math.min(1, Math.max(0, ratio)) * duration;
|
||||
updatePlayerUI();
|
||||
}
|
||||
|
||||
function seekFromPointer(e) {
|
||||
const rect = el.playerTrack.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
seekToRatio((e.clientX - rect.left) / rect.width);
|
||||
}
|
||||
|
||||
function bindPlayer() {
|
||||
el.playerToggle.addEventListener("click", togglePlay);
|
||||
|
||||
el.audio.addEventListener("play", function () {
|
||||
el.player.classList.add("is-playing");
|
||||
el.playerToggle.setAttribute("aria-label", t("player.pause"));
|
||||
});
|
||||
el.audio.addEventListener("pause", function () {
|
||||
el.player.classList.remove("is-playing");
|
||||
el.playerToggle.setAttribute("aria-label", t("player.play"));
|
||||
});
|
||||
el.audio.addEventListener("ended", function () {
|
||||
el.player.classList.remove("is-playing");
|
||||
el.audio.currentTime = 0;
|
||||
updatePlayerUI();
|
||||
});
|
||||
el.audio.addEventListener("timeupdate", updatePlayerUI);
|
||||
el.audio.addEventListener("loadedmetadata", updatePlayerUI);
|
||||
el.audio.addEventListener("durationchange", updatePlayerUI);
|
||||
|
||||
let dragging = false;
|
||||
el.playerTrack.addEventListener("pointerdown", function (e) {
|
||||
dragging = true;
|
||||
try {
|
||||
el.playerTrack.setPointerCapture(e.pointerId);
|
||||
} catch (err) {
|
||||
/* pointer capture unsupported — dragging still works while over the track */
|
||||
}
|
||||
seekFromPointer(e);
|
||||
});
|
||||
el.playerTrack.addEventListener("pointermove", function (e) {
|
||||
if (dragging) seekFromPointer(e);
|
||||
});
|
||||
el.playerTrack.addEventListener("pointerup", function () {
|
||||
dragging = false;
|
||||
});
|
||||
el.playerTrack.addEventListener("pointercancel", function () {
|
||||
dragging = false;
|
||||
});
|
||||
|
||||
el.playerTrack.addEventListener("keydown", function (e) {
|
||||
const duration = el.audio.duration || 0;
|
||||
if (e.key === "ArrowRight") {
|
||||
el.audio.currentTime = Math.min(duration, el.audio.currentTime + 5);
|
||||
updatePlayerUI();
|
||||
e.preventDefault();
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
el.audio.currentTime = Math.max(0, el.audio.currentTime - 5);
|
||||
updatePlayerUI();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
Background download
|
||||
--------------------------------------------------------- */
|
||||
let downloadResetTimer = null;
|
||||
|
||||
function downloadLabel() {
|
||||
return el.modalDownload.querySelector("span[data-i18n]");
|
||||
}
|
||||
|
||||
function setDownloadState(state) {
|
||||
clearTimeout(downloadResetTimer);
|
||||
const label = downloadLabel();
|
||||
el.modalDownload.classList.toggle("is-loading", state === "loading");
|
||||
el.modalDownload.disabled = state === "loading";
|
||||
|
||||
if (state === "loading") {
|
||||
if (label) label.textContent = t("modal.downloading");
|
||||
} else if (state === "done") {
|
||||
if (label) label.textContent = t("modal.downloaded");
|
||||
downloadResetTimer = setTimeout(function () {
|
||||
if (label) label.textContent = t("modal.download");
|
||||
}, 2500);
|
||||
} else if (label) {
|
||||
label.textContent = t("modal.download");
|
||||
}
|
||||
}
|
||||
|
||||
function triggerDownload(href, filename) {
|
||||
const a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.download = filename;
|
||||
a.rel = "noopener";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
async function downloadAudio() {
|
||||
const url = el.modalDownload.dataset.url;
|
||||
if (!url || el.modalDownload.disabled) return;
|
||||
const filename = el.modalDownload.dataset.filename || "episode.mp3";
|
||||
|
||||
setDownloadState("loading");
|
||||
try {
|
||||
const res = await fetch(url, { mode: "cors" });
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const blob = await res.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
triggerDownload(objectUrl, filename);
|
||||
setTimeout(function () {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}, 10000);
|
||||
setDownloadState("done");
|
||||
} catch (err) {
|
||||
// CORS or network failure: fall back to letting the browser fetch it directly
|
||||
triggerDownload(url, filename);
|
||||
setDownloadState("idle");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
Events
|
||||
--------------------------------------------------------- */
|
||||
@@ -502,6 +703,9 @@
|
||||
el.modal.addEventListener("click", function (e) {
|
||||
if (e.target.closest("[data-close]")) closeModal();
|
||||
});
|
||||
|
||||
el.modalDownload.addEventListener("click", downloadAudio);
|
||||
bindPlayer();
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user