selection wip

This commit is contained in:
Ondrej Zara 2020-03-09 14:26:39 +01:00
parent bb5e2d1fb6
commit b81d1edea2
No known key found for this signature in database
GPG key ID: B0A5751E616840C5
17 changed files with 452 additions and 185 deletions

View file

@ -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;
}

View file

@ -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
View file

View file

@ -1,4 +0,0 @@
main {
flex-grow: 1;
overflow: auto;
}

View file

@ -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;
} }
} }
} }

View file

@ -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
View 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;
}
}

View file

@ -1,4 +1,4 @@
#yt { cyp-yt {
.component; .component;
header { header {

View file

@ -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 &amp; Download</button> <button class="search-download" data-icon="magnify">Search &amp; 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>

View file

@ -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;
}
} }
} }

View file

@ -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");

View file

@ -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
View 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();
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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();
}

View file

@ -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)}`);
}
}
_onComponentChange(c, isThis) {
this.hidden = !isThis;
} }
} }
function download() { customElements.define("cyp-yt", YT);
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());
}