diff --git a/app/app.css b/app/app.css
index 0d7de98..306335a 100644
--- a/app/app.css
+++ b/app/app.css
@@ -625,72 +625,72 @@ cyp-queue .current {
#fs .info h2 {
font-weight: normal;
}
-#playlists header {
+cyp-playlists header {
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
-#playlists header:not([hidden]) {
+cyp-playlists header:not([hidden]) {
display: flex;
}
-#playlists header button {
+cyp-playlists header button {
font-size: var(--font-size-large);
font-weight: bold;
overflow: hidden;
}
-#playlists header button .icon {
+cyp-playlists header button .icon {
margin-right: var(--icon-spacing);
}
-#playlists ul {
+cyp-playlists ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
}
-#playlists li {
+cyp-playlists li {
flex-direction: row;
align-items: center;
}
-#playlists li:not([hidden]) {
+cyp-playlists li:not([hidden]) {
display: flex;
}
-#playlists li .info {
+cyp-playlists li .info {
flex-grow: 1;
overflow: hidden;
}
-#playlists li .info .icon {
+cyp-playlists li .info .icon {
color: var(--primary);
margin-right: var(--icon-spacing);
filter: drop-shadow(var(--text-shadow));
}
-#playlists li .info h2 {
+cyp-playlists li .info h2 {
font-size: var(--font-size-large);
margin: 0;
}
-#playlists li .info h2,
-#playlists li .info div {
+cyp-playlists li .info h2,
+cyp-playlists li .info div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-#playlists li:not(.has-art) {
+cyp-playlists li:not(.has-art) {
padding: 8px;
}
-#playlists li button .icon {
+cyp-playlists li button .icon {
width: 32px;
}
-#playlists li:nth-child(odd) {
+cyp-playlists li:nth-child(odd) {
background-color: var(--bg-alt);
}
-#playlists .info {
+cyp-playlists .info {
flex-direction: row;
align-items: center;
}
-#playlists .info:not([hidden]) {
+cyp-playlists .info:not([hidden]) {
display: flex;
}
-#playlists .info h2 {
+cyp-playlists .info h2 {
font-weight: normal;
}
#yt header {
diff --git a/app/css/playlists.less b/app/css/playlists.less
index 7a6cfbf..fe50244 100644
--- a/app/css/playlists.less
+++ b/app/css/playlists.less
@@ -1,4 +1,4 @@
-#playlists {
+cyp-playlists {
.component;
.info {
diff --git a/app/index.html b/app/index.html
index b8e5277..b4939de 100644
--- a/app/index.html
+++ b/app/index.html
@@ -7,7 +7,7 @@
-
+
-
+
diff --git a/app/js/app.js b/app/js/app.js
index 8c68635..e16c3fb 100644
--- a/app/js/app.js
+++ b/app/js/app.js
@@ -14,44 +14,50 @@ import * as yt from "./yt.js";
import * as settings from "./settings.js";
function initIcons() {
- Array.from(document.querySelectorAll("[data-icon]")).forEach(node => {
+ Array.from(document.querySelectorAll("[data-icon]")).forEach(/** @param {HTMLElement} node */ node => {
let icon = html.icon(node.dataset.icon);
node.insertBefore(icon, node.firstChild);
});
}
-async function mpdExecutor(resolve, reject) {
- try {
- await mpd.init();
- resolve(mpd);
- } catch (e) {
- resolve(mpdMock);
- console.error(e);
- reject(e);
- }
-}
-
class App extends HTMLElement {
+ static get observedAttributes() { return ["component"]; }
+
constructor() {
super();
- initIcons();
- this._mpd = new Promise(mpdExecutor);
+ initIcons();
this._load();
}
- get mpd() { return this._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;
+ }
+
const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
await Promise.all(promises);
+ this.dispatchEvent(new CustomEvent("load"));
+
const onHashChange = () => {
const hash = location.hash.substring(1);
this._activate(hash || "queue");
}
-
window.addEventListener("hashchange", onHashChange);
onHashChange();
}
@@ -59,9 +65,6 @@ class App extends HTMLElement {
_activate(what) {
location.hash = what;
this.setAttribute("component", what);
-
- const component = this.querySelector(`cyp-${what}`);
- // component.activate();
}
}
diff --git a/app/js/component.js b/app/js/component.js
index 5cd709c..f0133e9 100644
--- a/app/js/component.js
+++ b/app/js/component.js
@@ -1,32 +1,22 @@
const APP = "cyp-app";
-export default class Component extends HTMLElement {
+export class HasApp extends HTMLElement {
+ get _app() { return this.closest("cyp-app"); }
+ get _mpd() { return this._app.mpd; }
+}
+
+export default class Component extends HasApp {
constructor() {
super();
- this._app.then(app => {
- let mo = new MutationObserver(mrs => {
- mrs.forEach(mr => this._onAppAttributeChange(mr));
- });
- mo.observe(app, {attributes:true});
+ this._app.addEventListener("load", _ => this._onAppLoad());
+ this._app.addEventListener("component-change", _ => {
+ const component = this._app.getAttribute("component");
+ const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`);
+ this._onComponentChange(component, isThis);
});
}
- _onAppAttributeChange(mr) {
- if (mr.attributeName != "component") { return; }
- const component = mr.target.getAttribute(mr.attributeName);
- const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`);
- this._onComponentChange(component, isThis);
- }
-
- get _app() {
- return customElements.whenDefined(APP)
- .then(() => this.closest(APP));
- }
-
- get _mpd() {
- return this._app.then(app => app.mpd);
- }
-
- _onComponentChange(component) {}
+ _onComponentChange(_component, _isThis) {}
+ _onAppLoad() {}
}
diff --git a/app/js/lib/range.js b/app/js/lib/range.js
deleted file mode 100644
index b267531..0000000
--- a/app/js/lib/range.js
+++ /dev/null
@@ -1,170 +0,0 @@
-class Range extends HTMLElement {
- static get observedAttributes() { return ["min", "max", "value", "step", "disabled"]; }
-
- constructor() {
- super();
-
- this._dom = {};
-
- this.addEventListener("mousedown", this);
- this.addEventListener("keydown", this);
- }
-
- get _valueAsNumber() {
- let raw = (this.hasAttribute("value") ? Number(this.getAttribute("value")) : 50);
- return this._constrain(raw);
- }
- get _minAsNumber() {
- return (this.hasAttribute("min") ? Number(this.getAttribute("min")) : 0);
- }
- get _maxAsNumber() {
- return (this.hasAttribute("max") ? Number(this.getAttribute("max")) : 100);
- }
- get _stepAsNumber() {
- return (this.hasAttribute("step") ? Number(this.getAttribute("step")) : 1);
- }
-
- get value() { return String(this._valueAsNumber); }
- get valueAsNumber() { return this._valueAsNumber; }
- get min() { return this.hasAttribute("min") ? this.getAttribute("min") : ""; }
- get max() { return this.hasAttribute("max") ? this.getAttribute("max") : ""; }
- get step() { return this.hasAttribute("step") ? this.getAttribute("step") : ""; }
- get disabled() { return this.hasAttribute("disabled"); }
-
- set _valueAsNumber(value) { this.value = String(value); }
- set min(min) { this.setAttribute("min", min); }
- set max(max) { this.setAttribute("max", max); }
- set value(value) { this.setAttribute("value", value); }
- set step(step) { this.setAttribute("step", step); }
- set disabled(disabled) {
- disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
- }
-
- connectedCallback() {
- if (this.firstChild) { return; }
-
- this.innerHTML = `
-
-
-
-
-
-
- `;
-
- Array.from(this.querySelectorAll("[class^='-']")).forEach(node => {
- let name = node.className.substring(1);
- this._dom[name] = node;
- });
-
- this._update();
- }
-
- attributeChangedCallback(name, oldValue, newValue) {
- switch (name) {
- case "min":
- case "max":
- case "value":
- case "step":
- this._update();
- break;
- }
- }
-
- handleEvent(e) {
- switch (e.type) {
- case "mousedown":
- if (this.disabled) { return; }
- document.addEventListener("mousemove", this);
- document.addEventListener("mouseup", this);
- this._setToMouse(e);
- break;
-
- case "mousemove":
- this._setToMouse(e);
- break;
-
- case "mouseup":
- document.removeEventListener("mousemove", this);
- document.removeEventListener("mouseup", this);
- this.dispatchEvent(new CustomEvent("change"));
- break;
-
- case "keydown":
- if (this.disabled) { return; }
- this._handleKey(e.code);
- this.dispatchEvent(new CustomEvent("input"));
- this.dispatchEvent(new CustomEvent("change"));
- break;
- }
- }
-
- _handleKey(code) {
- let min = this._minAsNumber;
- let max = this._maxAsNumber;
- let range = max - min;
- let step = this._stepAsNumber;
-
- switch (code) {
- case "ArrowLeft":
- case "ArrowDown":
- this._valueAsNumber = this._constrain(this._valueAsNumber - step);
- break;
-
- case "ArrowRight":
- case "ArrowUp":
- this._valueAsNumber = this._constrain(this._valueAsNumber + step);
- break;
-
- case "Home": this._valueAsNumber = this._constrain(min); break;
- case "End": this._valueAsNumber = this._constrain(max); break;
-
- case "PageUp": this._valueAsNumber = this._constrain(this._valueAsNumber + range/10); break;
- case "PageDown": this._valueAsNumber = this._constrain(this._valueAsNumber - range/10); break;
- }
- }
-
- _constrain(value) {
- const min = this._minAsNumber;
- const max = this._maxAsNumber;
- const step = this._stepAsNumber;
-
- value = Math.max(value, min);
- value = Math.min(value, max);
-
- value -= min;
- value = Math.round(value / step) * step;
- value += min;
- if (value > max) { value -= step; }
-
- return value;
- }
-
- _update() {
- let min = this._minAsNumber;
- let max = this._maxAsNumber;
- let frac = (this._valueAsNumber-min) / (max-min);
- this._dom.thumb.style.left = `${frac * 100}%`;
- this._dom.remaining.style.left = `${frac * 100}%`;
- this._dom.elapsed.style.width = `${frac * 100}%`;
- }
-
- _setToMouse(e) {
- let rect = this._dom.inner.getBoundingClientRect();
- let x = e.clientX;
- x = Math.max(x, rect.left);
- x = Math.min(x, rect.right);
-
- let min = this._minAsNumber;
- let max = this._maxAsNumber;
-
- let frac = (x-rect.left) / (rect.right-rect.left);
- let value = this._constrain(min + frac * (max-min));
- if (value == this._valueAsNumber) { return; }
-
- this._valueAsNumber = value;
- this.dispatchEvent(new CustomEvent("input"));
- }
-}
-
-customElements.define('x-range', Range);
diff --git a/app/js/lib/range.js b/app/js/lib/range.js
new file mode 120000
index 0000000..fcc7d2c
--- /dev/null
+++ b/app/js/lib/range.js
@@ -0,0 +1 @@
+../../../node_modules/custom-range/range.js
\ No newline at end of file
diff --git a/app/js/player.js b/app/js/player.js
index 1dfe891..d4a180d 100644
--- a/app/js/player.js
+++ b/app/js/player.js
@@ -12,6 +12,9 @@ class Player extends Component {
this._toggledVolume = 0;
this._idleTimeout = null;
this._dom = this._initDOM();
+ }
+
+ _onAppLoad() {
this._update();
}
@@ -40,9 +43,8 @@ class Player extends Component {
}
async _command(cmd) {
- const mpd = await this._mpd;
this._clearIdle();
- const data = await mpd.commandAndStatus(cmd);
+ const data = await this._mpd.commandAndStatus(cmd);
this._sync(data);
this._idle();
}
@@ -57,9 +59,8 @@ class Player extends Component {
}
async _update() {
- const mpd = await this._mpd;
this._clearIdle();
- const data = await mpd.status();
+ const data = await this._mpd.status();
this._sync(data);
this._idle();
}
@@ -136,10 +137,9 @@ class Player extends Component {
this._current = data;
}
- async _dispatchSongChange(detail) {
- const app = await this._app;
+ _dispatchSongChange(detail) {
const e = new CustomEvent("song-change", {detail});
- app.dispatchEvent(e);
+ this._app.dispatchEvent(e);
}
}
diff --git a/app/js/playlists.js b/app/js/playlists.js
index 00ed1a5..36227fb 100644
--- a/app/js/playlists.js
+++ b/app/js/playlists.js
@@ -1,31 +1,38 @@
-import * as mpd from "./lib/mpd.js";
import * as html from "./lib/html.js";
-import * as pubsub from "./lib/pubsub.js";
import * as ui from "./lib/ui.js";
-let node;
+import Component from "./component.js";
-function buildLists(lists) {
- let ul = node.querySelector("ul");
- html.clear(ul);
- lists.map(list => ui.playlist(list, ul));
+class Playlists extends Component {
+ handleEvent(e) {
+ switch (e.type) {
+ case "playlists-change":
+ this._sync();
+ break;
+ }
+ }
+
+ _onAppLoad() {
+ this._app.addEventListener("playlists-change", this);
+ }
+
+ _onComponentChange(c, isThis) {
+ this.hidden = !isThis;
+ if (isThis) { this._sync(); }
+ }
+
+ async _sync() {
+ let lists = await this._mpd.listPlaylists();
+ this._buildLists(lists);
+ }
+
+ _buildLists(lists) {
+ let ul = this.querySelector("ul");
+ html.clear(ul);
+
+ lists.map(list => ui.playlist(list, ul));
+ }
}
-async function syncLists() {
- let lists = await mpd.listPlaylists();
- buildLists(lists);
-}
-
-function onPlaylistsChange(message, publisher, data) {
- syncLists();
-}
-
-export async function activate() {
- syncLists();
-}
-
-export function init(n) {
- node = n;
- pubsub.subscribe("playlists-change", onPlaylistsChange);
-}
+customElements.define("cyp-playlists", Playlists);
diff --git a/app/js/queue.js b/app/js/queue.js
index 78ebc10..0243b7d 100644
--- a/app/js/queue.js
+++ b/app/js/queue.js
@@ -9,23 +9,15 @@ class Queue extends Component {
this._currentId = null;
this.querySelector(".clear").addEventListener("click", async _ => {
- const mpd = await this._mpd;
- await mpd.command("clear");
+ await this._mpd.command("clear");
this._sync();
});
- this.querySelector(".save").addEventListener("click", async _ => {
+ this.querySelector(".save").addEventListener("click", _ => {
let name = prompt("Save current queue as a playlist?", "name");
if (name === null) { return; }
- const mpd = await this._mpd;
- mpd.command(`save "${mpd.escape(name)}"`);
+ this._mpd.command(`save "${this._mpd.escape(name)}"`);
});
-
- this._app.then(app => {
- app.addEventListener("song-change", this);
- app.addEventListener("queue-change", this);
- })
- this._sync();
}
handleEvent(e) {
@@ -41,6 +33,12 @@ class Queue extends Component {
}
}
+ _onAppLoad() {
+ this._app.addEventListener("song-change", this);
+ this._app.addEventListener("queue-change", this);
+ this._sync();
+ }
+
_onComponentChange(c, isThis) {
this.hidden = !isThis;
@@ -48,8 +46,7 @@ class Queue extends Component {
}
async _sync() {
- const mpd = await this._mpd;
- let songs = await mpd.listQueue();
+ let songs = await this._mpd.listQueue();
this._buildSongs(songs);
// FIXME pubsub?
@@ -57,8 +54,7 @@ class Queue extends Component {
}
_updateCurrent() {
- let all = Array.from(this.querySelectorAll("[data-song-id]"));
- all.forEach(node => {
+ Array.from(this.querySelectorAll("[data-song-id]")).forEach(/** @param {HTMLElement} node */ node => {
node.classList.toggle("current", node.dataset.songId == this._currentId);
});
}
diff --git a/app/js/settings.js b/app/js/settings.js
index ff000f4..5442dd9 100644
--- a/app/js/settings.js
+++ b/app/js/settings.js
@@ -17,13 +17,24 @@ class Settings extends Component {
theme: this.querySelector("[name=theme]"),
color: Array.from(this.querySelectorAll("[name=color]"))
};
+ }
- this._load();
+ _onAppLoad() {
+ let mo = new MutationObserver(mrs => {
+ mrs.forEach(mr => this._onAppAttributeChange(mr));
+ });
+ mo.observe(this._app, {attributes:true});
this._inputs.theme.addEventListener("change", e => this._setTheme(e.target.value));
this._inputs.color.forEach(input => {
input.addEventListener("click", e => this._setColor(e.target.value));
});
+
+ const theme = loadFromStorage("theme");
+ (theme ? this._app.setAttribute("theme", theme) : this._syncTheme());
+
+ const color = loadFromStorage("color");
+ (color ? this._app.setAttribute("color", color) : this._syncColor());
}
_onAppAttributeChange(mr) {
@@ -31,39 +42,29 @@ class Settings extends Component {
if (mr.attributeName == "color") { this._syncColor(); }
}
- async _syncTheme() {
- const app = await this._app;
- this._inputs.theme.value = app.getAttribute("theme");
+ _syncTheme() {
+ this._inputs.theme.value = this._app.getAttribute("theme");
}
- async _syncColor() {
- const app = await this._app;
+ _syncColor() {
this._inputs.color.forEach(input => {
- input.checked = (input.value == app.getAttribute("color"));
+ input.checked = (input.value == this._app.getAttribute("color"));
input.parentNode.style.color = input.value;
});
}
- async _load() {
- const app = await this._app;
-
- const theme = loadFromStorage("theme");
- (theme ? app.setAttribute("theme", theme) : this._syncTheme());
-
- const color = loadFromStorage("color");
- (color ? app.setAttribute("color", color) : this._syncColor());
- }
-
- async _setTheme(theme) {
- const app = await this._app;
+ _setTheme(theme) {
saveToStorage("theme", theme);
- app.setAttribute("theme", theme);
+ this._app.setAttribute("theme", theme);
}
- async _setColor(color) {
- const app = await this._app;
+ _setColor(color) {
saveToStorage("color", color);
- app.setAttribute("color", color);
+ this._app.setAttribute("color", color);
+ }
+
+ _onComponentChange(c, isThis) {
+ this.hidden = !isThis;
}
}
diff --git a/app/js/yt.js b/app/js/yt.js
index ec598c3..2117a63 100644
--- a/app/js/yt.js
+++ b/app/js/yt.js
@@ -1,7 +1,5 @@
import * as mpd from "./lib/mpd.js";
import * as html from "./lib/html.js";
-import * as pubsub from "./lib/pubsub.js";
-import * as ui from "./lib/ui.js";
import * as conf from "./conf.js";
let node;