Implemented support for adding tracks to playlists
This commit is contained in:
parent
0b6b29f043
commit
1ad72a2695
7 changed files with 210 additions and 14 deletions
|
@ -130,6 +130,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
.empty {
|
||||||
|
font-size: 1em;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
@include animation(active-track 5s infinite);
|
@include animation(active-track 5s infinite);
|
||||||
|
@ -144,6 +150,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playlist-add {
|
||||||
|
.playlist-add-controls {
|
||||||
|
background: $playlist-controls-bg;
|
||||||
|
border-bottom: $playlist-controls-border;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: $filter-bar-shadow;
|
||||||
|
margin: -2.5rem -2rem 0 -2rem;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlists-container {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0 -2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
@extend .vertical-center;
|
@extend .vertical-center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -296,12 +320,16 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#music-mpd-info {
|
#music-mpd-info {
|
||||||
.modal {
|
.modal {
|
||||||
.body {
|
.body {
|
||||||
|
@ -333,6 +361,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#music-mpd-playlist-add {
|
||||||
|
.modal {
|
||||||
|
min-width: 50rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media #{map-get($widths, 's')} {
|
@media #{map-get($widths, 's')} {
|
||||||
#music-mpd-info {
|
#music-mpd-info {
|
||||||
.modal {
|
.modal {
|
||||||
|
|
|
@ -26,4 +26,5 @@ $search-modal-footer-border: 1px solid #ccc;
|
||||||
$info-modal-row-border: 1px solid #ddd;
|
$info-modal-row-border: 1px solid #ddd;
|
||||||
$info-modal-attr-color: #777;
|
$info-modal-attr-color: #777;
|
||||||
$track-info-hover-color: rgb(46,190,110);
|
$track-info-hover-color: rgb(46,190,110);
|
||||||
|
$filter-bar-shadow: 0 2.5px 4px 0 #bbb;
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ Vue.component('music-mpd', {
|
||||||
status: {},
|
status: {},
|
||||||
timer: null,
|
timer: null,
|
||||||
playlist: [],
|
playlist: [],
|
||||||
|
playlists: [],
|
||||||
playlistFilter: '',
|
playlistFilter: '',
|
||||||
browserFilter: '',
|
browserFilter: '',
|
||||||
|
playlistAddFilter: '',
|
||||||
browserPath: [],
|
browserPath: [],
|
||||||
browserItems: [],
|
browserItems: [],
|
||||||
|
|
||||||
|
@ -26,9 +28,12 @@ Vue.component('music-mpd', {
|
||||||
infoItem: {},
|
infoItem: {},
|
||||||
modalVisible: {
|
modalVisible: {
|
||||||
info: false,
|
info: false,
|
||||||
|
playlistAdd: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addToPlaylistItems: [],
|
||||||
selectedPlaylistItems: {},
|
selectedPlaylistItems: {},
|
||||||
|
selectedPlaylistAddItems: {},
|
||||||
selectedBrowserItems: {},
|
selectedBrowserItems: {},
|
||||||
|
|
||||||
syncTime: {
|
syncTime: {
|
||||||
|
@ -84,6 +89,12 @@ Vue.component('music-mpd', {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Add to playlist',
|
text: 'Add to playlist',
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
|
click: async function() {
|
||||||
|
self.addToPlaylistItems = Object.values(self.selectedPlaylistItems).map(_ => _.file);
|
||||||
|
self.selectedPlaylistItems = {};
|
||||||
|
self.modalVisible.playlistAdd = true;
|
||||||
|
await self.listplaylists();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(this.selectedPlaylistItems).length < this.playlist.length) {
|
if (Object.keys(this.selectedPlaylistItems).length < this.playlist.length) {
|
||||||
|
@ -233,6 +244,21 @@ Vue.component('music-mpd', {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Object.values(this.selectedBrowserItems).filter(_ => _.type === 'file').length === Object.values(this.selectedBrowserItems).length) {
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
text: 'Add to playlist',
|
||||||
|
icon: 'list',
|
||||||
|
click: async function() {
|
||||||
|
self.addToPlaylistItems = Object.keys(self.selectedBrowserItems);
|
||||||
|
self.modalVisible.playlistAdd = true;
|
||||||
|
await self.listplaylists();
|
||||||
|
self.selectedBrowserItems = {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(this.selectedBrowserItems).length === 1
|
if (Object.keys(this.selectedBrowserItems).length === 1
|
||||||
&& Object.values(this.selectedBrowserItems)[0].type === 'playlist') {
|
&& Object.values(this.selectedBrowserItems)[0].type === 'playlist') {
|
||||||
items.push({
|
items.push({
|
||||||
|
@ -263,6 +289,9 @@ Vue.component('music-mpd', {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'View info',
|
text: 'View info',
|
||||||
icon: 'info',
|
icon: 'info',
|
||||||
|
click: async function() {
|
||||||
|
await self.info(Object.values(self.selectedBrowserItems)[0].name);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +367,7 @@ Vue.component('music-mpd', {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [attr, value] of Object.entries(track)) {
|
for (const [attr, value] of Object.entries(track)) {
|
||||||
if (['id','pos','time'].indexOf(attr) >= 0) {
|
if (['id','pos','time','track','disc'].indexOf(attr) >= 0) {
|
||||||
Vue.set(this.track, attr, parseInt(value));
|
Vue.set(this.track, attr, parseInt(value));
|
||||||
} else {
|
} else {
|
||||||
Vue.set(this.track, attr, value);
|
Vue.set(this.track, attr, value);
|
||||||
|
@ -355,7 +384,7 @@ Vue.component('music-mpd', {
|
||||||
|
|
||||||
for (var track of playlist) {
|
for (var track of playlist) {
|
||||||
for (const [attr, value] of Object.entries(track)) {
|
for (const [attr, value] of Object.entries(track)) {
|
||||||
if (['time','pos','id'].indexOf(attr) >= 0) {
|
if (['time','pos','id','track','disc'].indexOf(attr) >= 0) {
|
||||||
track[attr] = parseInt(value);
|
track[attr] = parseInt(value);
|
||||||
} else {
|
} else {
|
||||||
track[attr] = value;
|
track[attr] = value;
|
||||||
|
@ -599,7 +628,8 @@ Vue.component('music-mpd', {
|
||||||
var info = item;
|
var info = item;
|
||||||
|
|
||||||
if (typeof(item) === 'string') {
|
if (typeof(item) === 'string') {
|
||||||
item = await request('music.mpd.search', {filter: {file: info}});
|
var items = await request('music.mpd.search', {filter: ['file', info]});
|
||||||
|
item = items.length ? items[0] : {file: info};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.infoItem = item;
|
this.infoItem = item;
|
||||||
|
@ -630,6 +660,42 @@ Vue.component('music-mpd', {
|
||||||
await this.$refs.search.search();
|
await this.$refs.search.search();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listplaylists: async function() {
|
||||||
|
this.playlists = [];
|
||||||
|
let playlists = await request('music.mpd.listplaylists');
|
||||||
|
|
||||||
|
for (const p of playlists) {
|
||||||
|
this.playlists.push(p);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
playlistadd: async function(items=[], playlists=[]) {
|
||||||
|
if (!playlists.length) {
|
||||||
|
if (this.modalVisible.playlistAdd) {
|
||||||
|
playlists = Object.keys(this.selectedPlaylistAddItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
items = this.addToPlaylistItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.length || !playlists.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var promises = [];
|
||||||
|
for (const playlist of playlists) {
|
||||||
|
promises.push(request('music.mpd.playlistadd', {
|
||||||
|
name: playlist, uri: items.map(_ => typeof(_) === 'object' ? _.file : _)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
this.modalVisible.playlistAdd = false;
|
||||||
|
this.addToPlaylistItems = [];
|
||||||
|
},
|
||||||
|
|
||||||
onNewPlayingTrack: async function(event) {
|
onNewPlayingTrack: async function(event) {
|
||||||
var previousTrack = {
|
var previousTrack = {
|
||||||
file: this.track.file,
|
file: this.track.file,
|
||||||
|
@ -777,7 +843,15 @@ Vue.component('music-mpd', {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
const filter = this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ');
|
const filter = this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ');
|
||||||
return item.name.toLocaleLowerCase().indexOf(filter) >= 0;
|
return (item.artist || '').concat(item.name).toLocaleLowerCase().indexOf(filter) >= 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
matchesPlaylistAddFilter: function(item) {
|
||||||
|
if (this.playlistAddFilter.length === 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const filter = this.playlistAddFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ');
|
||||||
|
return (item.playlist || '').toLocaleLowerCase().indexOf(filter) >= 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
onPlaylistItemClick: async function(track) {
|
onPlaylistItemClick: async function(track) {
|
||||||
|
@ -831,6 +905,14 @@ Vue.component('music-mpd', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPlaylistAddItemClick: function(playlist) {
|
||||||
|
if (playlist.playlist in this.selectedPlaylistAddItems) {
|
||||||
|
Vue.delete(this.selectedPlaylistAddItems, playlist.playlist);
|
||||||
|
} else {
|
||||||
|
Vue.set(this.selectedPlaylistAddItems, playlist.playlist, playlist);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
togglePlaylistSelectionMode: function() {
|
togglePlaylistSelectionMode: function() {
|
||||||
if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) {
|
if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) {
|
||||||
openDropdown(this.$refs.playlistDropdown.$el);
|
openDropdown(this.$refs.playlistDropdown.$el);
|
||||||
|
|
|
@ -66,6 +66,21 @@ Vue.component('music-mpd-search', {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Object.values(this.selectedItems).filter(_ => _.time != null).length === Object.values(this.selectedItems).length) {
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
text: 'Add to playlist',
|
||||||
|
icon: 'list',
|
||||||
|
click: async function() {
|
||||||
|
self.mpd.addToPlaylistItems = Object.values(self.selectedItems).map(_ => _.file);
|
||||||
|
self.mpd.modalVisible.playlistAdd = true;
|
||||||
|
await self.mpd.listplaylists();
|
||||||
|
self.selectedItems = {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(this.selectedItems).length === 1) {
|
if (Object.keys(this.selectedItems).length === 1) {
|
||||||
const item = Object.values(this.selectedItems)[0];
|
const item = Object.values(this.selectedItems)[0];
|
||||||
|
|
||||||
|
@ -118,11 +133,17 @@ Vue.component('music-mpd-search', {
|
||||||
var results = await request('music.mpd.search', {filter: filter});
|
var results = await request('music.mpd.search', {filter: filter});
|
||||||
|
|
||||||
this.results = results.sort((a,b) => {
|
this.results = results.sort((a,b) => {
|
||||||
const tokenize = (t) => {
|
if (a.artist != b.artist)
|
||||||
return ''.concat(t.artist || '', '-', t.album || '', '-', t.disc || '', '-', t.track || '', t.title || '').toLocaleLowerCase();
|
return (a.artist || '').localeCompare(b.artist || '');
|
||||||
};
|
if (a.album != b.album)
|
||||||
|
return (a.album || '').localeCompare(b.album || '');
|
||||||
return tokenize(a).localeCompare(tokenize(b));
|
if (a.track != b.track)
|
||||||
|
return parseInt(a.track || 0) > parseInt(b.track || 0);
|
||||||
|
if (a.title != b.title)
|
||||||
|
return (a.title || '').localeCompare(b.title || '');
|
||||||
|
if (a.file != b.file)
|
||||||
|
return (a.file || '').localeCompare(b.file || '');
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.showResults = true;
|
this.showResults = true;
|
||||||
|
@ -177,6 +198,16 @@ Vue.component('music-mpd-search', {
|
||||||
this.query[attr] = '';
|
this.query[attr] = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetForm: function() {
|
||||||
|
this.resetQuery();
|
||||||
|
this.showResults = false;
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
self.$refs.form.querySelector('input[type=text]:first-child').focus()
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,36 @@
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
|
<modal id="music-mpd-playlist-add" title="Add to playlist" v-model="modalVisible.playlistAdd" ref="modalPlaylistAdd">
|
||||||
|
<div class="playlist-add">
|
||||||
|
<div class="playlist-add-controls">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 filter-container">
|
||||||
|
<i class="fa fa-filter input-icon"></i>
|
||||||
|
<input type="text" class="with-icon" v-model="playlistAddFilter">
|
||||||
|
</div>
|
||||||
|
<div class="col-5 pull-right">
|
||||||
|
<button class="btn-primary" type="button"
|
||||||
|
@click="playlistadd"
|
||||||
|
:disabled="Object.keys(selectedPlaylistAddItems).length === 0">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlists-container">
|
||||||
|
<div class="item"
|
||||||
|
:class="{selected: p.playlist in selectedPlaylistAddItems}"
|
||||||
|
v-for="p in playlists"
|
||||||
|
v-if="matchesPlaylistAddFilter(p)"
|
||||||
|
v-text="p.playlist"
|
||||||
|
@click="onPlaylistAddItemClick(p)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
|
||||||
<div class="row panels">
|
<div class="row panels">
|
||||||
<!-- Browser section -->
|
<!-- Browser section -->
|
||||||
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="left col-6">
|
<div class="left col-6">
|
||||||
<button class="btn-default" v-if="results.length" @click="showResults = true" title="Show results">
|
<button class="btn-default" v-if="results.length" @click="$event.preventDefault(); showResults = true" title="Show results">
|
||||||
<i class="fa fa-list"></i>
|
<i class="fa fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
@click="selectAll">
|
@click="selectAll">
|
||||||
<i class="fa fa-check-double"></i>
|
<i class="fa fa-check-double"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-default" @click="showResults = false" title="Show search form">
|
<button class="btn-default" title="Show search form" @click="resetForm">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -564,6 +564,24 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return sorted(self._exec('listplaylists', return_status=False),
|
return sorted(self._exec('listplaylists', return_status=False),
|
||||||
key=lambda p: p['playlist'])
|
key=lambda p: p['playlist'])
|
||||||
|
|
||||||
|
@action
|
||||||
|
def playlistadd(self, name, uri):
|
||||||
|
"""
|
||||||
|
Add one or multiple resources to a playlist.
|
||||||
|
|
||||||
|
:param name: Playlist name
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:param uri: URI or path of the resource(s) to be added
|
||||||
|
:type uri: str or list[str]
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(uri, str):
|
||||||
|
uri = [uri]
|
||||||
|
|
||||||
|
for res in uri:
|
||||||
|
self._exec('playlistadd', name, res)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def lsinfo(self, uri=None):
|
def lsinfo(self, uri=None):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue