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 {
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
cyp-app {
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
@ -27,9 +31,19 @@ cyp-app:not([hidden]) {
|
|||
}
|
||||
header,
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
footer {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
footer {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
|
@ -108,6 +122,12 @@ select {
|
|||
.multiline h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
.selectable {
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.selectable.selected {
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
x-range {
|
||||
--thumb-size: 8px;
|
||||
--thumb-color: #fff;
|
||||
|
@ -170,49 +190,6 @@ x-range[disabled] {
|
|||
x-range:not([disabled]) .-thumb:hover {
|
||||
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 {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -693,80 +670,80 @@ cyp-playlists .info:not([hidden]) {
|
|||
cyp-playlists .info h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#yt header {
|
||||
cyp-yt header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#yt header:not([hidden]) {
|
||||
cyp-yt header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#yt header button {
|
||||
cyp-yt header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
}
|
||||
#yt header button .icon {
|
||||
cyp-yt header button .icon {
|
||||
margin-right: var(--icon-spacing);
|
||||
}
|
||||
#yt ul {
|
||||
cyp-yt ul {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#yt li {
|
||||
cyp-yt li {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#yt li:not([hidden]) {
|
||||
cyp-yt li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#yt li .info {
|
||||
cyp-yt li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
#yt li .info .icon {
|
||||
cyp-yt li .info .icon {
|
||||
color: var(--primary);
|
||||
margin-right: var(--icon-spacing);
|
||||
filter: drop-shadow(var(--text-shadow));
|
||||
}
|
||||
#yt li .info h2 {
|
||||
cyp-yt li .info h2 {
|
||||
font-size: var(--font-size-large);
|
||||
margin: 0;
|
||||
}
|
||||
#yt li .info h2,
|
||||
#yt li .info div {
|
||||
cyp-yt li .info h2,
|
||||
cyp-yt li .info div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#yt li:not(.has-art) {
|
||||
cyp-yt li:not(.has-art) {
|
||||
padding: 8px;
|
||||
}
|
||||
#yt li button .icon {
|
||||
cyp-yt li button .icon {
|
||||
width: 32px;
|
||||
}
|
||||
#yt li:nth-child(odd) {
|
||||
cyp-yt li:nth-child(odd) {
|
||||
background-color: var(--bg-alt);
|
||||
}
|
||||
#yt header {
|
||||
cyp-yt header {
|
||||
border-bottom: 1px solid var(--fg);
|
||||
}
|
||||
#yt header button + button {
|
||||
cyp-yt header button + button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
#yt .clear {
|
||||
cyp-yt .clear {
|
||||
margin-left: auto;
|
||||
}
|
||||
#yt pre {
|
||||
cyp-yt pre {
|
||||
margin: 0.5em 0.5ch;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#yt.pending header {
|
||||
cyp-yt.pending header {
|
||||
background-image: linear-gradient(var(--primary), var(--primary));
|
||||
background-repeat: no-repeat;
|
||||
background-size: 25% 4px;
|
||||
|
@ -896,3 +873,102 @@ cyp-app[color=limegreen] {
|
|||
--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;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
cyp-app {
|
||||
.flex-column;
|
||||
|
||||
|
@ -24,10 +29,19 @@ cyp-app {
|
|||
}
|
||||
|
||||
header, footer {
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
footer {
|
||||
position: relative;
|
||||
height: 56px;
|
||||
@media (max-width: 480px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
|
@ -60,8 +74,6 @@ select {
|
|||
@import "icons.less";
|
||||
@import "mixins.less";
|
||||
@import "range.less";
|
||||
@import "main.less";
|
||||
@import "menu.less";
|
||||
@import "player.less";
|
||||
@import "component.less";
|
||||
@import "queue.less";
|
||||
|
@ -73,3 +85,6 @@ select {
|
|||
@import "search.less";
|
||||
@import "art.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,33 +1,56 @@
|
|||
cyp-menu {
|
||||
cyp-menu, cyp-commands {
|
||||
.flex-row;
|
||||
height: 56px;
|
||||
height: 100%;
|
||||
|
||||
button {
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
|
||||
.flex-column;
|
||||
align-items: center;
|
||||
padding-top: 4px;
|
||||
|
||||
border-top: 4px solid transparent;
|
||||
|
||||
.icon {
|
||||
margin-right: var(--icon-spacing);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-top-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background-color: rgb(var(--primary-raw), 0.1);
|
||||
}
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
&:not([data-for=queue]) .icon { margin-right: 0; }
|
||||
span:not([id]) { display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cyp-menu button {
|
||||
flex: 1 0 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
|
||||
.icon {
|
||||
margin-right: var(--icon-spacing);
|
||||
}
|
||||
|
||||
&.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);
|
||||
|
||||
&[hidden] {
|
||||
display: flex;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 0 0 80px;
|
||||
&.last {
|
||||
order: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,3 +20,11 @@
|
|||
|
||||
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;
|
||||
|
||||
header {
|
||||
|
|
|
@ -40,11 +40,12 @@
|
|||
</header>
|
||||
<main>
|
||||
<cyp-queue>
|
||||
<!--
|
||||
<header>
|
||||
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
||||
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
||||
</header>
|
||||
<ul></ul>
|
||||
-->
|
||||
</cyp-queue>
|
||||
<cyp-playlists>
|
||||
<ul></ul>
|
||||
|
@ -57,14 +58,14 @@
|
|||
<header></header>
|
||||
<ul></ul>
|
||||
</section>
|
||||
<section id="yt">
|
||||
<cyp-yt>
|
||||
<header>
|
||||
<button class="download" data-icon="download">Download</button>
|
||||
<button class="search-download" data-icon="magnify">Search & Download</button>
|
||||
<button class="clear" data-icon="close">Clear</button>
|
||||
</header>
|
||||
<pre></pre>
|
||||
</section>
|
||||
</cyp-yt>
|
||||
<cyp-settings>
|
||||
<dl>
|
||||
<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 {
|
||||
static get observedAttributes() { return ["component"]; }
|
||||
|
||||
|
@ -28,43 +38,33 @@ class App extends HTMLElement {
|
|||
|
||||
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));
|
||||
promises.push(this._mpdPromise);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("load"));
|
||||
|
||||
const onHashChange = () => {
|
||||
const hash = location.hash.substring(1);
|
||||
this._activate(hash || "queue");
|
||||
this.setAttribute("component", hash || "queue");
|
||||
}
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
onHashChange();
|
||||
}
|
||||
|
||||
_activate(what) {
|
||||
location.hash = what;
|
||||
this.setAttribute("component", what);
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
switch (name) {
|
||||
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 {
|
||||
get _app() { return this.closest("cyp-app"); }
|
||||
|
@ -8,7 +8,10 @@ export class HasApp extends HTMLElement {
|
|||
export default class Component extends HasApp {
|
||||
constructor() {
|
||||
super();
|
||||
this.selection = new Selection(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._app.addEventListener("load", _ => this._onAppLoad());
|
||||
this._app.addEventListener("component-change", _ => {
|
||||
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 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 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");
|
||||
pubsub.publish("queue-change");
|
||||
}
|
||||
player.update();
|
||||
button.closest("cyp-app").querySelector("cyp-player").update(); // FIXME nejde to lepe?
|
||||
});
|
||||
|
||||
return button;
|
||||
|
||||
}
|
||||
|
||||
function deleteButton(type, id, parent) {
|
||||
|
@ -108,7 +107,7 @@ function deleteButton(type, id, parent) {
|
|||
await mpd.command(`deleteid ${id}`);
|
||||
pubsub.publish("queue-change");
|
||||
return;
|
||||
case TYPE_PLAYLIST:
|
||||
case TYPE_PLAYLIST:
|
||||
let ok = confirm(`Really delete playlist '${id}'?`);
|
||||
if (!ok) { return; }
|
||||
await mpd.command(`rm "${mpd.escape(id)}"`);
|
||||
|
|
|
@ -14,8 +14,15 @@ class Player extends Component {
|
|||
this._dom = this._initDOM();
|
||||
}
|
||||
|
||||
async update() {
|
||||
this._clearIdle();
|
||||
const data = await this._mpd.status();
|
||||
this._sync(data);
|
||||
this._idle();
|
||||
}
|
||||
|
||||
_onAppLoad() {
|
||||
this._update();
|
||||
this.update();
|
||||
}
|
||||
|
||||
_initDOM() {
|
||||
|
@ -50,7 +57,7 @@ class Player extends Component {
|
|||
}
|
||||
|
||||
_idle() {
|
||||
this._idleTimeout = setTimeout(() => this._update(), DELAY);
|
||||
this._idleTimeout = setTimeout(() => this.update(), DELAY);
|
||||
}
|
||||
|
||||
_clearIdle() {
|
||||
|
@ -58,13 +65,6 @@ class Player extends Component {
|
|||
this._idleTimeout = null;
|
||||
}
|
||||
|
||||
async _update() {
|
||||
this._clearIdle();
|
||||
const data = await this._mpd.status();
|
||||
this._sync(data);
|
||||
this._idle();
|
||||
}
|
||||
|
||||
_sync(data) {
|
||||
const DOM = this._dom;
|
||||
if ("volume" in data) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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 {
|
||||
constructor() {
|
||||
super();
|
||||
this._currentId = null;
|
||||
|
||||
/*
|
||||
this.querySelector(".clear").addEventListener("click", async _ => {
|
||||
await this._mpd.command("clear");
|
||||
this._sync();
|
||||
|
@ -18,6 +18,7 @@ class Queue extends Component {
|
|||
if (name === null) { return; }
|
||||
this._mpd.command(`save "${this._mpd.escape(name)}"`);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
|
@ -54,19 +55,72 @@ class Queue extends Component {
|
|||
}
|
||||
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
||||
_buildSongs(songs) {
|
||||
let ul = this.querySelector("ul");
|
||||
html.clear(ul);
|
||||
html.clear(this);
|
||||
|
||||
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
|
||||
songs.forEach(song => this.appendChild(new Song(song)));
|
||||
|
||||
this._updateCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
101
app/js/yt.js
101
app/js/yt.js
|
@ -1,8 +1,9 @@
|
|||
import * as mpd from "./lib/mpd.js";
|
||||
import * as html from "./lib/html.js";
|
||||
import * as conf from "./conf.js";
|
||||
|
||||
let node;
|
||||
import Component from "./component.js";
|
||||
|
||||
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
function decodeChunk(byteArray) {
|
||||
|
@ -10,54 +11,60 @@ function decodeChunk(byteArray) {
|
|||
return decoder.decode(byteArray).replace(/\u000d/g, "\n");
|
||||
}
|
||||
|
||||
async function post(q) {
|
||||
let pre = node.querySelector("pre");
|
||||
html.clear(pre);
|
||||
|
||||
node.classList.add("pending");
|
||||
|
||||
let body = new URLSearchParams();
|
||||
body.set("q", q);
|
||||
let response = await fetch("/youtube", {method:"POST", body});
|
||||
|
||||
let reader = response.body.getReader();
|
||||
while (true) {
|
||||
let { done, value } = await reader.read();
|
||||
if (done) { break; }
|
||||
pre.textContent += decodeChunk(value);
|
||||
pre.scrollTop = pre.scrollHeight;
|
||||
class YT extends Component {
|
||||
_onAppLoad() {
|
||||
this.querySelector(".download").addEventListener("click", _ => this._download());
|
||||
this.querySelector(".search-download").addEventListener("click", _ => this._search());
|
||||
this.querySelector(".clear").addEventListener("click", _ => this._clear());
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
node.classList.remove("pending");
|
||||
_download() {
|
||||
let url = prompt("Please enter a YouTube URL:");
|
||||
if (!url) { return; }
|
||||
|
||||
if (response.status == 200) {
|
||||
mpd.command(`update ${mpd.escape(conf.ytPath)}`);
|
||||
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);
|
||||
|
||||
this.classList.add("pending");
|
||||
|
||||
let body = new URLSearchParams();
|
||||
body.set("q", q);
|
||||
let response = await fetch("/youtube", {method:"POST", body});
|
||||
|
||||
let reader = response.body.getReader();
|
||||
while (true) {
|
||||
let { done, value } = await reader.read();
|
||||
if (done) { break; }
|
||||
pre.textContent += decodeChunk(value);
|
||||
pre.scrollTop = pre.scrollHeight;
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
this.classList.remove("pending");
|
||||
|
||||
if (response.status == 200) {
|
||||
this._mpd.command(`update ${this._mpd.escape(conf.ytPath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_onComponentChange(c, isThis) {
|
||||
this.hidden = !isThis;
|
||||
}
|
||||
}
|
||||
|
||||
function download() {
|
||||
let url = prompt("Please enter a YouTube URL:");
|
||||
if (!url) { return; }
|
||||
|
||||
post(url);
|
||||
}
|
||||
|
||||
function search() {
|
||||
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());
|
||||
}
|
||||
customElements.define("cyp-yt", YT);
|
Loading…
Reference in a new issue