library+fs
This commit is contained in:
parent
f0d4f1c3b0
commit
8dc753b815
21 changed files with 247 additions and 74 deletions
|
@ -83,4 +83,6 @@ select {
|
|||
@import "elements/range.less";
|
||||
@import "elements/playlist.less";
|
||||
@import "elements/library.less";
|
||||
@import "elements/artist.less";
|
||||
@import "elements/tag.less";
|
||||
@import "elements/back.less";
|
||||
@import "elements/path.less";
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
cyp-artist {
|
||||
.item;
|
||||
}
|
3
app/css/elements/back.less
Normal file
3
app/css/elements/back.less
Normal file
|
@ -0,0 +1,3 @@
|
|||
cyp-back {
|
||||
.item;
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
cyp-library {
|
||||
|
||||
cyp-song .subtitle { display: none; }
|
||||
}
|
||||
|
|
3
app/css/elements/path.less
Normal file
3
app/css/elements/path.less
Normal file
|
@ -0,0 +1,3 @@
|
|||
cyp-path {
|
||||
.item;
|
||||
}
|
3
app/css/elements/tag.less
Normal file
3
app/css/elements/tag.less
Normal file
|
@ -0,0 +1,3 @@
|
|||
cyp-tag {
|
||||
.item;
|
||||
}
|
File diff suppressed because one or more lines are too long
1
app/icons/keyboard-backspace.svg
Normal file
1
app/icons/keyboard-backspace.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M21,11H6.83L10.41,7.41L9,6L3,12L9,18L10.41,16.58L6.83,13H21V11Z" /></svg>
|
After Width: | Height: | Size: 358 B |
|
@ -105,5 +105,8 @@
|
|||
<script type="module" src="js/elements/yt.js"></script>
|
||||
<script type="module" src="js/elements/song.js"></script>
|
||||
<script type="module" src="js/elements/library.js"></script>
|
||||
<script type="module" src="js/elements/tag.js"></script>
|
||||
<script type="module" src="js/elements/back.js"></script>
|
||||
<script type="module" src="js/elements/path.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import * as html from "../html.js";
|
||||
import Item from "../item.js";
|
||||
|
||||
|
||||
export default class Artist extends Item {
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
html.icon("artist", this);
|
||||
this._buildTitle(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-artist", Artist);
|
16
app/js/elements/back.js
Normal file
16
app/js/elements/back.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Item from "../item.js";
|
||||
import * as html from "../html.js";
|
||||
|
||||
|
||||
export default class Back extends Item {
|
||||
constructor(title) {
|
||||
super();
|
||||
this._title = title;
|
||||
}
|
||||
connectedCallback() {
|
||||
this.appendChild(html.icon("keyboard-backspace"));
|
||||
this._buildTitle(this._title);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-back", Back);
|
|
@ -1,11 +1,32 @@
|
|||
import * as html from "../html.js";
|
||||
import Component from "../component.js";
|
||||
import Artist from "./artist.js";
|
||||
import Tag from "./tag.js";
|
||||
import Path from "./path.js";
|
||||
import Back from "./back.js";
|
||||
import Song from "./song.js";
|
||||
import { escape, serializeFilter } from "../mpd.js";
|
||||
|
||||
|
||||
const SORT = "-Track";
|
||||
|
||||
function createEnqueueCommand(node) {
|
||||
if (node instanceof Song) {
|
||||
return `add "${escape(node.data["file"])}"`;
|
||||
} else if (node instanceof Tag) {
|
||||
return [
|
||||
"findadd",
|
||||
serializeFilter(node.createChildFilter()),
|
||||
`sort ${SORT}`
|
||||
].join(" ");
|
||||
} else {
|
||||
throw new Exception(`Cannot create enqueue command for "${node.nodeName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
class Library extends Component {
|
||||
constructor() {
|
||||
super({selection:"multi"});
|
||||
this._initCommands();
|
||||
}
|
||||
|
||||
_onAppLoad() {
|
||||
|
@ -25,38 +46,124 @@ class Library extends Component {
|
|||
html.button({icon:"artist"}, "Artists and albums", this)
|
||||
.addEventListener("click", _ => this._listTags("AlbumArtist"));
|
||||
|
||||
html.button({icon:"folder"}, "Files and directories", this)
|
||||
.addEventListener("click", _ => this._listFS(""));
|
||||
html.button({icon:"folder"}, "Files and directories", this)
|
||||
.addEventListener("click", _ => this._listPath(""));
|
||||
|
||||
html.button({icon:"magnify"}, "Search", this)
|
||||
html.button({icon:"magnify"}, "Search", this)
|
||||
.addEventListener("click", _ => this._showSearch());
|
||||
}
|
||||
|
||||
async _listTags(tag, filter = {}) {
|
||||
const values = await this._mpd.listTags(tag, filter);
|
||||
|
||||
html.clear(this);
|
||||
|
||||
values.forEach(value => this._buildTagValue(tag, value));
|
||||
if ("AlbumArtist" in filter) { this._buildBack(filter); }
|
||||
values.forEach(value => this._buildTag(tag, value, filter));
|
||||
}
|
||||
|
||||
async _listFS(path) {
|
||||
async _listPath(path) {
|
||||
let paths = await this._mpd.listPath(path);
|
||||
html.clear(this);
|
||||
|
||||
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);
|
||||
html.clear(this);
|
||||
this._buildBack(filter);
|
||||
songs.forEach(song => this.appendChild(new Song(song)));
|
||||
}
|
||||
|
||||
_showSearch() {
|
||||
|
||||
}
|
||||
|
||||
_buildTagValue(tag, value) {
|
||||
_buildTag(tag, value, filter) {
|
||||
let node;
|
||||
switch (tag) {
|
||||
case "AlbumArtist":
|
||||
node = new Artist(value);
|
||||
node.onClick = () => this._listTags("Album", {[tag]:value});
|
||||
node = new Tag(tag, value, filter);
|
||||
this.appendChild(node);
|
||||
node.onClick = () => this._listTags("Album", node.createChildFilter());
|
||||
break;
|
||||
|
||||
case "Album":
|
||||
node = new Tag(tag, value, filter);
|
||||
this.appendChild(node);
|
||||
node.addButton("chevron-double-right", _ => this._listSongs(node.createChildFilter()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_buildBack(filterOrPath) {
|
||||
if (typeof(filterOrPath) == "string") {
|
||||
const path = filterOrPath.split("/").slice(0, -1).join("");
|
||||
const node = new Back("..");
|
||||
this.appendChild(node);
|
||||
node.onClick = () => {
|
||||
this.selection.clear();
|
||||
this._listPath(path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = Object.assign({}, filterOrPath)
|
||||
let tag, title;
|
||||
|
||||
if ("Album" in filter) {
|
||||
tag = "Album";
|
||||
title = filter["AlbumArtist"];
|
||||
} else if ("AlbumArtist" in filter) {
|
||||
tag = "AlbumArtist";
|
||||
title = "Artists";
|
||||
}
|
||||
|
||||
delete filter[tag];
|
||||
const node = new Back(title);
|
||||
this.appendChild(node);
|
||||
node.onClick = () => {
|
||||
this.selection.clear();
|
||||
this._listTags(tag, filter);
|
||||
}
|
||||
}
|
||||
|
||||
_buildPath(data) {
|
||||
let node = new Path(data);
|
||||
this.appendChild(node);
|
||||
|
||||
if ("directory" in data) {
|
||||
const path = data["directory"];
|
||||
node.addButton("chevron-double-right", _ => this._listPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
_initCommands() {
|
||||
const sel = this.selection;
|
||||
|
||||
sel.addCommandAll();
|
||||
|
||||
sel.addCommand(async items => {
|
||||
const commands = [
|
||||
"clear",
|
||||
...items.map(createEnqueueCommand),
|
||||
"play"
|
||||
];
|
||||
await this._mpd.command(commands);
|
||||
this.selection.clear();
|
||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||
}, {label:"Play", icon:"play"});
|
||||
|
||||
sel.addCommand(async items => {
|
||||
const commands = items.map(createEnqueueCommand);
|
||||
await this._mpd.command(commands);
|
||||
this.selection.clear();
|
||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||
}, {label:"Enqueue", icon:"plus"});
|
||||
|
||||
sel.addCommandCancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
28
app/js/elements/path.js
Normal file
28
app/js/elements/path.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Item from "../item.js";
|
||||
import * as html from "../html.js";
|
||||
|
||||
|
||||
function baseName(path) {
|
||||
return path.split("/").pop();
|
||||
}
|
||||
|
||||
export default class Path extends Item {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.data = data;
|
||||
// FIXME spis ._data a .url
|
||||
}
|
||||
connectedCallback() {
|
||||
let path;
|
||||
if ("directory" in this.data) {
|
||||
this.appendChild(html.icon("folder"));
|
||||
path = this.data["directory"];
|
||||
} else {
|
||||
this.appendChild(html.icon("music"));
|
||||
path = this.data["file"];
|
||||
}
|
||||
this._buildTitle(path);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-path", Path);
|
|
@ -1,7 +1,7 @@
|
|||
import * as html from "../html.js";
|
||||
import Component from "../component.js";
|
||||
import Playlist from "./playlist.js";
|
||||
|
||||
import { escape } from "../mpd.js";
|
||||
|
||||
class Playlists extends Component {
|
||||
constructor() {
|
||||
|
@ -31,7 +31,7 @@ class Playlists extends Component {
|
|||
|
||||
sel.addCommand(async item => {
|
||||
const name = item.name;
|
||||
const commands = ["clear", `load "${this._mpd.escape(name)}"`, "play"];
|
||||
const commands = ["clear", `load "${escape(name)}"`, "play"];
|
||||
await this._mpd.command(commands);
|
||||
this.selection.clear();
|
||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||
|
@ -39,7 +39,7 @@ class Playlists extends Component {
|
|||
|
||||
sel.addCommand(async item => {
|
||||
const name = item.name;
|
||||
await this._mpd.command(`load "${this._mpd.escape(name)}"`);
|
||||
await this._mpd.command(`load "${escape(name)}"`);
|
||||
this.selection.clear();
|
||||
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
|
||||
}, {label:"Enqueue", icon:"plus"});
|
||||
|
@ -48,7 +48,7 @@ class Playlists extends Component {
|
|||
const name = item.name;
|
||||
if (!confirm(`Really delete playlist '${name}'?`)) { return; }
|
||||
|
||||
await this._mpd.command(`rm "${this._mpd.escape(name)}"`);
|
||||
await this._mpd.command(`rm "${escape(name)}"`);
|
||||
this._sync();
|
||||
}, {label:"Delete", icon:"delete"});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as html from "../html.js";
|
||||
import Component from "../component.js";
|
||||
import Song from "./song.js";
|
||||
import { escape } from "../mpd.js";
|
||||
|
||||
|
||||
function generateMoveCommands(items, diff, all) {
|
||||
|
@ -69,8 +70,7 @@ class Queue extends Component {
|
|||
const node = new Song(song);
|
||||
this.appendChild(node);
|
||||
|
||||
html.button({icon:"play"}, "", node).addEventListener("click", async e => {
|
||||
e.stopPropagation(); // do not select
|
||||
node.addButton("play", async _ => {
|
||||
await this._mpd.command(`playid ${song["Id"]}`);
|
||||
});
|
||||
});
|
||||
|
@ -99,9 +99,9 @@ class Queue extends Component {
|
|||
let name = prompt("Save selected songs as a playlist?", "name");
|
||||
if (name === null) { return; }
|
||||
|
||||
name = this._mpd.escape(name);
|
||||
name = escape(name);
|
||||
const commands = items.map(item => {
|
||||
return `playlistadd "${name}" "${this._mpd.escape(item.data["file"])}"`;
|
||||
return `playlistadd "${name}" "${escape(item.data["file"])}"`;
|
||||
});
|
||||
|
||||
await this._mpd.command(commands); // FIXME notify?
|
||||
|
|
29
app/js/elements/tag.js
Normal file
29
app/js/elements/tag.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import * as html from "../html.js";
|
||||
import Item from "../item.js";
|
||||
|
||||
const ICONS = {
|
||||
"AlbumArtist": "artist",
|
||||
"Album": "album"
|
||||
}
|
||||
|
||||
|
||||
export default class Tag extends Item {
|
||||
constructor(type, value, filter) {
|
||||
super();
|
||||
this._type = type;
|
||||
this._value = value;
|
||||
this._filter = filter;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const icon = ICONS[this._type];
|
||||
html.icon(icon, this);
|
||||
this._buildTitle(this._value);
|
||||
}
|
||||
|
||||
createChildFilter() {
|
||||
return Object.assign({[this._type]:this._value}, this._filter);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-tag", Tag);
|
|
@ -1,6 +1,6 @@
|
|||
import * as html from "../html.js";
|
||||
import * as conf from "../conf.js";
|
||||
|
||||
import { escape } from "../mpd.js";
|
||||
import Component from "../component.js";
|
||||
|
||||
|
||||
|
@ -58,7 +58,7 @@ class YT extends Component {
|
|||
this.classList.remove("pending");
|
||||
|
||||
if (response.status == 200) {
|
||||
this._mpd.command(`update ${this._mpd.escape(conf.ytPath)}`);
|
||||
this._mpd.command(`update ${escape(conf.ytPath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@ ICONS["artist"] = `<svg viewBox="0 0 24 24">
|
|||
ICONS["volume-off"] = `<svg viewBox="0 0 24 24">
|
||||
<path d="M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,10.23 15.5,8.71 14,7.97V10.18L16.45,12.63C16.5,12.43 16.5,12.21 16.5,12Z"/>
|
||||
</svg>`;
|
||||
ICONS["keyboard-backspace"] = `<svg viewBox="0 0 24 24">
|
||||
<path d="M21,11H6.83L10.41,7.41L9,6L3,12L9,18L10.41,16.58L6.83,13H21V11Z"/>
|
||||
</svg>`;
|
||||
ICONS["cancel"] = `<svg viewBox="0 0 24 24">
|
||||
<path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,13.85 4.63,15.55 5.68,16.91L16.91,5.68C15.55,4.63 13.85,4 12,4M12,20A8,8 0 0,0 20,12C20,10.15 19.37,8.45 18.32,7.09L7.09,18.32C8.45,19.37 10.15,20 12,20Z"/>
|
||||
</svg>`;
|
||||
|
|
|
@ -6,6 +6,13 @@ export default class Item extends HTMLElement {
|
|||
this.addEventListener("click", _ => this.onClick());
|
||||
}
|
||||
|
||||
addButton(icon, cb) {
|
||||
html.button({icon}, "", this).addEventListener("click", e => {
|
||||
e.stopPropagation(); // do not select
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
onClick() { this.parentNode.selection.toggle(this); }
|
||||
|
||||
_buildTitle(title) {
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import * as mpd from "./mpd.js";
|
||||
|
||||
export const escape = mpd.escape;
|
||||
|
||||
export function command(cmd) {
|
||||
console.warn(`mpd-mock does not know "${cmd}"`);
|
||||
}
|
||||
|
@ -34,7 +30,7 @@ export function listQueue() {
|
|||
];
|
||||
}
|
||||
|
||||
export async function listPlaylists() {
|
||||
export function listPlaylists() {
|
||||
return [
|
||||
"Playlist 1",
|
||||
"Playlist 2",
|
||||
|
@ -42,34 +38,33 @@ export async function listPlaylists() {
|
|||
];
|
||||
}
|
||||
|
||||
export async function enqueueByFilter(filter, sort = null) {
|
||||
let tokens = ["findadd"];
|
||||
tokens.push(serializeFilter(filter));
|
||||
// sort && tokens.push("sort", sort); FIXME not implemented in MPD
|
||||
return command(tokens.join(" "));
|
||||
export function listPath(path) {
|
||||
return {
|
||||
"directory": [
|
||||
{"directory": "Dir 1"},
|
||||
{"directory": "Dir 2"},
|
||||
{"directory": "Dir 3"}
|
||||
],
|
||||
"file": [
|
||||
{"file": "File 1"},
|
||||
{"file": "File 2"},
|
||||
{"file": "File 3"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPath(path) {
|
||||
let lines = await command(`lsinfo "${escape(path)}"`);
|
||||
return parser.pathContents(lines);
|
||||
}
|
||||
|
||||
export async function listTags(tag, filter = null) {
|
||||
export function listTags(tag, filter = null) {
|
||||
switch (tag) {
|
||||
case "AlbumArtist": return ["Artist 1", "Artist 2", "Artist 3"];
|
||||
case "Album": return ["Album 1", "Album 2", "Album 3"];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listSongs(filter, window = null) {
|
||||
let tokens = ["find"];
|
||||
tokens.push(serializeFilter(filter));
|
||||
if (window) { tokens.push("window", window.join(":")); }
|
||||
let lines = await command(tokens.join(" "));
|
||||
return parser.songList(lines);
|
||||
export function listSongs(filter, window = null) {
|
||||
return listQueue();
|
||||
}
|
||||
|
||||
export async function albumArt(songUrl) {
|
||||
export function albumArt(songUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ function processQueue() {
|
|||
ws.send(current.cmd);
|
||||
}
|
||||
|
||||
function serializeFilter(filter) {
|
||||
export function serializeFilter(filter) {
|
||||
let tokens = ["("];
|
||||
Object.entries(filter).forEach(([key, value], index) => {
|
||||
index && tokens.push(" AND ");
|
||||
|
@ -90,13 +90,6 @@ export async function listPlaylists() {
|
|||
return (list instanceof Array ? list : [list]);
|
||||
}
|
||||
|
||||
export async function enqueueByFilter(filter, sort = null) {
|
||||
let tokens = ["findadd"];
|
||||
tokens.push(serializeFilter(filter));
|
||||
// sort && tokens.push("sort", sort); FIXME not implemented in MPD
|
||||
return command(tokens.join(" "));
|
||||
}
|
||||
|
||||
export async function listPath(path) {
|
||||
let lines = await command(`lsinfo "${escape(path)}"`);
|
||||
return parser.pathContents(lines);
|
||||
|
|
Loading…
Reference in a new issue