diff --git a/.gitignore b/.gitignore index 3c3629e..4028752 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +_youtube diff --git a/app/app.css b/app/app.css index eec9112..1deaf11 100644 --- a/app/app.css +++ b/app/app.css @@ -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; } diff --git a/app/css/app.less b/app/css/app.less index 91f728c..1f3eb26 100644 --- a/app/css/app.less +++ b/app/css/app.less @@ -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; diff --git a/app/css/component.less b/app/css/component.less index 2947bd8..523a60b 100644 --- a/app/css/component.less +++ b/app/css/component.less @@ -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; } diff --git a/app/css/nav.less b/app/css/nav.less index be71238..41c289c 100644 --- a/app/css/nav.less +++ b/app/css/nav.less @@ -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; } } } diff --git a/app/index.html b/app/index.html index f486add..63788d8 100644 --- a/app/index.html +++ b/app/index.html @@ -28,8 +28,8 @@
- - + +
diff --git a/app/js/fs.js b/app/js/fs.js index f9e6459..2e9756e 100644 --- a/app/js/fs.js +++ b/app/js/fs.js @@ -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) { diff --git a/app/js/lib/mpd.js b/app/js/lib/mpd.js index c8b9f5f..c74606b 100644 --- a/app/js/lib/mpd.js +++ b/app/js/lib/mpd.js @@ -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 { diff --git a/app/js/lib/ui.js b/app/js/lib/ui.js index 0a12976..d368a56 100644 --- a/app/js/lib/ui.js +++ b/app/js/lib/ui.js @@ -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; } diff --git a/app/js/library.js b/app/js/library.js index 2b4a5e0..4494d3e 100644 --- a/app/js/library.js +++ b/app/js/library.js @@ -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) { diff --git a/app/js/playlists.js b/app/js/playlists.js index f4187a8..00ed1a5 100644 --- a/app/js/playlists.js +++ b/app/js/playlists.js @@ -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); } diff --git a/app/js/queue.js b/app/js/queue.js index 2d9e13b..b8c06dc 100644 --- a/app/js/queue.js +++ b/app/js/queue.js @@ -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)}"`); }); } diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..68860e8 --- /dev/null +++ b/app/tsconfig.json @@ -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"] +}