diff --git a/app/app.css b/app/app.css index 4451f80..48888ef 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; + line-height: 1.25; background-color: var(--bg); color: var(--fg); text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8); @@ -28,6 +28,7 @@ input, select, button { color: inherit; + font: inherit; } button { -webkit-appearance: none; @@ -43,6 +44,19 @@ button { .art img { vertical-align: top; } +.long-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.multiline { + display: flex; + flex-direction: row; + align-items: center; +} +.multiline h2 { + font-weight: normal; +} @font-face { font-family: 'Lato'; src: url('font/LatoLatin-Regular.woff2') format('woff2'); @@ -104,8 +118,19 @@ nav ul li.active { #player:not([data-flags~=repeat]) .repeat { opacity: 0.5; } +#player .art { + margin-right: var(--icon-spacing); +} +#player h2 { + font-size: 125%; + margin-top: 0; +} #player .info { flex-grow: 1; + overflow: hidden; +} +#player .title, +#player .subtitle { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -115,15 +140,17 @@ nav ul li.active { text-align: center; } #player .controls .icon { - width: 64px; + width: 32px; + margin: 8px; } #player .misc { display: flex; flex-direction: column; - width: 48px; + align-self: stretch; + justify-content: space-around; } #player .misc .icon { - width: 48px; + width: 32px; } .component { height: 100%; @@ -141,28 +168,33 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -.component h2 { +.component li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +.component li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +.component li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +.component li .info h2, +.component li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.component h2 .icon { - margin-right: 4px; - color: var(--primary); +.component li:not(.has-art) { + padding: 8px; } .component li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - .component .icon { - width: 32px; - } +.component button .icon { + width: 32px; } #queue { height: 100%; @@ -180,31 +212,36 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -#queue h2 { +#queue li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +#queue li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +#queue li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +#queue li .info h2, +#queue li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#queue h2 .icon { - margin-right: 4px; - color: var(--primary); +#queue li:not(.has-art) { + padding: 8px; } #queue li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - #queue .icon { - width: 32px; - } +#queue button .icon { + width: 32px; } -#queue .current * { - font-weight: bold !important; +#queue .current { + color: var(--primary); } #library { height: 100%; @@ -222,31 +259,44 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -#library h2 { +#library li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +#library li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +#library li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +#library li .info h2, +#library li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#library h2 .icon { - margin-right: 4px; - color: var(--primary); +#library li:not(.has-art) { + padding: 8px; } #library li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - #library .icon { - width: 32px; - } +#library button .icon { + width: 32px; } -#library .art img { +#library .art img, +#library .art .icon { width: 64px; + margin-right: var(--icon-spacing); +} +#library .group { + cursor: pointer; +} +#library .group h2 { + font-weight: normal; } #library .tiles { display: grid; @@ -279,32 +329,45 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -#fs h2 { +#fs li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +#fs li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +#fs li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +#fs li .info h2, +#fs li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#fs h2 .icon { - margin-right: 4px; - color: var(--primary); +#fs li:not(.has-art) { + padding: 8px; } #fs li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - #fs .icon { - width: 32px; - } +#fs button .icon { + width: 32px; } #fs .group { cursor: pointer; } +#fs .info { + display: flex; + flex-direction: row; + align-items: center; +} +#fs .info h2 { + font-weight: normal; +} #playlists { height: 100%; display: flex; @@ -321,28 +384,41 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -#playlists h2 { +#playlists li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +#playlists li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +#playlists li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +#playlists li .info h2, +#playlists li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#playlists h2 .icon { - margin-right: 4px; - color: var(--primary); +#playlists li:not(.has-art) { + padding: 8px; } #playlists li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - #playlists .icon { - width: 32px; - } +#playlists button .icon { + width: 32px; +} +#playlists .info { + display: flex; + flex-direction: row; + align-items: center; +} +#playlists .info h2 { + font-weight: normal; } #yt { height: 100%; @@ -362,28 +438,33 @@ nav ul li.active { display: flex; flex-direction: row; align-items: center; - white-space: nowrap; } -#yt h2 { +#yt li .info { flex-grow: 1; - font-size: 100%; - font-weight: normal; + overflow: hidden; +} +#yt li .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); +} +#yt li .info h2 { + font-size: var(--font-size-large); margin: 0; - margin-left: 4px; +} +#yt li .info h2, +#yt li .info div { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#yt h2 .icon { - margin-right: 4px; - color: var(--primary); +#yt li:not(.has-art) { + padding: 8px; } #yt li:nth-child(odd) { background-color: #555; } -@media (pointer: coarse) { - #yt .icon { - width: 32px; - } +#yt button .icon { + width: 32px; } #yt .go { width: 96px; @@ -416,8 +497,25 @@ nav ul li.active { transform: rotate(360deg); } } +.search .icon { + width: 32px; +} +.search input { + border: none; + color: inherit; + background-color: inherit; + border-bottom: 1px solid var(--fg); + transition: all 300ms; + padding: 0; + width: 30%; +} +.search:not(.open) input { + width: 0; +} :root { --primary: dodgerblue; --fg: #fff; --bg: #333; + --font-size-large: 112.5%; + --icon-spacing: 4px; } diff --git a/app/css/app.less b/app/css/app.less index 4655ea4..66b53d4 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; + line-height: 1.25; background-color: var(--bg); color: var(--fg); text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8); @@ -25,6 +25,7 @@ body { input, select, button { color: inherit; + font: inherit; } button { @@ -44,6 +45,20 @@ button { vertical-align: top; } +.long-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.multiline { + display: flex; + flex-direction: row; + align-items: center; + + h2 { font-weight: normal; } +} + @import "font.less"; @import "icons.less"; @import "main.less"; @@ -55,4 +70,5 @@ button { @import "fs.less"; @import "playlists.less"; @import "yt.less"; +@import "search.less"; @import "variables.less"; diff --git a/app/css/component.less b/app/css/component.less index d29b9d9..fc5d3e8 100644 --- a/app/css/component.less +++ b/app/css/component.less @@ -15,21 +15,30 @@ display: flex; flex-direction: row; align-items: center; - white-space: nowrap; - } - h2 { - flex-grow: 1; - font-size: 100%; - font-weight: normal; - margin: 0; - margin-left: 4px; - overflow: hidden; - text-overflow: ellipsis; + .info { + flex-grow: 1; + overflow: hidden; - .icon { - margin-right: 4px; - color: var(--primary); + .icon { + color: var(--primary); + margin-right: var(--icon-spacing); + } + + h2 { + font-size: var(--font-size-large); + margin: 0; + } + + h2, div { .long-line; } + } + + &.has-art { + + } + + &:not(.has-art) { + padding: 8px; } } @@ -37,7 +46,6 @@ background-color: #555; } - @media (pointer: coarse) { - .icon { width: 32px; } - } + button .icon { width: 32px; } + } diff --git a/app/css/fs.less b/app/css/fs.less index 166ba0b..0f8c32d 100644 --- a/app/css/fs.less +++ b/app/css/fs.less @@ -4,4 +4,8 @@ .group { cursor: pointer; } + + .info { + .multiline; + } } diff --git a/app/css/library.less b/app/css/library.less index 8ff9bd0..b1e0d53 100644 --- a/app/css/library.less +++ b/app/css/library.less @@ -1,8 +1,14 @@ #library { .component; - .art img { + .art img, .art .icon { width: 64px; + margin-right: var(--icon-spacing); + } + + .group { + cursor: pointer; + h2 { font-weight: normal; } } .tiles { diff --git a/app/css/player.less b/app/css/player.less index f1e4ee0..d1adc1b 100644 --- a/app/css/player.less +++ b/app/css/player.less @@ -7,25 +7,37 @@ &[data-state=play] .play { display: none; } &:not([data-flags~=random]) .random, &:not([data-flags~=repeat]) .repeat { opacity: 0.5; } + .art { + margin-right: var(--icon-spacing); + } + + h2 { + font-size: 125%; + margin-top: 0; + } + .info { flex-grow: 1; - white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; } + .title, .subtitle { .long-line; } + .controls { white-space: nowrap; text-align: center; - .icon { width: 64px; } + .icon { + width: 32px; + margin: 8px; + } } .misc { display: flex; flex-direction: column; - @size: 96px / 2; - width: @size; - .icon { width: @size; } + align-self: stretch; + justify-content: space-around; + .icon { width: 32px; } } } \ No newline at end of file diff --git a/app/css/playlists.less b/app/css/playlists.less index c691763..7a6cfbf 100644 --- a/app/css/playlists.less +++ b/app/css/playlists.less @@ -1,3 +1,7 @@ #playlists { .component; + + .info { + .multiline; + } } \ No newline at end of file diff --git a/app/css/queue.less b/app/css/queue.less index f168575..f8f39ef 100644 --- a/app/css/queue.less +++ b/app/css/queue.less @@ -1,4 +1,5 @@ #queue { .component; - .current * { font-weight: bold !important; } + + .current { color: var(--primary); } } diff --git a/app/css/search.less b/app/css/search.less new file mode 100644 index 0000000..bc3b04a --- /dev/null +++ b/app/css/search.less @@ -0,0 +1,21 @@ +.search { + .icon { + width: 32px; + } + + input { + border: none; + color: inherit; + background-color: inherit; + border-bottom: 1px solid var(--fg); + transition: all 300ms; + padding: 0; + width: 30%; + } + + &:not(.open) { + input { + width: 0; + } + } +} \ No newline at end of file diff --git a/app/css/variables.less b/app/css/variables.less index 1bd6d65..ccc5172 100644 --- a/app/css/variables.less +++ b/app/css/variables.less @@ -2,4 +2,7 @@ --primary: dodgerblue; --fg: #fff; --bg: #333; + + --font-size-large: 112.5%; + --icon-spacing: 4px; } diff --git a/app/icons/account-multiple.svg b/app/icons/account-multiple.svg new file mode 100644 index 0000000..134818a --- /dev/null +++ b/app/icons/account-multiple.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/album.svg b/app/icons/album.svg new file mode 100644 index 0000000..2fe18d9 --- /dev/null +++ b/app/icons/album.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/artist.svg b/app/icons/artist.svg new file mode 100644 index 0000000..a0bd1e8 --- /dev/null +++ b/app/icons/artist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/index.html b/app/index.html index 14c91ca..1e2c3b5 100644 --- a/app/index.html +++ b/app/index.html @@ -12,7 +12,7 @@

- +
diff --git a/app/js/lib/format.js b/app/js/lib/format.js index 499508f..b9fb645 100644 --- a/app/js/lib/format.js +++ b/app/js/lib/format.js @@ -1,3 +1,5 @@ +const SEPARATOR = " · "; + export function time(sec) { sec = Math.round(sec); let m = Math.floor(sec / 60); @@ -5,9 +7,10 @@ export function time(sec) { return `${m}:${s.toString().padStart(2, "0")}`; } -export function artistAlbum(artist, album) { +export function subtitle(data) { let tokens = []; - artist && tokens.push(artist); - album && tokens.push(album); - return tokens.join(" – "); + data["Artist"] && tokens.push(data["Artist"]); + data["Album"] && tokens.push(data["Album"]); + data["duration"] && tokens.push(time(Number(data["duration"]))); + return tokens.join(SEPARATOR); } \ No newline at end of file diff --git a/app/js/lib/icons.js b/app/js/lib/icons.js index 7003642..8fda23a 100644 --- a/app/js/lib/icons.js +++ b/app/js/lib/icons.js @@ -17,6 +17,9 @@ ICONS["play"] = ` `; +ICONS["album"] = ` + +`; ICONS["plus"] = ` `; @@ -32,6 +35,9 @@ ICONS["shuffle"] = ` `; +ICONS["account-multiple"] = ` + +`; ICONS["fast-forward"] = ` `; @@ -77,6 +83,9 @@ ICONS["plus-circle-outline"] = ` `; +ICONS["artist"] = ` + +`; ICONS["play-circle"] = ` `; diff --git a/app/js/lib/search.js b/app/js/lib/search.js index eee9ca0..a57d08b 100644 --- a/app/js/lib/search.js +++ b/app/js/lib/search.js @@ -1,5 +1,7 @@ import * as html from "./html.js"; +const OPEN = "open"; + export function normalize(str) { // FIXME diac/translit return str.toLowerCase(); @@ -8,13 +10,27 @@ export function normalize(str) { export default class Search extends EventTarget { constructor(parent) { super(); - this._node = html.node("div", {className:"search"}); - let icon = html.icon("magnify", this._node); + this._node = html.node("label", {className:"search"}); + + this._input = html.node("input", {type:"text"}, "", this._node); + html.icon("magnify", this._node); + + this._node.addEventListener("click", e => { + if (e.target == this._input) { return; } + if (this._node.classList.contains(OPEN)) { + this.reset() + } else { + this._node.classList.add(OPEN); + } + }); } getNode() { return this._node; } getValue() { return this._input.value; } - reset() { this._input.value = ""; } + reset() { + this._input.value = ""; + this._node.classList.remove(OPEN); + } } diff --git a/app/js/lib/ui.js b/app/js/lib/ui.js index 71a6cb5..f89398f 100644 --- a/app/js/lib/ui.js +++ b/app/js/lib/ui.js @@ -42,7 +42,7 @@ async function fillArt(parent, filter) { if (src) { html.node("img", {src}, "", parent); } else { - html.icon("music", parent); + html.icon(album ? "album" : "artist", parent); } } @@ -50,25 +50,29 @@ function fileName(data) { return data["file"].split("/").pop(); } -function formatTitle(ctx, data) { +function formatSongInfo(ctx, data) { + let lines = []; let tokens = []; switch (ctx) { - case CTX_FS: return fileName(data); break; + case CTX_FS: lines.push(fileName(data)); break; case CTX_LIBRARY: - data["Track"] && tokens.push(data["Track"].padStart(2, "0")); - data["Title"] && tokens.push(data["Title"]); - if (!tokens.length) {tokens.push(fileName(data)); } - return tokens.join(" "); - break; - case CTX_QUEUE: - data["Artist"] && tokens.push(data["Artist"]); - data["Title"] && tokens.push(data["Title"]); - if (!tokens.length) { tokens.push(fileName(data)); } - return tokens.join(" - "); + if (data["Title"]) { + if (ctx == CTX_LIBRARY && data["Track"]) { + tokens.push(data["Track"].padStart(2, "0")); + } + tokens.push(data["Title"]); + lines.push(tokens.join(" ")); + lines.push(format.subtitle(data)); + } else { + lines.push(fileName(data)); + lines.push("\u00A0"); + } break; } + + return lines; } function playButton(type, what, parent) { @@ -128,13 +132,14 @@ function addButton(type, what, parent) { export function song(ctx, data, parent) { let node = html.node("li", {className:"song"}, "", parent); + let info = html.node("div", {className:"info"}, "", node); - let title = formatTitle(ctx, data); - let h2 = html.node("h2", {}, "", node); - if (ctx == CTX_FS || ctx == CTX_LIBRARY) { html.icon("music", h2); } - html.text(title, h2); + if (ctx == CTX_FS) { html.icon("music", info); } + + let lines = formatSongInfo(ctx, data); + html.node("h2", {}, lines.shift(), info); + lines.length && html.node("div", {}, lines.shift(), info); - html.node("span", {className:"duration"}, format.time(Number(data["duration"])), node); switch (ctx) { case CTX_QUEUE: @@ -159,14 +164,14 @@ export function group(ctx, label, urlOrFilter, parent) { let node = html.node("li", {className:"group"}, "", parent); if (ctx == CTX_LIBRARY) { + node.classList.add("has-art"); let art = html.node("span", {className:"art"}, "", node); fillArt(art, urlOrFilter); } - let h2 = html.node("h2", {}, "", node); - if (ctx == CTX_FS) { html.icon("folder", h2); } - html.text(label, h2); - + let info = html.node("span", {className:"info"}, "", node); + if (ctx == CTX_FS) { html.icon("folder", info); } + html.node("h2", {}, label, info); let type = (ctx == CTX_FS ? TYPE_URL : TYPE_FILTER); @@ -179,9 +184,9 @@ export function group(ctx, label, urlOrFilter, parent) { export function playlist(name, parent) { let node = html.node("li", {}, "", parent); - let h2 = html.node("h2", {}, "", node); - html.icon("playlist-music", h2) - html.text(name, h2); + let info = html.node("span", {className:"info"}, "", node); + html.icon("playlist-music", info) + html.node("h2", {}, name, info); playButton(TYPE_PLAYLIST, name, node); addButton(TYPE_PLAYLIST, name, node); diff --git a/app/js/player.js b/app/js/player.js index 400ba65..0cc59f5 100644 --- a/app/js/player.js +++ b/app/js/player.js @@ -18,11 +18,11 @@ function sync(data) { if (data["file"]) { // playing at all? DOM.duration.textContent = format.time(Number(data["duration"] || 0)); DOM.title.textContent = data["Title"] || data["file"].split("/").pop(); - DOM["artist-album"].textContent = format.artistAlbum(data["Artist"], data["Album"]); + DOM.subtitle.textContent = format.subtitle(data); } else { DOM.duration.textContent = ""; DOM.title.textContent = ""; - DOM["artist-album"].textContent = ""; + DOM.subtitle.textContent = ""; } pubsub.publish("song-change", null, data);