From cf46a0ffb1e1fcb71578ec5e17130a7786e52572 Mon Sep 17 00:00:00 2001 From: Ondrej Zara Date: Mon, 16 Mar 2020 22:57:13 +0100 Subject: [PATCH] yt finished, cleanup --- app/css/cyp.less | 3 +- app/css/elements/library.less | 9 -- app/css/elements/player.less | 2 +- app/css/elements/search.less | 23 ++++ app/css/elements/yt-result.less | 8 ++ app/css/elements/yt.less | 24 ---- app/css/icons.less | 2 +- app/css/search.less | 27 ----- app/css/variables.less | 1 + app/cyp.css | 2 +- app/cyp.js | 187 +++++++++++++++++++++----------- app/index.html | 2 +- app/js/component.js | 4 + app/js/conf.js | 1 - app/js/elements/app.js | 2 +- app/js/elements/library.js | 33 +++--- app/js/elements/menu.js | 7 ++ app/js/elements/queue.js | 4 +- app/js/elements/search.js | 33 ++++++ app/js/elements/yt-result.js | 19 ++++ app/js/elements/yt.js | 82 +++++++------- app/js/selection.js | 10 +- index.js | 15 +-- 23 files changed, 301 insertions(+), 199 deletions(-) create mode 100644 app/css/elements/search.less create mode 100644 app/css/elements/yt-result.less delete mode 100644 app/css/search.less create mode 100644 app/js/elements/search.js create mode 100644 app/js/elements/yt-result.js diff --git a/app/css/cyp.less b/app/css/cyp.less index b4b6848..93f6af8 100644 --- a/app/css/cyp.less +++ b/app/css/cyp.less @@ -70,7 +70,6 @@ select { @import "font.less"; @import "icons.less"; @import "mixins.less"; -@import "search.less"; @import "art.less"; @import "variables.less"; @@ -84,7 +83,9 @@ select { @import "elements/yt.less"; @import "elements/range.less"; @import "elements/playlist.less"; +@import "elements/search.less"; @import "elements/library.less"; @import "elements/tag.less"; @import "elements/back.less"; @import "elements/path.less"; +@import "elements/yt-result.less"; diff --git a/app/css/elements/library.less b/app/css/elements/library.less index a159b43..b99523c 100644 --- a/app/css/elements/library.less +++ b/app/css/elements/library.less @@ -15,13 +15,4 @@ cyp-library { } } } - - form { - .item; - .flex-row; - - button:first-of-type { - margin-left: var(--icon-spacing); - } - } } diff --git a/app/css/elements/player.less b/app/css/elements/player.less index 973e3fc..621e7eb 100644 --- a/app/css/elements/player.less +++ b/app/css/elements/player.less @@ -37,7 +37,7 @@ cyp-player { .timeline { flex: none; - height: 24px; + height: var(--icon-size); margin-bottom: 4px; .flex-row; diff --git a/app/css/elements/search.less b/app/css/elements/search.less new file mode 100644 index 0000000..e9425fb --- /dev/null +++ b/app/css/elements/search.less @@ -0,0 +1,23 @@ +cyp-search { + form { + .item; + align-items: stretch; + + button:first-of-type { + margin-left: var(--icon-spacing); + } + } + + &.pending form { + background-image: linear-gradient(var(--primary), var(--primary)); + background-repeat: no-repeat; + background-size: 25% var(--border-width); + animation: bar ease-in-out 3s alternate infinite; + } +} + + +@keyframes bar { + 0% { background-position: 0 100%; } + 100% { background-position: 100% 100%; } +} diff --git a/app/css/elements/yt-result.less b/app/css/elements/yt-result.less new file mode 100644 index 0000000..d6a33fd --- /dev/null +++ b/app/css/elements/yt-result.less @@ -0,0 +1,8 @@ +cyp-yt-result { + .item; + cursor: default; + + button .icon { + width: var(--icon-size); + } +} diff --git a/app/css/elements/yt.less b/app/css/elements/yt.less index c87b7cc..be15d14 100644 --- a/app/css/elements/yt.less +++ b/app/css/elements/yt.less @@ -1,32 +1,8 @@ cyp-yt { - header { - border-bottom: 1px solid var(--fg); - - button + button { - margin-left: 16px; - } - } - - .clear { - margin-left: auto; - } - pre { margin: 0.5em 0.5ch; flex-grow: 1; overflow: auto; white-space: pre-wrap; } - - &.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%; } } diff --git a/app/css/icons.less b/app/css/icons.less index 3d36bcc..227b4e5 100644 --- a/app/css/icons.less +++ b/app/css/icons.less @@ -1,5 +1,5 @@ .icon { - width: 24px; + width: var(--icon-size); flex: none; path, polygon, circle { diff --git a/app/css/search.less b/app/css/search.less deleted file mode 100644 index 0280401..0000000 --- a/app/css/search.less +++ /dev/null @@ -1,27 +0,0 @@ -.search { - .flex-row; - margin-left: auto; - transition: all 300ms; - width: 32px; - max-width: 20ch; - - .icon { - width: 32px; - cursor: pointer; - } - - input { - border: none; - outline: none; - color: inherit; - background-color: inherit; - border-bottom: 1px solid var(--fg); - width: 0; - padding: 0; - flex-grow: 1; - } - - &.open { - flex: 1; - } -} \ No newline at end of file diff --git a/app/css/variables.less b/app/css/variables.less index 38e8f1f..902ee25 100644 --- a/app/css/variables.less +++ b/app/css/variables.less @@ -1,6 +1,7 @@ @breakpoint-menu: 480px; cyp-app { + --icon-size: 24px; --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 6e7f306..a6d0299 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{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 .icon,.art img{display:block;width:100%}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:auto;--elapsed-color:var(--primary)}cyp-player .art{width:96px;height:96px}cyp-player .info{flex:auto;min-width:0;flex-direction:column;justify-content:space-between;padding:0 var(--icon-spacing)}cyp-player .info:not([hidden]){display:flex}cyp-player .info .title{margin-top:8px;font-size:18px;line-height:24px;font-weight:bold}cyp-player .info .title,cyp-player .info .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-player .timeline{flex:none;height:24px;margin-bottom:4px;flex-direction:row;align-items:center}cyp-player .timeline:not([hidden]){display:flex}cyp-player .timeline .duration,cyp-player .timeline .elapsed{flex:none;width:5ch;text-align:center}cyp-player .controls{width:220px;min-width:0;flex-direction:column}cyp-player .controls:not([hidden]){display:flex}cyp-player .controls .playback{flex:auto;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:none;margin-bottom:4px;flex-direction:row;align-items:center}cyp-player .controls .volume:not([hidden]){display:flex}cyp-player .controls .volume .mute{margin-right:var(--icon-spacing)}cyp-player .misc{flex:none;flex-direction:column;justify-content:space-around}cyp-player .misc:not([hidden]){display:flex}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:var(--icon-spacing)}cyp-tag{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;padding-left:0;padding-top:0;padding-bottom: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{margin-right:var(--icon-spacing);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 +*,*::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:var(--icon-size);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}.art .icon,.art img{display:block;width:100%}cyp-app{--icon-size:24px;--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:auto;--elapsed-color:var(--primary)}cyp-player .art{width:96px;height:96px}cyp-player .info{flex:auto;min-width:0;flex-direction:column;justify-content:space-between;padding:0 var(--icon-spacing)}cyp-player .info:not([hidden]){display:flex}cyp-player .info .title{margin-top:8px;font-size:18px;line-height:24px;font-weight:bold}cyp-player .info .title,cyp-player .info .subtitle{overflow:hidden;text-overflow:ellipsis}cyp-player .timeline{flex:none;height:var(--icon-size);margin-bottom:4px;flex-direction:row;align-items:center}cyp-player .timeline:not([hidden]){display:flex}cyp-player .timeline .duration,cyp-player .timeline .elapsed{flex:none;width:5ch;text-align:center}cyp-player .controls{width:220px;min-width:0;flex-direction:column}cyp-player .controls:not([hidden]){display:flex}cyp-player .controls .playback{flex:auto;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:none;margin-bottom:4px;flex-direction:row;align-items:center}cyp-player .controls .volume:not([hidden]){display:flex}cyp-player .controls .volume .mute{margin-right:var(--icon-spacing)}cyp-player .misc{flex:none;flex-direction:column;justify-content:space-around}cyp-player .misc:not([hidden]){display:flex}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 pre{margin:.5em .5ch;flex-grow:1;overflow:auto;white-space:pre-wrap}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-search form{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;align-items:stretch}cyp-search form:not([hidden]){display:flex}cyp-search form.selected{xbackground-color:var(--primary-tint)}cyp-search form.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-search form.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-search form:nth-child(odd){background-color:var(--bg-alt)}cyp-search form>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-search form .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-search form button:first-of-type{margin-left:auto}cyp-search form button .icon{width:32px}cyp-search form button:first-of-type{margin-left:var(--icon-spacing)}cyp-search.pending form{background-image:linear-gradient(var(--primary), var(--primary));background-repeat:no-repeat;background-size:25% var(--border-width);animation:bar ease-in-out 3s alternate infinite}@keyframes bar{0%{background-position:0 100%}100%{background-position:100% 100%}}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-tag{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;padding-left:0;padding-top:0;padding-bottom: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{margin-right:var(--icon-spacing);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}cyp-yt-result{flex-direction:row;align-items:center;cursor:pointer;position:relative;padding:8px;cursor:default}cyp-yt-result:not([hidden]){display:flex}cyp-yt-result.selected{xbackground-color:var(--primary-tint)}cyp-yt-result.selected::before{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--border-width);background-color:var(--primary)}cyp-yt-result.selected::after{content:"";position:absolute;left:0;top:0;bottom:0;right:0;background-color:var(--primary-tint)}cyp-yt-result:nth-child(odd){background-color:var(--bg-alt)}cyp-yt-result>.icon{margin-right:var(--icon-spacing);filter:drop-shadow(var(--text-shadow))}cyp-yt-result .title{font-size:18px;line-height:24px;overflow:hidden;text-overflow:ellipsis;font-weight:bold;min-width:0}cyp-yt-result button:first-of-type{margin-left:auto}cyp-yt-result button .icon{width:32px}cyp-yt-result button .icon{width:var(--icon-size)} \ No newline at end of file diff --git a/app/cyp.js b/app/cyp.js index 136d2a3..e70f6af 100644 --- a/app/cyp.js +++ b/app/cyp.js @@ -654,7 +654,6 @@ async function initMpd() { await init(); return mpd; } catch (e) { - console.error(e); return mpdMock; } } @@ -675,6 +674,7 @@ class App extends HTMLElement { const names = children.map(node => node.nodeName.toLowerCase()) .filter(name => name.startsWith("cyp-")); const unique = new Set(names); + console.log(unique); const promises = [...unique].map(name => customElements.whenDefined(name)); await Promise.all(promises); @@ -712,10 +712,12 @@ class Selection { this._component = component; /** @type {"single" | "multi"} */ this._mode = mode; - this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh? + this._items = []; this._node = node("cyp-commands", {hidden:true}); } + appendTo(parent) { parent.appendChild(this._node); } + clear() { while (this._items.length) { this.remove(this._items[0]); } } @@ -770,18 +772,16 @@ class Selection { } _show() { - const parent = this._component.closest("cyp-app").querySelector("footer"); // FIXME jde lepe? - parent.appendChild(this._node); - this._node.offsetWidth; // FIXME jde lepe? this._node.hidden = false; } _hide() { this._node.hidden = true; - this._node.remove(); } } +customElements.define("cyp-commands", class extends HTMLElement {}); + class Component extends HTMLElement { constructor(options = {}) { super(); @@ -789,6 +789,10 @@ class Component extends HTMLElement { } connectedCallback() { + if (this.selection) { + const parent = this._app.querySelector("footer"); + this.selection.appendTo(parent); + } this._app.addEventListener("load", _ => this._onAppLoad()); this._app.addEventListener("component-change", _ => { const component = this._app.component; @@ -814,6 +818,13 @@ class Menu extends Component { }); } + _onAppLoad() { + this._app.addEventListener("queue-length-change", e => { + this.querySelector(".queue-length").textContent = `(${e.detail})`; + }); + + } + async _activate(component) { const app = await this._app; app.setAttribute("component", component); @@ -1157,8 +1168,8 @@ class Queue extends Component { let songs = await this._mpd.listQueue(); this._buildSongs(songs); - // FIXME pubsub? - document.querySelector("#queue-length").textContent = `(${songs.length})`; + let e = new CustomEvent("queue-length-change", {detail:songs.length}); + this._app.dispatchEvent(e); } _updateCurrent() { @@ -1366,6 +1377,54 @@ class Settings extends Component { customElements.define("cyp-settings", Settings); +class Search extends HTMLElement { + constructor() { + super(); + this._built = false; + } + + get value() { return this._input.value.trim(); } + set value(value) { this._input.value = value; } + get _input() { return this.querySelector("input"); } + + onSubmit() {} + focus() { this._input.focus(); } + pending(pending) { this.classList.toggle("pending", pending); } + + connectedCallback() { + if (this._built) { return; } + + const form = node("form", {}, "", this); + node("input", {type:"text"}, "", form); + button({icon:"magnify"}, "", form); + + form.addEventListener("submit", e => { + e.preventDefault(); + this.onSubmit(); + }); + + this._built = true; + } +} + +customElements.define("cyp-search", Search); + +class YtResult extends Item { + constructor(title) { + super(); + this._title = title; + } + + connectedCallback() { + this.appendChild(icon("magnify")); + this._buildTitle(this._title); + } + + onClick() {} +} + +customElements.define("cyp-yt-result", YtResult); + const decoder = new TextDecoder("utf-8"); function decodeChunk(byteArray) { @@ -1374,57 +1433,52 @@ function decodeChunk(byteArray) { } class YT extends Component { + constructor() { + super(); + this._search = new Search(); + + this._search.onSubmit = _ => { + let query = this._search.value; + query && this._doSearch(query); + }; + } + connectedCallback() { super.connectedCallback(); - const form = node("form", {}, "", this); - const input = node("input", {type:"text"}, "", form); - button({icon:"magnify"}, "", form); - form.addEventListener("submit", e => { - e.preventDefault(); - const query = input.value.trim(); - if (!query.length) { return; } - this._doSearch(query, form); - }); - } - - async _doSearch(query, form) { - let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`); - let data = await response.json(); - - clear(this); - this.appendChild(form); - - console.log(data); - } - - - _download() { - let url = prompt("Please enter a YouTube URL:"); - if (!url) { return; } - - this._post(url); - } - - _search() { - let q = prompt("Please enter a search string:"); - if (!q) { return; } - - this._post(`ytsearch:${q}`); + this._clear(); } _clear() { - clear(this.querySelector("pre")); + clear(this); + this.appendChild(this._search); } - async _post(q) { - let pre = this.querySelector("pre"); - clear(pre); + async _doSearch(query) { + this._clear(); + this._search.pending(true); - this.classList.add("pending"); + let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`); + let results = await response.json(); + + this._search.pending(false); + + results.forEach(result => { + let node = new YtResult(result.title); + this.appendChild(node); + node.addButton("download", () => this._download(result.id)); + }); + } + + + async _download(id) { + this._clear(); + + let pre = node("pre", {}, "", this); + this._search.pending(true); let body = new URLSearchParams(); - body.set("q", q); + body.set("id", id); let response = await fetch("/youtube", {method:"POST", body}); let reader = response.body.getReader(); @@ -1436,7 +1490,7 @@ class YT extends Component { } reader.releaseLock(); - this.classList.remove("pending"); + this._search.pending(false); if (response.status == 200) { this._mpd.command(`update ${escape(ytPath)}`); @@ -1444,7 +1498,10 @@ class YT extends Component { } _onComponentChange(c, isThis) { + const wasHidden = this.hidden; this.hidden = !isThis; + + if (!wasHidden && isThis) { this._showRoot(); } } } @@ -1560,6 +1617,13 @@ class Library extends Component { super({selection:"multi"}); this._stateStack = []; this._initCommands(); + + this._search = new Search(); + this._search.onSubmit = _ => { + let query = this._search.value; + if (query.length < 3) { return; } + this._doSearch(query); + }; } _popState() { @@ -1642,29 +1706,26 @@ class Library extends Component { _showSearch(query = "") { clear(this); - const form = node("form", {}, "", this); - const input = node("input", {type:"text", value:query}, "", form); - button({icon:"magnify"}, "", form); - form.addEventListener("submit", e => { - e.preventDefault(); - const query = input.value.trim(); - if (query.length < 3) { return; } - this._doSearch(query, form); - }); + this.appendChild(this._search); + this._search.value = query; + this._search.focus(); - input.focus(); - if (query) { this._doSearch(query, form); } + query && this._search.onSubmit(); } - async _doSearch(query, form) { + async _doSearch(query) { let state = this._stateStack[this._stateStack.length-1]; state.query = query; + clear(this); + this.appendChild(this._search); + this._search.pending(true); + const songs1 = await this._mpd.searchSongs({"AlbumArtist": query}); const songs2 = await this._mpd.searchSongs({"Album": query}); const songs3 = await this._mpd.searchSongs({"Title": query}); - clear(this); - this.appendChild(form); + + this._search.pending(false); this._aggregateSearch(songs1, "AlbumArtist"); this._aggregateSearch(songs2, "Album"); diff --git a/app/index.html b/app/index.html index 0998b41..77e4cb3 100644 --- a/app/index.html +++ b/app/index.html @@ -76,7 +76,7 @@ diff --git a/app/js/component.js b/app/js/component.js index 6d6a37b..c1cee29 100644 --- a/app/js/component.js +++ b/app/js/component.js @@ -8,6 +8,10 @@ export default class Component extends HTMLElement { } connectedCallback() { + if (this.selection) { + const parent = this._app.querySelector("footer"); + this.selection.appendTo(parent); + } this._app.addEventListener("load", _ => this._onAppLoad()); this._app.addEventListener("component-change", _ => { const component = this._app.component; diff --git a/app/js/conf.js b/app/js/conf.js index 7dac997..ec0856e 100644 --- a/app/js/conf.js +++ b/app/js/conf.js @@ -1,3 +1,2 @@ export const artSize = 96; export const ytPath = "_youtube"; -export const locale = "cs"; diff --git a/app/js/elements/app.js b/app/js/elements/app.js index 7d3b4ef..2bdfcc3 100644 --- a/app/js/elements/app.js +++ b/app/js/elements/app.js @@ -14,7 +14,6 @@ async function initMpd() { await mpd.init(); return mpd; } catch (e) { - console.error(e); return mpdMock; } } @@ -35,6 +34,7 @@ class App extends HTMLElement { const names = children.map(node => node.nodeName.toLowerCase()) .filter(name => name.startsWith("cyp-")); const unique = new Set(names); + console.log(unique); const promises = [...unique].map(name => customElements.whenDefined(name)); await Promise.all(promises); diff --git a/app/js/elements/library.js b/app/js/elements/library.js index 56975b4..9801c8b 100644 --- a/app/js/elements/library.js +++ b/app/js/elements/library.js @@ -4,6 +4,7 @@ import Tag from "./tag.js"; import Path from "./path.js"; import Back from "./back.js"; import Song from "./song.js"; +import Search from "./search.js"; import { escape, serializeFilter } from "../mpd.js"; @@ -36,6 +37,13 @@ class Library extends Component { super({selection:"multi"}); this._stateStack = []; this._initCommands(); + + this._search = new Search(); + this._search.onSubmit = _ => { + let query = this._search.value; + if (query.length < 3) { return; } + this._doSearch(query); + } } _popState() { @@ -118,29 +126,26 @@ class Library extends Component { _showSearch(query = "") { html.clear(this); - const form = html.node("form", {}, "", this); - const input = html.node("input", {type:"text", value:query}, "", form); - html.button({icon:"magnify"}, "", form); - form.addEventListener("submit", e => { - e.preventDefault(); - const query = input.value.trim(); - if (query.length < 3) { return; } - this._doSearch(query, form); - }); + this.appendChild(this._search); + this._search.value = query; + this._search.focus(); - input.focus(); - if (query) { this._doSearch(query, form); } + query && this._search.onSubmit(); } - async _doSearch(query, form) { + async _doSearch(query) { let state = this._stateStack[this._stateStack.length-1]; state.query = query; + html.clear(this); + this.appendChild(this._search); + this._search.pending(true); + const songs1 = await this._mpd.searchSongs({"AlbumArtist": query}); const songs2 = await this._mpd.searchSongs({"Album": query}); const songs3 = await this._mpd.searchSongs({"Title": query}); - html.clear(this); - this.appendChild(form); + + this._search.pending(false); this._aggregateSearch(songs1, "AlbumArtist"); this._aggregateSearch(songs2, "Album"); diff --git a/app/js/elements/menu.js b/app/js/elements/menu.js index 22e8768..00bc229 100644 --- a/app/js/elements/menu.js +++ b/app/js/elements/menu.js @@ -10,6 +10,13 @@ class Menu extends Component { }); } + _onAppLoad() { + this._app.addEventListener("queue-length-change", e => { + this.querySelector(".queue-length").textContent = `(${e.detail})`; + }); + + } + async _activate(component) { const app = await this._app; app.setAttribute("component", component); diff --git a/app/js/elements/queue.js b/app/js/elements/queue.js index 9c08357..dbd7d95 100644 --- a/app/js/elements/queue.js +++ b/app/js/elements/queue.js @@ -52,8 +52,8 @@ class Queue extends Component { let songs = await this._mpd.listQueue(); this._buildSongs(songs); - // FIXME pubsub? - document.querySelector("#queue-length").textContent = `(${songs.length})`; + let e = new CustomEvent("queue-length-change", {detail:songs.length}); + this._app.dispatchEvent(e); } _updateCurrent() { diff --git a/app/js/elements/search.js b/app/js/elements/search.js new file mode 100644 index 0000000..e2f0a92 --- /dev/null +++ b/app/js/elements/search.js @@ -0,0 +1,33 @@ +import * as html from "../html.js"; + +export default class Search extends HTMLElement { + constructor() { + super(); + this._built = false; + } + + get value() { return this._input.value.trim(); } + set value(value) { this._input.value = value; } + get _input() { return this.querySelector("input"); } + + onSubmit() {} + focus() { this._input.focus(); } + pending(pending) { this.classList.toggle("pending", pending); } + + connectedCallback() { + if (this._built) { return; } + + const form = html.node("form", {}, "", this); + html.node("input", {type:"text"}, "", form); + html.button({icon:"magnify"}, "", form); + + form.addEventListener("submit", e => { + e.preventDefault(); + this.onSubmit(); + }); + + this._built = true; + } +} + +customElements.define("cyp-search", Search); \ No newline at end of file diff --git a/app/js/elements/yt-result.js b/app/js/elements/yt-result.js new file mode 100644 index 0000000..f1b947c --- /dev/null +++ b/app/js/elements/yt-result.js @@ -0,0 +1,19 @@ +import Item from "../item.js"; +import * as html from "../html.js"; + + +export default class YtResult extends Item { + constructor(title) { + super(); + this._title = title; + } + + connectedCallback() { + this.appendChild(html.icon("magnify")); + this._buildTitle(this._title); + } + + onClick() {} +} + +customElements.define("cyp-yt-result", YtResult); diff --git a/app/js/elements/yt.js b/app/js/elements/yt.js index 142c9b2..d530469 100644 --- a/app/js/elements/yt.js +++ b/app/js/elements/yt.js @@ -2,6 +2,8 @@ import * as html from "../html.js"; import * as conf from "../conf.js"; import { escape } from "../mpd.js"; import Component from "../component.js"; +import Search from "./search.js"; +import Result from "./yt-result.js"; const decoder = new TextDecoder("utf-8"); @@ -12,57 +14,52 @@ function decodeChunk(byteArray) { } class YT extends Component { + constructor() { + super(); + this._search = new Search(); + + this._search.onSubmit = _ => { + let query = this._search.value; + query && this._doSearch(query); + } + } + connectedCallback() { super.connectedCallback(); - 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 query = input.value.trim(); - if (!query.length) { return; } - this._doSearch(query, form); - }); - } - - async _doSearch(query, form) { - let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`); - let data = await response.json(); - - html.clear(this); - this.appendChild(form); - - console.log(data); - } - - - _download() { - let url = prompt("Please enter a YouTube URL:"); - if (!url) { return; } - - this._post(url); - } - - _search() { - let q = prompt("Please enter a search string:"); - if (!q) { return; } - - this._post(`ytsearch:${q}`); + this._clear(); } _clear() { - html.clear(this.querySelector("pre")); + html.clear(this); + this.appendChild(this._search); } - async _post(q) { - let pre = this.querySelector("pre"); - html.clear(pre); + async _doSearch(query) { + this._clear(); + this._search.pending(true); - this.classList.add("pending"); + let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`); + let results = await response.json(); + + this._search.pending(false); + + results.forEach(result => { + let node = new Result(result.title); + this.appendChild(node); + node.addButton("download", () => this._download(result.id)); + }); + } + + + async _download(id) { + this._clear(); + + let pre = html.node("pre", {}, "", this); + this._search.pending(true); let body = new URLSearchParams(); - body.set("q", q); + body.set("id", id); let response = await fetch("/youtube", {method:"POST", body}); let reader = response.body.getReader(); @@ -74,7 +71,7 @@ class YT extends Component { } reader.releaseLock(); - this.classList.remove("pending"); + this._search.pending(false); if (response.status == 200) { this._mpd.command(`update ${escape(conf.ytPath)}`); @@ -82,7 +79,10 @@ class YT extends Component { } _onComponentChange(c, isThis) { + const wasHidden = this.hidden; this.hidden = !isThis; + + if (!wasHidden && isThis) { this._showRoot(); } } } diff --git a/app/js/selection.js b/app/js/selection.js index f50a49c..96d1cf9 100644 --- a/app/js/selection.js +++ b/app/js/selection.js @@ -8,10 +8,12 @@ export default class Selection { this._component = component; /** @type {"single" | "multi"} */ this._mode = mode; - this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh? + this._items = []; this._node = html.node("cyp-commands", {hidden:true}); } + appendTo(parent) { parent.appendChild(this._node); } + clear() { while (this._items.length) { this.remove(this._items[0]); } } @@ -66,14 +68,12 @@ export default class Selection { } _show() { - const parent = this._component.closest("cyp-app").querySelector("footer"); // FIXME jde lepe? - parent.appendChild(this._node); - this._node.offsetWidth; // FIXME jde lepe? this._node.hidden = false; } _hide() { this._node.hidden = true; - this._node.remove(); } } + +customElements.define("cyp-commands", class extends HTMLElement {}); \ No newline at end of file diff --git a/index.js b/index.js index b054e86..4eca3b4 100644 --- a/index.js +++ b/index.js @@ -14,8 +14,9 @@ function searchYoutube(q, response) { response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks console.log("YouTube searching", q); - q = escape(`ytsearch10:${q}`); + q = escape(`ytsearch3:${q}`); const command = `${cmd} -j ${q} | jq "{id,title}" | jq -s .`; + require("child_process").exec(command, {}, (error, stdout, stderr) => { if (error) { console.log("error", error); @@ -28,14 +29,14 @@ function searchYoutube(q, response) { } -function downloadYoutube(q, response) { +function downloadYoutube(id, response) { response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks - console.log("YouTube downloading", q); + console.log("YouTube downloading", id); let args = [ "-f", "bestaudio", "-o", `${__dirname}/_youtube/%(title)s-%(id)s.%(ext)s`, - q + id ] let child = require("child_process").spawn(cmd, args); @@ -70,9 +71,9 @@ function handleYoutubeDownload(request, response) { request.setEncoding("utf8"); request.on("data", chunk => str += chunk); request.on("end", () => { - let q = require("querystring").parse(str)["id"]; - if (q) { - downloadYoutube(q, response); + let id = require("querystring").parse(str)["id"]; + if (id) { + downloadYoutube(id, response); } else { response.writeHead(404); response.end();