more refactor

This commit is contained in:
Ondrej Zara 2020-03-09 09:26:10 +01:00
parent 14569a9415
commit bb5e2d1fb6
No known key found for this signature in database
GPG key ID: B0A5751E616840C5
11 changed files with 131 additions and 305 deletions

View file

@ -625,72 +625,72 @@ cyp-queue .current {
#fs .info h2 { #fs .info h2 {
font-weight: normal; font-weight: normal;
} }
#playlists header { cyp-playlists header {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: var(--spacing); padding: var(--spacing);
} }
#playlists header:not([hidden]) { cyp-playlists header:not([hidden]) {
display: flex; display: flex;
} }
#playlists header button { cyp-playlists header button {
font-size: var(--font-size-large); font-size: var(--font-size-large);
font-weight: bold; font-weight: bold;
overflow: hidden; overflow: hidden;
} }
#playlists header button .icon { cyp-playlists header button .icon {
margin-right: var(--icon-spacing); margin-right: var(--icon-spacing);
} }
#playlists ul { cyp-playlists 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;
} }
#playlists li { cyp-playlists li {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
#playlists li:not([hidden]) { cyp-playlists li:not([hidden]) {
display: flex; display: flex;
} }
#playlists li .info { cyp-playlists li .info {
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
} }
#playlists li .info .icon { cyp-playlists 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));
} }
#playlists li .info h2 { cyp-playlists li .info h2 {
font-size: var(--font-size-large); font-size: var(--font-size-large);
margin: 0; margin: 0;
} }
#playlists li .info h2, cyp-playlists li .info h2,
#playlists li .info div { cyp-playlists li .info div {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#playlists li:not(.has-art) { cyp-playlists li:not(.has-art) {
padding: 8px; padding: 8px;
} }
#playlists li button .icon { cyp-playlists li button .icon {
width: 32px; width: 32px;
} }
#playlists li:nth-child(odd) { cyp-playlists li:nth-child(odd) {
background-color: var(--bg-alt); background-color: var(--bg-alt);
} }
#playlists .info { cyp-playlists .info {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
#playlists .info:not([hidden]) { cyp-playlists .info:not([hidden]) {
display: flex; display: flex;
} }
#playlists .info h2 { cyp-playlists .info h2 {
font-weight: normal; font-weight: normal;
} }
#yt header { #yt header {

View file

@ -1,4 +1,4 @@
#playlists { cyp-playlists {
.component; .component;
.info { .info {

View file

@ -7,7 +7,7 @@
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
</head> </head>
<body> <body>
<cyp-app component="queue" theme="dark" color="dodgerblue"> <cyp-app theme="dark" color="dodgerblue">
<header> <header>
<cyp-player> <cyp-player>
<span class="art"></span> <span class="art"></span>
@ -46,9 +46,9 @@
</header> </header>
<ul></ul> <ul></ul>
</cyp-queue> </cyp-queue>
<section id="playlists"> <cyp-playlists>
<ul></ul> <ul></ul>
</section> </cyp-playlists>
<section id="library"> <section id="library">
<header></header> <header></header>
<ul></ul> <ul></ul>

View file

@ -14,44 +14,50 @@ import * as yt from "./yt.js";
import * as settings from "./settings.js"; import * as settings from "./settings.js";
function initIcons() { 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); let icon = html.icon(node.dataset.icon);
node.insertBefore(icon, node.firstChild); 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 { class App extends HTMLElement {
static get observedAttributes() { return ["component"]; }
constructor() { constructor() {
super(); super();
initIcons();
this._mpd = new Promise(mpdExecutor); initIcons();
this._load(); 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() { 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)); const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
await Promise.all(promises); await Promise.all(promises);
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._activate(hash || "queue");
} }
window.addEventListener("hashchange", onHashChange); window.addEventListener("hashchange", onHashChange);
onHashChange(); onHashChange();
} }
@ -59,9 +65,6 @@ class App extends HTMLElement {
_activate(what) { _activate(what) {
location.hash = what; location.hash = what;
this.setAttribute("component", what); this.setAttribute("component", what);
const component = this.querySelector(`cyp-${what}`);
// component.activate();
} }
} }

View file

@ -1,32 +1,22 @@
const APP = "cyp-app"; 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() { constructor() {
super(); super();
this._app.then(app => { this._app.addEventListener("load", _ => this._onAppLoad());
let mo = new MutationObserver(mrs => { this._app.addEventListener("component-change", _ => {
mrs.forEach(mr => this._onAppAttributeChange(mr)); const component = this._app.getAttribute("component");
});
mo.observe(app, {attributes:true});
});
}
_onAppAttributeChange(mr) {
if (mr.attributeName != "component") { return; }
const component = mr.target.getAttribute(mr.attributeName);
const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`); const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`);
this._onComponentChange(component, isThis); this._onComponentChange(component, isThis);
});
} }
get _app() { _onComponentChange(_component, _isThis) {}
return customElements.whenDefined(APP) _onAppLoad() {}
.then(() => this.closest(APP));
}
get _mpd() {
return this._app.then(app => app.mpd);
}
_onComponentChange(component) {}
} }

View file

@ -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 = `
<span class="-track"></span>
<span class="-elapsed"></span>
<span class="-remaining"></span>
<div class="-inner">
<button class="-thumb"></button>
</div>
`;
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);

1
app/js/lib/range.js Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/custom-range/range.js

View file

