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
_youtube

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,13 +24,13 @@ function buildHeader(path) {
function buildDirectory(data, parent) {
let path = data["directory"];
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));
return node;
}
function buildFile(data, parent) {
return ui.song(ui.SONG_FILE, data, parent);
return ui.song(ui.CTX_FS, data, parent);
}
function buildResults(results) {

View file

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

View file

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

View file

@ -29,14 +29,14 @@ function buildHeader(filter) {
function buildAlbum(album, filter, parent) {
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));
return node;
}
function buildArtist(artist, filter, parent) {
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));
return node;
}
@ -47,7 +47,7 @@ function buildSongs(songs, filter) {
ul.classList.remove("tiles");
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) {

View file

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

View file

@ -17,7 +17,7 @@ function buildSongs(songs) {
let ul = node.querySelector("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();
}
@ -59,6 +59,6 @@ export function init(n) {
save.addEventListener("click", e => {
let name = prompt("Save current queue as a playlist?", "name");
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"]
}