From 5c9be9ceac1a2871d484ae67acbfd535a6858369 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Thu, 28 Mar 2019 15:23:28 +0100 Subject: [PATCH] playlists --- app/app.css | 102 ++++++++++++++++++++++++++++------- app/css/app.less | 12 +---- app/css/component.less | 13 +++-- app/css/icons.less | 10 ++++ app/css/player.less | 7 ++- app/css/playlists.less | 3 ++ app/icons/content-save.svg | 1 + app/icons/playlist-music.svg | 1 + app/index.html | 9 +++- app/js/app.js | 3 +- app/js/lib/icons.js | 74 +++++++++++++------------ app/js/lib/mpd.js | 13 +++++ app/js/lib/ui.js | 19 +++++-- app/js/playlists.js | 26 +++++++++ app/js/queue.js | 15 ++++++ 15 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 app/css/icons.less create mode 100644 app/css/playlists.less create mode 100644 app/icons/content-save.svg create mode 100644 app/icons/playlist-music.svg create mode 100644 app/js/playlists.js diff --git a/app/app.css b/app/app.css index 9c80f2a..eec9112 100644 --- a/app/app.css +++ b/app/app.css @@ -39,14 +39,6 @@ button { line-height: 1; cursor: pointer; } -.icon { - width: 24px; -} -.icon path:not([fill]), -.icon polygon:not([fill]), -.icon circle:not([fill]) { - fill: currentColor; -} @font-face { font-family: 'Lato'; src: url('font/LatoLatin-Regular.woff2') format('woff2'); @@ -59,6 +51,15 @@ button { font-style: bold; font-weight: normal; } +.icon { + vertical-align: middle; + width: 24px; +} +.icon path:not([fill]), +.icon polygon:not([fill]), +.icon circle:not([fill]) { + fill: currentColor; +} main { flex-grow: 1; overflow: hidden; @@ -90,6 +91,9 @@ nav ul li.active { } #player .info { flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } #player:not([data-state=play]) .pause { display: none; @@ -128,6 +132,7 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; + padding: 0 4px; } .component .grid li h2 { flex-grow: 1; @@ -138,12 +143,17 @@ nav ul li.active { overflow: hidden; text-overflow: ellipsis; } +.component .grid li button { + flex-shrink: 0; +} +@media (pointer: coarse) { + .component .grid li button .icon { + width: 32px; + } +} .component .grid li:nth-child(odd) { background-color: #555; } -.component .grid button { - flex-shrink: 0; -} #queue { height: 100%; display: flex; @@ -160,6 +170,7 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; + padding: 0 4px; } #queue .grid li h2 { flex-grow: 1; @@ -170,12 +181,17 @@ nav ul li.active { overflow: hidden; text-overflow: ellipsis; } +#queue .grid li button { + flex-shrink: 0; +} +@media (pointer: coarse) { + #queue .grid li button .icon { + width: 32px; + } +} #queue .grid li:nth-child(odd) { background-color: #555; } -#queue .grid button { - flex-shrink: 0; -} #queue .current { font-weight: bold; } @@ -195,6 +211,7 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; + padding: 0 4px; } #library .grid li h2 { flex-grow: 1; @@ -205,12 +222,17 @@ nav ul li.active { overflow: hidden; text-overflow: ellipsis; } +#library .grid li button { + flex-shrink: 0; +} +@media (pointer: coarse) { + #library .grid li button .icon { + width: 32px; + } +} #library .grid li:nth-child(odd) { background-color: #555; } -#library .grid button { - flex-shrink: 0; -} #fs { height: 100%; display: flex; @@ -227,6 +249,7 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; + padding: 0 4px; } #fs .grid li h2 { flex-grow: 1; @@ -237,9 +260,52 @@ nav ul li.active { overflow: hidden; text-overflow: ellipsis; } +#fs .grid li button { + flex-shrink: 0; +} +@media (pointer: coarse) { + #fs .grid li button .icon { + width: 32px; + } +} #fs .grid li:nth-child(odd) { background-color: #555; } -#fs .grid button { +#playlists { + height: 100%; + display: flex; + flex-direction: column; +} +#playlists ul { + flex-grow: 1; + overflow: auto; + list-style: none; + margin: 0; + padding: 0; +} +#playlists .grid li { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 4px; +} +#playlists .grid li h2 { + flex-grow: 1; + font-size: 100%; + font-weight: normal; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#playlists .grid li button { flex-shrink: 0; } +@media (pointer: coarse) { + #playlists .grid li button .icon { + width: 32px; + } +} +#playlists .grid li:nth-child(odd) { + background-color: #555; +} diff --git a/app/css/app.less b/app/css/app.less index aa265e3..91f728c 100644 --- a/app/css/app.less +++ b/app/css/app.less @@ -39,18 +39,9 @@ button { cursor: pointer; } -.icon { - width: 24px; - - path, polygon, circle { - &:not([fill]) { - fill: currentColor; - } - } -} - @import "font.less"; +@import "icons.less"; @import "main.less"; @import "nav.less"; @import "player.less"; @@ -58,3 +49,4 @@ button { @import "queue.less"; @import "library.less"; @import "fs.less"; +@import "playlists.less"; diff --git a/app/css/component.less b/app/css/component.less index a6f8dfd..2947bd8 100644 --- a/app/css/component.less +++ b/app/css/component.less @@ -16,6 +16,7 @@ display: flex; flex-direction: row; align-items: center; + padding: 0 4px; h2 { flex-grow: 1; @@ -26,14 +27,18 @@ overflow: hidden; text-overflow: ellipsis; } + + button { + flex-shrink: 0; + + @media (pointer: coarse) { + .icon { width: 32px; } + } + } } li:nth-child(odd) { background-color: #555; } - - button { - flex-shrink: 0; - } } } diff --git a/app/css/icons.less b/app/css/icons.less new file mode 100644 index 0000000..cdd93b6 --- /dev/null +++ b/app/css/icons.less @@ -0,0 +1,10 @@ +.icon { + vertical-align: middle; + width: 24px; + + path, polygon, circle { + &:not([fill]) { + fill: currentColor; + } + } +} diff --git a/app/css/player.less b/app/css/player.less index 43d8659..da1d729 100644 --- a/app/css/player.less +++ b/app/css/player.less @@ -3,7 +3,12 @@ flex-direction: row; .art img { vertical-align: top; } - .info { flex-grow: 1; } + .info { + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } &:not([data-state=play]) .pause { display: none; } &[data-state=play] .play { display: none; } diff --git a/app/css/playlists.less b/app/css/playlists.less new file mode 100644 index 0000000..c691763 --- /dev/null +++ b/app/css/playlists.less @@ -0,0 +1,3 @@ +#playlists { + .component; +} \ No newline at end of file diff --git a/app/icons/content-save.svg b/app/icons/content-save.svg new file mode 100644 index 0000000..bbd8d59 --- /dev/null +++ b/app/icons/content-save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/playlist-music.svg b/app/icons/playlist-music.svg new file mode 100644 index 0000000..255ca83 --- /dev/null +++ b/app/icons/playlist-music.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/index.html b/app/index.html index 532d60b..f486add 100644 --- a/app/index.html +++ b/app/index.html @@ -27,10 +27,15 @@
-
+
+ + +
+
    +
    +
      -
        diff --git a/app/js/app.js b/app/js/app.js index f2bc719..9ddc754 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -5,8 +5,9 @@ import * as player from "./player.js"; import * as queue from "./queue.js"; import * as library from "./library.js"; import * as fs from "./fs.js"; +import * as playlists from "./playlists.js"; -const components = { queue, library, fs }; +const components = { queue, library, fs, playlists }; export function activate(what) { for (let id in components) { diff --git a/app/js/lib/icons.js b/app/js/lib/icons.js index f09a849..8d71c07 100644 --- a/app/js/lib/icons.js +++ b/app/js/lib/icons.js @@ -1,58 +1,64 @@ let ICONS={}; -ICONS["play-circle-outline"] = ` - -`; -ICONS["shuffle"] = ` - -`; -ICONS["minus-circle"] = ` - +ICONS["pause"] = ` + `; ICONS["rewind"] = ` `; -ICONS["pause"] = ` - -`; -ICONS["pause-circle"] = ` - -`; -ICONS["close-circle-outline"] = ` - -`; -ICONS["close"] = ` - -`; -ICONS["minus"] = ` - -`; -ICONS["close-circle"] = ` - -`; -ICONS["repeat"] = ` - -`; ICONS["play"] = ` `; -ICONS["pause-circle-outline"] = ` - +ICONS["play-circle-outline"] = ` + `; ICONS["plus"] = ` `; +ICONS["close-circle"] = ` + +`; +ICONS["minus"] = ` + +`; +ICONS["shuffle"] = ` + +`; +ICONS["content-save"] = ` + +`; ICONS["fast-forward"] = ` `; +ICONS["close"] = ` + +`; +ICONS["pause-circle"] = ` + +`; +ICONS["minus-circle"] = ` + +`; +ICONS["plus-circle"] = ` + +`; ICONS["minus-circle-outline"] = ` `; +ICONS["playlist-music"] = ` + +`; +ICONS["repeat"] = ` + +`; +ICONS["close-circle-outline"] = ` + +`; +ICONS["pause-circle-outline"] = ` + +`; ICONS["plus-circle-outline"] = ` `; -ICONS["plus-circle"] = ` - -`; ICONS["play-circle"] = ` `; diff --git a/app/js/lib/mpd.js b/app/js/lib/mpd.js index 5dd706b..c8b9f5f 100644 --- a/app/js/lib/mpd.js +++ b/app/js/lib/mpd.js @@ -77,6 +77,15 @@ export async function listQueue() { return parser.songList(lines); } +export async function listPlaylists() { + let lines = await command("listplaylists"); + let parsed = parser.linesToStruct(lines); + + let list = parsed["playlist"]; + if (!list) { return []; } + return (list instanceof Array ? list : [list]); +} + export async function enqueue(urlOrFilter, sort = null) { if (typeof(urlOrFilter) == "string") { return command(`add "${escape(urlOrFilter)}"`); @@ -126,6 +135,10 @@ export async function albumArt(songUrl) { } } +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 75488b8..0a12976 100644 --- a/app/js/lib/ui.js +++ b/app/js/lib/ui.js @@ -45,7 +45,7 @@ function playButton(id, parent) { } function deleteButton(id, parent) { - let button = html.button({icon:"close"}, "", parent); + let button = html.button({icon:"close", title:"Delete from queue"}, "", parent); button.addEventListener("click", async e => { await mpd.command(`deleteid ${id}`); pubsub.publish("queue-change"); @@ -67,7 +67,7 @@ function addAndPlayButton(urlOrFilter, parent) { } function addButton(urlOrFilter, parent) { - let button = html.button({icon:"plus"}, "", parent); + let button = html.button({icon:"plus", title:"Add to queue"}, "", parent); button.addEventListener("click", async e => { e.stopPropagation(); await mpd.enqueue(urlOrFilter, SORT); @@ -109,4 +109,17 @@ export function group(type, label, urlOrFilter, parent) { addButton(urlOrFilter, node); return node; -} \ No newline at end of file +} + +export function playlist(name, parent) { + let node = html.node("li", {}, "", parent); + + html.icon("playlist-music", node) + html.node("h2", {}, name, node); + +// addAndPlayButton(url, node); +// addButton(url, node); +// deleteButton(id, node); + + return node; +} diff --git a/app/js/playlists.js b/app/js/playlists.js new file mode 100644 index 0000000..f4187a8 --- /dev/null +++ b/app/js/playlists.js @@ -0,0 +1,26 @@ +import * as mpd from "./lib/mpd.js"; +import * as html from "./lib/html.js"; +import * as pubsub from "./lib/pubsub.js"; +import * as ui from "./lib/ui.js"; + +let node; + +function buildLists(lists) { + let ul = node.querySelector("ul"); + html.clear(ul); + + lists.map(list => ui.playlist(list, ul)); +} + +async function syncLists() { + let lists = await mpd.listPlaylists(); + buildLists(lists); +} + +export async function activate() { + syncLists(); +} + +export function init(n) { + node = n; +} diff --git a/app/js/queue.js b/app/js/queue.js index 7dcaa66..2d9e13b 100644 --- a/app/js/queue.js +++ b/app/js/queue.js @@ -46,4 +46,19 @@ export function init(n) { syncQueue(); pubsub.subscribe("song-change", onSongChange); pubsub.subscribe("queue-change", onQueueChange); + + let clear = node.querySelector(".clear"); + clear.appendChild(html.icon("close")); + clear.addEventListener("click", async e => { + await mpd.command("clear"); + syncQueue(); + }); + + let save = node.querySelector(".save"); + save.appendChild(html.icon("content-save")); + save.addEventListener("click", e => { + let name = prompt("Save current queue as a playlist?", "name"); + if (name === null) { return; } + mpd.save(name); + }); }