aboutsummaryrefslogtreecommitdiffstats
path: root/static/modules/music.js
blob: b78a4011823134edfad2472c3f36fec71749435c (plain)
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// deck-it - music panel
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

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="music">
        <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>
      </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)}`;
  },
};