Implemented support for adding tracks to playlists

This commit is contained in:
Fabio Manganiello 2019-06-07 17:17:58 +02:00
parent 0b6b29f043
commit 1ad72a2695
7 changed files with 210 additions and 14 deletions

View file

@ -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 {

View file

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

View file

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

View file

@ -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)
},
}, },
}); });

View file

@ -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">

View file

@ -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>

View file

@ -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):
""" """