search
This commit is contained in:
parent
c3871fa486
commit
a57207f80e
21 changed files with 166 additions and 421 deletions
|
@ -28,12 +28,17 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input, select, button {
|
input, select {
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
|
@ -7,6 +7,7 @@ cyp-app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
font-family: lato, sans-serif;
|
font-family: lato, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
|
|
|
@ -1,2 +1,27 @@
|
||||||
cyp-library {
|
cyp-library {
|
||||||
|
nav {
|
||||||
|
.flex-column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
.font-large;
|
||||||
|
width: 200px;
|
||||||
|
margin-top: 2em;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 32px;
|
||||||
|
margin-right: var(--icon-spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
.item;
|
||||||
|
.flex-row;
|
||||||
|
|
||||||
|
button:first-of-type {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
cyp-settings {
|
cyp-settings {
|
||||||
--spacing: 8px;
|
--spacing: 8px;
|
||||||
|
|
||||||
font-size: var(--font-size-large);
|
.font-large;
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
margin: var(--spacing);
|
margin: var(--spacing);
|
||||||
|
|
|
@ -5,12 +5,6 @@ cyp-song {
|
||||||
.flex-column;
|
.flex-column;
|
||||||
min-width: 0; // bez tohoto se odmita zmensit
|
min-width: 0; // bez tohoto se odmita zmensit
|
||||||
|
|
||||||
/* FIXME toto je relikt z .component
|
|
||||||
.icon {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.subtitle { .ellipsis; }
|
.subtitle { .ellipsis; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-large {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.selectable {
|
.selectable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative; // kotva pro selected::before
|
position: relative; // kotva pro selected::before
|
||||||
|
@ -59,11 +64,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: bold;
|
.font-large;
|
||||||
// FIXME take line-height, at vychazi celociselne
|
|
||||||
font-size: var(--font-size-large);
|
|
||||||
min-width: 0;
|
|
||||||
.ellipsis;
|
.ellipsis;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
@breakpoint-menu: 480px;
|
@breakpoint-menu: 480px;
|
||||||
|
|
||||||
cyp-app {
|
cyp-app {
|
||||||
--font-size-large: 112.5%;
|
|
||||||
--icon-spacing: 4px;
|
--icon-spacing: 4px;
|
||||||
--primary: rgb(var(--primary-raw));
|
--primary: rgb(var(--primary-raw));
|
||||||
--primary-tint: rgba(var(--primary-raw), 0.1);
|
--primary-tint: rgba(var(--primary-raw), 0.1);
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
||||||
import * as html from "./html.js";
|
import * as html from "./html.js";
|
||||||
import * as conf from "./conf.js";
|
import * as conf from "./conf.js";
|
||||||
|
|
||||||
let cache = {};
|
const cache = {};
|
||||||
const MIME = "image/jpeg";
|
const MIME = "image/jpeg";
|
||||||
const STORAGE_PREFIX = `art-${conf.artSize}` ;
|
const STORAGE_PREFIX = `art-${conf.artSize}` ;
|
||||||
|
|
||||||
|
@ -14,26 +14,26 @@ function load(key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bytesToImage(bytes) {
|
async function bytesToImage(bytes) {
|
||||||
let blob = new Blob([bytes]);
|
const blob = new Blob([bytes]);
|
||||||
let src = URL.createObjectURL(blob);
|
const src = URL.createObjectURL(blob);
|
||||||
let image = html.node("img", {src});
|
const image = html.node("img", {src});
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
image.onload = () => resolve(image);
|
image.onload = () => resolve(image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resize(image) {
|
function resize(image) {
|
||||||
let canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize});
|
const canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize});
|
||||||
let ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(mpd, artist, album, songUrl = null) {
|
export async function get(mpd, artist, album, songUrl = null) {
|
||||||
let key = `${artist}-${album}`;
|
const key = `${artist}-${album}`;
|
||||||
if (key in cache) { return cache[key]; }
|
if (key in cache) { return cache[key]; }
|
||||||
|
|
||||||
let loaded = await load(key);
|
const loaded = await load(key);
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
cache[key] = loaded;
|
cache[key] = loaded;
|
||||||
return loaded;
|
return loaded;
|
||||||
|
@ -43,18 +43,18 @@ export async function get(mpd, artist, album, songUrl = null) {
|
||||||
|
|
||||||
// promise to be returned in the meantime
|
// promise to be returned in the meantime
|
||||||
let resolve;
|
let resolve;
|
||||||
let promise = new Promise(res => resolve = res);
|
const promise = new Promise(res => resolve = res);
|
||||||
cache[key] = promise;
|
cache[key] = promise;
|
||||||
|
|
||||||
try {
|
const data = await mpd.albumArt(songUrl);
|
||||||
let data = await mpd.albumArt(songUrl);
|
if (data) {
|
||||||
let bytes = new Uint8Array(data);
|
const bytes = new Uint8Array(data);
|
||||||
let image = await bytesToImage(bytes);
|
const image = await bytesToImage(bytes);
|
||||||
let url = resize(image).toDataURL(MIME);
|
const url = resize(image).toDataURL(MIME);
|
||||||
store(key, url);
|
store(key, url);
|
||||||
cache[key] = url;
|
cache[key] = url;
|
||||||
resolve(url);
|
resolve(url);
|
||||||
} catch (e) {
|
} else {
|
||||||
cache[key] = null;
|
cache[key] = null;
|
||||||
}
|
}
|
||||||
return cache[key];
|
return cache[key];
|
||||||
|
|
|
@ -47,13 +47,15 @@ class Library extends Component {
|
||||||
_showRoot() {
|
_showRoot() {
|
||||||
html.clear(this);
|
html.clear(this);
|
||||||
|
|
||||||
html.button({icon:"artist"}, "Artists and albums", this)
|
const nav = html.node("nav", {}, "", this);
|
||||||
|
|
||||||
|
html.button({icon:"artist"}, "Artists and albums", nav)
|
||||||
.addEventListener("click", _ => this._listTags("AlbumArtist"));
|
.addEventListener("click", _ => this._listTags("AlbumArtist"));
|
||||||
|
|
||||||
html.button({icon:"folder"}, "Files and directories", this)
|
html.button({icon:"folder"}, "Files and directories", nav)
|
||||||
.addEventListener("click", _ => this._listPath(""));
|
.addEventListener("click", _ => this._listPath(""));
|
||||||
|
|
||||||
html.button({icon:"magnify"}, "Search", this)
|
html.button({icon:"magnify"}, "Search", nav)
|
||||||
.addEventListener("click", _ => this._showSearch());
|
.addEventListener("click", _ => this._showSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ class Library extends Component {
|
||||||
path && this._buildBack(path);
|
path && this._buildBack(path);
|
||||||
paths["directory"].forEach(path => this._buildPath(path));
|
paths["directory"].forEach(path => this._buildPath(path));
|
||||||
paths["file"].forEach(path => this._buildPath(path));
|
paths["file"].forEach(path => this._buildPath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _listSongs(filter) {
|
async _listSongs(filter) {
|
||||||
const songs = await this._mpd.listSongs(filter);
|
const songs = await this._mpd.listSongs(filter);
|
||||||
|
@ -82,7 +84,50 @@ class Library extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_showSearch() {
|
_showSearch() {
|
||||||
|
html.clear(this);
|
||||||
|
|
||||||
|
const form = html.node("form", {}, "", this);
|
||||||
|
const input = html.node("input", {type:"text"}, "", form);
|
||||||
|
html.button({icon:"magnify"}, "", form);
|
||||||
|
form.addEventListener("submit", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const q = input.value.trim();
|
||||||
|
if (q.length < 3) { return; }
|
||||||
|
this._doSearch(q, form);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doSearch(q, form) {
|
||||||
|
const songs1 = await this._mpd.searchSongs({"AlbumArtist": q});
|
||||||
|
const songs2 = await this._mpd.searchSongs({"Album": q});
|
||||||
|
const songs3 = await this._mpd.searchSongs({"Title": q});
|
||||||
|
html.clear(this);
|
||||||
|
this.appendChild(form);
|
||||||
|
|
||||||
|
this._aggregateSearch(songs1, "AlbumArtist");
|
||||||
|
this._aggregateSearch(songs2, "Album");
|
||||||
|
songs3.forEach(song => this.appendChild(new Song(song)));
|
||||||
|
}
|
||||||
|
|
||||||
|
_aggregateSearch(songs, tag) {
|
||||||
|
let results = new Map();
|
||||||
|
songs.forEach(song => {
|
||||||
|
let filter = {}, value;
|
||||||
|
const artist = song["AlbumArtist"] || song["Artist"]
|
||||||
|
|
||||||
|
if (tag == "Album") {
|
||||||
|
value = song[tag];
|
||||||
|
if (artist) { filter["AlbumArtist"] = artist; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == "AlbumArtist") { value = artist; }
|
||||||
|
|
||||||
|
results.set(value, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.forEach((filter, value) => this._buildTag(tag, value, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTag(tag, value, filter) {
|
_buildTag(tag, value, filter) {
|
||||||
|
@ -150,11 +195,7 @@ class Library extends Component {
|
||||||
sel.addCommandAll();
|
sel.addCommandAll();
|
||||||
|
|
||||||
sel.addCommand(async items => {
|
sel.addCommand(async items => {
|
||||||
const commands = [
|
const commands = ["clear",...items.map(createEnqueueCommand), "play"];
|
||||||
"clear",
|
|
||||||
...items.map(createEnqueueCommand),
|
|
||||||
"play"
|
|
||||||
];
|
|
||||||
await this._mpd.command(commands);
|
await this._mpd.command(commands);
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||||
|
|
|
@ -1,29 +1,20 @@
|
||||||
import Item from "../item.js";
|
import Item from "../item.js";
|
||||||
import * as html from "../html.js";
|
import * as html from "../html.js";
|
||||||
|
import * as format from "../format.js";
|
||||||
|
|
||||||
|
|
||||||
function baseName(path) {
|
|
||||||
return path.split("/").pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Path extends Item {
|
export default class Path extends Item {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
super();
|
super();
|
||||||
this._data = data;
|
this._data = data;
|
||||||
|
this._isDirectory = ("directory" in this._data);
|
||||||
if ("directory" in this._data) {
|
|
||||||
this.file = data["directory"];
|
|
||||||
} else {
|
|
||||||
this.file = data["file"];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get file() { return (this._isDirectory ? this._data["directory"] : this._data["file"]); }
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if ("directory" in this._data) {
|
this.appendChild(html.icon(this._isDirectory ? "folder" : "music"));
|
||||||
this.appendChild(html.icon("folder"));
|
this._buildTitle(format.fileName(this.file));
|
||||||
} else {
|
|
||||||
this.appendChild(html.icon("music"));
|
|
||||||
}
|
|
||||||
this._buildTitle(baseName(this.file));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import * as art from "../art.js";
|
import * as art from "../art.js";
|
||||||
import * as html from "../html.js";
|
import * as html from "../html.js";
|
||||||
import * as format from "../format.js";
|
import * as format from "../format.js";
|
||||||
|
|
||||||
import Component from "../component.js";
|
import Component from "../component.js";
|
||||||
|
|
||||||
|
|
||||||
const DELAY = 1000;
|
const DELAY = 1000;
|
||||||
|
|
||||||
class Player extends Component {
|
class Player extends Component {
|
||||||
|
@ -104,7 +104,7 @@ class Player extends Component {
|
||||||
DOM.duration.textContent = format.time(duration);
|
DOM.duration.textContent = format.time(duration);
|
||||||
DOM.progress.max = duration;
|
DOM.progress.max = duration;
|
||||||
DOM.progress.disabled = false;
|
DOM.progress.disabled = false;
|
||||||
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
DOM.title.textContent = data["Title"] || format.fileName(data["file"]);
|
||||||
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
|
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
|
||||||
} else {
|
} else {
|
||||||
DOM.title.textContent = "";
|
DOM.title.textContent = "";
|
||||||
|
@ -118,6 +118,7 @@ class Player extends Component {
|
||||||
|
|
||||||
let artistNew = data["AlbumArtist"] || data["Artist"];
|
let artistNew = data["AlbumArtist"] || data["Artist"];
|
||||||
let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
|
let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
|
||||||
|
|
||||||
if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
|
if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
|
||||||
html.clear(DOM.art);
|
html.clear(DOM.art);
|
||||||
art.get(this._mpd, artistNew, data["Album"], data["file"]).then(src => {
|
art.get(this._mpd, artistNew, data["Album"], data["file"]).then(src => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ function generateMoveCommands(items, diff, all) {
|
||||||
.map(item => {
|
.map(item => {
|
||||||
let index = all.indexOf(item) + diff;
|
let index = all.indexOf(item) + diff;
|
||||||
if (index < 0 || index >= all.length) { return null; } // this does not move
|
if (index < 0 || index >= all.length) { return null; } // this does not move
|
||||||
return `moveid ${item.data["Id"]} ${index}`;
|
return `moveid ${item.songId} ${index}`;
|
||||||
})
|
})
|
||||||
.filter(command => command);
|
.filter(command => command);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class Queue extends Component {
|
||||||
|
|
||||||
_updateCurrent() {
|
_updateCurrent() {
|
||||||
Array.from(this.children).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.songId == this._currentId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class Queue extends Component {
|
||||||
this.appendChild(node);
|
this.appendChild(node);
|
||||||
|
|
||||||
node.addButton("play", async _ => {
|
node.addButton("play", async _ => {
|
||||||
await this._mpd.command(`playid ${song["Id"]}`);
|
await this._mpd.command(`playid ${node.songId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ class Queue extends Component {
|
||||||
|
|
||||||
name = escape(name);
|
name = escape(name);
|
||||||
const commands = items.map(item => {
|
const commands = items.map(item => {
|
||||||
return `playlistadd "${name}" "${escape(item.data["file"])}"`;
|
return `playlistadd "${name}" "${escape(item.file)}"`;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._mpd.command(commands); // FIXME notify?
|
await this._mpd.command(commands); // FIXME notify?
|
||||||
|
@ -110,7 +110,7 @@ class Queue extends Component {
|
||||||
sel.addCommand(async items => {
|
sel.addCommand(async items => {
|
||||||
if (!confirm(`Remove these ${items.length} songs from the queue?`)) { return; }
|
if (!confirm(`Remove these ${items.length} songs from the queue?`)) { return; }
|
||||||
|
|
||||||
const commands = items.map(item => `deleteid ${item.data["Id"]}`);
|
const commands = items.map(item => `deleteid ${item.songId}`);
|
||||||
await this._mpd.command(commands);
|
await this._mpd.command(commands);
|
||||||
|
|
||||||
this._sync();
|
this._sync();
|
||||||
|
|
|
@ -5,12 +5,14 @@ import Item from "../item.js";
|
||||||
export default class Song extends Item {
|
export default class Song extends Item {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
super();
|
super();
|
||||||
this.data = data; // FIXME verejne?
|
this._data = data;
|
||||||
this.dataset.songId = data["Id"]; // FIXME toto maji jen ve fronte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get file() { return this._data["file"]; }
|
||||||
|
get songId() { return this._data["Id"]; }
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const data = this.data;
|
const data = this._data;
|
||||||
|
|
||||||
const block = html.node("div", {className:"multiline"}, "", this);
|
const block = html.node("div", {className:"multiline"}, "", this);
|
||||||
|
|
||||||
|
@ -29,13 +31,8 @@ export default class Song extends Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTitle(data) {
|
_buildTitle(data) {
|
||||||
return super._buildTitle(data["Title"] || fileName(data));
|
return super._buildTitle(data["Title"] || format.fileName(this.file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("cyp-song", Song);
|
customElements.define("cyp-song", Song);
|
||||||
|
|
||||||
// FIXME vyfaktorovat nekam do haje
|
|
||||||
function fileName(data) {
|
|
||||||
return data["file"].split("/").pop();
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,3 +14,7 @@ export function subtitle(data, options = {duration:true}) {
|
||||||
options.duration && data["duration"] && tokens.push(time(Number(data["duration"])));
|
options.duration && data["duration"] && tokens.push(time(Number(data["duration"])));
|
||||||
return tokens.join(SEPARATOR);
|
return tokens.join(SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fileName(file) {
|
||||||
|
return file.split("/").pop();
|
||||||
|
}
|
||||||
|
|
70
app/js/fs.js
70
app/js/fs.js
|
@ -1,70 +0,0 @@
|
||||||
import * as mpd from "./lib/mpd.js";
|
|
||||||
import * as html from "./lib/html.js";
|
|
||||||
import * as ui from "./lib/ui.js";
|
|
||||||
|
|
||||||
import Search from "./lib/search.js";
|
|
||||||
|
|
||||||
let node, search;
|
|
||||||
|
|
||||||
function buildHeader(path) {
|
|
||||||
let header = node.querySelector("header");
|
|
||||||
html.clear(header);
|
|
||||||
|
|
||||||
search.reset();
|
|
||||||
header.appendChild(search.getNode());
|
|
||||||
|
|
||||||
path.split("/").filter(x => x).forEach((name, index, all) => {
|
|
||||||
index && html.node("span", {}, " / ", header);
|
|
||||||
let button = html.button({icon:"folder"}, name, header);
|
|
||||||
let path = all.slice(0, index+1).join("/");
|
|
||||||
button.addEventListener("click", e => list(path));
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDirectory(data, parent) {
|
|
||||||
let path = data["directory"];
|
|
||||||
let name = path.split("/").pop();
|
|
||||||
let node = ui.group(ui.CTX_FS, name, path, parent);
|
|
||||||
node.addEventListener("click", e => list(path));
|
|
||||||
node.dataset.name = name;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFile(data, parent) {
|
|
||||||
let node = ui.song(ui.CTX_FS, data, parent);
|
|
||||||
let name = data["file"].split("/").pop();
|
|
||||||
node.dataset.name = name;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResults(results) {
|
|
||||||
let ul = node.querySelector("ul");
|
|
||||||
html.clear(ul);
|
|
||||||
|
|
||||||
results["directory"].forEach(directory => buildDirectory(directory, ul));
|
|
||||||
results["file"].forEach(file => buildFile(file, ul));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function list(path) {
|
|
||||||
let results = await mpd.listPath(path);
|
|
||||||
buildResults(results);
|
|
||||||
buildHeader(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearch(e) {
|
|
||||||
Array.from(node.querySelectorAll("[data-name]")).forEach(node => {
|
|
||||||
let name = node.dataset.name;
|
|
||||||
node.style.display = (search.match(name) ? "" : "none");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function activate() {
|
|
||||||
list("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function init(n) {
|
|
||||||
node = n;
|
|
||||||
search = new Search(node.querySelector(".search"));
|
|
||||||
search.addEventListener("input", onSearch);
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
import * as mpd from "./lib/mpd.js";
|
|
||||||
import * as html from "./lib/html.js";
|
|
||||||
import * as ui from "./lib/ui.js";
|
|
||||||
import * as format from "./lib/format.js";
|
|
||||||
|
|
||||||
import Search from "./lib/search.js";
|
|
||||||
|
|
||||||
let node, search;
|
|
||||||
|
|
||||||
function nonempty(x) { return x.length > 0; }
|
|
||||||
|
|
||||||
function buildHeader(filter) {
|
|
||||||
filter = filter || {};
|
|
||||||
let header = node.querySelector("header");
|
|
||||||
html.clear(header);
|
|
||||||
|
|
||||||
search.reset();
|
|
||||||
header.appendChild(search.getNode());
|
|
||||||
|
|
||||||
let artist = filter["AlbumArtist"];
|
|
||||||
if (artist) {
|
|
||||||
let artistFilter = {"AlbumArtist":artist};
|
|
||||||
let button = html.button({icon:"artist"}, artist, header);
|
|
||||||
button.addEventListener("click", e => listAlbums(artistFilter));
|
|
||||||
|
|
||||||
let album = filter["Album"];
|
|
||||||
if (album) {
|
|
||||||
html.node("span", {}, format.SEPARATOR, header);
|
|
||||||
let albumFilter = Object.assign({}, artistFilter, {"Album":album});
|
|
||||||
let button = html.button({icon:"album"}, album, header);
|
|
||||||
button.addEventListener("click", e => listSongs(albumFilter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAlbum(album, filter, parent) {
|
|
||||||
let childFilter = Object.assign({}, filter, {"Album": album});
|
|
||||||
let node = ui.group(ui.CTX_LIBRARY, album, childFilter, parent);
|
|
||||||
node.addEventListener("click", e => listSongs(childFilter));
|
|
||||||
node.dataset.name = album;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildArtist(artist, filter, parent) {
|
|
||||||
let childFilter = Object.assign({}, filter, {"AlbumArtist": artist});
|
|
||||||
let node = ui.group(ui.CTX_LIBRARY, artist, childFilter, parent);
|
|
||||||
node.addEventListener("click", e => listAlbums(childFilter));
|
|
||||||
node.dataset.name = artist;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSongs(songs, filter) {
|
|
||||||
let ul = node.querySelector("ul");
|
|
||||||
html.clear(ul);
|
|
||||||
|
|
||||||
songs.map(song => {
|
|
||||||
let node = ui.song(ui.CTX_LIBRARY, song, ul);
|
|
||||||
node.dataset.name = song["Title"];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAlbums(albums, filter) {
|
|
||||||
let ul = node.querySelector("ul");
|
|
||||||
html.clear(ul);
|
|
||||||
|
|
||||||
albums.filter(nonempty).map(album => buildAlbum(album, filter, ul));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildArtists(artists, filter) {
|
|
||||||
let ul = node.querySelector("ul");
|
|
||||||
html.clear(ul);
|
|
||||||
|
|
||||||
artists.filter(nonempty).map(artist => buildArtist(artist, filter, ul));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listSongs(filter) {
|
|
||||||
let songs = await mpd.listSongs(filter);
|
|
||||||
buildSongs(songs, filter);
|
|
||||||
buildHeader(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listAlbums(filter) {
|
|
||||||
let albums = await mpd.listTags("Album", filter);
|
|
||||||
buildAlbums(albums, filter);
|
|
||||||
buildHeader(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listArtists(filter) {
|
|
||||||
let artists = await mpd.listTags("AlbumArtist", filter);
|
|
||||||
buildArtists(artists, filter);
|
|
||||||
buildHeader(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearch(e) {
|
|
||||||
Array.from(node.querySelectorAll("[data-name]")).forEach(node => {
|
|
||||||
let name = node.dataset.name;
|
|
||||||
node.style.display = (search.match(name) ? "" : "none");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function activate() {
|
|
||||||
listArtists();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function init(n) {
|
|
||||||
node = n;
|
|
||||||
|
|
||||||
search = new Search(node.querySelector(".search"));
|
|
||||||
search.addEventListener("input", onSearch);
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ export function status() {
|
||||||
Title: "Title of song",
|
Title: "Title of song",
|
||||||
Artist: "Artist of song",
|
Artist: "Artist of song",
|
||||||
Album: "Album of song",
|
Album: "Album of song",
|
||||||
Track: 6,
|
Track: "6",
|
||||||
state: "play",
|
state: "play",
|
||||||
Id: 2
|
Id: 2
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@ export function status() {
|
||||||
|
|
||||||
export function listQueue() {
|
export function listQueue() {
|
||||||
return [
|
return [
|
||||||
{Id:1, Track:5, Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30},
|
{Id:1, Track:"5", Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30},
|
||||||
status(),
|
status(),
|
||||||
{Id:3, Track:7, Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230},
|
{Id:3, Track:"7", Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,10 @@ export function listSongs(filter, window = null) {
|
||||||
return listQueue();
|
return listQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchSongs(filter) {
|
||||||
|
return listQueue();
|
||||||
|
}
|
||||||
|
|
||||||
export function albumArt(songUrl) {
|
export function albumArt(songUrl) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,11 +37,11 @@ function processQueue() {
|
||||||
ws.send(current.cmd);
|
ws.send(current.cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeFilter(filter) {
|
export function serializeFilter(filter, operator = "==") {
|
||||||
let tokens = ["("];
|
let tokens = ["("];
|
||||||
Object.entries(filter).forEach(([key, value], index) => {
|
Object.entries(filter).forEach(([key, value], index) => {
|
||||||
index && tokens.push(" AND ");
|
index && tokens.push(" AND ");
|
||||||
tokens.push(`(${key} == "${escape(value)}")`);
|
tokens.push(`(${key} ${operator} "${escape(value)}")`);
|
||||||
});
|
});
|
||||||
tokens.push(")");
|
tokens.push(")");
|
||||||
|
|
||||||
|
@ -109,23 +109,33 @@ export async function listTags(tag, filter = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSongs(filter, window = null) {
|
export async function listSongs(filter, window = null) {
|
||||||
let tokens = ["find"];
|
let tokens = ["find", ...serializeFilter(filter)];
|
||||||
tokens.push(serializeFilter(filter));
|
window && tokens.push("window", window.join(":"));
|
||||||
if (window) { tokens.push("window", window.join(":")); }
|
|
||||||
let lines = await command(tokens.join(" "));
|
let lines = await command(tokens.join(" "));
|
||||||
return parser.songList(lines);
|
return parser.songList(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchSongs(filter) {
|
||||||
|
let tokens = ["find", ...serializeFilter(filter, "contains")];
|
||||||
|
let lines = await command(tokens.join(" "));
|
||||||
|
return parser.songList(lines);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function albumArt(songUrl) {
|
export async function albumArt(songUrl) {
|
||||||
let data = [];
|
let data = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
let params = ["albumart", `"${escape(songUrl)}"`, offset];
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
let params = ["albumart", `"${escape(songUrl)}"`, offset];
|
params[2] = offset;
|
||||||
let lines = await command(params.join(" "));
|
try {
|
||||||
data = data.concat(lines[2]);
|
let lines = await command(params.join(" "));
|
||||||
let metadata = parser.linesToStruct(lines.slice(0, 2));
|
data = data.concat(lines[2]);
|
||||||
if (data.length >= Number(metadata["size"])) { return data; }
|
let metadata = parser.linesToStruct(lines.slice(0, 2));
|
||||||
offset += Number(metadata["binary"]);
|
if (data.length >= Number(metadata["size"])) { return data; }
|
||||||
|
offset += Number(metadata["binary"]);
|
||||||
|
} catch (e) { return null; }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,9 @@ export default class Selection {
|
||||||
|
|
||||||
addCommandAll() {
|
addCommandAll() {
|
||||||
this.addCommand(_ => {
|
this.addCommand(_ => {
|
||||||
Array.from(this._component.children).forEach(node => this.add(node));
|
Array.from(this._component.children)
|
||||||
|
.filter(node => node.tagName.toLowerCase().startsWith("cyp-"))
|
||||||
|
.forEach(node => this.add(node));
|
||||||
}, {label:"Select all", icon:"checkbox-marked-outline"});
|
}, {label:"Select all", icon:"checkbox-marked-outline"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
153
app/js/ui.js
153
app/js/ui.js
|
@ -1,153 +0,0 @@
|
||||||
import * as mpd from "./mpd.js";
|
|
||||||
import * as html from "./html.js";
|
|
||||||
import * as format from "./format.js";
|
|
||||||
import * as art from "./art.js";
|
|
||||||
|
|
||||||
export const CTX_FS = 1;
|
|
||||||
export const CTX_QUEUE = 2;
|
|
||||||
export const CTX_LIBRARY = 3;
|
|
||||||
|
|
||||||
const TYPE_ID = 1;
|
|
||||||
const TYPE_URL = 2;
|
|
||||||
const TYPE_FILTER = 3;
|
|
||||||
const TYPE_PLAYLIST = 4;
|
|
||||||
|
|
||||||
const SORT = "-Track";
|
|
||||||
|
|
||||||
async function enqueue(type, what) {
|
|
||||||
switch (type) {
|
|
||||||
case TYPE_URL: return mpd.command(`add "${mpd.escape(what)}"`); break;
|
|
||||||
case TYPE_FILTER: return mpd.enqueueByFilter(what, SORT); break;
|
|
||||||
case TYPE_PLAYLIST: return mpd.command(`load "${mpd.escape(what)}"`); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillArt(parent, filter) {
|
|
||||||
let artist = filter["AlbumArtist"];
|
|
||||||
let album = filter["Album"];
|
|
||||||
let src = null;
|
|
||||||
|
|
||||||
if (artist && album) {
|
|
||||||
src = await art.get(artist, album);
|
|
||||||
if (!src) {
|
|
||||||
let songs = await mpd.listSongs(filter, [0,1]);
|
|
||||||
if (songs.length) {
|
|
||||||
src = await art.get(artist, album, songs[0]["file"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
html.node("img", {src}, "", parent);
|
|
||||||
} else {
|
|
||||||
html.icon(album ? "album" : "artist", parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileName(data) {
|
|
||||||
return data["file"].split("/").pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSongInfo(ctx, data) {
|
|
||||||
let lines = [];
|
|
||||||
let tokens = [];
|
|
||||||
switch (ctx) {
|
|
||||||
case CTX_FS: lines.push(fileName(data)); break;
|
|
||||||
|
|
||||||
case CTX_LIBRARY:
|
|
||||||
case CTX_QUEUE:
|
|
||||||
if (data["Title"]) {
|
|
||||||
if (ctx == CTX_LIBRARY && data["Track"]) {
|
|
||||||
tokens.push(data["Track"].padStart(2, "0"));
|
|
||||||
}
|
|
||||||
tokens.push(data["Title"]);
|
|
||||||
lines.push(tokens.join(" "));
|
|
||||||
lines.push(format.subtitle(data));
|
|
||||||
} else {
|
|
||||||
lines.push(fileName(data));
|
|
||||||
lines.push("\u00A0");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
function playButton(type, what, parent) {
|
|
||||||
let button = html.button({icon:"play", title:"Play"}, "", parent);
|
|
||||||
button.addEventListener("click", async e => {
|
|
||||||
if (type == TYPE_ID) {
|
|
||||||
await mpd.command(`playid ${what}`);
|
|
||||||
} else {
|
|
||||||
await mpd.command("clear");
|
|
||||||
await enqueue(type, what);
|
|
||||||
await mpd.command("play");
|
|
||||||
pubsub.publish("queue-change");
|
|
||||||
}
|
|
||||||
button.closest("cyp-app").querySelector("cyp-player").update(); // FIXME nejde to lepe?
|
|
||||||
});
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addButton(type, what, parent) {
|
|
||||||
let button = html.button({icon:"plus", title:"Add to queue"}, "", parent);
|
|
||||||
button.addEventListener("click", async e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
await enqueue(type, what);
|
|
||||||
pubsub.publish("queue-change");
|
|
||||||
// fixme notification?
|
|
||||||
});
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function song(ctx, data, parent) {
|
|
||||||
let node = html.node("li", {className:"song"}, "", parent);
|
|
||||||
let info = html.node("div", {className:"info"}, "", node);
|
|
||||||
|
|
||||||
if (ctx == CTX_FS) { html.icon("music", info); }
|
|
||||||
|
|
||||||
let lines = formatSongInfo(ctx, data);
|
|
||||||
html.node("h2", {}, lines.shift(), info);
|
|
||||||
lines.length && html.node("div", {}, lines.shift(), info);
|
|
||||||
|
|
||||||
|
|
||||||
switch (ctx) {
|
|
||||||
case CTX_QUEUE:
|
|
||||||
let id = data["Id"];
|
|
||||||
node.dataset.songId = id;
|
|
||||||
playButton(TYPE_ID, id, node);
|
|
||||||
deleteButton(TYPE_ID, id, node);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CTX_LIBRARY:
|
|
||||||
case CTX_FS:
|
|
||||||
let url = data["file"];
|
|
||||||
playButton(TYPE_URL, url, node);
|
|
||||||
addButton(TYPE_URL, url, node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function group(ctx, label, urlOrFilter, parent) {
|
|
||||||
let node = html.node("li", {className:"group"}, "", parent);
|
|
||||||
|
|
||||||
if (ctx == CTX_LIBRARY) {
|
|
||||||
node.classList.add("has-art");
|
|
||||||
let art = html.node("span", {className:"art"}, "", node);
|
|
||||||
fillArt(art, urlOrFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = html.node("span", {className:"info"}, "", node);
|
|
||||||
if (ctx == CTX_FS) { html.icon("folder", info); }
|
|
||||||
html.node("h2", {}, label, info);
|
|
||||||
|
|
||||||
let type = (ctx == CTX_FS ? TYPE_URL : TYPE_FILTER);
|
|
||||||
|
|
||||||
playButton(type, urlOrFilter, node);
|
|
||||||
addButton(type, urlOrFilter, node);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
Loading…
Reference in a new issue