function clock(s) {
s = Math.max(0, Math.floor(s || 0));
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
}
async function post(action, body = {}) {
try {
await fetch(`/api/music/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch {
// the next poll shows the real state
}
}
function button(label, action) {
const b = document.createElement("button");
b.textContent = label;
b.onclick = () => post(action);
return b;
}
export default {
title: "Music",
interval: 2,
mount(el) {
el.innerHTML = `
0:00
`;
el.querySelector(".controls").append(
button("⏮", "prev"), button("⏯", "play"), button("⏭", "next"),
button("Vol−", "voldown"), button("Mute", "mute"), button("Vol+", "volup"));
const seek = el.querySelector(".seek input");
seek.addEventListener("pointerdown", () => { el._dragging = true; });
seek.addEventListener("pointerup", () => { el._dragging = false; });
seek.addEventListener("change", () => post("seek", { position: Number(seek.value) }));
},
update(st, el) {
if (st.art) {
let img = el.querySelector("img.art");
if (!img) {
img = document.createElement("img");
img.className = "art";
img.alt = "";
el.querySelector(".art").replaceWith(img);
}
if (img.getAttribute("src") !== st.art) img.src = st.art;
}
el.querySelector(".title").textContent = st.title || "Nothing playing";
el.querySelector(".artist").textContent = st.artist || "";
el.querySelector(".album").textContent = st.album || "";
el.querySelector(".controls button:nth-child(2)").textContent =
st.status === "Playing" ? "⏸" : "▶";
const mute = el.querySelector(".controls button:nth-child(5)");
mute.classList.toggle("on", Boolean(st.volume?.muted));
mute.textContent = st.volume?.muted ? "Unmute" : "Mute";
const seek = el.querySelector(".seek input");
seek.max = Math.max(1, Math.floor(st.length || 0));
if (!el._dragging) seek.value = Math.min(Number(seek.max), Math.floor(st.position || 0));
el.querySelector(".time").textContent = `${clock(st.position)} / ${clock(st.length)}`;
},
};