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();