This commit is contained in:
Ondrej Zara 2019-03-28 20:28:55 +01:00
parent 5c9be9ceac
commit 5417037db6
13 changed files with 129 additions and 83 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules node_modules
_youtube

View File

@ -9,7 +9,7 @@ html {
body { body {
box-sizing: border-box; box-sizing: border-box;
font-family: lato, sans-serif; font-family: lato, sans-serif;
line-height: 1.3; line-height: 1;
background-color: #333; background-color: #333;
color: #fff; color: #fff;
text-shadow: 0 1px 1px #000; text-shadow: 0 1px 1px #000;
@ -75,12 +75,12 @@ nav ul li {
text-align: center; text-align: center;
flex: 1 0 0; flex: 1 0 0;
line-height: 40px; line-height: 40px;
} cursor: pointer;
nav ul li:hover { border-top: 4px solid transparent;
background-color: red; border-bottom: 4px solid transparent;
} }
nav ul li.active { nav ul li.active {
background-color: green; border-top-color: dodgerblue;
} }
#player { #player {
display: flex; display: flex;
@ -133,13 +133,13 @@ nav ul li.active {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
} }
.component .grid li h2 { .component .grid li h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -171,13 +171,13 @@ nav ul li.active {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
} }
#queue .grid li h2 { #queue .grid li h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -212,13 +212,13 @@ nav ul li.active {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
} }
#library .grid li h2 { #library .grid li h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -250,13 +250,13 @@ nav ul li.active {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
} }
#fs .grid li h2 { #fs .grid li h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -288,13 +288,13 @@ nav ul li.active {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
} }
#playlists .grid li h2 { #playlists .grid li h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }

View File