@ -12,6 +12,9 @@ class Player extends Component {
this._toggledVolume = 0; this._toggledVolume = 0;
this._idleTimeout = null; this._idleTimeout = null;
this._dom = this._initDOM(); this._dom = this._initDOM();
}
_onAppLoad() {
this._update(); this._update();
} }
@ -40,9 +43,8 @@ class Player extends Component {
} }
async _command(cmd) { async _command(cmd) {
const mpd = await this._mpd;
this._clearIdle(); this._clearIdle();
const data = await mpd.commandAndStatus(cmd); const data = await this._mpd.commandAndStatus(cmd);
this._sync(data); this._sync(data);
this._idle(); this._idle();
} }
@ -57,9 +59,8 @@ class Player extends Component {
} }
async _update() { async _update() {
const mpd = await this._mpd;
this._clearIdle(); this._clearIdle();
const data = await mpd.status(); const data = await this._mpd.status();
this._sync(data); this._sync(data);
this._idle(); this._idle();
} }
@ -136,10 +137,9 @@ class Player extends Component {
this._current = data; this._current = data;
} }
async _dispatchSongChange(detail) { _dispatchSongChange(detail) {
const app = await this._app;
const e = new CustomEvent("song-change", {detail}); const e = new CustomEvent("song-change", {detail});
app.dispatchEvent(e); this._app.dispatchEvent(e);
} }
} }

View file

@ -1,31 +1,38 @@
import * as mpd from "./lib/mpd.js";
import * as html from "./lib/html.js"; import * as html from "./lib/html.js";
import * as pubsub from "./lib/pubsub.js";
import * as ui from "./lib/ui.js"; import * as ui from "./lib/ui.js";
let node; import Component from "./component.js";
function buildLists(lists) {
let ul = node.querySelector("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); html.clear(ul);
lists.map(list => ui.playlist(list, ul)); lists.map(list => ui.playlist(list, ul));
}
} }
async function syncLists() { customElements.define("cyp-playlists", Playlists);
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);
}

View file

@ -9,23 +9,15 @@ class Queue extends Component {
this._currentId = null; this._currentId = null;
this.querySelector(".clear").addEventListener("click", async _ => { this.querySelector(".clear").addEventListener("click", async _ => {
const mpd = await this._mpd; await this._mpd.command("clear");
await mpd.command("clear");
this._sync(); this._sync();
}); });
this.querySelector(".save").addEventListener("click", async _ => { this.querySelector(".save").addEventListener("click", _ => {
let name = prompt("Save current queue as a playlist?", "name"); let name = prompt("Save current queue as a playlist?", "name");
if (name === null) { return; } if (name === null) { return; }
const mpd = await this._mpd; this._mpd.command(`save "${this._mpd.escape(name)}"`);
mpd.command(`save "${mpd.escape(name)}"`);
}); });
this._app.then(app => {
app.addEventListener("song-change", this);
app.addEventListener("queue-change", this);
})
this._sync();
} }
handleEvent(e) { 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) { _onComponentChange(c, isThis) {
this.hidden = !isThis; this.hidden = !isThis;
@ -48,8 +46,7 @@ class Queue extends Component {
} }
async _sync() { async _sync() {
const mpd = await this._mpd; let songs = await this._mpd.listQueue();
let songs = await mpd.listQueue();
this._buildSongs(songs); this._buildSongs(songs);
// FIXME pubsub? // FIXME pubsub?
@ -57,8 +54,7 @@ class Queue extends Component {
} }
_updateCurrent() { _updateCurrent() {
let all = Array.from(this.querySelectorAll("[data-song-id]")); Array.from(this.querySelectorAll("[data-song-id]")).forEach(/** @param {HTMLElement} node */ node => {
all.forEach(node => {
node.classList.toggle("current", node.dataset.songId == this._currentId); node.classList.toggle("current", node.dataset.songId == this._currentId);
}); });
} }

View file

@ -17,13 +17,24 @@ class Settings extends Component {
theme: this.querySelector("[name=theme]"), theme: this.querySelector("[name=theme]"),
color: Array.from(this.querySelectorAll("[name=color]")) 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.theme.addEventListener("change", e => this._setTheme(e.target.value));
this._inputs.color.forEach(input => { this._inputs.color.forEach(input => {
input.addEventListener("click", e => this._setColor(e.target.value)); 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) { _onAppAttributeChange(mr) {
@ -31,39 +42,29 @@ class Settings extends Component {
if (mr.attributeName == "color") { this._syncColor(); } if (mr.attributeName == "color") { this._syncColor(); }
} }
async _syncTheme() { _syncTheme() {
const app = await this._app; this._inputs.theme.value = this._app.getAttribute("theme");
this._inputs.theme.value = app.getAttribute("theme");
} }
async _syncColor() { _syncColor() {
const app = await this._app;
this._inputs.color.forEach(input => { 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; input.parentNode.style.color = input.value;
}); });
} }
async _load() { _setTheme(theme) {
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;
saveToStorage("theme", theme); saveToStorage("theme", theme);
app.setAttribute("theme", theme); this._app.setAttribute("theme", theme);
} }
async _setColor(color) { _setColor(color) {
const app = await this._app;
saveToStorage("color", color); saveToStorage("color", color);
app.setAttribute("color", color); this._app.setAttribute("color", color);
}
_onComponentChange(c, isThis) {
this.hidden = !isThis;
} }
} }

View file

@ -1,7 +1,5 @@
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 pubsub from "./lib/pubsub.js";
import * as ui from "./lib/ui.js";
import * as conf from "./conf.js"; import * as conf from "./conf.js";
let node; let node;