This commit is contained in:
oliver committed 2026-09-25 13:59:25 -03:00
1 parent 86547731c8
commit ce5eae7bce
3 files changed
+298 -8

No files matched your search

+207 -3
View File
@@ -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();
}
/* ---------------------------------------------------------