@ -7,7 +7,7 @@ html {
body { body {
box-sizing: border-box; box-sizing: border-box;
font-family: lato, sans-serif; font-family: lato, sans-serif;
line-height: 1.3; line-height: 1;
background-color: #333; background-color: #333;
color: #fff; color: #fff;
text-shadow: 0 1px 1px #000; text-shadow: 0 1px 1px #000;

View File

@ -17,13 +17,13 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 4px; padding: 0 4px;
white-space: nowrap;
h2 { h2 {
flex-grow: 1; flex-grow: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }

View File

@ -9,11 +9,13 @@ nav ul {
text-align: center; text-align: center;
flex: 1 0 0; flex: 1 0 0;
line-height: 40px; line-height: 40px;
cursor: pointer;
&:hover { background-color:red;} border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
&.active { &.active {
background-color: green; border-top-color: dodgerblue;
} }
} }
} }

View File

@ -28,8 +28,8 @@
<main> <main>
<section id="queue"> <section id="queue">
<header> <header>
<button class="clear"></button> <button class="clear" title="Clear the queue"></button>
<button class="save"></button> <button class="save" title="Save the queue"></button>
</header> </header>
<ul class="grid"></ul> <ul class="grid"></ul>
</section> </section>

View File

@ -24,13 +24,13 @@ function buildHeader(path) {
function buildDirectory(data, parent) { function buildDirectory(data, parent) {
let path = data["directory"]; let path = data["directory"];
let name = path.split("/").pop(); let name = path.split("/").pop();
let node = ui.group(ui.GROUP_DIRECTORY, name, path, parent); let node = ui.group(ui.CTX_FS, name, path, parent);
node.addEventListener("click", e => list(path)); node.addEventListener("click", e => list(path));
return node; return node;
} }
function buildFile(data, parent) { function buildFile(data, parent) {
return ui.song(ui.SONG_FILE, data, parent); return ui.song(ui.CTX_FS, data, parent);
} }
function buildResults(results) { function buildResults(results) {

View File

@ -46,7 +46,7 @@ function serializeFilter(filter) {
return `"${escape(filterStr)}"`; return `"${escape(filterStr)}"`;
} }
function escape(str) { export function escape(str) {
return str.replace(/(['"\\])/g, "\\$1"); return str.replace(/(['"\\])/g, "\\$1");
} }
@ -86,13 +86,9 @@ export async function listPlaylists() {
return (list instanceof Array ? list : [list]); return (list instanceof Array ? list : [list]);
} }
export async function enqueue(urlOrFilter, sort = null) { export async function enqueueByFilter(filter, sort = null) {
if (typeof(urlOrFilter) == "string") {
return command(`add "${escape(urlOrFilter)}"`);
}
let tokens = ["findadd"]; let tokens = ["findadd"];
tokens.push(serializeFilter(urlOrFilter)); tokens.push(serializeFilter(filter));
// sort && tokens.push("sort", sort); FIXME not implemented in MPD // sort && tokens.push("sort", sort); FIXME not implemented in MPD
return command(tokens.join(" ")); return command(tokens.join(" "));
} }
@ -133,12 +129,9 @@ export async function albumArt(songUrl) {
if (data.length >= Number(metadata["size"])) { return data; } if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]); offset += Number(metadata["binary"]);
} }
return null;
} }
export async function save(name) {
return command(`save "${escape(name)}"`);
}
export async function init() { export async function init() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {

View File

@ -4,29 +4,40 @@ import * as pubsub from "./pubsub.js";
import * as format from "./format.js"; import * as format from "./format.js";
import * as player from "../player.js"; import * as player from "../player.js";
export const SONG_FILE = 1; export const CTX_FS = 1;
export const SONG_LIBRARY = 2; export const CTX_QUEUE = 2;
export const SONG_QUEUE = 3; export const CTX_LIBRARY = 3;
export const GROUP_DIRECTORY = 4;
export const GROUP_LIBRARY = 5; const TYPE_ID = 1;
const TYPE_URL = 2;
const TYPE_FILTER = 3;
const TYPE_PLAYLIST = 4;
const SORT = "-Track"; const SORT = "-Track";
function enqueue(type, what) {
switch (type) {
case TYPE_URL: return mpd.command(`add "${mpd.escape(what)}"`); break;
case TYPE_FILTER: return mpd.enqueueByFilter(what, SORT); break;
case TYPE_PLAYLIST: return mpd.command(`load "${mpd.escape(what)}"`); break;
}
}
function fileName(data) { function fileName(data) {
return data["file"].split("/").pop(); return data["file"].split("/").pop();
} }
function formatTitle(type, data) { function formatTitle(ctx, data) {
switch (type) { switch (ctx) {
case SONG_FILE: case CTX_FS:
return `🎵 ${fileName(data)}`; return `🎵 ${fileName(data)}`;
break; break;
case SONG_LIBRARY: case CTX_LIBRARY:
return data["Artist"] || fileName(data); return data["Artist"] || fileName(data);
break; break;
case SONG_QUEUE: case CTX_QUEUE:
let tokens = []; let tokens = [];
data["Artist"] && tokens.push(data["Artist"]); data["Artist"] && tokens.push(data["Artist"]);
data["Title"] && tokens.push(data["Title"]); data["Title"] && tokens.push(data["Title"]);
@ -36,77 +47,97 @@ function formatTitle(type, data) {
} }
} }
function playButton(id, parent) { function playButton(type, what, parent) {
let button = html.button({icon:"play", title:"Play"}, "", parent); let button = html.button({icon:"play", title:"Play"}, "", parent);
button.addEventListener("click", async e => { button.addEventListener("click", async e => {
await mpd.command(`playid ${id}`); if (type == TYPE_ID) {
await mpd.command(`playid ${what}`);
} else {
await mpd.command("clear");
await enqueue(type, what);
await mpd.command("play");
pubsub.publish("queue-change");
}
player.update(); player.update();
}); });
return button;
} }
function deleteButton(id, parent) { function deleteButton(type, id, parent) {
let button = html.button({icon:"close", title:"Delete from queue"}, "", parent); let title;
switch (type) {
case TYPE_ID: title = "Delete from queue"; break;
case TYPE_PLAYLIST: title = "Delete playlist"; break;
}
let button = html.button({icon:"close", title}, "", parent);
button.addEventListener("click", async e => { button.addEventListener("click", async e => {
await mpd.command(`deleteid ${id}`); switch (type) {
pubsub.publish("queue-change"); case TYPE_ID:
await mpd.command(`deleteid ${id}`);
pubsub.publish("queue-change");
return;
case TYPE_PLAYLIST:
let ok = confirm(`Really delete playlist '${id}'?`);
if (!ok) { return; }
await mpd.command(`rm "${mpd.escape(id)}"`);
pubsub.publish("playlists-change");
return;
}
}); });
return button; return button;
} }
function addAndPlayButton(urlOrFilter, parent) { function addButton(type, what, parent) {
let button = html.button({icon:"play", title:"Play"}, "", parent);
button.addEventListener("click", async e => {
e.stopPropagation();
await mpd.command("clear");
await mpd.enqueue(urlOrFilter, SORT);
await mpd.command("play");
pubsub.publish("queue-change");
player.update();
});
return button;
}
function addButton(urlOrFilter, parent) {
let button = html.button({icon:"plus", title:"Add to queue"}, "", parent); let button = html.button({icon:"plus", title:"Add to queue"}, "", parent);
button.addEventListener("click", async e => { button.addEventListener("click", async e => {
e.stopPropagation(); e.stopPropagation();
await mpd.enqueue(urlOrFilter, SORT); await enqueue(type, what);
pubsub.publish("queue-change"); pubsub.publish("queue-change");
// fixme notification? // fixme notification?
}); });
return button; return button;
} }
export function song(type, data, parent) { export function song(ctx, data, parent) {
let node = html.node("li", {}, "", parent); let node = html.node("li", {}, "", parent);
let title = formatTitle(type, data); let title = formatTitle(ctx, data);
html.node("h2", {}, title, node); html.node("h2", {}, title, node);
html.node("span", {className:"duration"}, format.time(Number(data["duration"])), node); html.node("span", {className:"duration"}, format.time(Number(data["duration"])), node);
if (type == SONG_QUEUE) { switch (ctx) {
let id = data["Id"]; case CTX_QUEUE:
node.dataset.songId = id; let id = data["Id"];
playButton(id, node); node.dataset.songId = id;
deleteButton(id, node); playButton(TYPE_ID, id, node);
} else { deleteButton(TYPE_ID, id, node);
let url = data["file"]; break;
addAndPlayButton(url, node);
addButton(url, node); case CTX_FS:
let url = data["file"];
playButton(TYPE_URL, url, node);
addButton(TYPE_URL, url, node);
break;
} }
return node; return node;
} }
export function group(type, label, urlOrFilter, parent) { export function group(ctx, label, urlOrFilter, parent) {
let node = html.node("li", {}, "", parent); let node = html.node("li", {}, "", parent);
if (type == GROUP_DIRECTORY) { label = `📁 ${label}`; } if (ctx == CTX_FS) { label = `📁 ${label}`; }
html.node("h2", {}, label, node); html.node("h2", {}, label, node);
addAndPlayButton(urlOrFilter, node); let type = (ctx == CTX_FS ? TYPE_URL : TYPE_FILTER);
addButton(urlOrFilter, node);
playButton(type, urlOrFilter, node);
addButton(type, urlOrFilter, node);
return node; return node;
} }
@ -117,9 +148,9 @@ export function playlist(name, parent) {
html.icon("playlist-music", node) html.icon("playlist-music", node)
html.node("h2", {}, name, node); html.node("h2", {}, name, node);
// addAndPlayButton(url, node); playButton(TYPE_PLAYLIST, name, node);
// addButton(url, node); addButton(TYPE_PLAYLIST, name, node);
// deleteButton(id, node); deleteButton(TYPE_PLAYLIST, name, node);
return node; return node;
} }

View File

@ -29,14 +29,14 @@ function buildHeader(filter) {
function buildAlbum(album, filter, parent) { function buildAlbum(album, filter, parent) {
let childFilter = Object.assign({}, filter, {"Album": album}); let childFilter = Object.assign({}, filter, {"Album": album});
let node = ui.group(ui.GROUP_LIBRARY, album, childFilter, parent); let node = ui.group(ui.CTX_LIBRARY, album, childFilter, parent);
node.addEventListener("click", e => listSongs(childFilter)); node.addEventListener("click", e => listSongs(childFilter));
return node; return node;
} }
function buildArtist(artist, filter, parent) { function buildArtist(artist, filter, parent) {
let childFilter = Object.assign({}, filter, {"Artist": artist}); let childFilter = Object.assign({}, filter, {"Artist": artist});
let node = ui.group(ui.GROUP_LIBRARY, artist, childFilter, parent); let node = ui.group(ui.CTX_LIBRARY, artist, childFilter, parent);
node.addEventListener("click", e => listAlbums(childFilter)); node.addEventListener("click", e => listAlbums(childFilter));
return node; return node;
} }
@ -47,7 +47,7 @@ function buildSongs(songs, filter) {
ul.classList.remove("tiles"); ul.classList.remove("tiles");
ul.classList.add("grid"); ul.classList.add("grid");
songs.map(song => ui.song(ui.SONG_LIBRARY, song, ul)); songs.map(song => ui.song(ui.CTX_LIBRARY, song, ul));
} }
function buildAlbums(albums, filter) { function buildAlbums(albums, filter) {

View File

@ -17,10 +17,15 @@ async function syncLists() {
buildLists(lists); buildLists(lists);
} }
function onPlaylistsChange(message, publisher, data) {
syncLists();
}
export async function activate() { export async function activate() {
syncLists(); syncLists();
} }
export function init(n) { export function init(n) {
node = n; node = n;
pubsub.subscribe("playlists-change", onPlaylistsChange);
} }

View File

@ -17,7 +17,7 @@ function buildSongs(songs) {
let ul = node.querySelector("ul"); let ul = node.querySelector("ul");
html.clear(ul); html.clear(ul);
songs.map(song => ui.song(ui.SONG_QUEUE, song, ul)); songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
updateCurrent(); updateCurrent();
} }
@ -59,6 +59,6 @@ export function init(n) {
save.addEventListener("click", e => { save.addEventListener("click", e => {
let name = prompt("Save current queue as a playlist?", "name"); let name = prompt("Save current queue as a playlist?", "name");
if (name === null) { return; } if (name === null) { return; }
mpd.save(name); mpd.command(`save "${mpd.escape(name)}"`);
}); });
} }

14
app/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"lib": ["es2017", "dom"],
"target": "es6",
"baseUrl": "js/app",
"noImplicitReturns": true,
"strictFunctionTypes": true
},
"files": ["js/app.js"]
}