library+fs

This commit is contained in:
Ondrej Zara 2020-03-12 23:03:26 +01:00
parent f0d4f1c3b0
commit 8dc753b815
No known key found for this signature in database
GPG key ID: B0A5751E616840C5
21 changed files with 247 additions and 74 deletions

View file

@ -83,4 +83,6 @@ select {
@import "elements/range.less"; @import "elements/range.less";
@import "elements/playlist.less"; @import "elements/playlist.less";
@import "elements/library.less"; @import "elements/library.less";
@import "elements/artist.less"; @import "elements/tag.less";
@import "elements/back.less";
@import "elements/path.less";

View file

@ -1,3 +0,0 @@
cyp-artist {
.item;
}

View file

@ -0,0 +1,3 @@
cyp-back {
.item;
}

View file

@ -1,3 +1,3 @@
cyp-library { cyp-library {
cyp-song .subtitle { display: none; }
} }

View file

@ -0,0 +1,3 @@
cyp-path {
.item;
}

View file

@ -0,0 +1,3 @@
cyp-tag {
.item;
}

File diff suppressed because one or more lines are too long

View 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

View file

@ -105,5 +105,8 @@
<script type="module" src="js/elements/yt.js"></script> <script type="module" src="js/elements/yt.js"></script>
<script type="module" src="js/elements/song.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/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> </body>
</html> </html>

View file

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

View file

@ -1,11 +1,32 @@
import * as html from "../html.js"; import * as html from "../html.js";
import Component from "../component.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 { class Library extends Component {
constructor() { constructor() {
super({selection:"multi"}); super({selection:"multi"});
this._initCommands();
} }
_onAppLoad() { _onAppLoad() {
@ -25,38 +46,124 @@ class Library extends Component {
html.button({icon:"artist"}, "Artists and albums", this) html.button({icon:"artist"}, "Artists and albums", this)
.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", this)
.addEventListener("click", _ => this._listFS("")); .addEventListener("click", _ => this._listPath(""));
html.button({icon:"magnify"}, "Search", this) html.button({icon:"magnify"}, "Search", this)
.addEventListener("click", _ => this._showSearch()); .addEventListener("click", _ => this._showSearch());
} }
async _listTags(tag, filter = {}) { async _listTags(tag, filter = {}) {
const values = await this._mpd.listTags(tag, filter); const values = await this._mpd.listTags(tag, filter);
html.clear(this); 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() { _showSearch() {
} }
_buildTagValue(tag, value) { _buildTag(tag, value, filter) {
let node; let node;
switch (tag) { switch (tag) {
case "AlbumArtist": case "AlbumArtist":
node = new Artist(value); node = new Tag(tag, value, filter);
node.onClick = () => this._listTags("Album", {[tag]:value}); 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; 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); 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
View 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);

View file

@ -1,7 +1,7 @@
import * as html from "../html.js"; import * as html from "../html.js";
import Component from "../component.js"; import Component from "../component.js";
import Playlist from "./playlist.js"; import Playlist from "./playlist.js";
import { escape } from "../mpd.js";
class Playlists extends Component { class Playlists extends Component {
constructor() { constructor() {
@ -31,7 +31,7 @@ class Playlists extends Component {
sel.addCommand(async item => { sel.addCommand(async item => {
const name = item.name; 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); 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?
@ -39,7 +39,7 @@ class Playlists extends Component {
sel.addCommand(async item => { sel.addCommand(async item => {
const name = item.name; const name = item.name;
await this._mpd.command(`load "${this._mpd.escape(name)}"`); await this._mpd.command(`load "${escape(name)}"`);
this.selection.clear(); this.selection.clear();
this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification? this._app.dispatchEvent(new CustomEvent("queue-change")); // fixme notification?
}, {label:"Enqueue", icon:"plus"}); }, {label:"Enqueue", icon:"plus"});
@ -48,7 +48,7 @@ class Playlists extends Component {
const name = item.name; const name = item.name;
if (!confirm(`Really delete playlist '${name}'?`)) { return; } 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(); this._sync();
}, {label:"Delete", icon:"delete"}); }, {label:"Delete", icon:"delete"});

View file

@ -1,6 +1,7 @@
import * as html from "../html.js"; import * as html from "../html.js";
import Component from "../component.js"; import Component from "../component.js";
import Song from "./song.js"; import Song from "./song.js";
import { escape } from "../mpd.js";
function generateMoveCommands(items, diff, all) { function generateMoveCommands(items, diff, all) {
@ -69,8 +70,7 @@ class Queue extends Component {
const node = new Song(song); const node = new Song(song);
this.appendChild(node); this.appendChild(node);
html.button({icon:"play"}, "", node).addEventListener("click", async e => { node.addButton("play", async _ => {
e.stopPropagation(); // do not select
await this._mpd.command(`playid ${song["Id"]}`); 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"); let name = prompt("Save selected songs as a playlist?", "name");
if (name === null) { return; } if (name === null) { return; }
name = this._mpd.escape(name); name = escape(name);
const commands = items.map(item => { 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? await this._mpd.command(commands); // FIXME notify?

29
app/js/elements/tag.js Normal file
View 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);

View file

@ -1,6 +1,6 @@
import * as html from "../html.js"; import * as html from "../html.js";
import * as conf from "../conf.js"; import * as conf from "../conf.js";
import { escape } from "../mpd.js";
import Component from "../component.js"; import Component from "../component.js";
@ -58,7 +58,7 @@ class YT extends Component {
this.classList.remove("pending"); this.classList.remove("pending");
if (response.status == 200) { if (response.status == 200) {
this._mpd.command(`update ${this._mpd.escape(conf.ytPath)}`); this._mpd.command(`update ${escape(conf.ytPath)}`);
} }
} }

View file

@ -23,6 +23,9 @@ ICONS["artist"] = `<svg viewBox="0 0 24 24">
ICONS["volume-off"] = `<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"/> <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>`; </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"> 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"/> <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>`; </svg>`;

View file

@ -6,6 +6,13 @@ export default class Item extends HTMLElement {
this.addEventListener("click", _ => this.onClick()); 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); } onClick() { this.parentNode.selection.toggle(this); }
_buildTitle(title) { _buildTitle(title) {

View file

@ -1,7 +1,3 @@
import * as mpd from "./mpd.js";
export const escape = mpd.escape;
export function command(cmd) { export function command(cmd) {
console.warn(`mpd-mock does not know "${cmd}"`); console.warn(`mpd-mock does not know "${cmd}"`);
} }
@ -34,7 +30,7 @@ export function listQueue() {
]; ];
} }
export async function listPlaylists() { export function listPlaylists() {
return [ return [
"Playlist 1", "Playlist 1",
"Playlist 2", "Playlist 2",
@ -42,34 +38,33 @@ export async function listPlaylists() {
]; ];
} }
export async function enqueueByFilter(filter, sort = null) { export function listPath(path) {
let tokens = ["findadd"]; return {
tokens.push(serializeFilter(filter)); "directory": [
// sort && tokens.push("sort", sort); FIXME not implemented in MPD {"directory": "Dir 1"},
return command(tokens.join(" ")); {"directory": "Dir 2"},
{"directory": "Dir 3"}
],
"file": [
{"file": "File 1"},
{"file": "File 2"},
{"file": "File 3"}
]
}
} }
export async function listPath(path) { export function listTags(tag, filter = null) {
let lines = await command(`lsinfo "${escape(path)}"`);
return parser.pathContents(lines);
}
export async function listTags(tag, filter = null) {
switch (tag) { switch (tag) {
case "AlbumArtist": return ["Artist 1", "Artist 2", "Artist 3"]; case "AlbumArtist": return ["Artist 1", "Artist 2", "Artist 3"];
case "Album": return ["Album 1", "Album 2", "Album 3"]; case "Album": return ["Album 1", "Album 2", "Album 3"];
} }
} }
export async function listSongs(filter, window = null) { export function listSongs(filter, window = null) {
let tokens = ["find"]; return listQueue();
tokens.push(serializeFilter(filter));
if (window) { tokens.push("window", window.join(":")); }
let lines = await command(tokens.join(" "));
return parser.songList(lines);
} }
export async function albumArt(songUrl) { export function albumArt(songUrl) {
return null; return null;
} }

View file

@ -37,7 +37,7 @@ function processQueue() {
ws.send(current.cmd); ws.send(current.cmd);
} }
function serializeFilter(filter) { export function serializeFilter(filter) {
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 ");
@ -90,13 +90,6 @@ export async function listPlaylists() {
return (list instanceof Array ? list : [list]); 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) { export async function listPath(path) {
let lines = await command(`lsinfo "${escape(path)}"`); let lines = await command(`lsinfo "${escape(path)}"`);
return parser.pathContents(lines); return parser.pathContents(lines);