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 {
|
||||
color: inherit;
|
||||
input, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
select {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
|
|
@ -7,6 +7,7 @@ cyp-app {
|
|||
height: 100vh;
|
||||
|
||||
font-family: lato, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
|
|
|
@ -1,2 +1,27 @@
|
|||
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 {
|
||||
--spacing: 8px;
|
||||
|
||||
font-size: var(--font-size-large);
|
||||
.font-large;
|
||||
|
||||
dl {
|
||||
margin: var(--spacing);
|
||||
|
|
|
@ -5,12 +5,6 @@ cyp-song {
|
|||
.flex-column;
|
||||
min-width: 0; // bez tohoto se odmita zmensit
|
||||
|
||||
/* FIXME toto je relikt z .component
|
||||
.icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
*/
|
||||
|
||||
.subtitle { .ellipsis; }
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.font-large {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.selectable {
|
||||
cursor: pointer;
|
||||
position: relative; // kotva pro selected::before
|
||||
|
@ -59,11 +64,11 @@
|
|||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
// FIXME take line-height, at vychazi celociselne
|
||||
font-size: var(--font-size-large);
|
||||
min-width: 0;
|
||||
.font-large;
|
||||
.ellipsis;
|
||||
|
||||
font-weight: bold;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@breakpoint-menu: 480px;
|
||||
|
||||
cyp-app {
|
||||
--font-size-large: 112.5%;
|
||||
--icon-spacing: 4px;
|
||||
--primary: rgb(var(--primary-raw));
|
||||
--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 conf from "./conf.js";
|
||||
|
||||
let cache = {};
|
||||
const cache = {};
|
||||
const MIME = "image/jpeg";
|
||||
const STORAGE_PREFIX = `art-${conf.artSize}` ;
|
||||
|
||||
|
@ -14,26 +14,26 @@ function load(key) {
|
|||
}
|
||||
|
||||
async function bytesToImage(bytes) {
|
||||
let blob = new Blob([bytes]);
|
||||
let src = URL.createObjectURL(blob);
|
||||
let image = html.node("img", {src});
|
||||
const blob = new Blob([bytes]);
|
||||
const src = URL.createObjectURL(blob);
|
||||
const image = html.node("img", {src});
|
||||
return new Promise(resolve => {
|
||||
image.onload = () => resolve(image);
|
||||
});
|
||||
}
|
||||
|
||||
function resize(image) {
|
||||
let canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize});
|
||||
let ctx = canvas.getContext("2d");
|
||||
const canvas = html.node("canvas", {width:conf.artSize, height:conf.artSize});
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export async function get(mpd, artist, album, songUrl = null) {
|
||||
let key = `${artist}-${album}`;
|
||||
const key = `${artist}-${album}`;
|
||||
if (key in cache) { return cache[key]; }
|
||||
|
||||
let loaded = await load(key);
|
||||
const loaded = await load(key);
|
||||
if (loaded) {
|
||||
cache[key] = loaded;
|
||||
return loaded;
|
||||
|
@ -43,18 +43,18 @@ export async function get(mpd, artist, album, songUrl = null) {
|
|||
|
||||
// promise to be returned in the meantime
|
||||
let resolve;
|
||||
let promise = new Promise(res => resolve = res);
|
||||
const promise = new Promise(res => resolve = res);
|
||||
cache[key] = promise;
|
||||
|
||||
try {
|
||||
let data = await mpd.albumArt(songUrl);
|
||||
let bytes = new Uint8Array(data);
|
||||
let image = await bytesToImage(bytes);
|
||||
let url = resize(image).toDataURL(MIME);
|
||||
const data = await mpd.albumArt(songUrl);
|
||||
if (data) {
|
||||
const bytes = new Uint8Array(data);
|
||||
const image = await bytesToImage(bytes);
|
||||
const url = resize(image).toDataURL(MIME);
|
||||
store(key, url);
|
||||
cache[key] = url;
|
||||
resolve(url);
|
||||
} catch (e) {
|
||||
} else {
|
||||
cache[key] = null;
|
||||
}
|
||||
return cache[key];
|
||||
|
|
|
@ -47,13 +47,15 @@ class Library extends Component {
|
|||
_showRoot() {
|
||||
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"));
|
||||
|
||||
html.button({icon:"folder"}, "Files and directories", this)
|
||||
html.button({icon:"folder"}, "Files and directories", nav)
|
||||
.addEventListener("click", _ => this._listPath(""));
|
||||
|
||||
html.button({icon:"magnify"}, "Search", this)
|
||||
html.button({icon:"magnify"}, "Search", nav)
|
||||
.addEventListener("click", _ => this._showSearch());
|
||||
}
|
||||
|
||||
|
@ -72,7 +74,7 @@ class Library extends Component {
|
|||
path && this._buildBack(path);
|
||||
paths["directory"].forEach(path => this._buildPath(path));
|
||||
paths["file"].forEach(path => this._buildPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
async _listSongs(filter) {
|
||||
const songs = await this._mpd.listSongs(filter);
|
||||
|
@ -82,7 +84,50 @@ class Library extends Component {
|
|||
}
|
||||
|
||||
_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) {
|
||||
|
@ -150,11 +195,7 @@ class Library extends Component {
|
|||
sel.addCommandAll();
|
||||
|
||||
sel.addCommand(async items => {
|
||||
const commands = [
|
||||
"clear",
|
||||
...items.map(createEnqueueCommand),
|
||||
"play"
|
||||
];
|
||||
const commands = ["clear",...items.map(createEnqueueCommand), "play"];
|
||||
await this._mpd.command(commands);
|
||||
this.selection.clear();
|
||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
import Item from "../item.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 {
|
||||
constructor(data) {
|
||||
super();
|
||||
this._data = data;
|
||||
|
||||
if ("directory" in this._data) {
|
||||
this.file = data["directory"];
|
||||
} else {
|
||||
this.file = data["file"];
|
||||
}
|
||||
this._isDirectory = ("directory" in this._data);
|
||||
}
|
||||
|
||||
get file() { return (this._isDirectory ? this._data["directory"] : this._data["file"]); }
|
||||
|
||||
connectedCallback() {
|
||||
if ("directory" in this._data) {
|
||||
this.appendChild(html.icon("folder"));
|
||||
} else {
|
||||
this.appendChild(html.icon("music"));
|
||||
}
|
||||
this._buildTitle(baseName(this.file));
|
||||
this.appendChild(html.icon(this._isDirectory ? "folder" : "music"));
|
||||
this._buildTitle(format.fileName(this.file));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as art from "../art.js";
|
||||
import * as html from "../html.js";
|
||||
import * as format from "../format.js";
|
||||
|
||||
import Component from "../component.js";
|
||||
|
||||
|
||||
const DELAY = 1000;
|
||||
|
||||
class Player extends Component {
|
||||
|
@ -104,7 +104,7 @@ class Player extends Component {
|
|||
DOM.duration.textContent = format.time(duration);
|
||||
DOM.progress.max = duration;
|
||||
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});
|
||||
} else {
|
||||
DOM.title.textContent = "";
|
||||
|
@ -118,6 +118,7 @@ class Player extends Component {
|
|||
|
||||
let artistNew = data["AlbumArtist"] || data["Artist"];
|
||||
let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
|
||||
|
||||
if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
|
||||
html.clear(DOM.art);
|
||||
art.get(this._mpd, artistNew, data["Album"], data["file"]).then(src => {
|
||||
|
|
|
@ -11,7 +11,7 @@ function generateMoveCommands(items, diff, all) {
|
|||
.map(item => {
|
||||
let index = all.indexOf(item) + diff;
|
||||
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);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class Queue extends Component {
|
|||
|
||||
_updateCurrent() {
|
||||
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);
|
||||
|
||||
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);
|
||||
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?
|
||||
|
@ -110,7 +110,7 @@ class Queue extends Component {
|
|||
sel.addCommand(async items => {
|
||||
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);
|
||||
|
||||
this._sync();
|
||||
|
|
|
@ -5,12 +5,14 @@ import Item from "../item.js";
|
|||
export default class Song extends Item {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.data = data; // FIXME verejne?
|
||||
this.dataset.songId = data["Id"]; // FIXME toto maji jen ve fronte
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get file() { return this._data["file"]; }
|
||||
get songId() { return this._data["Id"]; }
|
||||
|
||||
connectedCallback() {
|
||||
const data = this.data;
|
||||
const data = this._data;
|
||||
|
||||
const block = html.node("div", {className:"multiline"}, "", this);
|
||||
|
||||
|
@ -29,13 +31,8 @@ export default class Song extends Item {
|
|||
}
|
||||
|
||||
_buildTitle(data) {
|
||||
return super._buildTitle(data["Title"] || fileName(data));
|
||||
return super._buildTitle(data["Title"] || format.fileName(this.file));
|
||||
}
|
||||
}
|
||||
|
||||
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"])));
|
||||
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",
|
||||
Artist: "Artist of song",
|
||||
Album: "Album of song",
|
||||
Track: 6,
|
||||
Track: "6",
|
||||
state: "play",
|
||||
Id: 2
|
||||
}
|
||||
|
@ -24,9 +24,9 @@ export function status() {
|
|||
|
||||
export function listQueue() {
|
||||
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(),
|
||||
{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();
|
||||
}
|
||||
|
||||
export function searchSongs(filter) {
|
||||
return listQueue();
|
||||
}
|
||||
|
||||
export function albumArt(songUrl) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -37,11 +37,11 @@ function processQueue() {
|
|||
ws.send(current.cmd);
|
||||
}
|
||||
|
||||
export function serializeFilter(filter) {
|
||||
export function serializeFilter(filter, operator = "==") {
|
||||
let tokens = ["("];
|
||||
Object.entries(filter).forEach(([key, value], index) => {
|
||||
index && tokens.push(" AND ");
|
||||
tokens.push(`(${key} == "${escape(value)}")`);
|
||||
tokens.push(`(${key} ${operator} "${escape(value)}")`);
|
||||
});
|
||||
tokens.push(")");
|
||||
|
||||
|
@ -109,23 +109,33 @@ export async function listTags(tag, filter = {}) {
|
|||
}
|
||||
|
||||
export async function listSongs(filter, window = null) {
|
||||
let tokens = ["find"];
|
||||
tokens.push(serializeFilter(filter));
|
||||
if (window) { tokens.push("window", window.join(":")); }
|
||||
let tokens = ["find", ...serializeFilter(filter)];
|
||||
window && tokens.push("window", window.join(":"));
|
||||
let lines = await command(tokens.join(" "));
|
||||
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) {
|
||||
let data = [];
|
||||
let offset = 0;
|
||||
let params = ["albumart", `"${escape(songUrl)}"`, offset];
|
||||
|
||||
while (1) {
|
||||
let params = ["albumart", `"${escape(songUrl)}"`, offset];
|
||||
let lines = await command(params.join(" "));
|
||||
data = data.concat(lines[2]);
|
||||
let metadata = parser.linesToStruct(lines.slice(0, 2));
|
||||
if (data.length >= Number(metadata["size"])) { return data; }
|
||||
offset += Number(metadata["binary"]);
|
||||
params[2] = offset;
|
||||
try {
|
||||
let lines = await command(params.join(" "));
|
||||
data = data.concat(lines[2]);
|
||||
let metadata = parser.linesToStruct(lines.slice(0, 2));
|
||||
if (data.length >= Number(metadata["size"])) { return data; }
|
||||
offset += Number(metadata["binary"]);
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@ export default class Selection {
|
|||
|
||||
addCommandAll() {
|
||||
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"});
|
||||
}
|
||||
|
||||
|
|
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