selection wip
This commit is contained in:
parent
bb5e2d1fb6
commit
b81d1edea2
17 changed files with 452 additions and 185 deletions
202
app/app.css
202
app/app.css
|
@ -9,6 +9,10 @@ html {
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
cyp-app {
|
cyp-app {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -27,9 +31,19 @@ cyp-app:not([hidden]) {
|
||||||
}
|
}
|
||||||
header,
|
header,
|
||||||
footer {
|
footer {
|
||||||
|
flex-shrink: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
footer {
|
||||||
|
position: relative;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
footer {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
button {
|
button {
|
||||||
|
@ -108,6 +122,12 @@ select {
|
||||||
.multiline h2 {
|
.multiline h2 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
.selectable {
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
.selectable.selected {
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
}
|
||||||
x-range {
|
x-range {
|
||||||
--thumb-size: 8px;
|
--thumb-size: 8px;
|
||||||
--thumb-color: #fff;
|
--thumb-color: #fff;
|
||||||
|
@ -170,49 +190,6 @@ x-range[disabled] {
|
||||||
x-range:not([disabled]) .-thumb:hover {
|
x-range:not([disabled]) .-thumb:hover {
|
||||||
background-color: var(--thumb-hover-color);
|
background-color: var(--thumb-hover-color);
|
||||||
}
|
}
|
||||||
main {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
cyp-menu {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: 56px;
|
|
||||||
}
|
|
||||||
cyp-menu:not([hidden]) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
cyp-menu button {
|
|
||||||
flex: 1 0 0;
|
|
||||||
height: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 4px;
|
|
||||||
border-top: 4px solid transparent;
|
|
||||||
}
|
|
||||||
cyp-menu button:not([hidden]) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
cyp-menu button .icon {
|
|
||||||
margin-right: var(--icon-spacing);
|
|
||||||
}
|
|
||||||
cyp-menu button.active {
|
|
||||||
border-top-color: var(--primary);
|
|
||||||
color: var(--primary);
|
|
||||||
background-color: rgb(var(--primary-raw), 0.1);
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
cyp-menu button {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
cyp-menu button:not([data-for=queue]) .icon {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
cyp-menu button span:not([id]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cyp-player {
|
cyp-player {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -693,80 +670,80 @@ cyp-playlists .info:not([hidden]) {
|
||||||
cyp-playlists .info h2 {
|
cyp-playlists .info h2 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
#yt header {
|
cyp-yt header {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing);
|
padding: var(--spacing);
|
||||||
}
|
}
|
||||||
#yt header:not([hidden]) {
|
cyp-yt header:not([hidden]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
#yt header button {
|
cyp-yt header button {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#yt header button .icon {
|
cyp-yt header button .icon {
|
||||||
margin-right: var(--icon-spacing);
|
margin-right: var(--icon-spacing);
|
||||||
}
|
}
|
||||||
#yt ul {
|
cyp-yt ul {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#yt li {
|
cyp-yt li {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#yt li:not([hidden]) {
|
cyp-yt li:not([hidden]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
#yt li .info {
|
cyp-yt li .info {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#yt li .info .icon {
|
cyp-yt li .info .icon {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
margin-right: var(--icon-spacing);
|
margin-right: var(--icon-spacing);
|
||||||
filter: drop-shadow(var(--text-shadow));
|
filter: drop-shadow(var(--text-shadow));
|
||||||
}
|
}
|
||||||
#yt li .info h2 {
|
cyp-yt li .info h2 {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#yt li .info h2,
|
cyp-yt li .info h2,
|
||||||
#yt li .info div {
|
cyp-yt li .info div {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#yt li:not(.has-art) {
|
cyp-yt li:not(.has-art) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
#yt li button .icon {
|
cyp-yt li button .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
#yt li:nth-child(odd) {
|
cyp-yt li:nth-child(odd) {
|
||||||
background-color: var(--bg-alt);
|
background-color: var(--bg-alt);
|
||||||
}
|
}
|
||||||
#yt header {
|
cyp-yt header {
|
||||||
border-bottom: 1px solid var(--fg);
|
border-bottom: 1px solid var(--fg);
|
||||||
}
|
}
|
||||||
#yt header button + button {
|
cyp-yt header button + button {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
#yt .clear {
|
cyp-yt .clear {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
#yt pre {
|
cyp-yt pre {
|
||||||
margin: 0.5em 0.5ch;
|
margin: 0.5em 0.5ch;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
#yt.pending header {
|
cyp-yt.pending header {
|
||||||
background-image: linear-gradient(var(--primary), var(--primary));
|
background-image: linear-gradient(var(--primary), var(--primary));
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 25% 4px;
|
background-size: 25% 4px;
|
||||||
|
@ -896,3 +873,102 @@ cyp-app[color=limegreen] {
|
||||||
--spacing: var(--icon-spacing);
|
--spacing: var(--icon-spacing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cyp-song {
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
cyp-song.selected {
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
}
|
||||||
|
cyp-song:not([hidden]) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
cyp-song .info {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
cyp-song .info .icon {
|
||||||
|
color: var(--primary);
|
||||||
|
margin-right: var(--icon-spacing);
|
||||||
|
filter: drop-shadow(var(--text-shadow));
|
||||||
|
}
|
||||||
|
cyp-song .info h2 {
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
cyp-song .info h2,
|
||||||
|
cyp-song .info div {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
cyp-song:nth-child(odd) {
|
||||||
|
background-color: var(--bg-alt);
|
||||||
|
}
|
||||||
|
cyp-song:not(.has-art) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
flex: 1 0 0;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px 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: rgb(var(--primary-raw), 0.1);
|
||||||
|
}
|
||||||
|
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 0 80px;
|
||||||
|
}
|
||||||
|
cyp-commands button.last {
|
||||||
|
order: 1;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
cyp-app {
|
cyp-app {
|
||||||
.flex-column;
|
.flex-column;
|
||||||
|
|
||||||
|
@ -24,10 +29,19 @@ cyp-app {
|
||||||
}
|
}
|
||||||
|
|
||||||
header, footer {
|
header, footer {
|
||||||
|
flex-shrink: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: relative;
|
||||||
|
height: 56px;
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input, select, button {
|
input, select, button {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
@ -60,8 +74,6 @@ select {
|
||||||
@import "icons.less";
|
@import "icons.less";
|
||||||
@import "mixins.less";
|
@import "mixins.less";
|
||||||
@import "range.less";
|
@import "range.less";
|
||||||
@import "main.less";
|
|
||||||
@import "menu.less";
|
|
||||||
@import "player.less";
|
@import "player.less";
|
||||||
@import "component.less";
|
@import "component.less";
|
||||||
@import "queue.less";
|
@import "queue.less";
|
||||||
|
@ -73,3 +85,6 @@ select {
|
||||||
@import "search.less";
|
@import "search.less";
|
||||||
@import "art.less";
|
@import "art.less";
|
||||||
@import "variables.less";
|
@import "variables.less";
|
||||||
|
|
||||||
|
@import "song.less";
|
||||||
|
@import "menu.less";
|
||||||
|
|
0
app/css/commands.less
Normal file
0
app/css/commands.less
Normal file
|
@ -1,4 +0,0 @@
|
||||||
main {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
|
@ -1,16 +1,25 @@
|
||||||
cyp-menu {
|
cyp-menu, cyp-commands {
|
||||||
.flex-row;
|
.flex-row;
|
||||||
height: 56px;
|
height: 100%;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
flex: 1 0 0;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.flex-column;
|
.flex-column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 4px;
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
flex-direction: row;
|
||||||
|
span:not([id]) { display: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cyp-menu button {
|
||||||
|
flex: 1 0 0;
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: var(--icon-spacing);
|
margin-right: var(--icon-spacing);
|
||||||
|
@ -21,13 +30,27 @@ cyp-menu {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
background-color: rgb(var(--primary-raw), 0.1);
|
background-color: rgb(var(--primary-raw), 0.1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
cyp-commands {
|
||||||
flex-direction: row;
|
position: absolute;
|
||||||
justify-content: center;
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
transition: top 300ms;
|
||||||
|
|
||||||
&:not([data-for=queue]) .icon { margin-right: 0; }
|
background-color: var(--bg);
|
||||||
span:not([id]) { display: none; }
|
|
||||||
|
&[hidden] {
|
||||||
|
display: flex;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
&.last {
|
||||||
|
order: 1;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,3 +20,11 @@
|
||||||
|
|
||||||
h2 { font-weight: normal; }
|
h2 { font-weight: normal; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectable {
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
30
app/css/song.less
Normal file
30
app/css/song.less
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
cyp-song {
|
||||||
|
.selectable;
|
||||||
|
.flex-row;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--primary);
|
||||||
|
margin-right: var(--icon-spacing);
|
||||||
|
filter: drop-shadow(var(--text-shadow));
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, div { .long-line; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
background-color: var(--bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.has-art) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
#yt {
|
cyp-yt {
|
||||||
.component;
|
.component;
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
|
|
@ -40,11 +40,12 @@
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<cyp-queue>
|
<cyp-queue>
|
||||||
|
<!--
|
||||||
<header>
|
<header>
|
||||||
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
||||||
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
||||||
</header>
|
</header>
|
||||||
<ul></ul>
|
-->
|
||||||
</cyp-queue>
|
</cyp-queue>
|
||||||
<cyp-playlists>
|
<cyp-playlists>
|
||||||
<ul></ul>
|
<ul></ul>
|
||||||
|
@ -57,14 +58,14 @@
|
||||||
<header></header>
|
<header></header>
|
||||||
<ul></ul>
|
<ul></ul>
|
||||||
</section>
|
</section>
|
||||||
<section id="yt">
|
<cyp-yt>
|
||||||
<header>
|
<header>
|
||||||
<button class="download" data-icon="download">Download</button>
|
<button class="download" data-icon="download">Download</button>
|
||||||
<button class="search-download" data-icon="magnify">Search & Download</button>
|
<button class="search-download" data-icon="magnify">Search & Download</button>
|
||||||
<button class="clear" data-icon="close">Clear</button>
|
<button class="clear" data-icon="close">Clear</button>
|
||||||
</header>
|
</header>
|
||||||
<pre></pre>
|
<pre></pre>
|
||||||
</section>
|
</cyp-yt>
|
||||||
<cyp-settings>
|
<cyp-settings>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Theme</dt>
|
<dt>Theme</dt>
|
||||||
|
|
|
@ -20,6 +20,16 @@ function initIcons() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initMpd() {
|
||||||
|
try {
|
||||||
|
await mpd.init();
|
||||||
|
return mpd;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return mpdMock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class App extends HTMLElement {
|
class App extends HTMLElement {
|
||||||
static get observedAttributes() { return ["component"]; }
|
static get observedAttributes() { return ["component"]; }
|
||||||
|
|
||||||
|
@ -28,43 +38,33 @@ class App extends HTMLElement {
|
||||||
|
|
||||||
initIcons();
|
initIcons();
|
||||||
|
|
||||||
this._load();
|
this._mpdPromise = initMpd().then(mpd => this.mpd = mpd);
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
|
||||||
switch (name) {
|
|
||||||
case "component":
|
|
||||||
const e = new CustomEvent("component-change");
|
|
||||||
this.dispatchEvent(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _load() {
|
|
||||||
try {
|
|
||||||
await mpd.init();
|
|
||||||
this.mpd = mpd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.mpd = mpdMock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
|
const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
|
||||||
|
promises.push(this._mpdPromise);
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("load"));
|
this.dispatchEvent(new CustomEvent("load"));
|
||||||
|
|
||||||
const onHashChange = () => {
|
const onHashChange = () => {
|
||||||
const hash = location.hash.substring(1);
|
const hash = location.hash.substring(1);
|
||||||
this._activate(hash || "queue");
|
this.setAttribute("component", hash || "queue");
|
||||||
}
|
}
|
||||||
window.addEventListener("hashchange", onHashChange);
|
window.addEventListener("hashchange", onHashChange);
|
||||||
onHashChange();
|
onHashChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
_activate(what) {
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
location.hash = what;
|
switch (name) {
|
||||||
this.setAttribute("component", what);
|
case "component":
|
||||||
|
location.hash = newValue;
|
||||||
|
const e = new CustomEvent("component-change");
|
||||||
|
this.dispatchEvent(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const APP = "cyp-app";
|
import Selection from "./lib/selection.js";
|
||||||
|
|
||||||
export class HasApp extends HTMLElement {
|
export class HasApp extends HTMLElement {
|
||||||
get _app() { return this.closest("cyp-app"); }
|
get _app() { return this.closest("cyp-app"); }
|
||||||
|
@ -8,7 +8,10 @@ export class HasApp extends HTMLElement {
|
||||||
export default class Component extends HasApp {
|
export default class Component extends HasApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.selection = new Selection(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
this._app.addEventListener("load", _ => this._onAppLoad());
|
this._app.addEventListener("load", _ => this._onAppLoad());
|
||||||
this._app.addEventListener("component-change", _ => {
|
this._app.addEventListener("component-change", _ => {
|
||||||
const component = this._app.getAttribute("component");
|
const component = this._app.getAttribute("component");
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import * as app from "./app.js";
|
|
||||||
import * as mpd from "./lib/mpd.js";
|
import * as mpd from "./lib/mpd.js";
|
||||||
import * as html from "./lib/html.js";
|
import * as html from "./lib/html.js";
|
||||||
import * as player from "./player.js";
|
|
||||||
import * as format from "./lib/format.js";
|
|
||||||
import * as ui from "./lib/ui.js";
|
import * as ui from "./lib/ui.js";
|
||||||
|
|
||||||
import Search from "./lib/search.js";
|
import Search from "./lib/search.js";
|
||||||
|
|
58
app/js/lib/selection.js
Normal file
58
app/js/lib/selection.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import * as html from "./html.js";
|
||||||
|
|
||||||
|
export default class Selection {
|
||||||
|
constructor(component) {
|
||||||
|
this._component = component;
|
||||||
|
this._items = new Set();
|
||||||
|
this._node = html.node("cyp-commands", {hidden:true});
|
||||||
|
|
||||||
|
const button = this.addCommand(_ => this.clear(), {icon:"close", label:"Clear"});
|
||||||
|
button.classList.add("last");
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const nodes = Array.from(this._items);
|
||||||
|
while (nodes.length) { this._remove(nodes.pop()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(node) {
|
||||||
|
if (this._items.has(node)) {
|
||||||
|
this._remove(node);
|
||||||
|
} else {
|
||||||
|
this._add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommand(cb, options) {
|
||||||
|
const button = html.button({icon:options.icon}, "", this._node);
|
||||||
|
html.node("span", {}, options.label, button);
|
||||||
|
button.addEventListener("click", _ => cb(this._items));
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
_add(node) {
|
||||||
|
const size = this._items.size;
|
||||||
|
this._items.add(node);
|
||||||
|
node.classList.add("selected");
|
||||||
|
if (size == 0) { this._show(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
_remove(node) {
|
||||||
|
this._items.delete(node);
|
||||||
|
node.classList.remove("selected");
|
||||||
|
if (this._items.size == 0) { this._hide(); }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_show() {
|
||||||
|
const parent = this._component.closest("cyp-app").querySelector("footer");
|
||||||
|
parent.appendChild(this._node);
|
||||||
|
this._node.offsetWidth; // FIXME jde lepe?
|
||||||
|
this._node.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hide() {
|
||||||
|
this._node.hidden = true;
|
||||||
|
this._node.remove();
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,11 +86,10 @@ function playButton(type, what, parent) {
|
||||||
await mpd.command("play");
|
await mpd.command("play");
|
||||||
pubsub.publish("queue-change");
|
pubsub.publish("queue-change");
|
||||||
}
|
}
|
||||||
player.update();
|
button.closest("cyp-app").querySelector("cyp-player").update(); // FIXME nejde to lepe?
|
||||||
});
|
});
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteButton(type, id, parent) {
|
function deleteButton(type, id, parent) {
|
||||||
|
|
|
@ -14,8 +14,15 @@ class Player extends Component {
|
||||||
this._dom = this._initDOM();
|
this._dom = this._initDOM();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
this._clearIdle();
|
||||||
|
const data = await this._mpd.status();
|
||||||
|
this._sync(data);
|
||||||
|
this._idle();
|
||||||
|
}
|
||||||
|
|
||||||
_onAppLoad() {
|
_onAppLoad() {
|
||||||
this._update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
_initDOM() {
|
_initDOM() {
|
||||||
|
@ -50,7 +57,7 @@ class Player extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_idle() {
|
_idle() {
|
||||||
this._idleTimeout = setTimeout(() => this._update(), DELAY);
|
this._idleTimeout = setTimeout(() => this.update(), DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearIdle() {
|
_clearIdle() {
|
||||||
|
@ -58,13 +65,6 @@ class Player extends Component {
|
||||||
this._idleTimeout = null;
|
this._idleTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _update() {
|
|
||||||
this._clearIdle();
|
|
||||||
const data = await this._mpd.status();
|
|
||||||
this._sync(data);
|
|
||||||
this._idle();
|
|
||||||
}
|
|
||||||
|
|
||||||
_sync(data) {
|
_sync(data) {
|
||||||
const DOM = this._dom;
|
const DOM = this._dom;
|
||||||
if ("volume" in data) {
|
if ("volume" in data) {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import * as html from "./lib/html.js";
|
import * as html from "./lib/html.js";
|
||||||
import * as ui from "./lib/ui.js";
|
import * as format from "./lib/format.js";
|
||||||
|
|
||||||
import Component from "./component.js";
|
import Component, { HasApp } from "./component.js";
|
||||||
|
|
||||||
class Queue extends Component {
|
class Queue extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._currentId = null;
|
this._currentId = null;
|
||||||
|
/*
|
||||||
this.querySelector(".clear").addEventListener("click", async _ => {
|
this.querySelector(".clear").addEventListener("click", async _ => {
|
||||||
await this._mpd.command("clear");
|
await this._mpd.command("clear");
|
||||||
this._sync();
|
this._sync();
|
||||||
|
@ -18,6 +18,7 @@ class Queue extends Component {
|
||||||
if (name === null) { return; }
|
if (name === null) { return; }
|
||||||
this._mpd.command(`save "${this._mpd.escape(name)}"`);
|
this._mpd.command(`save "${this._mpd.escape(name)}"`);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(e) {
|
handleEvent(e) {
|
||||||
|
@ -54,19 +55,72 @@ class Queue extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateCurrent() {
|
_updateCurrent() {
|
||||||
Array.from(this.querySelectorAll("[data-song-id]")).forEach(/** @param {HTMLElement} node */ node => {
|
Array.from(this.children).forEach(/** @param {HTMLElement} node */ node => {
|
||||||
node.classList.toggle("current", node.dataset.songId == this._currentId);
|
node.classList.toggle("current", node.dataset.songId == this._currentId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSongs(songs) {
|
_buildSongs(songs) {
|
||||||
let ul = this.querySelector("ul");
|
html.clear(this);
|
||||||
html.clear(ul);
|
|
||||||
|
|
||||||
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
|
songs.forEach(song => this.appendChild(new Song(song)));
|
||||||
|
|
||||||
this._updateCurrent();
|
this._updateCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("cyp-queue", Queue);
|
customElements.define("cyp-queue", Queue);
|
||||||
|
|
||||||
|
class Item extends HasApp {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.addEventListener("click", e => this.parentNode.selection.toggle(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Song extends Item {
|
||||||
|
constructor(data) {
|
||||||
|
super();
|
||||||
|
this._data = data;
|
||||||
|
this.dataset.songId = data["Id"];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
let info = html.node("div", {className:"info"}, "", this);
|
||||||
|
|
||||||
|
let lines = formatSongInfo(this._data);
|
||||||
|
html.node("h2", {}, lines.shift(), info);
|
||||||
|
lines.length && html.node("div", {}, lines.shift(), info);
|
||||||
|
|
||||||
|
/*
|
||||||
|
playButton(TYPE_ID, id, node);
|
||||||
|
deleteButton(TYPE_ID, id, node);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-song", Song);
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME vyfaktorovat nekam do haje
|
||||||
|
function formatSongInfo(data) {
|
||||||
|
let lines = [];
|
||||||
|
let tokens = [];
|
||||||
|
|
||||||
|
if (data["Title"]) {
|
||||||
|
tokens.push(data["Title"]);
|
||||||
|
lines.push(tokens.join(" "));
|
||||||
|
lines.push(format.subtitle(data));
|
||||||
|
} else {
|
||||||
|
lines.push(fileName(data));
|
||||||
|
lines.push("\u00A0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME vyfaktorovat nekam do haje
|
||||||
|
function fileName(data) {
|
||||||
|
return data["file"].split("/").pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
67
app/js/yt.js
67
app/js/yt.js
|
@ -1,8 +1,9 @@
|
||||||
import * as mpd from "./lib/mpd.js";
|
|
||||||
import * as html from "./lib/html.js";
|
import * as html from "./lib/html.js";
|
||||||
import * as conf from "./conf.js";
|
import * as conf from "./conf.js";
|
||||||
|
|
||||||
let node;
|
import Component from "./component.js";
|
||||||
|
|
||||||
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
function decodeChunk(byteArray) {
|
function decodeChunk(byteArray) {
|
||||||
|
@ -10,11 +11,36 @@ function decodeChunk(byteArray) {
|
||||||
return decoder.decode(byteArray).replace(/\u000d/g, "\n");
|
return decoder.decode(byteArray).replace(/\u000d/g, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(q) {
|
class YT extends Component {
|
||||||
let pre = node.querySelector("pre");
|
_onAppLoad() {
|
||||||
|
this.querySelector(".download").addEventListener("click", _ => this._download());
|
||||||
|
this.querySelector(".search-download").addEventListener("click", _ => this._search());
|
||||||
|
this.querySelector(".clear").addEventListener("click", _ => this._clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
_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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clear() {
|
||||||
|
html.clear(this.querySelector("pre"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _post(q) {
|
||||||
|
let pre = this.querySelector("pre");
|
||||||
html.clear(pre);
|
html.clear(pre);
|
||||||
|
|
||||||
node.classList.add("pending");
|
this.classList.add("pending");
|
||||||
|
|
||||||
let body = new URLSearchParams();
|
let body = new URLSearchParams();
|
||||||
body.set("q", q);
|
body.set("q", q);
|
||||||
|
@ -29,35 +55,16 @@ async function post(q) {
|
||||||
}
|
}
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
|
|
||||||
node.classList.remove("pending");
|
this.classList.remove("pending");
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
mpd.command(`update ${mpd.escape(conf.ytPath)}`);
|
this._mpd.command(`update ${this._mpd.escape(conf.ytPath)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function download() {
|
_onComponentChange(c, isThis) {
|
||||||
let url = prompt("Please enter a YouTube URL:");
|
this.hidden = !isThis;
|
||||||
if (!url) { return; }
|
}
|
||||||
|
|
||||||
post(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function search() {
|
customElements.define("cyp-yt", YT);
|
||||||
let q = prompt("Please enter a search string:");
|
|
||||||
if (!q) { return; }
|
|
||||||
post(`ytsearch:${q}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
html.clear(node.querySelector("pre"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function activate() {}
|
|
||||||
|
|
||||||
export function init(n) {
|
|
||||||
node = n;
|
|
||||||
node.querySelector(".download").addEventListener("click", e => download());
|
|
||||||
node.querySelector(".search-download").addEventListener("click", e => search());
|
|
||||||
node.querySelector(".clear").addEventListener("click", e => clear());
|
|
||||||
}
|
|
Loading…
Reference in a new issue