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 @@ - +
@@ -46,9 +46,9 @@
-
+
    -
    +
      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;