music.mpd vue.js refactoring WIP

This commit is contained in:
Fabio Manganiello 2019-06-03 23:37:19 +02:00
parent e1ddf7bb3b
commit 85bdd54f7e
14 changed files with 531 additions and 81 deletions

View file

@ -9,7 +9,17 @@
background: $slider-bg; background: $slider-bg;
outline: none; 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 { &::-moz-range-thumb {
@include appearance(none); @include appearance(none);
width: 25px; width: 25px;
@ -20,27 +30,36 @@
cursor: pointer; cursor: pointer;
} }
&[disabled]::-webkit-slider-thumb, &[disabled]::-webkit-slider-thumb {
display: none;
width: 0;
}
&[disabled]::-moz-range-thumb { &[disabled]::-moz-range-thumb {
display: none; display: none;
width: 0; width: 0;
} }
&.disabled { &.disabled { opacity: 0.3; }
opacity: 0.3;
}
&::-moz-range-track { &::-moz-range-track {
@include appearance(none); @include appearance(none);
} }
&::-webkit-progress-value, &::-webkit-progress-value {
background: $slider-progress-bg;
height: 15px;
}
&::-moz-range-progress { &::-moz-range-progress {
background: $slider-progress-bg; background: $slider-progress-bg;
height: 15px; height: 15px;
} }
&[disabled]::-webkit-progress-value, &[disabled]::-webkit-progress-value {
background: none;
}
&[disabled]::-moz-range-progress { &[disabled]::-moz-range-progress {
background: none; background: none;
} }

View file

@ -69,7 +69,8 @@
} }
* > .fa { * > .fa {
font-size: 3rem; font-size: 2.5rem;
color: $light-hue-icon-color;
} }
* > .color-logo { * > .color-logo {

View file

@ -1,4 +1,5 @@
$light-hue-properties-bg: rgba(239,239,240,0.5); $light-hue-properties-bg: rgba(239,239,240,0.5);
$light-hue-properties-hover-bg: white; $light-hue-properties-hover-bg: white;
$light-hue-properties-shadow: 0 0 4px 2px rgba(187,187,187,0.75); $light-hue-properties-shadow: 0 0 4px 2px rgba(187,187,187,0.75);
$light-hue-icon-color: #555;

View file

@ -18,7 +18,7 @@
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); } &:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
&:nth-child(even) { background: $default-bg-3; } &:nth-child(even) { background: $default-bg-3; }
&:hover { background: $hover-bg !important; } &:hover { background: $hover-bg !important; }
&.selected { background: $selected-bg; } &.selected { background: $selected-bg !important; }
.artist { .artist {
font-size: $artist-font-size; font-size: $artist-font-size;
@ -38,7 +38,8 @@
} }
&.enabled { &.enabled {
color: $button-enabled-color; color: $button-enabled-color !important;
.fa { color: $button-enabled-color !important; }
} }
&:hover { &:hover {
@ -51,8 +52,11 @@
.panels { .panels {
display: flex; display: flex;
.spacer {
height: 5rem;
}
.browser, .playlist { .browser, .playlist {
height: 100vh - 16rem;
overflow: auto; overflow: auto;
} }
@ -64,9 +68,6 @@
.item { .item {
background: none; background: none;
&:nth-of-type(2) {
margin-top: 4.5rem;
}
} }
.fa { .fa {
@ -116,10 +117,6 @@
} }
} }
.spacer {
height: 5rem;
}
.empty { .empty {
display: flex; display: flex;
align-items: center; align-items: center;
@ -135,10 +132,6 @@
height: 4rem; height: 4rem;
@include animation(active-track 5s infinite); @include animation(active-track 5s infinite);
} }
&:first-child {
margin-top: 4.5rem;
}
} }
} }
} }
@ -239,7 +232,7 @@
} }
} }
#music-mpd-playlist-dropdown { .dropdown {
width: 20rem; width: 20rem;
} }

@ -1 +1 @@
Subproject commit bdfa9823c8b1e25a5c822f6c719ec0e38ead7f71 Subproject commit 3afe50bda5308c27f7c8eee597663948ffbd084e

View file

@ -100,5 +100,20 @@ function openDropdown(element) {
document.addEventListener('click', clickHndl); document.addEventListener('click', clickHndl);
element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' '); element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' ');
openedDropdown = element; 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';
}
}
} }

View file

@ -1,6 +1,24 @@
Vue.component('music-mpd-browser-item', { Vue.component('music-mpd-browser-item', {
template: '#tmpl-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: { methods: {
}, },

View file

@ -32,15 +32,20 @@ Vue.component('music-mpd', {
computed: { computed: {
playlistDropdownItems: function() { playlistDropdownItems: function() {
var self = this; var self = this;
var items = [];
return [ if (Object.keys(this.selectedPlaylistItems).length === 1) {
{ items.push({
text: 'Play', text: 'Play',
icon: 'play', icon: 'play',
click: async function() { click: async function() {
await self.playpos(); await self.playpos();
self.selectedPlaylistItems = {};
}, },
}, });
}
items.push(
{ {
text: 'Add to playlist', text: 'Add to playlist',
icon: 'list', icon: 'list',
@ -54,13 +59,141 @@ Vue.component('music-mpd', {
icon: 'trash', icon: 'trash',
click: async function() { click: async function() {
await self.del(); 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', text: 'Replace and play',
icon: 'info', 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) { _parseStatus: async function(status) {
if (!status || status.length === 0) { if (!status || status.length === 0) {
status = await request('music.mpd.status'); status = await request('music.mpd.status');
@ -146,15 +303,24 @@ Vue.component('music-mpd', {
for (var item of browserItems) { for (var item of browserItems) {
if (item.directory) { if (item.directory) {
this.browserItems.push({ this.browserItems.push({
id: 'directory:' + item.directory,
type: 'directory', type: 'directory',
name: item.directory, name: item.directory,
}); });
} else if (item.playlist) { } else if (item.playlist) {
this.browserItems.push({ this.browserItems.push({
id: 'playlist:' + item.playlist,
type: 'playlist', type: 'playlist',
name: item.playlist, name: item.playlist,
'last-modified': item['last-modified'], '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); 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() { playPause: async function() {
await request('music.mpd.pause'); await request('music.mpd.pause');
let status = await request('music.mpd.status'); let status = await request('music.mpd.status');
@ -277,6 +455,27 @@ Vue.component('music-mpd', {
this._parseStatus(status); 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() { del: async function() {
const positions = Object.keys(this.selectedPlaylistItems); const positions = Object.keys(this.selectedPlaylistItems);
if (!positions.length) { 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() { swap: async function() {
if (Object.keys(this.selectedPlaylistItems).length !== 2) { if (Object.keys(this.selectedPlaylistItems).length !== 2) {
return; return;
@ -306,6 +521,21 @@ Vue.component('music-mpd', {
this._parsePlaylist(playlist); 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) { onNewPlayingTrack: async function(event) {
var previousTrack = { var previousTrack = {
file: this.track.file, file: this.track.file,
@ -406,6 +636,14 @@ Vue.component('music-mpd', {
this.status.random = event.state; this.status.random = event.state;
}, },
onConsumeChange: function(event) {
this.status.consume = event.state;
},
onSingleChange: function(event) {
this.status.single = event.state;
},
startTimer: function() { startTimer: function() {
if (this.timer != null) { if (this.timer != null) {
this.stopTimer(); this.stopTimer();
@ -458,7 +696,6 @@ Vue.component('music-mpd', {
Vue.set(this.selectedPlaylistItems, track.pos, track); Vue.set(this.selectedPlaylistItems, track.pos, track);
} }
} else if (track.pos in this.selectedPlaylistItems) { } else if (track.pos in this.selectedPlaylistItems) {
// TODO when track clicked twice
Vue.delete(this.selectedPlaylistItems, track.pos); Vue.delete(this.selectedPlaylistItems, track.pos);
} else { } else {
this.selectedPlaylistItems = {}; this.selectedPlaylistItems = {};
@ -467,18 +704,68 @@ Vue.component('music-mpd', {
} }
}, },
togglePlaylistSelectionMode: function() { onBrowserItemClick: function(item) {
this.selectionMode.playlist = !this.selectionMode.playlist; if (item.type === 'directory' && item.name === '..') {
if (!this.selectionMode.playlist) { this.selectedBrowserItems = {};
this.selectedPlaylistItems = {}; 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() { togglePlaylistSelectionMode: function() {
this.selectionMode.browser = !this.selectionMode.browser; if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) {
if (!this.selectionMode.browser) { openDropdown(this.$refs.playlistDropdown.$el);
this.selectedBrowserItems = {};
} }
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() { scrollToActiveTrack: function() {
@ -486,6 +773,28 @@ Vue.component('music-mpd', {
this.$refs.activePlaylistTrack[0].$el.scrollIntoView({behavior: 'smooth'}); 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() { created: function() {
@ -499,9 +808,12 @@ Vue.component('music-mpd', {
registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent'); registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent');
registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent'); registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent'); 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() { mounted: function() {
this.adjustLayout();
this.scrollToActiveTrack(); this.scrollToActiveTrack();
}, },
}); });

View file

@ -6,7 +6,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"> <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='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') }}"> <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> <script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.js') }}"></script>

View file

@ -29,7 +29,7 @@
<div class="row slider-container bri-properties" v-if="value.bri !== undefined"> <div class="row slider-container bri-properties" v-if="value.bri !== undefined">
<div class="col-2"> <div class="col-2">
<i class="fa fa-lightbulb-o"></i> <i class="fa fa-lightbulb"></i>
</div> </div>
<div class="slider-container col-10"> <div class="slider-container col-10">
<input class="slider bri" type="range" min="0" max="255" v-model="value.bri" @change="changed"> <input class="slider bri" type="range" min="0" max="255" v-model="value.bri" @change="changed">

View file

@ -1,13 +1,16 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/browser.js') }}"></script> <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"> <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"> <div class="col-1 icon">
<i class="fa fa-folder" v-if="type == 'directory'"></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-list" v-else-if="type === 'playlist'"></i>
<i class="fa fa-music" v-else-if="type === 'file'"></i>
</div> </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> </div>
</script> </script>

View file

@ -5,58 +5,84 @@
<div class="row music-mpd-container"> <div class="row music-mpd-container">
<div class="row panels"> <div class="row panels">
<!-- Browser section --> <!-- Browser section -->
<div class="col-no-margin-l-3 col-no-margin-m-4 s-hidden browser"> <div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
<div class="col-s-12 col-m-4 col-l-3 browser-controls"> <div class="col-s-12 col-no-margin-m-3 col-no-margin-l-3 browser-controls">
<div class="col-8 filter-container"> <div class="col-7 filter-container">
<i class="fa fa-filter input-icon"></i> <i class="fa fa-filter input-icon"></i>
<input type="text" class="with-icon" v-model="browserFilter"> <input type="text" class="with-icon" v-model="browserFilter">
</div> </div>
<div class="col-4 buttons pull-right"> <div class="col-5 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>
<button :class="{enabled: selectionMode.browser}" <button :class="{enabled: selectionMode.browser}"
:title="selectionMode.browser ? 'End selection' : 'Start selection'" :title="selectionMode.browser ? 'End selection' : 'Start selection'"
@click="toggleBrowserSelectionMode"> @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> </button>
</div> </div>
</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 <music-mpd-browser-item
v-for="item in browserItems" v-for="item in browserItems"
v-if="matchesBrowserFilter(item)" v-if="matchesBrowserFilter(item)"
:key="item.type + '-' + item.name" :key="item.id"
:id="item.id"
:type="item.type" :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> </music-mpd-browser-item>
</div> </div>
<!-- Playlist section --> <!-- 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"> <div class="row empty" v-if="playlist.length === 0">
<i class="fa fa-list"></i> <i class="fa fa-list"></i>
</div> </div>
<div class="col-s-12 col-m-8 col-l-9 playlist-controls" v-else> <div class="col-s-12 col-m-9 col-l-9 playlist-controls" v-else>
<div class="col-8 filter-container"> <div class="col-7 filter-container">
<i class="fa fa-filter input-icon"></i> <i class="fa fa-filter input-icon"></i>
<input type="text" class="with-icon" v-model="playlistFilter"> <input type="text" class="with-icon" v-model="playlistFilter">
</div> </div>
<div class="col-4 buttons pull-right"> <div class="col-5 buttons pull-right">
<button title="Search"> <button title="Search">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
<button title="Add item"> <button title="Add item" @click="addToPlaylistPrompt">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
</button> </button>
<button title="Save playlist" v-if="playlist.length"> <button title="Save playlist" v-if="playlist.length">
<i class="fa fa-save"></i> <i class="fa fa-save" @click="savePlaylistPrompt"></i>
</button> </button>
<button title="Swap tracks" <button title="Swap tracks"
v-if="selectionMode.playlist && playlist.length > 1" v-if="selectionMode.playlist && playlist.length > 1"
@ -64,15 +90,14 @@
@click="swap"> @click="swap">
<i class="fa fa-retweet"></i> <i class="fa fa-retweet"></i>
</button> </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}" <button :class="{enabled: selectionMode.playlist}"
:title="selectionMode.playlist ? 'End selection' : 'Start selection'" :title="selectionMode.playlist ? 'End selection' : 'Start selection'"
@click="togglePlaylistSelectionMode"> @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>
<button title="Clear playlist" @click="clear"> <button title="Clear playlist" @click="clear">
<i class="fa fa-ban"></i> <i class="fa fa-ban"></i>
@ -141,11 +166,17 @@
<div class="col-3 pull-right"> <div class="col-3 pull-right">
<div class="row"> <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"> <button @click="random" :class="{enabled: status.random}" title="Toggle shuffle">
<i class="fa fa-random"></i> <i class="fa fa-random"></i>
</button> </button>
<button @click="repeat" :class="{enabled: status.repeat}" title="Toggle repeat"> <button @click="repeat" :class="{enabled: status.repeat}" title="Toggle repeat">
<i class="fa fa-repeat"></i> <i class="fa fa-redo"></i>
</button> </button>
</div> </div>

View file

@ -1,7 +1,7 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/playlist.js') }}"></script> <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"> <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}" :class="{selected: selected, active: active}"
@click="$emit('input', track)"> @click="$emit('input', track)">
<div class="col-5 artist" v-text="track.artist"></div> <div class="col-5 artist" v-text="track.artist"></div>

View file

@ -225,6 +225,34 @@ class MusicMpdPlugin(MusicPlugin):
value = 1 if value == 0 else 0 value = 1 if value == 0 else 0
return self._exec('random', value) 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 @action
def repeat(self, value=None): def repeat(self, value=None):
""" """
@ -248,17 +276,24 @@ class MusicMpdPlugin(MusicPlugin):
return self._exec('shuffle') return self._exec('shuffle')
@action @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 Add a resource (track, album, artist, folder etc.) to the current playlist
:param resource: Resource path or URI :param resource: Resource path or URI
:type resource: str :type resource: str
:param queue: If true then the tracks will be queued after the currently playing track (default: False) :param position: Position where the track(s) will be inserted (default: end of the playlist)
:type queue: bool
:param position: Position where the track(s) will be inserted if queue is false (default: end of the playlist)
:type position: int :type position: int
""" """
@ -278,7 +313,7 @@ class MusicMpdPlugin(MusicPlugin):
r = self._parse_resource(resource) r = self._parse_resource(resource)
if position is None: if position is None:
return self._exec('insert' if queue else 'add', r) return self._exec('add', r)
return self._exec('addid', r, position) return self._exec('addid', r, position)
@action @action
@ -288,8 +323,30 @@ class MusicMpdPlugin(MusicPlugin):
:param positions: Positions of the tracks to be removed :param positions: Positions of the tracks to be removed
:type positions: list[int] :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 @action
def move(self, from_pos, to_pos): def move(self, from_pos, to_pos):