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);
+}