diff --git a/app/Makefile b/app/Makefile index 8ea2bd8..725e23a 100644 --- a/app/Makefile +++ b/app/Makefile @@ -5,3 +5,6 @@ all: $(APP) $(APP): css/* $(LESS) css/app.less > $@ + +watch: all + while inotifywait -e MODIFY -r css js ; do make $^ ; done diff --git a/app/app.css b/app/app.css index 8750517..bbf0b8c 100644 --- a/app/app.css +++ b/app/app.css @@ -24,13 +24,19 @@ nav ul li { nav ul li:hover { background-color: red; } +nav ul li.active { + background-color: green; +} #player:not([data-state=play]) .pause { display: none; } #player[data-state=play] .play { display: none; } -#player:not(.random) .random, -#player:not(.repeat) .repeat { +#player:not([data-flags~=random]) .random, +#player:not([data-flags~=repeat]) .repeat { opacity: 0.5; } +#queue .current { + font-weight: bold; +} diff --git a/app/css/app.less b/app/css/app.less index 7bac648..9f3e342 100644 --- a/app/css/app.less +++ b/app/css/app.less @@ -8,3 +8,4 @@ body { @import "main.less"; @import "nav.less"; @import "player.less"; +@import "queue.less"; diff --git a/app/css/nav.less b/app/css/nav.less index 7e653fc..3c84192 100644 --- a/app/css/nav.less +++ b/app/css/nav.less @@ -11,5 +11,9 @@ nav ul { line-height: 40px; &:hover { background-color:red;} + + &.active { + background-color: green; + } } } diff --git a/app/css/player.less b/app/css/player.less index eac23f6..5799312 100644 --- a/app/css/player.less +++ b/app/css/player.less @@ -2,5 +2,6 @@ &:not([data-state=play]) .pause { display: none; } &[data-state=play] .play { display: none; } - &:not(.random) .random, &:not(.repeat) .repeat { opacity: 0.5; } + + &:not([data-flags~=random]) .random, &:not([data-flags~=repeat]) .repeat { opacity: 0.5; } } \ No newline at end of file diff --git a/app/css/queue.less b/app/css/queue.less new file mode 100644 index 0000000..2ed3a17 --- /dev/null +++ b/app/css/queue.less @@ -0,0 +1,3 @@ +#queue { + .current { font-weight: bold; } +} \ No newline at end of file diff --git a/app/index.html b/app/index.html index 29d15f1..69e33e4 100644 --- a/app/index.html +++ b/app/index.html @@ -23,25 +23,18 @@
- main
- main
- main
- main
- main
- main
- main
- main
- main
- main
- main
+
+
+
+
diff --git a/app/js/app.js b/app/js/app.js index 494d942..cea3180 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -1,11 +1,37 @@ -import * as mpd from "./mpd.js"; +import * as nav from "./nav.js"; +import * as mpd from "./lib/mpd.js"; import * as player from "./player.js"; -import * as art from "./art.js"; + +import * as queue from "./queue.js"; + +const components = { queue }; + +export function activate(what) { + for (let id in components) { + let node = document.querySelector(`#${id}`); + if (what == id) { + node.style.display = ""; + components[id].activate(); + } else { + node.style.display = "none"; + } + } + nav.active(what); +} async function init() { + nav.init(document.querySelector("nav")); + for (let id in components) { + let node = document.querySelector(`#${id}`); + components[id].init(node); + } + await mpd.init(); - player.init(); + player.init(document.querySelector("#player")); + + activate("queue"); window.mpd = mpd; } + init(); diff --git a/app/js/art.js b/app/js/lib/art.js similarity index 90% rename from app/js/art.js rename to app/js/lib/art.js index 6ed3c2b..0e8e767 100644 --- a/app/js/art.js +++ b/app/js/lib/art.js @@ -1,5 +1,6 @@ import * as mpd from "./mpd.js"; import * as parser from "./parser.js"; +import * as html from "./html.js"; let cache = {}; const SIZE = 64; @@ -29,17 +30,15 @@ async function getImageData(songUrl) { async function bytesToImage(bytes) { let blob = new Blob([bytes]); - let image = document.createElement("img"); - image.src = URL.createObjectURL(blob); + let src = URL.createObjectURL(blob); + let image = html.node("img", {src}); return new Promise(resolve => { image.onload = () => resolve(image); }); } function resize(image) { - let canvas = document.createElement("canvas"); - canvas.width = SIZE; - canvas.height = SIZE; + let canvas = html.node("canvas", {width:SIZE, height:SIZE}); let ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, SIZE, SIZE); return canvas; diff --git a/app/js/lib/format.js b/app/js/lib/format.js new file mode 100644 index 0000000..d739aa1 --- /dev/null +++ b/app/js/lib/format.js @@ -0,0 +1,6 @@ +export function time(sec) { + sec = Math.round(sec); + let m = Math.floor(sec / 60); + let s = sec % 60; + return `${m}:${s.toString().padStart(2, "0")}`; +} diff --git a/app/js/lib/html.js b/app/js/lib/html.js new file mode 100644 index 0000000..50bd354 --- /dev/null +++ b/app/js/lib/html.js @@ -0,0 +1,29 @@ +export function node(name, attrs, content, parent) { + let n = document.createElement(name); + Object.assign(n, attrs); + + if (attrs && attrs.title) { n.setAttribute("aria-label", attrs.title); } + + content && text(content, n); + parent && parent.appendChild(n); + return n; +} + +export function button(attrs, content, parent) { + return node("button", attrs, content, parent); +} + +export function clear(node) { + while (node.firstChild) { node.firstChild.parentNode.removeChild(node.firstChild); } + return node; +} + +export function text(txt, parent) { + let n = document.createTextNode(txt); + parent && parent.appendChild(n); + return n; +} + +export function fragment() { + return document.createDocumentFragment(); +} diff --git a/app/js/mpd.js b/app/js/lib/mpd.js similarity index 92% rename from app/js/mpd.js rename to app/js/lib/mpd.js index 7f6ba28..fc1adea 100644 --- a/app/js/mpd.js +++ b/app/js/lib/mpd.js @@ -57,6 +57,11 @@ export async function status() { return parser.linesToStruct(lines); } +export async function listQueue() { + let lines = await command("playlistinfo"); + return parser.songList(lines); +} + export async function init() { return new Promise((resolve, reject) => { try { diff --git a/app/js/lib/parser.js b/app/js/lib/parser.js new file mode 100644 index 0000000..be08a9e --- /dev/null +++ b/app/js/lib/parser.js @@ -0,0 +1,29 @@ +export function linesToStruct(lines) { + let result = {}; + lines.forEach(line => { + let cindex = line.indexOf(":"); + if (cindex == -1) { throw new Error(`Malformed line "${line}"`); } + result[line.substring(0, cindex)] = line.substring(cindex+2); + }); + return result; +} + +export function songList(lines) { + let songs = []; + let batch = []; + while (lines.length) { + let line = lines[0]; + if (line.startsWith("file:") && batch.length) { + let song = linesToStruct(batch); + songs.push(song); + } + batch.push(lines.shift()); + } + + if (batch.length) { + let song = linesToStruct(batch); + songs.push(song); + } + + return songs; +} \ No newline at end of file diff --git a/app/js/lib/pubsub.js b/app/js/lib/pubsub.js new file mode 100644 index 0000000..92854aa --- /dev/null +++ b/app/js/lib/pubsub.js @@ -0,0 +1,17 @@ +let storage = new Map(); + +export function publish(message, publisher, data) { + console.log(message, publisher, data); + if (!storage.has(message)) { return; } + storage.get(message).forEach(listener => listener(message, publisher, data)); +} + +export function subscribe(message, listener) { + if (!storage.has(message)) { storage.set(message, new Set()); } + storage.get(message).add(listener); +} + +export function unsubscribe(message, listener) { + if (!storage.has(message)) { storage.set(message, new Set()); } + storage.get(message).remove(listener); +} diff --git a/app/js/nav.js b/app/js/nav.js new file mode 100644 index 0000000..fb888c6 --- /dev/null +++ b/app/js/nav.js @@ -0,0 +1,16 @@ +import * as app from "./app.js"; + +let tabs = []; + +export function init(node) { + tabs = Array.from(node.querySelectorAll("[data-for]")); + tabs.forEach(tab => { + tab.addEventListener("click", e => app.activate(tab.dataset.for)); + }); +} + +export function active(id) { + tabs.forEach(tab => { + tab.classList.toggle("active", tab.dataset.for == id); + }); +} diff --git a/app/js/parser.js b/app/js/parser.js deleted file mode 100644 index d8c1620..0000000 --- a/app/js/parser.js +++ /dev/null @@ -1,10 +0,0 @@ -export function linesToStruct(lines) { - let result = {}; - lines.forEach(line => { - let cindex = line.indexOf(":"); - if (cindex == -1) { throw new Error(`Malformed line "${line}"`); } - result[line.substring(0, cindex)] = line.substring(cindex+2); - }); - return result; -} - diff --git a/app/js/player.js b/app/js/player.js index 1104e58..a540766 100644 --- a/app/js/player.js +++ b/app/js/player.js @@ -1,5 +1,8 @@ -import * as mpd from "./mpd.js"; -import * as art from "./art.js"; +import * as mpd from "./lib/mpd.js"; +import * as art from "./lib/art.js"; +import * as html from "./lib/html.js"; +import * as format from "./lib/format.js"; +import * as pubsub from "./lib/pubsub.js"; const DELAY = 2000; const DOM = {}; @@ -8,48 +11,38 @@ let current = {}; let node; let idleTimeout = null; -function formatTime(sec) { - sec = Math.round(sec); - let m = Math.floor(sec / 60); - let s = sec % 60; - return `${m}:${s.toString().padStart(2, "0")}`; -} - -function update(data) { - DOM.elapsed.textContent = formatTime(Number(data["elapsed"] || 0)); // changed time +function sync(data) { + DOM.elapsed.textContent = format.time(Number(data["elapsed"] || 0)); // changed time if (data["file"] != current["file"]) { // changed song - DOM.duration.textContent = formatTime(Number(data["duration"] || 0)); + DOM.duration.textContent = format.time(Number(data["duration"] || 0)); DOM.title.textContent = data["Title"] || ""; DOM.album.textContent = data["Album"] || ""; DOM.artist.textContent = data["Artist"] || ""; + pubsub.publish("song-change", null, data); } if (data["Artist"] != current["Artist"] || data["Album"] != current["Album"]) { // changed album (art) DOM.art.innerHTML = ""; art.get(data["Artist"], data["Album"], data["file"]).then(src => { if (!src) { return; } - let image = document.createElement("img"); - image.src = src; + let image = html.node("img", {src}); DOM.art.appendChild(image); }); } - node.classList.toggle("random", data["random"] == "1"); - node.classList.toggle("repeat", data["repeat"] == "1"); + let flags = []; + if (data["random"] == "1") { flags.push("random"); } + if (data["repeat"] == "1") { flags.push("repeat"); } + node.dataset.flags = flags.join(" "); + node.dataset.state = data["state"]; current = data; } -async function sync() { - let data = await mpd.status(); - update(data); - idle(); -} - function idle() { - idleTimeout = setTimeout(sync, DELAY); + idleTimeout = setTimeout(update, DELAY); } function clearIdle() { @@ -60,12 +53,19 @@ function clearIdle() { async function command(cmd) { clearIdle(); let data = await mpd.commandAndStatus(cmd); - update(data); + sync(data); idle(); } -export function init() { - node = document.querySelector("#player"); +export async function update() { + clearIdle(); + let data = await mpd.status(); + sync(data); + idle(); +} + +export function init(n) { + node = n; let all = node.querySelectorAll("[class]"); Array.from(all).forEach(node => DOM[node.className] = node); @@ -77,5 +77,5 @@ export function init() { DOM.random.addEventListener("click", e => command(`random ${current["random"] == "1" ? "0" : "1"}`)); DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`)); - sync(); + update(); } diff --git a/app/js/queue.js b/app/js/queue.js new file mode 100644 index 0000000..928a9bd --- /dev/null +++ b/app/js/queue.js @@ -0,0 +1,57 @@ +import * as mpd from "./lib/mpd.js"; +import * as html from "./lib/html.js"; +import * as player from "./player.js"; +import * as pubsub from "./lib/pubsub.js"; + +let node; +let currentId; + +function updateCurrent() { + let all = Array.from(node.querySelectorAll("[data-song-id]")); + all.forEach(node => { + node.classList.toggle("current", node.dataset.songId == currentId); + }); +} + +async function playSong(id) { + await mpd.command(`playid ${id}`); + player.update(); +} + +function buildSong(song) { + let id = Number(song["Id"]); + + let node = html.node("li"); + node.dataset.songId = id; + + node.textContent = song["file"]; + let play = html.button({}, "▶", node); + play.addEventListener("click", e => playSong(id)); + + return node; +} + +function buildSongs(songs) { + html.clear(node); + + let ul = html.node("ul"); + songs.map(buildSong).forEach(li => ul.appendChild(li)); + + node.appendChild(ul); + updateCurrent(); +} + +function onSongChange(message, publisher, data) { + currentId = data["Id"]; + updateCurrent(); +} + +export async function activate() { + let songs = await mpd.listQueue(); + buildSongs(songs); +} + +export function init(n) { + node = n; + pubsub.subscribe("song-change", onSongChange); +}