cyp/app/js/elements/library.js

240 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

2020-03-12 05:46:28 +08:00
import * as html from "../html.js";
import Component from "../component.js";
2020-03-13 06:03:26 +08:00
import Tag from "./tag.js";
import Path from "./path.js";
import Back from "./back.js";
import Song from "./song.js";
2020-03-17 05:57:13 +08:00
import Search from "./search.js";
2020-03-13 06:03:26 +08:00
import { escape, serializeFilter } from "../mpd.js";
const SORT = "-Track";
2020-03-15 03:33:41 +08:00
const TAGS = {
"Album": "Albums",
"AlbumArtist": "Artists"
}
2020-03-13 06:03:26 +08:00
2020-03-13 23:52:24 +08:00
function nonempty(str) { return (str.length > 0); }
2020-03-13 06:03:26 +08:00
function createEnqueueCommand(node) {
if (node instanceof Song) {
return `add "${escape(node.data["file"])}"`;
2020-03-13 23:52:24 +08:00
} else if (node instanceof Path) {
return `add "${escape(node.file)}"`;
2020-03-13 06:03:26 +08:00
} else if (node instanceof Tag) {
return [
"findadd",
serializeFilter(node.createChildFilter()),
2020-03-13 23:52:24 +08:00
// `sort ${SORT}` // MPD >= 0.22, not yet released
2020-03-13 06:03:26 +08:00
].join(" ");
} else {
2020-03-13 17:36:13 +08:00
throw new Error(`Cannot create enqueue command for "${node.nodeName}"`);
2020-03-13 06:03:26 +08:00
}
}
2020-03-12 05:46:28 +08:00
class Library extends Component {
constructor() {
super({selection:"multi"});
2020-03-15 03:33:41 +08:00
this._stateStack = [];
2020-03-13 06:03:26 +08:00
this._initCommands();
2020-03-17 05:57:13 +08:00
this._search = new Search();
this._search.onSubmit = _ => {
let query = this._search.value;
if (query.length < 3) { return; }
this._doSearch(query);
}
2020-03-12 05:46:28 +08:00
}
2020-03-15 03:33:41 +08:00
_popState() {
this.selection.clear();
this._stateStack.pop();
2020-03-15 03:33:41 +08:00
if (this._stateStack.length > 0) {
let state = this._stateStack[this._stateStack.length-1];
this._showState(state);
} else {
this._showRoot();
}
}
2020-03-12 05:46:28 +08:00
_onAppLoad() {
this._showRoot();
}
_onComponentChange(c, isThis) {
const wasHidden = this.hidden;
this.hidden = !isThis;
2020-03-13 23:52:24 +08:00
if (!wasHidden && isThis) { this._showRoot(); }
2020-03-12 05:46:28 +08:00
}
_showRoot() {
2020-03-15 03:33:41 +08:00
this._stateStack = [];
2020-03-12 05:46:28 +08:00
html.clear(this);
2020-03-14 06:01:16 +08:00
const nav = html.node("nav", {}, "", this);
html.button({icon:"artist"}, "Artists and albums", nav)
2020-03-15 03:33:41 +08:00
.addEventListener("click", _ => this._pushState({type:"tags", tag:"AlbumArtist"}));
2020-03-12 05:46:28 +08:00
2020-03-14 06:01:16 +08:00
html.button({icon:"folder"}, "Files and directories", nav)
2020-03-15 03:33:41 +08:00
.addEventListener("click", _ => this._pushState({type:"path", path:""}));
2020-03-12 05:46:28 +08:00
2020-03-14 06:01:16 +08:00
html.button({icon:"magnify"}, "Search", nav)
2020-03-15 03:33:41 +08:00
.addEventListener("click", _ => this._pushState({type:"search"}));
}
_pushState(state) {
this.selection.clear();
2020-03-15 03:33:41 +08:00
this._stateStack.push(state);
2020-03-15 03:33:41 +08:00
this._showState(state);
}
_showState(state) {
switch (state.type) {
case "tags": this._listTags(state.tag, state.filter); break;
case "songs": this._listSongs(state.filter); break;
case "path": this._listPath(state.path); break;
case "search": this._showSearch(state.query); break;
}
2020-03-12 05:46:28 +08:00
}
async _listTags(tag, filter = {}) {
const values = await this._mpd.listTags(tag, filter);
html.clear(this);
2020-03-15 03:33:41 +08:00
if ("AlbumArtist" in filter) { this._buildBack(); }
2020-03-13 23:52:24 +08:00
values.filter(nonempty).forEach(value => this._buildTag(tag, value, filter));
2020-03-12 05:46:28 +08:00
}
2020-03-13 06:03:26 +08:00
async _listPath(path) {
let paths = await this._mpd.listPath(path);
html.clear(this);
2020-03-15 03:33:41 +08:00
path && this._buildBack();
2020-03-13 06:03:26 +08:00
paths["directory"].forEach(path => this._buildPath(path));
paths["file"].forEach(path => this._buildPath(path));
2020-03-14 06:01:16 +08:00
}
2020-03-12 05:46:28 +08:00
2020-03-13 06:03:26 +08:00
async _listSongs(filter) {
const songs = await this._mpd.listSongs(filter);
html.clear(this);
2020-03-15 03:33:41 +08:00
this._buildBack();
2020-03-13 06:03:26 +08:00
songs.forEach(song => this.appendChild(new Song(song)));
2020-03-12 05:46:28 +08:00
}
2020-03-15 03:33:41 +08:00
_showSearch(query = "") {
2020-03-14 06:01:16 +08:00
html.clear(this);
2020-03-17 05:57:13 +08:00
this.appendChild(this._search);
this._search.value = query;
this._search.focus();
2020-03-14 06:01:16 +08:00
2020-03-17 05:57:13 +08:00
query && this._search.onSubmit();
2020-03-14 06:01:16 +08:00
}
2020-03-17 05:57:13 +08:00
async _doSearch(query) {
2020-03-15 03:33:41 +08:00
let state = this._stateStack[this._stateStack.length-1];
state.query = query;
2020-03-17 05:57:13 +08:00
html.clear(this);
this.appendChild(this._search);
this._search.pending(true);
2020-03-15 03:33:41 +08:00
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
const songs2 = await this._mpd.searchSongs({"Album": query});
const songs3 = await this._mpd.searchSongs({"Title": query});
2020-03-17 05:57:13 +08:00
this._search.pending(false);
2020-03-14 06:01:16 +08:00
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);
});
2020-03-12 05:46:28 +08:00
2020-03-14 06:01:16 +08:00
results.forEach((filter, value) => this._buildTag(tag, value, filter));
2020-03-12 05:46:28 +08:00
}
2020-03-13 06:03:26 +08:00
_buildTag(tag, value, filter) {
2020-03-12 05:46:28 +08:00
let node;
switch (tag) {
case "AlbumArtist":
2020-03-13 06:03:26 +08:00
node = new Tag(tag, value, filter);
this.appendChild(node);
2020-03-15 03:33:41 +08:00
node.onClick = () => this._pushState({type:"tags", tag:"Album", filter:node.createChildFilter()});
2020-03-13 06:03:26 +08:00
break;
case "Album":
node = new Tag(tag, value, filter);
this.appendChild(node);
2020-03-15 03:33:41 +08:00
node.addButton("chevron-double-right", _ => this._pushState({type:"songs", filter:node.createChildFilter()}));
2020-03-12 05:46:28 +08:00
break;
}
2020-03-15 05:11:14 +08:00
node.fillArt(this._mpd);
2020-03-13 06:03:26 +08:00
}
2020-03-15 03:33:41 +08:00
_buildBack() {
const backState = this._stateStack[this._stateStack.length-2];
let title;
switch (backState.type) {
case "path": title = ".."; break;
case "search": title = "Search"; break;
case "tags": title = TAGS[backState.tag]; break;
2020-03-13 06:03:26 +08:00
}
const node = new Back(title);
this.appendChild(node);
2020-03-15 03:33:41 +08:00
node.onClick = () => this._popState();
2020-03-13 06:03:26 +08:00
}
_buildPath(data) {
let node = new Path(data);
2020-03-12 05:46:28 +08:00
this.appendChild(node);
2020-03-13 06:03:26 +08:00
if ("directory" in data) {
const path = data["directory"];
2020-03-15 03:33:41 +08:00
node.addButton("chevron-double-right", _ => this._pushState({type:"path", path}));
2020-03-13 06:03:26 +08:00
}
}
_initCommands() {
const sel = this.selection;
sel.addCommandAll();
sel.addCommand(async items => {
2020-03-14 06:01:16 +08:00
const commands = ["clear",...items.map(createEnqueueCommand), "play"];
2020-03-13 06:03:26 +08:00
await this._mpd.command(commands);
2020-05-06 05:03:36 +08:00
this.selection.clear(); // fixme notification?
2020-03-13 06:03:26 +08:00
}, {label:"Play", icon:"play"});
sel.addCommand(async items => {
const commands = items.map(createEnqueueCommand);
await this._mpd.command(commands);
2020-05-06 05:03:36 +08:00
this.selection.clear(); // fixme notification?
2020-03-13 06:03:26 +08:00
}, {label:"Enqueue", icon:"plus"});
sel.addCommandCancel();
2020-03-12 05:46:28 +08:00
}
}
customElements.define("cyp-library", Library);