app/enhance/podcast.ts
94 lines and 13 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| duration | 12 to 13 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| position | 21 to 21 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| paint | 23 to 31 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| paintButton | 33 to 35 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| seekTo | 42 to 67 | 5 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:51> | 51 to 55 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:73> | 73 to 76 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:74> | 74 to 74 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:79> | 79 to 79 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:82> | 82 to 85 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:86> | 86 to 89 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:91> | 91 to 95 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:100> | 100 to 103 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/lib/podcast/clock.mjs. No module of this repo imports it.
Source
import { clockTime } from "~/lib/podcast/clock.mjs";
for (const root of document.querySelectorAll<HTMLElement>("[data-podcast]")) {
const audio = root.querySelector<HTMLAudioElement>(".podcast-audio");
const controls = root.querySelector<HTMLElement>("[data-podcast-controls]");
const play = root.querySelector<HTMLButtonElement>("[data-podcast-play]");
const seek = root.querySelector<HTMLInputElement>("[data-podcast-seek]");
const elapsed = root.querySelector<HTMLElement>("[data-podcast-elapsed]");
const total = root.querySelector<HTMLElement>("[data-podcast-total]");
if (!audio || !controls || !play || !seek || !elapsed || !total) continue;
const duration = () =>
Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : Number(seek.max) || 0;
let dragging = false;
/**
* A seek asked for before the metadata has loaded. The element cannot take a position yet, so the
* slider and clock show this one instead of snapping back to 0:00, and a second skip adds to it.
*/
let pending: number | null = null;
const position = () => pending ?? audio.currentTime;
const paint = () => {
const now = position();
if (!dragging) seek.value = String(Math.floor(now));
elapsed.textContent = clockTime(dragging ? Number(seek.value) : now);
seek.setAttribute(
"aria-valuetext",
`${clockTime(dragging ? Number(seek.value) : now)} of ${clockTime(duration())}`,
);
};
const paintButton = () => {
play.textContent = audio.paused ? "Play" : "Pause";
};
/**
* A seek before the metadata has loaded is held and applied once it has. `preload="none"` means
* nothing is fetched until asked, so the first held seek asks for the metadata; a load already
* under way (a Play just pressed) is left alone, since `load()` would abort it.
*/
const seekTo = (seconds: number) => {
const target = Math.min(Math.max(0, seconds), duration() || seconds);
if (audio.readyState >= 1) {
pending = null;
audio.currentTime = target;
} else {
if (pending === null) {
audio.addEventListener(
"loadedmetadata",
() => {
if (pending !== null) audio.currentTime = pending;
pending = null;
paint();
},
{ once: true },
);
if (audio.networkState !== audio.NETWORK_LOADING) {
audio.preload = "metadata";
audio.load();
}
}
pending = target;
}
seek.value = String(Math.floor(target));
paint();
};
audio.controls = false;
controls.hidden = false;
paint();
play.addEventListener("click", () => {
if (audio.paused) void audio.play().catch(() => paintButton());
else audio.pause();
});
for (const button of root.querySelectorAll<HTMLButtonElement>("[data-podcast-skip]")) {
const delta = Number(button.dataset.podcastSkip);
button.addEventListener("click", () => seekTo(position() + delta));
}
seek.addEventListener("input", () => {
dragging = true;
paint();
});
seek.addEventListener("change", () => {
dragging = false;
seekTo(Number(seek.value));
});
audio.addEventListener("loadedmetadata", () => {
seek.max = String(Math.floor(duration()));
total.textContent = clockTime(duration());
paint();
});
audio.addEventListener("timeupdate", paint);
audio.addEventListener("play", paintButton);
audio.addEventListener("pause", paintButton);
audio.addEventListener("ended", paintButton);
audio.addEventListener("error", () => {
controls.hidden = true;
audio.controls = true;
});
}