From a57207f80ec99fce73ac6d88df31f408705ea9a9 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Fri, 13 Mar 2020 23:01:16 +0100 Subject: [PATCH] search --- app/css/cyp.less | 9 +- app/css/elements/app.less | 1 + app/css/elements/library.less | 25 ++++++ app/css/elements/settings.less | 2 +- app/css/elements/song.less | 6 -- app/css/mixins.less | 13 ++- app/css/variables.less | 1 - app/cyp.css | 2 +- app/js/art.js | 30 +++---- app/js/elements/library.js | 59 +++++++++++-- app/js/elements/path.js | 23 ++--- app/js/elements/player.js | 5 +- app/js/elements/queue.js | 10 +-- app/js/elements/song.js | 15 ++-- app/js/format.js | 6 +- app/js/fs.js | 70 --------------- app/js/library.js | 111 ------------------------ app/js/mpd-mock.js | 10 ++- app/js/mpd.js | 32 ++++--- app/js/selection.js | 4 +- app/js/ui.js | 153 --------------------------------- 21 files changed, 166 insertions(+), 421 deletions(-) delete mode 100644 app/js/fs.js delete mode 100644 app/js/library.js delete mode 100644 app/js/ui.js diff --git a/app/css/cyp.less b/app/css/cyp.less index 7a9cc6e..b4b6848 100644 --- a/app/css/cyp.less +++ b/app/css/cyp.less @@ -28,12 +28,17 @@ footer { } } -input, select, button { - color: inherit; +input, select { font: inherit; } +select { + color: inherit; +} + button { + color: inherit; + font: inherit; -webkit-appearance: none; -moz-appearance: none; appearance: none; diff --git a/app/css/elements/app.less b/app/css/elements/app.less index 9f2ae6d..ab01a8a 100644 --- a/app/css/elements/app.less +++ b/app/css/elements/app.less @@ -7,6 +7,7 @@ cyp-app { height: 100vh; font-family: lato, sans-serif; + font-size: 16px; line-height: 1.25; background-color: var(--bg); color: var(--fg); diff --git a/app/css/elements/library.less b/app/css/elements/library.less index 6d0f90f..9c8da29 100644 --- a/app/css/elements/library.less +++ b/app/css/elements/library.less @@ -1,2 +1,27 @@ cyp-library { + nav { + .flex-column; + align-items: center; + + button { + .font-large; + width: 200px; + margin-top: 2em; + text-decoration: underline; + + .icon { + width: 32px; + margin-right: var(--icon-spacing); + } + } + } + + form { + .item; + .flex-row; + + button:first-of-type { + margin-left: 8px; + } + } } diff --git a/app/css/elements/settings.less b/app/css/elements/settings.less index 79baf07..2c11cc2 100644 --- a/app/css/elements/settings.less +++ b/app/css/elements/settings.less @@ -1,7 +1,7 @@ cyp-settings { --spacing: 8px; - font-size: var(--font-size-large); + .font-large; dl { margin: var(--spacing); diff --git a/app/css/elements/song.less b/app/css/elements/song.less index 761e921..80a6e8c 100644 --- a/app/css/elements/song.less +++ b/app/css/elements/song.less @@ -5,12 +5,6 @@ cyp-song { .flex-column; min-width: 0; // bez tohoto se odmita zmensit - /* FIXME toto je relikt z .component - .icon { - color: var(--primary); - } - */ - .subtitle { .ellipsis; } } diff --git a/app/css/mixins.less b/app/css/mixins.less index 96c7fdc..15a13b0 100644 --- a/app/css/mixins.less +++ b/app/css/mixins.less @@ -14,6 +14,11 @@ text-overflow: ellipsis; } +.font-large { + font-size: 18px; + line-height: 24px; +} + .selectable { cursor: pointer; position: relative; // kotva pro selected::before @@ -59,11 +64,11 @@ } .title { - font-weight: bold; - // FIXME take line-height, at vychazi celociselne - font-size: var(--font-size-large); - min-width: 0; + .font-large; .ellipsis; + + font-weight: bold; + min-width: 0; } button { diff --git a/app/css/variables.less b/app/css/variables.less index 27f2862..38e8f1f 100644 --- a/app/css/variables.less +++ b/app/css/variables.less @@ -1,7 +1,6 @@ @breakpoint-menu: 480px; cyp-app { - --font-size-large: 112.5%; --icon-spacing: 4px; --primary: rgb(var(--primary-raw)); --primary-tint: rgba(var(--primary-raw), 0.1); diff --git a/app/cyp.css b/app/cyp.css index e7851c8..cf367b9 100644 --- a/app/cyp.css +++ b/app/cyp.css @@ -1 +1 @@ -*,*::before,*::after{box-sizing:inherit}html{background-color:var(--fg)}body{margin:0}main{flex:auto;overflow:auto}header,footer{flex:none;z-index:1;box-shadow:var(--box-shadow)}footer{position:relative;overflow:hidden;height:56px}@media (max-width:480px){footer{height:40px}}input,select,button{color:inherit;font:inherit}button{-webkit-appearance:none;-moz-appearance:none;appearance:none;flex-direction:row;align-items:center;flex:none;background-color:transparent;padding:0;border:none;line-height:1;cursor:pointer}button:not([hidden]){display:flex}@media (hover:hover){button:not(:disabled):not(:hover):not(.-thumb){opacity:.9}}select{background-color:transparent;border:1px solid var(--fg);border-radius:4px;padding:2px 4px}@font-face{font-family:"Lato";src:url("font/LatoLatin-Regular.woff2") format("woff2");font-style:normal;font-weight:normal}@font-face{font-family:"Lato";src:url("font/LatoLatin-Bold.woff2") format("woff2");font-style:normal;font-weight:bold}.icon{width:24px;flex:none}.icon path:not([fill]),.icon polygon:not([fill]),.icon circle:not([fill]){fill:currentColor}.flex-row{flex-direction:row;align-items:center}.flex-row:not([hidden]){display:flex}.flex-column{flex-direction:column}.flex-column:not([hidden]){display:flex}.ellipsis{overflow:hidden;text-overflow:ellipsis}.selectable{cursor:pointer;position:relative}.selectable.selected{xbackground-color:var(--primary-tint)}.selectable.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}.selectable.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}.item{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}.item:not([hidden]){display:flex}.item.selected{xbackground-color:var(--primary-tint)}.item.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}.item.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}.item:nth-child(odd){background-color:var(--bg-alt)}.item>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}.item .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}.item button:first-of-type{margin-left:auto}.item button .icon{width:32px}.search{flex-direction:row;align-items:center;margin-left:auto;transition:all 300ms;width:32px;max-width:20ch}.search:not([hidden]){display:flex}.search .icon{width:32px;cursor:pointer}.search input{border:none;outline:none;color:inherit;background-color:inherit;border-bottom:1px solid var(--fg);width:0;padding:0;flex-grow:1}.search.open{flex:1}.art{margin-right:var(--icon-spacing)}.art .icon,.art img{display:block}cyp-app{--font-size-large:112.5%;--icon-spacing:4px;--primary:rgb(var(--primary-raw));--primary-tint:rgba(var(--primary-raw), .1);--box-shadow:0 0 3px #000;--border-width:4px}cyp-app[theme=light]{--fg:#333;--bg:#f0f0f0;--bg-alt:#e0e0e0;--text-shadow:none}cyp-app[theme=dark]{--fg:#f0f0f0;--bg:#333;--bg-alt:#555;--text-shadow:0 1px 1px rgba(0,0,0,0.8)}@media (prefers-color-scheme:dark){cyp-app[theme=auto]{--fg:#f0f0f0;--bg:#333;--bg-alt:#555;--text-shadow:0 1px 1px rgba(0,0,0,0.8)}}@media (prefers-color-scheme:light){cyp-app[theme=auto]{--fg:#333;--bg:#f0f0f0;--bg-alt:#e0e0e0;--text-shadow:none}}cyp-app[color=dodgerblue]{--primary-raw:30, 144, 255}cyp-app[color=darkorange]{--primary-raw:255, 140, 0}cyp-app[color=limegreen]{--primary-raw:50, 205, 50}cyp-app{flex-direction:column;box-sizing:border-box;margin:0 auto;max-width:800px;height:100vh;font-family:lato,sans-serif;line-height:1.25;background-color:var(--bg);color:var(--fg);text-shadow:var(--text-shadow);white-space:nowrap}cyp-app:not([hidden]){display:flex}cyp-menu,cyp-commands{flex-direction:row;align-items:center;height:100%}cyp-menu:not([hidden]),cyp-commands:not([hidden]){display:flex}cyp-menu button,cyp-commands button{height:100%;flex-direction:column;align-items:center;justify-content:center}cyp-menu button:not([hidden]),cyp-commands button:not([hidden]){display:flex}@media (max-width:480px){cyp-menu button,cyp-commands button{flex-direction:row}cyp-menu button span:not([id]),cyp-commands button span:not([id]){display:none}}cyp-menu button .icon+*,cyp-commands button .icon+*{margin-top:2px}cyp-menu button{flex:1 0 0;border-top:var(--border-width) solid transparent;border-bottom:var(--border-width) solid transparent}cyp-menu button .icon{margin-right:var(--icon-spacing)}cyp-menu button.active{border-top-color:var(--primary);color:var(--primary);background-color:var(--primary-tint)}cyp-commands{position:absolute;left:0;top:0;width:100%;transition:top 300ms;background-color:var(--bg)}cyp-commands[hidden]{display:flex;top:100%}cyp-commands button{flex:0 1 80px}cyp-commands button.last{order:1;margin-left:auto}cyp-song{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-song:not([hidden]){display:flex}cyp-song.selected{xbackground-color:var(--primary-tint)}cyp-song.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-song.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-song:nth-child(odd){background-color:var(--bg-alt)}cyp-song>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-song .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}cyp-song button:first-of-type{margin-left:auto}cyp-song button .icon{width:32px}cyp-song .multiline{flex-direction:column;min-width:0}cyp-song .multiline:not([hidden]){display:flex}cyp-song .multiline .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-queue cyp-song .track{display:none}cyp-player{flex-direction:row;align-items:center;align-items:stretch}cyp-player:not([hidden]){display:flex}cyp-player:not([data-state=play]) .pause{display:none}cyp-player[data-state=play] .play{display:none}cyp-player:not([data-flags~=random]) .random,cyp-player:not([data-flags~=repeat]) .repeat{opacity:.5}cyp-player x-range{flex-grow:1;--elapsed-color:var(--primary)}cyp-player .art{margin-right:0;height:96px}cyp-player .art img,cyp-player .art .icon{width:96px}cyp-player .info{flex-grow:2;flex-basis:0;padding:0 var(--icon-spacing);overflow:hidden;flex-direction:column;justify-content:space-around}cyp-player .info:not([hidden]){display:flex}cyp-player .info h2{font-size:125%;margin:0}cyp-player .info .title,cyp-player .info .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-player .timeline{flex-direction:row;align-items:center}cyp-player .timeline:not([hidden]){display:flex}cyp-player .timeline .duration,cyp-player .timeline .elapsed{flex-basis:5ch;text-align:center}cyp-player .controls{flex-grow:1;flex-basis:0;max-width:220px;flex-direction:column;justify-content:space-around}cyp-player .controls:not([hidden]){display:flex}cyp-player .controls .playback{flex-direction:row;align-items:center;justify-content:space-around}cyp-player .controls .playback:not([hidden]){display:flex}cyp-player .controls .playback .icon{width:40px}cyp-player .controls .playback .icon-play,cyp-player .controls .playback .icon-pause{width:64px}cyp-player .controls .volume{flex-direction:row;align-items:center}cyp-player .controls .volume:not([hidden]){display:flex}cyp-player .controls .volume .mute{margin-right:4px}cyp-player .misc{display:flex;flex-direction:column;align-self:stretch;justify-content:space-around}cyp-player .misc .icon{width:32px}@media (max-width:519px){cyp-player{flex-wrap:wrap;justify-content:space-between}cyp-player .info{order:1;flex-basis:100%;height:96px}}cyp-queue .current{color:var(--primary)}cyp-settings{--spacing:8px;font-size:var(--font-size-large)}cyp-settings dl{margin:var(--spacing);display:grid;grid-template-columns:max-content 1fr;align-items:center;grid-gap:var(--spacing)}cyp-settings dt{font-weight:bold}cyp-settings dd{margin:0;flex-direction:column;align-items:start}cyp-settings dd:not([hidden]){display:flex}cyp-settings label{flex-direction:row;align-items:center}cyp-settings label:not([hidden]){display:flex}cyp-settings label [type=radio],cyp-settings label [type=checkbox]{margin:0 4px 0 0}cyp-yt header{border-bottom:1px solid var(--fg)}cyp-yt header button+button{margin-left:16px}cyp-yt .clear{margin-left:auto}cyp-yt pre{margin:.5em .5ch;flex-grow:1;overflow:auto;white-space:pre-wrap}cyp-yt.pending header{background-image:linear-gradient(var(--primary), var(--primary));background-repeat:no-repeat;background-size:25% 4px;animation:bar ease-in-out 3s alternate infinite}@keyframes bar{0%{background-position:0 100%}100%{background-position:100% 100%}}x-range{--thumb-size:8px;--thumb-color:#ddd;--thumb-shadow:#000;--thumb-hover-color:#fff;--track-size:4px;--track-color:#888;--track-shadow:#000;--elapsed-color:#ddd;--remaining-color:transparent;--radius:calc(var(--track-size)/2);display:inline-block;position:relative;width:192px;height:16px}x-range .-track,x-range .-elapsed,x-range .-remaining{position:absolute;top:calc(50% - var(--track-size)/2);height:var(--track-size);border-radius:var(--radius)}x-range .-track{width:100%;left:0;background-color:var(--track-color);box-shadow:0 0 1px var(--thumb-shadow)}x-range .-elapsed{left:0;background-color:var(--elapsed-color)}x-range .-remaining{right:0;background-color:var(--remaining-color)}x-range .-inner{position:absolute;left:var(--thumb-size);right:var(--thumb-size);top:0;bottom:0}x-range .-thumb{all:unset;position:absolute;top:50%;transform:translate(-50%, -50%);border-radius:50%;width:calc(2*var(--thumb-size));height:calc(2*var(--thumb-size));background-color:var(--thumb-color);box-shadow:0 0 2px var(--thumb-shadow)}x-range[disabled]{opacity:.5}x-range:not([disabled]) .-thumb:hover{background-color:var(--thumb-hover-color)}cyp-playlist{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-playlist:not([hidden]){display:flex}cyp-playlist.selected{xbackground-color:var(--primary-tint)}cyp-playlist.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-playlist.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-playlist:nth-child(odd){background-color:var(--bg-alt)}cyp-playlist>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-playlist .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}cyp-playlist button:first-of-type{margin-left:auto}cyp-playlist button .icon{width:32px}cyp-playlist:nth-child(odd){background-color:var(--bg-alt)}cyp-tag{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;padding:0}cyp-tag:not([hidden]){display:flex}cyp-tag.selected{xbackground-color:var(--primary-tint)}cyp-tag.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-tag.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-tag:nth-child(odd){background-color:var(--bg-alt)}cyp-tag>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-tag .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}cyp-tag button:first-of-type{margin-left:auto}cyp-tag button .icon{width:32px}cyp-tag .art img,cyp-tag .art .icon{width:64px;height:64px}cyp-tag .art .icon{filter:drop-shadow(var(--text-shadow))}cyp-back{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-back:not([hidden]){display:flex}cyp-back.selected{xbackground-color:var(--primary-tint)}cyp-back.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-back.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-back:nth-child(odd){background-color:var(--bg-alt)}cyp-back>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-back .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}cyp-back button:first-of-type{margin-left:auto}cyp-back button .icon{width:32px}cyp-path{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-path:not([hidden]){display:flex}cyp-path.selected{xbackground-color:var(--primary-tint)}cyp-path.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-path.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-path:nth-child(odd){background-color:var(--bg-alt)}cyp-path>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-path .title{font-weight:bold;font-size:var(--font-size-large);min-width:0;overflow:hidden;text-overflow:ellipsis}cyp-path button:first-of-type{margin-left:auto}cyp-path button .icon{width:32px} \ No newline at end of file +*,*::before,*::after{box-sizing:inherit}html{background-color:var(--fg)}body{margin:0}main{flex:auto;overflow:auto}header,footer{flex:none;z-index:1;box-shadow:var(--box-shadow)}footer{position:relative;overflow:hidden;height:56px}@media (max-width:480px){footer{height:40px}}input,select{font:inherit}select{color:inherit}button{color:inherit;font:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;flex-direction:row;align-items:center;flex:none;background-color:transparent;padding:0;border:none;line-height:1;cursor:pointer}button:not([hidden]){display:flex}@media (hover:hover){button:not(:disabled):not(:hover):not(.-thumb){opacity:.9}}select{background-color:transparent;border:1px solid var(--fg);border-radius:4px;padding:2px 4px}@font-face{font-family:"Lato";src:url("font/LatoLatin-Regular.woff2") format("woff2");font-style:normal;font-weight:normal}@font-face{font-family:"Lato";src:url("font/LatoLatin-Bold.woff2") format("woff2");font-style:normal;font-weight:bold}.icon{width:24px;flex:none}.icon path:not([fill]),.icon polygon:not([fill]),.icon circle:not([fill]){fill:currentColor}.flex-row{flex-direction:row;align-items:center}.flex-row:not([hidden]){display:flex}.flex-column{flex-direction:column}.flex-column:not([hidden]){display:flex}.ellipsis{overflow:hidden;text-overflow:ellipsis}.font-large{font-size:18px;line-height:24px}.selectable{cursor:pointer;position:relative}.selectable.selected{xbackground-color:var(--primary-tint)}.selectable.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}.selectable.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}.item{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}.item:not([hidden]){display:flex}.item.selected{xbackground-color:var(--primary-tint)}.item.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}.item.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}.item:nth-child(odd){background-color:var(--bg-alt)}.item>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}.item .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}.item button:first-of-type{margin-left:auto}.item button .icon{width:32px}.search{flex-direction:row;align-items:center;margin-left:auto;transition:all 300ms;width:32px;max-width:20ch}.search:not([hidden]){display:flex}.search .icon{width:32px;cursor:pointer}.search input{border:none;outline:none;color:inherit;background-color:inherit;border-bottom:1px solid var(--fg);width:0;padding:0;flex-grow:1}.search.open{flex:1}.art{margin-right:var(--icon-spacing)}.art .icon,.art img{display:block}cyp-app{--icon-spacing:4px;--primary:rgb(var(--primary-raw));--primary-tint:rgba(var(--primary-raw), .1);--box-shadow:0 0 3px #000;--border-width:4px}cyp-app[theme=light]{--fg:#333;--bg:#f0f0f0;--bg-alt:#e0e0e0;--text-shadow:none}cyp-app[theme=dark]{--fg:#f0f0f0;--bg:#333;--bg-alt:#555;--text-shadow:0 1px 1px rgba(0,0,0,0.8)}@media (prefers-color-scheme:dark){cyp-app[theme=auto]{--fg:#f0f0f0;--bg:#333;--bg-alt:#555;--text-shadow:0 1px 1px rgba(0,0,0,0.8)}}@media (prefers-color-scheme:light){cyp-app[theme=auto]{--fg:#333;--bg:#f0f0f0;--bg-alt:#e0e0e0;--text-shadow:none}}cyp-app[color=dodgerblue]{--primary-raw:30, 144, 255}cyp-app[color=darkorange]{--primary-raw:255, 140, 0}cyp-app[color=limegreen]{--primary-raw:50, 205, 50}cyp-app{flex-direction:column;box-sizing:border-box;margin:0 auto;max-width:800px;height:100vh;font-family:lato,sans-serif;font-size:16px;line-height:1.25;background-color:var(--bg);color:var(--fg);text-shadow:var(--text-shadow);white-space:nowrap}cyp-app:not([hidden]){display:flex}cyp-menu,cyp-commands{flex-direction:row;align-items:center;height:100%}cyp-menu:not([hidden]),cyp-commands:not([hidden]){display:flex}cyp-menu button,cyp-commands button{height:100%;flex-direction:column;align-items:center;justify-content:center}cyp-menu button:not([hidden]),cyp-commands button:not([hidden]){display:flex}@media (max-width:480px){cyp-menu button,cyp-commands button{flex-direction:row}cyp-menu button span:not([id]),cyp-commands button span:not([id]){display:none}}cyp-menu button .icon+*,cyp-commands button .icon+*{margin-top:2px}cyp-menu button{flex:1 0 0;border-top:var(--border-width) solid transparent;border-bottom:var(--border-width) solid transparent}cyp-menu button .icon{margin-right:var(--icon-spacing)}cyp-menu button.active{border-top-color:var(--primary);color:var(--primary);background-color:var(--primary-tint)}cyp-commands{position:absolute;left:0;top:0;width:100%;transition:top 300ms;background-color:var(--bg)}cyp-commands[hidden]{display:flex;top:100%}cyp-commands button{flex:0 1 80px}cyp-commands button.last{order:1;margin-left:auto}cyp-song{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-song:not([hidden]){display:flex}cyp-song.selected{xbackground-color:var(--primary-tint)}cyp-song.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-song.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-song:nth-child(odd){background-color:var(--bg-alt)}cyp-song>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-song .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-song button:first-of-type{margin-left:auto}cyp-song button .icon{width:32px}cyp-song .multiline{flex-direction:column;min-width:0}cyp-song .multiline:not([hidden]){display:flex}cyp-song .multiline .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-queue cyp-song .track{display:none}cyp-player{flex-direction:row;align-items:center;align-items:stretch}cyp-player:not([hidden]){display:flex}cyp-player:not([data-state=play]) .pause{display:none}cyp-player[data-state=play] .play{display:none}cyp-player:not([data-flags~=random]) .random,cyp-player:not([data-flags~=repeat]) .repeat{opacity:.5}cyp-player x-range{flex-grow:1;--elapsed-color:var(--primary)}cyp-player .art{margin-right:0;height:96px}cyp-player .art img,cyp-player .art .icon{width:96px}cyp-player .info{flex-grow:2;flex-basis:0;padding:0 var(--icon-spacing);overflow:hidden;flex-direction:column;justify-content:space-around}cyp-player .info:not([hidden]){display:flex}cyp-player .info h2{font-size:125%;margin:0}cyp-player .info .title,cyp-player .info .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-player .timeline{flex-direction:row;align-items:center}cyp-player .timeline:not([hidden]){display:flex}cyp-player .timeline .duration,cyp-player .timeline .elapsed{flex-basis:5ch;text-align:center}cyp-player .controls{flex-grow:1;flex-basis:0;max-width:220px;flex-direction:column;justify-content:space-around}cyp-player .controls:not([hidden]){display:flex}cyp-player .controls .playback{flex-direction:row;align-items:center;justify-content:space-around}cyp-player .controls .playback:not([hidden]){display:flex}cyp-player .controls .playback .icon{width:40px}cyp-player .controls .playback .icon-play,cyp-player .controls .playback .icon-pause{width:64px}cyp-player .controls .volume{flex-direction:row;align-items:center}cyp-player .controls .volume:not([hidden]){display:flex}cyp-player .controls .volume .mute{margin-right:4px}cyp-player .misc{display:flex;flex-direction:column;align-self:stretch;justify-content:space-around}cyp-player .misc .icon{width:32px}@media (max-width:519px){cyp-player{flex-wrap:wrap;justify-content:space-between}cyp-player .info{order:1;flex-basis:100%;height:96px}}cyp-queue .current{color:var(--primary)}cyp-settings{--spacing:8px;font-size:18px;line-height:24px}cyp-settings dl{margin:var(--spacing);display:grid;grid-template-columns:max-content 1fr;align-items:center;grid-gap:var(--spacing)}cyp-settings dt{font-weight:bold}cyp-settings dd{margin:0;flex-direction:column;align-items:start}cyp-settings dd:not([hidden]){display:flex}cyp-settings label{flex-direction:row;align-items:center}cyp-settings label:not([hidden]){display:flex}cyp-settings label [type=radio],cyp-settings label [type=checkbox]{margin:0 4px 0 0}cyp-yt header{border-bottom:1px solid var(--fg)}cyp-yt header button+button{margin-left:16px}cyp-yt .clear{margin-left:auto}cyp-yt pre{margin:.5em .5ch;flex-grow:1;overflow:auto;white-space:pre-wrap}cyp-yt.pending header{background-image:linear-gradient(var(--primary), var(--primary));background-repeat:no-repeat;background-size:25% 4px;animation:bar ease-in-out 3s alternate infinite}@keyframes bar{0%{background-position:0 100%}100%{background-position:100% 100%}}x-range{--thumb-size:8px;--thumb-color:#ddd;--thumb-shadow:#000;--thumb-hover-color:#fff;--track-size:4px;--track-color:#888;--track-shadow:#000;--elapsed-color:#ddd;--remaining-color:transparent;--radius:calc(var(--track-size)/2);display:inline-block;position:relative;width:192px;height:16px}x-range .-track,x-range .-elapsed,x-range .-remaining{position:absolute;top:calc(50% - var(--track-size)/2);height:var(--track-size);border-radius:var(--radius)}x-range .-track{width:100%;left:0;background-color:var(--track-color);box-shadow:0 0 1px var(--thumb-shadow)}x-range .-elapsed{left:0;background-color:var(--elapsed-color)}x-range .-remaining{right:0;background-color:var(--remaining-color)}x-range .-inner{position:absolute;left:var(--thumb-size);right:var(--thumb-size);top:0;bottom:0}x-range .-thumb{all:unset;position:absolute;top:50%;transform:translate(-50%, -50%);border-radius:50%;width:calc(2*var(--thumb-size));height:calc(2*var(--thumb-size));background-color:var(--thumb-color);box-shadow:0 0 2px var(--thumb-shadow)}x-range[disabled]{opacity:.5}x-range:not([disabled]) .-thumb:hover{background-color:var(--thumb-hover-color)}cyp-playlist{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-playlist:not([hidden]){display:flex}cyp-playlist.selected{xbackground-color:var(--primary-tint)}cyp-playlist.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-playlist.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-playlist:nth-child(odd){background-color:var(--bg-alt)}cyp-playlist>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-playlist .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-playlist button:first-of-type{margin-left:auto}cyp-playlist button .icon{width:32px}cyp-playlist:nth-child(odd){background-color:var(--bg-alt)}cyp-library nav{flex-direction:column;align-items:center}cyp-library nav:not([hidden]){display:flex}cyp-library nav button{font-size:18px;line-height:24px;width:200px;margin-top:2em;text-decoration:underline}cyp-library nav button .icon{width:32px;margin-right:var(--icon-spacing)}cyp-library form{cursor:pointer;position:relative;padding:8px;flex-direction:row;align-items:center}cyp-library form:not([hidden]){display:flex}cyp-library form.selected{xbackground-color:var(--primary-tint)}cyp-library form.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-library form.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-library form:nth-child(odd){background-color:var(--bg-alt)}cyp-library form>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-library form .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-library form button:first-of-type{margin-left:auto}cyp-library form button .icon{width:32px}cyp-library form:not([hidden]){display:flex}cyp-library form button:first-of-type{margin-left:8px}cyp-tag{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;padding:0}cyp-tag:not([hidden]){display:flex}cyp-tag.selected{xbackground-color:var(--primary-tint)}cyp-tag.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-tag.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-tag:nth-child(odd){background-color:var(--bg-alt)}cyp-tag>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-tag .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-tag button:first-of-type{margin-left:auto}cyp-tag button .icon{width:32px}cyp-tag .art img,cyp-tag .art .icon{width:64px;height:64px}cyp-tag .art .icon{filter:drop-shadow(var(--text-shadow))}cyp-back{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-back:not([hidden]){display:flex}cyp-back.selected{xbackground-color:var(--primary-tint)}cyp-back.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-back.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-back:nth-child(odd){background-color:var(--bg-alt)}cyp-back>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-back .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-back button:first-of-type{margin-left:auto}cyp-back button .icon{width:32px}cyp-path{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px}cyp-path:not([hidden]){display:flex}cyp-path.selected{xbackground-color:var(--primary-tint)}cyp-path.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-path.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-path:nth-child(odd){background-color:var(--bg-alt)}cyp-path>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-path .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-path button:first-of-type{margin-left:auto}cyp-path button .icon{width:32px} \ No newline at end of file diff --git a/app/js/art.js b/app/js/art.js index 7b4f3b0..58ec543 100644 --- a/app/js/art.js +++ b/app/js/art.js @@ -1,7 +1,7 @@ import * as html from "./html.js"; import * as conf from "./conf.js"; -let cache = {}; +const cache = {}; const MIME = "image/jpeg"; const STORAGE_PREFIX = `art-${conf.artSize}` ; @@ -14,26 +14,26 @@ function load(key) { } async function bytesToImage(bytes) { - let blob = new Blob([bytes]); - let src = URL.createObjectURL(blob); - let image = html.node("img", {src}); + const blob = new Blob([bytes]); + const src = URL.createObjectURL(blob); + const image = html.node("img", {src}); return new Promise(resolve => { image.onload = () => resolve(image); }); } function resize(image) { - let canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize}); - let ctx = canvas.getContext("2d"); + const canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize}); + const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); return canvas; } export async function get(mpd, artist, album, songUrl = null) { - let key = `${artist}-${album}`; + const key = `${artist}-${album}`; if (key in cache) { return cache[key]; } - let loaded = await load(key); + const loaded = await load(key); if (loaded) { cache[key] = loaded; return loaded; @@ -43,18 +43,18 @@ export async function get(mpd, artist, album, songUrl = null) { // promise to be returned in the meantime let resolve; - let promise = new Promise(res => resolve = res); + const promise = new Promise(res => resolve = res); cache[key] = promise; - try { - let data = await mpd.albumArt(songUrl); - let bytes = new Uint8Array(data); - let image = await bytesToImage(bytes); - let url = resize(image).toDataURL(MIME); + const data = await mpd.albumArt(songUrl); + if (data) { + const bytes = new Uint8Array(data); + const image = await bytesToImage(bytes); + const url = resize(image).toDataURL(MIME); store(key, url); cache[key] = url; resolve(url); - } catch (e) { + } else { cache[key] = null; } return cache[key]; diff --git a/app/js/elements/library.js b/app/js/elements/library.js index ab91001..ef5981f 100644 --- a/app/js/elements/library.js +++ b/app/js/elements/library.js @@ -47,13 +47,15 @@ class Library extends Component { _showRoot() { html.clear(this); - html.button({icon:"artist"}, "Artists and albums", this) + const nav = html.node("nav", {}, "", this); + + html.button({icon:"artist"}, "Artists and albums", nav) .addEventListener("click", _ => this._listTags("AlbumArtist")); - html.button({icon:"folder"}, "Files and directories", this) + html.button({icon:"folder"}, "Files and directories", nav) .addEventListener("click", _ => this._listPath("")); - html.button({icon:"magnify"}, "Search", this) + html.button({icon:"magnify"}, "Search", nav) .addEventListener("click", _ => this._showSearch()); } @@ -72,7 +74,7 @@ class Library extends Component { path && this._buildBack(path); paths["directory"].forEach(path => this._buildPath(path)); paths["file"].forEach(path => this._buildPath(path)); -} + } async _listSongs(filter) { const songs = await this._mpd.listSongs(filter); @@ -82,7 +84,50 @@ class Library extends Component { } _showSearch() { + html.clear(this); + const form = html.node("form", {}, "", this); + const input = html.node("input", {type:"text"}, "", form); + html.button({icon:"magnify"}, "", form); + form.addEventListener("submit", e => { + e.preventDefault(); + const q = input.value.trim(); + if (q.length < 3) { return; } + this._doSearch(q, form); + }); + + input.focus(); + } + + async _doSearch(q, form) { + const songs1 = await this._mpd.searchSongs({"AlbumArtist": q}); + const songs2 = await this._mpd.searchSongs({"Album": q}); + const songs3 = await this._mpd.searchSongs({"Title": q}); + html.clear(this); + this.appendChild(form); + + this._aggregateSearch(songs1, "AlbumArtist"); + this._aggregateSearch(songs2, "Album"); + songs3.forEach(song => this.appendChild(new Song(song))); + } + + _aggregateSearch(songs, tag) { + let results = new Map(); + songs.forEach(song => { + let filter = {}, value; + const artist = song["AlbumArtist"] || song["Artist"] + + if (tag == "Album") { + value = song[tag]; + if (artist) { filter["AlbumArtist"] = artist; } + } + + if (tag == "AlbumArtist") { value = artist; } + + results.set(value, filter); + }); + + results.forEach((filter, value) => this._buildTag(tag, value, filter)); } _buildTag(tag, value, filter) { @@ -150,11 +195,7 @@ class Library extends Component { sel.addCommandAll(); sel.addCommand(async items => { - const commands = [ - "clear", - ...items.map(createEnqueueCommand), - "play" - ]; + const commands = ["clear",...items.map(createEnqueueCommand), "play"]; await this._mpd.command(commands); this.selection.clear(); this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification? diff --git a/app/js/elements/path.js b/app/js/elements/path.js index b7d3ba8..8b7e3c1 100644 --- a/app/js/elements/path.js +++ b/app/js/elements/path.js @@ -1,29 +1,20 @@ import Item from "../item.js"; import * as html from "../html.js"; +import * as format from "../format.js"; -function baseName(path) { - return path.split("/").pop(); -} - export default class Path extends Item { constructor(data) { super(); this._data = data; - - if ("directory" in this._data) { - this.file = data["directory"]; - } else { - this.file = data["file"]; - } + this._isDirectory = ("directory" in this._data); } + + get file() { return (this._isDirectory ? this._data["directory"] : this._data["file"]); } + connectedCallback() { - if ("directory" in this._data) { - this.appendChild(html.icon("folder")); - } else { - this.appendChild(html.icon("music")); - } - this._buildTitle(baseName(this.file)); + this.appendChild(html.icon(this._isDirectory ? "folder" : "music")); + this._buildTitle(format.fileName(this.file)); } } diff --git a/app/js/elements/player.js b/app/js/elements/player.js index 31b438b..b7f2429 100644 --- a/app/js/elements/player.js +++ b/app/js/elements/player.js @@ -1,9 +1,9 @@ import * as art from "../art.js"; import * as html from "../html.js"; import * as format from "../format.js"; - import Component from "../component.js"; + const DELAY = 1000; class Player extends Component { @@ -104,7 +104,7 @@ class Player extends Component { DOM.duration.textContent = format.time(duration); DOM.progress.max = duration; DOM.progress.disabled = false; - DOM.title.textContent = data["Title"] || data["file"].split("/").pop(); + DOM.title.textContent = data["Title"] || format.fileName(data["file"]); DOM.subtitle.textContent = format.subtitle(data, {duration:false}); } else { DOM.title.textContent = ""; @@ -118,6 +118,7 @@ class Player extends Component { let artistNew = data["AlbumArtist"] || data["Artist"]; let artistOld = this._current["AlbumArtist"] || this._current["Artist"]; + if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art) html.clear(DOM.art); art.get(this._mpd, artistNew, data["Album"], data["file"]).then(src => { diff --git a/app/js/elements/queue.js b/app/js/elements/queue.js index 23d2983..9c08357 100644 --- a/app/js/elements/queue.js +++ b/app/js/elements/queue.js @@ -11,7 +11,7 @@ function generateMoveCommands(items, diff, all) { .map(item => { let index = all.indexOf(item) + diff; if (index < 0 || index >= all.length) { return null; } // this does not move - return `moveid ${item.data["Id"]} ${index}`; + return `moveid ${item.songId} ${index}`; }) .filter(command => command); } @@ -58,7 +58,7 @@ class Queue extends Component { _updateCurrent() { Array.from(this.children).forEach(/** @param {HTMLElement} node */ node => { - node.classList.toggle("current", node.dataset.songId == this._currentId); + node.classList.toggle("current", node.songId == this._currentId); }); } @@ -71,7 +71,7 @@ class Queue extends Component { this.appendChild(node); node.addButton("play", async _ => { - await this._mpd.command(`playid ${song["Id"]}`); + await this._mpd.command(`playid ${node.songId}`); }); }); @@ -101,7 +101,7 @@ class Queue extends Component { name = escape(name); const commands = items.map(item => { - return `playlistadd "${name}" "${escape(item.data["file"])}"`; + return `playlistadd "${name}" "${escape(item.file)}"`; }); await this._mpd.command(commands); // FIXME notify? @@ -110,7 +110,7 @@ class Queue extends Component { sel.addCommand(async items => { if (!confirm(`Remove these ${items.length} songs from the queue?`)) { return; } - const commands = items.map(item => `deleteid ${item.data["Id"]}`); + const commands = items.map(item => `deleteid ${item.songId}`); await this._mpd.command(commands); this._sync(); diff --git a/app/js/elements/song.js b/app/js/elements/song.js index c5fc439..809fddb 100644 --- a/app/js/elements/song.js +++ b/app/js/elements/song.js @@ -5,12 +5,14 @@ import Item from "../item.js"; export default class Song extends Item { constructor(data) { super(); - this.data = data; // FIXME verejne? - this.dataset.songId = data["Id"]; // FIXME toto maji jen ve fronte + this._data = data; } + get file() { return this._data["file"]; } + get songId() { return this._data["Id"]; } + connectedCallback() { - const data = this.data; + const data = this._data; const block = html.node("div", {className:"multiline"}, "", this); @@ -29,13 +31,8 @@ export default class Song extends Item { } _buildTitle(data) { - return super._buildTitle(data["Title"] || fileName(data)); + return super._buildTitle(data["Title"] || format.fileName(this.file)); } } customElements.define("cyp-song", Song); - -// FIXME vyfaktorovat nekam do haje -function fileName(data) { - return data["file"].split("/").pop(); -} diff --git a/app/js/format.js b/app/js/format.js index b8f2d15..a7076ac 100644 --- a/app/js/format.js +++ b/app/js/format.js @@ -13,4 +13,8 @@ export function subtitle(data, options = {duration:true}) { data["Album"] && tokens.push(data["Album"]); options.duration && data["duration"] && tokens.push(time(Number(data["duration"]))); return tokens.join(SEPARATOR); -} \ No newline at end of file +} + +export function fileName(file) { + return file.split("/").pop(); +} diff --git a/app/js/fs.js b/app/js/fs.js deleted file mode 100644 index e56f4c8..0000000 --- a/app/js/fs.js +++ /dev/null @@ -1,70 +0,0 @@ -import * as mpd from "./lib/mpd.js"; -import * as html from "./lib/html.js"; -import * as ui from "./lib/ui.js"; - -import Search from "./lib/search.js"; - -let node, search; - -function buildHeader(path) { - let header = node.querySelector("header"); - html.clear(header); - - search.reset(); - header.appendChild(search.getNode()); - - path.split("/").filter(x => x).forEach((name, index, all) => { - index && html.node("span", {}, " / ", header); - let button = html.button({icon:"folder"}, name, header); - let path = all.slice(0, index+1).join("/"); - button.addEventListener("click", e => list(path)); - }); - -} - -function buildDirectory(data, parent) { - let path = data["directory"]; - let name = path.split("/").pop(); - let node = ui.group(ui.CTX_FS, name, path, parent); - node.addEventListener("click", e => list(path)); - node.dataset.name = name; - return node; -} - -function buildFile(data, parent) { - let node = ui.song(ui.CTX_FS, data, parent); - let name = data["file"].split("/").pop(); - node.dataset.name = name; - return node; -} - -function buildResults(results) { - let ul = node.querySelector("ul"); - html.clear(ul); - - results["directory"].forEach(directory => buildDirectory(directory, ul)); - results["file"].forEach(file => buildFile(file, ul)); -} - -async function list(path) { - let results = await mpd.listPath(path); - buildResults(results); - buildHeader(path); -} - -function onSearch(e) { - Array.from(node.querySelectorAll("[data-name]")).forEach(node => { - let name = node.dataset.name; - node.style.display = (search.match(name) ? "" : "none"); - }); -} - -export async function activate() { - list(""); -} - -export function init(n) { - node = n; - search = new Search(node.querySelector(".search")); - search.addEventListener("input", onSearch); -} diff --git a/app/js/library.js b/app/js/library.js deleted file mode 100644 index 848821f..0000000 --- a/app/js/library.js +++ /dev/null @@ -1,111 +0,0 @@ -import * as mpd from "./lib/mpd.js"; -import * as html from "./lib/html.js"; -import * as ui from "./lib/ui.js"; -import * as format from "./lib/format.js"; - -import Search from "./lib/search.js"; - -let node, search; - -function nonempty(x) { return x.length > 0; } - -function buildHeader(filter) { - filter = filter || {}; - let header = node.querySelector("header"); - html.clear(header); - - search.reset(); - header.appendChild(search.getNode()); - - let artist = filter["AlbumArtist"]; - if (artist) { - let artistFilter = {"AlbumArtist":artist}; - let button = html.button({icon:"artist"}, artist, header); - button.addEventListener("click", e => listAlbums(artistFilter)); - - let album = filter["Album"]; - if (album) { - html.node("span", {}, format.SEPARATOR, header); - let albumFilter = Object.assign({}, artistFilter, {"Album":album}); - let button = html.button({icon:"album"}, album, header); - button.addEventListener("click", e => listSongs(albumFilter)); - } - } - -} - -function buildAlbum(album, filter, parent) { - let childFilter = Object.assign({}, filter, {"Album": album}); - let node = ui.group(ui.CTX_LIBRARY, album, childFilter, parent); - node.addEventListener("click", e => listSongs(childFilter)); - node.dataset.name = album; - return node; -} - -function buildArtist(artist, filter, parent) { - let childFilter = Object.assign({}, filter, {"AlbumArtist": artist}); - let node = ui.group(ui.CTX_LIBRARY, artist, childFilter, parent); - node.addEventListener("click", e => listAlbums(childFilter)); - node.dataset.name = artist; - return node; -} - -function buildSongs(songs, filter) { - let ul = node.querySelector("ul"); - html.clear(ul); - - songs.map(song => { - let node = ui.song(ui.CTX_LIBRARY, song, ul); - node.dataset.name = song["Title"]; - }); -} - -function buildAlbums(albums, filter) { - let ul = node.querySelector("ul"); - html.clear(ul); - - albums.filter(nonempty).map(album => buildAlbum(album, filter, ul)); -} - -function buildArtists(artists, filter) { - let ul = node.querySelector("ul"); - html.clear(ul); - - artists.filter(nonempty).map(artist => buildArtist(artist, filter, ul)); -} - -async function listSongs(filter) { - let songs = await mpd.listSongs(filter); - buildSongs(songs, filter); - buildHeader(filter); -} - -async function listAlbums(filter) { - let albums = await mpd.listTags("Album", filter); - buildAlbums(albums, filter); - buildHeader(filter); -} - -async function listArtists(filter) { - let artists = await mpd.listTags("AlbumArtist", filter); - buildArtists(artists, filter); - buildHeader(filter); -} - -function onSearch(e) { - Array.from(node.querySelectorAll("[data-name]")).forEach(node => { - let name = node.dataset.name; - node.style.display = (search.match(name) ? "" : "none"); - }); -} - -export async function activate() { - listArtists(); -} - -export function init(n) { - node = n; - - search = new Search(node.querySelector(".search")); - search.addEventListener("input", onSearch); -} diff --git a/app/js/mpd-mock.js b/app/js/mpd-mock.js index 7ec493d..f3ca888 100644 --- a/app/js/mpd-mock.js +++ b/app/js/mpd-mock.js @@ -16,7 +16,7 @@ export function status() { Title: "Title of song", Artist: "Artist of song", Album: "Album of song", - Track: 6, + Track: "6", state: "play", Id: 2 } @@ -24,9 +24,9 @@ export function status() { export function listQueue() { return [ - {Id:1, Track:5, Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30}, + {Id:1, Track:"5", Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30}, status(), - {Id:3, Track:7, Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230}, + {Id:3, Track:"7", Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230}, ]; } @@ -64,6 +64,10 @@ export function listSongs(filter, window = null) { return listQueue(); } +export function searchSongs(filter) { + return listQueue(); +} + export function albumArt(songUrl) { return null; } diff --git a/app/js/mpd.js b/app/js/mpd.js index 8cc41c4..3abb1e9 100644 --- a/app/js/mpd.js +++ b/app/js/mpd.js @@ -37,11 +37,11 @@ function processQueue() { ws.send(current.cmd); } -export function serializeFilter(filter) { +export function serializeFilter(filter, operator = "==") { let tokens = ["("]; Object.entries(filter).forEach(([key, value], index) => { index && tokens.push(" AND "); - tokens.push(`(${key} == "${escape(value)}")`); + tokens.push(`(${key} ${operator} "${escape(value)}")`); }); tokens.push(")"); @@ -109,23 +109,33 @@ export async function listTags(tag, filter = {}) { } export async function listSongs(filter, window = null) { - let tokens = ["find"]; - tokens.push(serializeFilter(filter)); - if (window) { tokens.push("window", window.join(":")); } + let tokens = ["find", ...serializeFilter(filter)]; + window && tokens.push("window", window.join(":")); let lines = await command(tokens.join(" ")); return parser.songList(lines); } +export async function searchSongs(filter) { + let tokens = ["find", ...serializeFilter(filter, "contains")]; + let lines = await command(tokens.join(" ")); + return parser.songList(lines); + +} + export async function albumArt(songUrl) { let data = []; let offset = 0; + let params = ["albumart", `"${escape(songUrl)}"`, offset]; + while (1) { - let params = ["albumart", `"${escape(songUrl)}"`, offset]; - let lines = await command(params.join(" ")); - data = data.concat(lines[2]); - let metadata = parser.linesToStruct(lines.slice(0, 2)); - if (data.length >= Number(metadata["size"])) { return data; } - offset += Number(metadata["binary"]); + params[2] = offset; + try { + let lines = await command(params.join(" ")); + data = data.concat(lines[2]); + let metadata = parser.linesToStruct(lines.slice(0, 2)); + if (data.length >= Number(metadata["size"])) { return data; } + offset += Number(metadata["binary"]); + } catch (e) { return null; } } return null; } diff --git a/app/js/selection.js b/app/js/selection.js index 4ba69e0..ae891b5 100644 --- a/app/js/selection.js +++ b/app/js/selection.js @@ -25,7 +25,9 @@ export default class Selection { addCommandAll() { this.addCommand(_ => { - Array.from(this._component.children).forEach(node => this.add(node)); + Array.from(this._component.children) + .filter(node => node.tagName.toLowerCase().startsWith("cyp-")) + .forEach(node => this.add(node)); }, {label:"Select all", icon:"checkbox-marked-outline"}); } diff --git a/app/js/ui.js b/app/js/ui.js deleted file mode 100644 index 97744fe..0000000 --- a/app/js/ui.js +++ /dev/null @@ -1,153 +0,0 @@ -import * as mpd from "./mpd.js"; -import * as html from "./html.js"; -import * as format from "./format.js"; -import * as art from "./art.js"; - -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"; - -async 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; - } -} - -async function fillArt(parent, filter) { - let artist = filter["AlbumArtist"]; - let album = filter["Album"]; - let src = null; - - if (artist && album) { - src = await art.get(artist, album); - if (!src) { - let songs = await mpd.listSongs(filter, [0,1]); - if (songs.length) { - src = await art.get(artist, album, songs[0]["file"]); - } - } - } - - if (src) { - html.node("img", {src}, "", parent); - } else { - html.icon(album ? "album" : "artist", parent); - } -} - -function fileName(data) { - return data["file"].split("/").pop(); -} - -function formatSongInfo(ctx, data) { - let lines = []; - let tokens = []; - switch (ctx) { - case CTX_FS: lines.push(fileName(data)); break; - - case CTX_LIBRARY: - case CTX_QUEUE: - 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) { - let button = html.button({icon:"play", title:"Play"}, "", parent); - button.addEventListener("click", async e => { - 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"); - } - button.closest("cyp-app").querySelector("cyp-player").update(); // FIXME nejde to lepe? - }); - - return button; -} - -function addButton(type, what, parent) { - let button = html.button({icon:"plus", title:"Add to queue"}, "", parent); - button.addEventListener("click", async e => { - e.stopPropagation(); - await enqueue(type, what); - pubsub.publish("queue-change"); - // fixme notification? - }); - return button; -} - -export function song(ctx, data, parent) { - let node = html.node("li", {className:"song"}, "", parent); - let info = html.node("div", {className:"info"}, "", node); - - 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); - - - 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_LIBRARY: - case CTX_FS: - let url = data["file"]; - playButton(TYPE_URL, url, node); - addButton(TYPE_URL, url, node); - break; - } - - return node; -} - -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 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); - - playButton(type, urlOrFilter, node); - addButton(type, urlOrFilter, node); - - return node; -}