queue
This commit is contained in:
parent
ecbb7e3d97
commit
76802bc630
18 changed files with 248 additions and 63 deletions
|
@ -5,3 +5,6 @@ all: $(APP)
|
||||||
|
|
||||||
$(APP): css/*
|
$(APP): css/*
|
||||||
$(LESS) css/app.less > $@
|
$(LESS) css/app.less > $@
|
||||||
|
|
||||||
|
watch: all
|
||||||
|
while inotifywait -e MODIFY -r css js ; do make $^ ; done
|
||||||
|
|
10
app/app.css
10
app/app.css
|
@ -24,13 +24,19 @@ nav ul li {
|
||||||
nav ul li:hover {
|
nav ul li:hover {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
nav ul li.active {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
#player:not([data-state=play]) .pause {
|
#player:not([data-state=play]) .pause {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#player[data-state=play] .play {
|
#player[data-state=play] .play {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#player:not(.random) .random,
|
#player:not([data-flags~=random]) .random,
|
||||||
#player:not(.repeat) .repeat {
|
#player:not([data-flags~=repeat]) .repeat {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
#queue .current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
|
@ -8,3 +8,4 @@ body {
|
||||||
@import "main.less";
|
@import "main.less";
|
||||||
@import "nav.less";
|
@import "nav.less";
|
||||||
@import "player.less";
|
@import "player.less";
|
||||||
|
@import "queue.less";
|
||||||
|
|
|
@ -11,5 +11,9 @@ nav ul {
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
|
|
||||||
&:hover { background-color:red;}
|
&:hover { background-color:red;}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
&:not([data-state=play]) .pause { display: none; }
|
&:not([data-state=play]) .pause { display: none; }
|
||||||
&[data-state=play] .play { 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; }
|
||||||
}
|
}
|
3
app/css/queue.less
Normal file
3
app/css/queue.less
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#queue {
|
||||||
|
.current { font-weight: bold; }
|
||||||
|
}
|
|
@ -23,25 +23,18 @@
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
main<br/>
|
<section id="queue"></section>
|
||||||
main<br/>
|
<section id="playlists"></section>
|
||||||
main<br/>
|
<section id="library"></section>
|
||||||
main<br/>
|
<section id="misc"></section>
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
main<br/>
|
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Q</li>
|
<li data-for="queue">Q</li>
|
||||||
<li>Playlists</li>
|
<li data-for="playlists">Playlists</li>
|
||||||
<li>Library</li>
|
<li data-for="library">Library</li>
|
||||||
<li>Misc</li>
|
<li data-for="misc">Misc</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -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 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() {
|
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();
|
await mpd.init();
|
||||||
player.init();
|
player.init(document.querySelector("#player"));
|
||||||
|
|
||||||
|
activate("queue");
|
||||||
window.mpd = mpd;
|
window.mpd = mpd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as mpd from "./mpd.js";
|
import * as mpd from "./mpd.js";
|
||||||
import * as parser from "./parser.js";
|
import * as parser from "./parser.js";
|
||||||
|
import * as html from "./html.js";
|
||||||
|
|
||||||
let cache = {};
|
let cache = {};
|
||||||
const SIZE = 64;
|
const SIZE = 64;
|
||||||
|
@ -29,17 +30,15 @@ async function getImageData(songUrl) {
|
||||||
|
|
||||||
async function bytesToImage(bytes) {
|
async function bytesToImage(bytes) {
|
||||||
let blob = new Blob([bytes]);
|
let blob = new Blob([bytes]);
|
||||||
let image = document.createElement("img");
|
let src = URL.createObjectURL(blob);
|
||||||
image.src = URL.createObjectURL(blob);
|
let image = html.node("img", {src});
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
image.onload = () => resolve(image);
|
image.onload = () => resolve(image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resize(image) {
|
function resize(image) {
|
||||||
let canvas = document.createElement("canvas");
|
let canvas = html.node("canvas", {width:SIZE, height:SIZE});
|
||||||
canvas.width = SIZE;
|
|
||||||
canvas.height = SIZE;
|
|
||||||
let ctx = canvas.getContext("2d");
|
let ctx = canvas.getContext("2d");
|
||||||
ctx.drawImage(image, 0, 0, SIZE, SIZE);
|
ctx.drawImage(image, 0, 0, SIZE, SIZE);
|
||||||
return canvas;
|
return canvas;
|
6
app/js/lib/format.js
Normal file
6
app/js/lib/format.js
Normal file
|
@ -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")}`;
|
||||||
|
}
|
29
app/js/lib/html.js
Normal file
29
app/js/lib/html.js
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -57,6 +57,11 @@ export async function status() {
|
||||||
return parser.linesToStruct(lines);
|
return parser.linesToStruct(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listQueue() {
|
||||||
|
let lines = await command("playlistinfo");
|
||||||
|
return parser.songList(lines);
|
||||||
|
}
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
29
app/js/lib/parser.js
Normal file
29
app/js/lib/parser.js
Normal file
|
@ -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;
|
||||||
|
}
|
17
app/js/lib/pubsub.js
Normal file
17
app/js/lib/pubsub.js
Normal file
|
@ -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);
|
||||||
|
}
|
16
app/js/nav.js
Normal file
16
app/js/nav.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import * as mpd from "./mpd.js";
|
import * as mpd from "./lib/mpd.js";
|
||||||
import * as art from "./art.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 DELAY = 2000;
|
||||||
const DOM = {};
|
const DOM = {};
|
||||||
|
@ -8,48 +11,38 @@ let current = {};
|
||||||
let node;
|
let node;
|
||||||
let idleTimeout = null;
|
let idleTimeout = null;
|
||||||
|
|
||||||
function formatTime(sec) {
|
function sync(data) {
|
||||||
sec = Math.round(sec);
|
DOM.elapsed.textContent = format.time(Number(data["elapsed"] || 0)); // changed time
|
||||||
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
|
|
||||||
|
|
||||||
if (data["file"] != current["file"]) { // changed song
|
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.title.textContent = data["Title"] || "";
|
||||||
DOM.album.textContent = data["Album"] || "";
|
DOM.album.textContent = data["Album"] || "";
|
||||||
DOM.artist.textContent = data["Artist"] || "";
|
DOM.artist.textContent = data["Artist"] || "";
|
||||||
|
pubsub.publish("song-change", null, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data["Artist"] != current["Artist"] || data["Album"] != current["Album"]) { // changed album (art)
|
if (data["Artist"] != current["Artist"] || data["Album"] != current["Album"]) { // changed album (art)
|
||||||
DOM.art.innerHTML = "";
|
DOM.art.innerHTML = "";
|
||||||
art.get(data["Artist"], data["Album"], data["file"]).then(src => {
|
art.get(data["Artist"], data["Album"], data["file"]).then(src => {
|
||||||
if (!src) { return; }
|
if (!src) { return; }
|
||||||
let image = document.createElement("img");
|
let image = html.node("img", {src});
|
||||||
image.src = src;
|
|
||||||
DOM.art.appendChild(image);
|
DOM.art.appendChild(image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
node.classList.toggle("random", data["random"] == "1");
|
let flags = [];
|
||||||
node.classList.toggle("repeat", data["repeat"] == "1");
|
if (data["random"] == "1") { flags.push("random"); }
|
||||||
|
if (data["repeat"] == "1") { flags.push("repeat"); }
|
||||||
|
node.dataset.flags = flags.join(" ");
|
||||||
|
|
||||||
node.dataset.state = data["state"];
|
node.dataset.state = data["state"];
|
||||||
|
|
||||||
current = data;
|
current = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sync() {
|
|
||||||
let data = await mpd.status();
|
|
||||||
update(data);
|
|
||||||
idle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function idle() {
|
function idle() {
|
||||||
idleTimeout = setTimeout(sync, DELAY);
|
idleTimeout = setTimeout(update, DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearIdle() {
|
function clearIdle() {
|
||||||
|
@ -60,12 +53,19 @@ function clearIdle() {
|
||||||
async function command(cmd) {
|
async function command(cmd) {
|
||||||
clearIdle();
|
clearIdle();
|
||||||
let data = await mpd.commandAndStatus(cmd);
|
let data = await mpd.commandAndStatus(cmd);
|
||||||
update(data);
|
sync(data);
|
||||||
idle();
|
idle();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init() {
|
export async function update() {
|
||||||
node = document.querySelector("#player");
|
clearIdle();
|
||||||
|
let data = await mpd.status();
|
||||||
|
sync(data);
|
||||||
|
idle();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init(n) {
|
||||||
|
node = n;
|
||||||
let all = node.querySelectorAll("[class]");
|
let all = node.querySelectorAll("[class]");
|
||||||
Array.from(all).forEach(node => DOM[node.className] = node);
|
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.random.addEventListener("click", e => command(`random ${current["random"] == "1" ? "0" : "1"}`));
|
||||||
DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`));
|
DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`));
|
||||||
|
|
||||||
sync();
|
update();
|
||||||
}
|
}
|
||||||
|
|
57
app/js/queue.js
Normal file
57
app/js/queue.js
Normal file
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue