music.mpd vue.js refactoring WIP
This commit is contained in:
parent
e1ddf7bb3b
commit
85bdd54f7e
14 changed files with 531 additions and 81 deletions
|
@ -9,7 +9,17 @@
|
|||
background: $slider-bg;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb,
|
||||
// Cursed be thy name Chrome for forcing designers to this hysterical redundancy
|
||||
&::-webkit-slider-thumb {
|
||||
@include appearance(none);
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: 0;
|
||||
background: $slider-thumb-bg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
@include appearance(none);
|
||||
width: 25px;
|
||||
|
@ -20,27 +30,36 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-slider-thumb,
|
||||
&[disabled]::-webkit-slider-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&[disabled]::-moz-range-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
&.disabled { opacity: 0.3; }
|
||||
|
||||
&::-moz-range-track {
|
||||
@include appearance(none);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value,
|
||||
&::-webkit-progress-value {
|
||||
background: $slider-progress-bg;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&::-moz-range-progress {
|
||||
background: $slider-progress-bg;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-progress-value,
|
||||
&[disabled]::-webkit-progress-value {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&[disabled]::-moz-range-progress {
|
||||
background: none;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,8 @@
|
|||
}
|
||||
|
||||
* > .fa {
|
||||
font-size: 3rem;
|
||||
font-size: 2.5rem;
|
||||
color: $light-hue-icon-color;
|
||||
}
|
||||
|
||||
* > .color-logo {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
$light-hue-properties-bg: rgba(239,239,240,0.5);
|
||||
$light-hue-properties-hover-bg: white;
|
||||
$light-hue-properties-shadow: 0 0 4px 2px rgba(187,187,187,0.75);
|
||||
$light-hue-icon-color: #555;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
|
||||
&:nth-child(even) { background: $default-bg-3; }
|
||||
&:hover { background: $hover-bg !important; }
|
||||
&.selected { background: $selected-bg; }
|
||||
&.selected { background: $selected-bg !important; }
|
||||
|
||||
.artist {
|
||||
font-size: $artist-font-size;
|
||||
|
@ -38,7 +38,8 @@
|
|||
}
|
||||
|
||||
&.enabled {
|
||||
color: $button-enabled-color;
|
||||
color: $button-enabled-color !important;
|
||||
.fa { color: $button-enabled-color !important; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -51,8 +52,11 @@
|
|||
.panels {
|
||||
display: flex;
|
||||
|
||||
.spacer {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.browser, .playlist {
|
||||
height: 100vh - 16rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
@ -64,9 +68,6 @@
|
|||
|
||||
.item {
|
||||
background: none;
|
||||
&:nth-of-type(2) {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
|
@ -116,10 +117,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -135,10 +132,6 @@
|
|||
height: 4rem;
|
||||
@include animation(active-track 5s infinite);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -239,7 +232,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
#music-mpd-playlist-dropdown {
|
||||
.dropdown {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit bdfa9823c8b1e25a5c822f6c719ec0e38ead7f71
|
||||
Subproject commit 3afe50bda5308c27f7c8eee597663948ffbd084e
|
|
@ -100,5 +100,20 @@ function openDropdown(element) {
|
|||
document.addEventListener('click', clickHndl);
|
||||
element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' ');
|
||||
openedDropdown = element;
|
||||
|
||||
const maxLeft = Math.min(window.innerWidth, element.parentElement.clientWidth) + element.parentElement.scrollLeft;
|
||||
const maxTop = Math.min(window.innerHeight, element.parentElement.clientHeight) + element.parentElement.scrollTop;
|
||||
|
||||
if (element.parentElement.offsetLeft + element.offsetLeft + parseFloat(getComputedStyle(element).width) >= maxLeft) {
|
||||
if (parseFloat(element.style.left) - parseFloat(getComputedStyle(element).width) >= 0) {
|
||||
element.style.left = (parseFloat(element.style.left) - parseFloat(getComputedStyle(element).width)) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
if (element.parentElement.offsetTop + element.offsetTop + parseFloat(getComputedStyle(element).height) >= maxTop) {
|
||||
if (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height) >= 0) {
|
||||
element.style.top = (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height)) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
Vue.component('music-mpd-browser-item', {
|
||||
template: '#tmpl-music-mpd-browser-item',
|
||||
props: ['type','name'],
|
||||
props: {
|
||||
id: { type: String, },
|
||||
type: { type: String, },
|
||||
name: { type: String, },
|
||||
file: { type: String, },
|
||||
time: { type: String, },
|
||||
artist: { type: String, },
|
||||
title: { type: String, },
|
||||
date: { type: String, },
|
||||
track: { type: String, },
|
||||
genre: { type: String, },
|
||||
lastModified: { type: String, },
|
||||
albumUri: { type: String, },
|
||||
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
},
|
||||
|
|
|
@ -32,15 +32,20 @@ Vue.component('music-mpd', {
|
|||
computed: {
|
||||
playlistDropdownItems: function() {
|
||||
var self = this;
|
||||
var items = [];
|
||||
|
||||
return [
|
||||
{
|
||||
if (Object.keys(this.selectedPlaylistItems).length === 1) {
|
||||
items.push({
|
||||
text: 'Play',
|
||||
icon: 'play',
|
||||
click: async function() {
|
||||
await self.playpos();
|
||||
self.selectedPlaylistItems = {};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
text: 'Add to playlist',
|
||||
icon: 'list',
|
||||
|
@ -54,13 +59,141 @@ Vue.component('music-mpd', {
|
|||
icon: 'trash',
|
||||
click: async function() {
|
||||
await self.del();
|
||||
self.selectedPlaylistItems = {};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (Object.keys(this.selectedPlaylistItems).length === 1) {
|
||||
items.push({
|
||||
text: 'View track info',
|
||||
icon: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
browserDropdownItems: function() {
|
||||
var self = this;
|
||||
var items = [];
|
||||
|
||||
if (Object.keys(this.selectedBrowserItems).length === 1 &&
|
||||
Object.values(this.selectedBrowserItems)[0].type === 'directory') {
|
||||
items.push({
|
||||
text: 'Open',
|
||||
icon: 'folder',
|
||||
click: async function() {
|
||||
await self.cd();
|
||||
self.selectedBrowserItems = {};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(this.selectedBrowserItems).length === 1) {
|
||||
items.push(
|
||||
{
|
||||
text: 'Add and play',
|
||||
icon: 'play',
|
||||
click: async function() {
|
||||
const item = Object.values(self.selectedBrowserItems)[0];
|
||||
var promise;
|
||||
|
||||
switch (item.type) {
|
||||
case 'playlist':
|
||||
promise = self.load(item.name);
|
||||
break;
|
||||
case 'file':
|
||||
promise = self.add(item.name, position=0);
|
||||
break;
|
||||
case 'directory':
|
||||
promise = self.add(item.name);
|
||||
break;
|
||||
default:
|
||||
console.warning('Unable to handle type: ' + item.type);
|
||||
break;
|
||||
}
|
||||
|
||||
await promise;
|
||||
await self.playpos(0);
|
||||
self.selectedBrowserItems = {};
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'View track info',
|
||||
icon: 'info',
|
||||
text: 'Replace and play',
|
||||
icon: 'play',
|
||||
click: async function() {
|
||||
await self.clear();
|
||||
|
||||
const item = Object.values(self.selectedBrowserItems)[0];
|
||||
var promise;
|
||||
|
||||
switch (item.type) {
|
||||
case 'playlist':
|
||||
promise = self.load(item.name);
|
||||
break;
|
||||
case 'file':
|
||||
promise = self.add(item.name, position=0);
|
||||
break;
|
||||
case 'directory':
|
||||
promise = self.add(item.name);
|
||||
break;
|
||||
default:
|
||||
console.warning('Unable to handle type: ' + item.type);
|
||||
break;
|
||||
}
|
||||
|
||||
await promise;
|
||||
await self.playpos(0);
|
||||
self.selectedBrowserItems = {};
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
text: 'Add to queue',
|
||||
icon: 'plus',
|
||||
click: async function() {
|
||||
const items = Object.values(self.selectedBrowserItems);
|
||||
const promises = items.map(item => item.type === 'playlist' ? self.load(item.name) : self.add(item.name));
|
||||
|
||||
await Promise.all(promises);
|
||||
self.selectedBrowserItems = {};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (Object.keys(this.selectedBrowserItems).length === 1
|
||||
&& Object.values(this.selectedBrowserItems)[0].type === 'playlist') {
|
||||
items.push({
|
||||
text: 'Edit',
|
||||
icon: 'pen',
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.values(this.selectedBrowserItems).filter(item => item.type === 'playlist').length === Object.values(this.selectedBrowserItems).length) {
|
||||
items.push({
|
||||
text: 'Remove',
|
||||
icon: 'trash',
|
||||
click: async function() {
|
||||
const items = Object.values(self.selectedBrowserItems);
|
||||
await self.rm(playlists);
|
||||
self.selectedBrowserItems = {};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(this.selectedBrowserItems).length === 1
|
||||
&& Object.values(this.selectedBrowserItems)[0].type === 'file') {
|
||||
items.push({
|
||||
text: 'View info',
|
||||
icon: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -83,6 +216,30 @@ Vue.component('music-mpd', {
|
|||
}
|
||||
},
|
||||
|
||||
// Hack-ish workaround to get the browser and playlist panels to keep their height
|
||||
// in sync with the nav and control bars, as both those elements are fixed.
|
||||
adjustLayout: function() {
|
||||
const adjust = (self) => {
|
||||
const nav = document.querySelector('nav');
|
||||
const panels = document.querySelectorAll('.music-mpd-container .panels .panel');
|
||||
const controls = document.querySelector('.music-mpd-container .controls');
|
||||
|
||||
return () => {
|
||||
const panelHeight = window.innerHeight - nav.clientHeight - controls.clientHeight - 5;
|
||||
if (panelHeight >= 0) {
|
||||
for (const panel of panels) {
|
||||
if (panelHeight != parseFloat(panel.style.height)) {
|
||||
panel.style.height = panelHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
adjust(this)();
|
||||
setInterval(adjust(this), 2000);
|
||||
},
|
||||
|
||||
_parseStatus: async function(status) {
|
||||
if (!status || status.length === 0) {
|
||||
status = await request('music.mpd.status');
|
||||
|
@ -146,15 +303,24 @@ Vue.component('music-mpd', {
|
|||
for (var item of browserItems) {
|
||||
if (item.directory) {
|
||||
this.browserItems.push({
|
||||
id: 'directory:' + item.directory,
|
||||
type: 'directory',
|
||||
name: item.directory,
|
||||
});
|
||||
} else if (item.playlist) {
|
||||
this.browserItems.push({
|
||||
id: 'playlist:' + item.playlist,
|
||||
type: 'playlist',
|
||||
name: item.playlist,
|
||||
'last-modified': item['last-modified'],
|
||||
});
|
||||
} else if (item.file) {
|
||||
this.browserItems.push({
|
||||
id: item.file,
|
||||
type: 'file',
|
||||
name: item.file,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -193,6 +359,18 @@ Vue.component('music-mpd', {
|
|||
this._parseStatus(status);
|
||||
},
|
||||
|
||||
consume: async function() {
|
||||
await request('music.mpd.consume');
|
||||
let status = await request('music.mpd.status');
|
||||
this._parseStatus(status);
|
||||
},
|
||||
|
||||
single: async function() {
|
||||
await request('music.mpd.single');
|
||||
let status = await request('music.mpd.status');
|
||||
this._parseStatus(status);
|
||||
},
|
||||
|
||||
playPause: async function() {
|
||||
await request('music.mpd.pause');
|
||||
let status = await request('music.mpd.status');
|
||||
|
@ -277,6 +455,27 @@ Vue.component('music-mpd', {
|
|||
this._parseStatus(status);
|
||||
},
|
||||
|
||||
add: async function(resource, position=null) {
|
||||
var args = {resource: resource};
|
||||
if (position != null) {
|
||||
args.position = position;
|
||||
}
|
||||
|
||||
let status = await request('music.mpd.add', args);
|
||||
this._parseStatus(status);
|
||||
|
||||
let playlist = await request('music.mpd.playlistinfo');
|
||||
this._parsePlaylist(playlist);
|
||||
},
|
||||
|
||||
load: async function(item) {
|
||||
let status = await request('music.mpd.load', {playlist:item});
|
||||
this._parseStatus(status);
|
||||
|
||||
let playlist = await request('music.mpd.playlistinfo');
|
||||
this._parsePlaylist(playlist);
|
||||
},
|
||||
|
||||
del: async function() {
|
||||
const positions = Object.keys(this.selectedPlaylistItems);
|
||||
if (!positions.length) {
|
||||
|
@ -291,6 +490,22 @@ Vue.component('music-mpd', {
|
|||
}
|
||||
},
|
||||
|
||||
rm: async function(items) {
|
||||
if (!items) {
|
||||
items = Object.values(this.selectedBrowserItems);
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let status = await request('music.mpd.rm', {resource: items.map(_ => _.name)});
|
||||
this._parseStatus(status);
|
||||
|
||||
items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')});
|
||||
this._parseBrowserItems(items);
|
||||
},
|
||||
|
||||
swap: async function() {
|
||||
if (Object.keys(this.selectedPlaylistItems).length !== 2) {
|
||||
return;
|
||||
|
@ -306,6 +521,21 @@ Vue.component('music-mpd', {
|
|||
this._parsePlaylist(playlist);
|
||||
},
|
||||
|
||||
cd: async function() {
|
||||
const item = Object.values(this.selectedBrowserItems)[0];
|
||||
|
||||
if (item.name === '..') {
|
||||
if (this.browserPath.length) {
|
||||
this.browserPath.pop();
|
||||
}
|
||||
} else {
|
||||
this.browserPath = item.name.split('/');
|
||||
}
|
||||
|
||||
const items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')});
|
||||
this._parseBrowserItems(items);
|
||||
},
|
||||
|
||||
onNewPlayingTrack: async function(event) {
|
||||
var previousTrack = {
|
||||
file: this.track.file,
|
||||
|
@ -406,6 +636,14 @@ Vue.component('music-mpd', {
|
|||
this.status.random = event.state;
|
||||
},
|
||||
|
||||
onConsumeChange: function(event) {
|
||||
this.status.consume = event.state;
|
||||
},
|
||||
|
||||
onSingleChange: function(event) {
|
||||
this.status.single = event.state;
|
||||
},
|
||||
|
||||
startTimer: function() {
|
||||
if (this.timer != null) {
|
||||
this.stopTimer();
|
||||
|
@ -458,7 +696,6 @@ Vue.component('music-mpd', {
|
|||
Vue.set(this.selectedPlaylistItems, track.pos, track);
|
||||
}
|
||||
} else if (track.pos in this.selectedPlaylistItems) {
|
||||
// TODO when track clicked twice
|
||||
Vue.delete(this.selectedPlaylistItems, track.pos);
|
||||
} else {
|
||||
this.selectedPlaylistItems = {};
|
||||
|
@ -467,18 +704,68 @@ Vue.component('music-mpd', {
|
|||
}
|
||||
},
|
||||
|
||||
togglePlaylistSelectionMode: function() {
|
||||
this.selectionMode.playlist = !this.selectionMode.playlist;
|
||||
if (!this.selectionMode.playlist) {
|
||||
this.selectedPlaylistItems = {};
|
||||
onBrowserItemClick: function(item) {
|
||||
if (item.type === 'directory' && item.name === '..') {
|
||||
this.selectedBrowserItems = {};
|
||||
this.selectedBrowserItems[item.id] = item;
|
||||
this.cd();
|
||||
this.selectedBrowserItems = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectionMode.browser) {
|
||||
if (item.id in this.selectedBrowserItems) {
|
||||
Vue.delete(this.selectedBrowserItems, item.id);
|
||||
} else {
|
||||
Vue.set(this.selectedBrowserItems, item.id, item);
|
||||
}
|
||||
} else if (item.id in this.selectedBrowserItems) {
|
||||
Vue.delete(this.selectedBrowserItems, item.id);
|
||||
} else {
|
||||
this.selectedBrowserItems = {};
|
||||
Vue.set(this.selectedBrowserItems, item.id, item);
|
||||
openDropdown(this.$refs.browserDropdown.$el);
|
||||
}
|
||||
},
|
||||
|
||||
toggleBrowserSelectionMode: function() {
|
||||
this.selectionMode.browser = !this.selectionMode.browser;
|
||||
if (!this.selectionMode.browser) {
|
||||
this.selectedBrowserItems = {};
|
||||
togglePlaylistSelectionMode: function() {
|
||||
if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) {
|
||||
openDropdown(this.$refs.playlistDropdown.$el);
|
||||
}
|
||||
|
||||
this.selectionMode.playlist = !this.selectionMode.playlist;
|
||||
},
|
||||
|
||||
playlistSelectAll: function() {
|
||||
this.selectedPlaylistItems = {};
|
||||
this.selectionMode.playlist = true;
|
||||
|
||||
for (var track of this.playlist) {
|
||||
this.selectedPlaylistItems[track.pos] = track;
|
||||
}
|
||||
|
||||
openDropdown(this.$refs.playlistDropdown.$el);
|
||||
},
|
||||
|
||||
toggleBrowserSelectionMode: function() {
|
||||
if (this.selectionMode.browser && Object.keys(this.selectedBrowserItems).length) {
|
||||
openDropdown(this.$refs.browserDropdown.$el);
|
||||
}
|
||||
|
||||
this.selectionMode.browser = !this.selectionMode.browser;
|
||||
},
|
||||
|
||||
browserSelectAll: function() {
|
||||
this.selectedBrowserItems = {};
|
||||
this.selectionMode.browser = true;
|
||||
|
||||
for (var item of this.browserItems) {
|
||||
if (item.type !== 'directory' && item.name !== '..') {
|
||||
this.selectedBrowserItems[item.id] = item;
|
||||
}
|
||||
}
|
||||
|
||||
openDropdown(this.$refs.browserDropdown.$el);
|
||||
},
|
||||
|
||||
scrollToActiveTrack: function() {
|
||||
|
@ -486,6 +773,28 @@ Vue.component('music-mpd', {
|
|||
this.$refs.activePlaylistTrack[0].$el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
},
|
||||
|
||||
addToPlaylistPrompt: async function() {
|
||||
var resource = prompt('Path or URI of the resource to add');
|
||||
if (!resource.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.add(resource);
|
||||
},
|
||||
|
||||
savePlaylistPrompt: async function() {
|
||||
var name = prompt('Playlist name');
|
||||
if (!name.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let status = await request('music.mpd.save', {name: name});
|
||||
this._parseStatus(status);
|
||||
|
||||
let items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')});
|
||||
this._parseBrowserItems(items);
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
|
@ -499,9 +808,12 @@ Vue.component('music-mpd', {
|
|||
registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent');
|
||||
registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
|
||||
registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
|
||||
registerEventHandler(this.onConsumeChange, 'platypush.message.event.music.PlaybackConsumeModeChangeEvent');
|
||||
registerEventHandler(this.onSingleChange, 'platypush.message.event.music.PlaybackSingleModeChangeEvent');
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.adjustLayout();
|
||||
this.scrollToActiveTrack();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/all.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dist/webpanel.css') }}">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.js') }}"></script>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<div class="row slider-container bri-properties" v-if="value.bri !== undefined">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-lightbulb-o"></i>
|
||||
<i class="fa fa-lightbulb"></i>
|
||||
</div>
|
||||
<div class="slider-container col-10">
|
||||
<input class="slider bri" type="range" min="0" max="255" v-model="value.bri" @change="changed">
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/browser.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-mpd-browser-item">
|
||||
<div class="row item">
|
||||
<div class="row item browser-item"
|
||||
:class="{selected: selected}"
|
||||
@click="$emit('input', {id: (type === 'file' ? '' : (type + ':')) + name, name: name, type: type})">
|
||||
<div class="col-1 icon">
|
||||
<i class="fa fa-folder" v-if="type == 'directory'"></i>
|
||||
<i class="fa fa-list" v-else-if="type == 'playlist'"></i>
|
||||
<i class="fa fa-folder" v-if="type === 'directory'"></i>
|
||||
<i class="fa fa-list" v-else-if="type === 'playlist'"></i>
|
||||
<i class="fa fa-music" v-else-if="type === 'file'"></i>
|
||||
</div>
|
||||
|
||||
<div class="col-11 name" v-text="name"></div>
|
||||
<div class="col-11 name">{% raw %}{{ type === 'file' ? (artist || '') + (artist ? ' - ' : '') + (title || '[No Title]') : name.split('/').pop() }}{% endraw %}</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
|
|
@ -5,58 +5,84 @@
|
|||
<div class="row music-mpd-container">
|
||||
<div class="row panels">
|
||||
<!-- Browser section -->
|
||||
<div class="col-no-margin-l-3 col-no-margin-m-4 s-hidden browser">
|
||||
<div class="col-s-12 col-m-4 col-l-3 browser-controls">
|
||||
<div class="col-8 filter-container">
|
||||
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
||||
<div class="col-s-12 col-no-margin-m-3 col-no-margin-l-3 browser-controls">
|
||||
<div class="col-7 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="browserFilter">
|
||||
</div>
|
||||
<div class="col-4 buttons pull-right">
|
||||
<button title="Add to queue">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button title="Remove tracks" v-if="selectionMode.playlist"
|
||||
:disabled="Object.keys(selectedPlaylistItems).length === 0"
|
||||
@click="del">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<div class="col-5 buttons pull-right">
|
||||
<button :class="{enabled: selectionMode.browser}"
|
||||
:title="selectionMode.browser ? 'End selection' : 'Start selection'"
|
||||
@click="toggleBrowserSelectionMode">
|
||||
<i class="fa fa-check-square"></i>
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button title="Select all"
|
||||
@click="browserSelectAll">
|
||||
<i class="fa fa-check-double"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dropdown id="music-mpd-browser-dropdown"
|
||||
v-if="browserItems.length > 0"
|
||||
ref="browserDropdown"
|
||||
:items="browserDropdownItems">
|
||||
</dropdown>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<music-mpd-browser-item
|
||||
v-if="browserPath.length > 0"
|
||||
key=".."
|
||||
id="directory:.."
|
||||
type="directory"
|
||||
name=".."
|
||||
:selected="'directory:..' in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
|
||||
<music-mpd-browser-item
|
||||
v-for="item in browserItems"
|
||||
v-if="matchesBrowserFilter(item)"
|
||||
:key="item.type + '-' + item.name"
|
||||
:key="item.id"
|
||||
:id="item.id"
|
||||
:type="item.type"
|
||||
:name="item.name">
|
||||
:name="item.name"
|
||||
:file="item.file"
|
||||
:time="item.time"
|
||||
:artist="item.artist"
|
||||
:title="item.title"
|
||||
:date="item.date"
|
||||
:track="item.track"
|
||||
:genre="item.genre"
|
||||
:lastModified="item['last-modified']"
|
||||
:albumUri="item['x-albumuri']"
|
||||
:selected="item.id in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
</div>
|
||||
|
||||
<!-- Playlist section -->
|
||||
<div class="col-s-12 col-no-margin-m-8 col-no-margin-l-9 playlist">
|
||||
<div class="col-s-12 col-no-margin-m-9 col-no-margin-l-9 panel playlist">
|
||||
<div class="row empty" v-if="playlist.length === 0">
|
||||
<i class="fa fa-list"></i>
|
||||
</div>
|
||||
|
||||
<div class="col-s-12 col-m-8 col-l-9 playlist-controls" v-else>
|
||||
<div class="col-8 filter-container">
|
||||
<div class="col-s-12 col-m-9 col-l-9 playlist-controls" v-else>
|
||||
<div class="col-7 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="playlistFilter">
|
||||
</div>
|
||||
<div class="col-4 buttons pull-right">
|
||||
<div class="col-5 buttons pull-right">
|
||||
<button title="Search">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button title="Add item">
|
||||
<button title="Add item" @click="addToPlaylistPrompt">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button title="Save playlist" v-if="playlist.length">
|
||||
<i class="fa fa-save"></i>
|
||||
<i class="fa fa-save" @click="savePlaylistPrompt"></i>
|
||||
</button>
|
||||
<button title="Swap tracks"
|
||||
v-if="selectionMode.playlist && playlist.length > 1"
|
||||
|
@ -64,15 +90,14 @@
|
|||
@click="swap">
|
||||
<i class="fa fa-retweet"></i>
|
||||
</button>
|
||||
<button title="Remove tracks" v-if="selectionMode.playlist"
|
||||
:disabled="Object.keys(selectedPlaylistItems).length === 0"
|
||||
@click="del">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button :class="{enabled: selectionMode.playlist}"
|
||||
:title="selectionMode.playlist ? 'End selection' : 'Start selection'"
|
||||
@click="togglePlaylistSelectionMode">
|
||||
<i class="fa fa-check-square"></i>
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button title="Select all"
|
||||
@click="playlistSelectAll">
|
||||
<i class="fa fa-check-double"></i>
|
||||
</button>
|
||||
<button title="Clear playlist" @click="clear">
|
||||
<i class="fa fa-ban"></i>
|
||||
|
@ -141,11 +166,17 @@
|
|||
|
||||
<div class="col-3 pull-right">
|
||||
<div class="row">
|
||||
<button @click="single" :class="{enabled: status.single}" title="Toggle single mode">
|
||||
<i class="fa fa-chess-pawn"></i>
|
||||
</button>
|
||||
<button @click="consume" :class="{enabled: status.consume}" title="Toggle consume mode">
|
||||
<i class="fa fa-utensils"></i>
|
||||
</button>
|
||||
<button @click="random" :class="{enabled: status.random}" title="Toggle shuffle">
|
||||
<i class="fa fa-random"></i>
|
||||
</button>
|
||||
<button @click="repeat" :class="{enabled: status.repeat}" title="Toggle repeat">
|
||||
<i class="fa fa-repeat"></i>
|
||||
<i class="fa fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/playlist.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-mpd-playlist-item">
|
||||
<div class="row item"
|
||||
<div class="row item playlist-item"
|
||||
:class="{selected: selected, active: active}"
|
||||
@click="$emit('input', track)">
|
||||
<div class="col-5 artist" v-text="track.artist"></div>
|
||||
|
|
|
@ -225,6 +225,34 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
value = 1 if value == 0 else 0
|
||||
return self._exec('random', value)
|
||||
|
||||
@action
|
||||
def consume(self, value=None):
|
||||
"""
|
||||
Set consume mode
|
||||
|
||||
:param value: If set, set the consume state this value (true/false). Default: None (toggle current state)
|
||||
:type value: bool
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
value = int(self.status().output['consume'])
|
||||
value = 1 if value == 0 else 0
|
||||
return self._exec('consume', value)
|
||||
|
||||
@action
|
||||
def single(self, value=None):
|
||||
"""
|
||||
Set single mode
|
||||
|
||||
:param value: If set, set the consume state this value (true/false). Default: None (toggle current state)
|
||||
:type value: bool
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
value = int(self.status().output['single'])
|
||||
value = 1 if value == 0 else 0
|
||||
return self._exec('single', value)
|
||||
|
||||
@action
|
||||
def repeat(self, value=None):
|
||||
"""
|
||||
|
@ -248,17 +276,24 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
return self._exec('shuffle')
|
||||
|
||||
@action
|
||||
def add(self, resource, queue=False, position=None):
|
||||
def save(self, name):
|
||||
"""
|
||||
Save the current tracklist to a new playlist with the specified name
|
||||
|
||||
:param name: Name of the playlist
|
||||
:type name: str
|
||||
"""
|
||||
return self._exec('save', name)
|
||||
|
||||
@action
|
||||
def add(self, resource, position=None):
|
||||
"""
|
||||
Add a resource (track, album, artist, folder etc.) to the current playlist
|
||||
|
||||
:param resource: Resource path or URI
|
||||
:type resource: str
|
||||
|
||||
:param queue: If true then the tracks will be queued after the currently playing track (default: False)
|
||||
:type queue: bool
|
||||
|
||||
:param position: Position where the track(s) will be inserted if queue is false (default: end of the playlist)
|
||||
:param position: Position where the track(s) will be inserted (default: end of the playlist)
|
||||
:type position: int
|
||||
"""
|
||||
|
||||
|
@ -278,7 +313,7 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
r = self._parse_resource(resource)
|
||||
|
||||
if position is None:
|
||||
return self._exec('insert' if queue else 'add', r)
|
||||
return self._exec('add', r)
|
||||
return self._exec('addid', r, position)
|
||||
|
||||
@action
|
||||
|
@ -288,8 +323,30 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
|
||||
:param positions: Positions of the tracks to be removed
|
||||
:type positions: list[int]
|
||||
|
||||
:return: The modified playlist
|
||||
"""
|
||||
return self._exec('delete', *positions)
|
||||
|
||||
for pos in sorted(positions, key=int, reverse=True):
|
||||
self._exec('delete', pos)
|
||||
return self.playlistinfo()
|
||||
|
||||
@action
|
||||
def rm(self, playlist):
|
||||
"""
|
||||
Permanently remove playlist(s) by name
|
||||
|
||||
:param playlist: Name or list of playlist names to remove
|
||||
:type playlist: str or list[str]
|
||||
"""
|
||||
|
||||
if isinstance(playlist, str):
|
||||
playlist = [playlist]
|
||||
elif not isinstance(playlist, list):
|
||||
raise RuntimeError('Invalid type for playlist: {}'.format(type(playlist)))
|
||||
|
||||
for p in playlist:
|
||||
self._exec('rm', p)
|
||||
|
||||
@action
|
||||
def move(self, from_pos, to_pos):
|
||||
|
|
Loading…
Reference in a new issue