1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
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 = `
<div class="top">
<div class="art empty">♪</div>
<div class="meta">
<div class="title">Nothing playing</div>
<div class="artist muted"></div>
<div class="album muted"></div>
</div>
</div>
<div class="seek">
<input type="range" min="0" max="1" step="1" value="0" aria-label="Seek">
<span class="time num">0:00</span>
</div>
<div class="controls"></div>`;
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)}`;
},
};
|