diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/index.scss index 7216a9f1..7288e5c8 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/index.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/index.scss @@ -130,6 +130,12 @@ } .item { + .empty { + font-size: 1em; + display: block; + height: auto; + } + &.active { height: 4rem; @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 { @extend .vertical-center; position: fixed; @@ -296,10 +320,14 @@ position: relative; } } -} -.dropdown { - width: 20rem; + .dropdown { + width: 20rem; + } + + .filter-container { + position: relative; + } } #music-mpd-info { @@ -333,6 +361,12 @@ } } +#music-mpd-playlist-add { + .modal { + min-width: 50rem; + } +} + @media #{map-get($widths, 's')} { #music-mpd-info { .modal { diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/vars.scss index 46364945..3424a028 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/vars.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/vars.scss @@ -26,4 +26,5 @@ $search-modal-footer-border: 1px solid #ccc; $info-modal-row-border: 1px solid #ddd; $info-modal-attr-color: #777; $track-info-hover-color: rgb(46,190,110); +$filter-bar-shadow: 0 2.5px 4px 0 #bbb; diff --git a/platypush/backend/http/static/js/plugins/music.mpd/index.js b/platypush/backend/http/static/js/plugins/music.mpd/index.js index 9d037794..910f24c4 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/index.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js @@ -8,8 +8,10 @@ Vue.component('music-mpd', { status: {}, timer: null, playlist: [], + playlists: [], playlistFilter: '', browserFilter: '', + playlistAddFilter: '', browserPath: [], browserItems: [], @@ -26,9 +28,12 @@ Vue.component('music-mpd', { infoItem: {}, modalVisible: { info: false, + playlistAdd: false, }, + addToPlaylistItems: [], selectedPlaylistItems: {}, + selectedPlaylistAddItems: {}, selectedBrowserItems: {}, syncTime: { @@ -84,6 +89,12 @@ Vue.component('music-mpd', { items.push({ text: 'Add to playlist', 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) { @@ -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 && Object.values(this.selectedBrowserItems)[0].type === 'playlist') { items.push({ @@ -263,6 +289,9 @@ Vue.component('music-mpd', { items.push({ text: 'View 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)) { - if (['id','pos','time'].indexOf(attr) >= 0) { + if (['id','pos','time','track','disc'].indexOf(attr) >= 0) { Vue.set(this.track, attr, parseInt(value)); } else { Vue.set(this.track, attr, value); @@ -355,7 +384,7 @@ Vue.component('music-mpd', { for (var track of playlist) { 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); } else { track[attr] = value; @@ -599,7 +628,8 @@ Vue.component('music-mpd', { var info = item; 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; @@ -630,6 +660,42 @@ Vue.component('music-mpd', { 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) { var previousTrack = { file: this.track.file, @@ -777,7 +843,15 @@ Vue.component('music-mpd', { return true; 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) { @@ -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() { if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) { openDropdown(this.$refs.playlistDropdown.$el); diff --git a/platypush/backend/http/static/js/plugins/music.mpd/search.js b/platypush/backend/http/static/js/plugins/music.mpd/search.js index a81ba63e..9ec38c4e 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/search.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/search.js @@ -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) { 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}); this.results = results.sort((a,b) => { - const tokenize = (t) => { - return ''.concat(t.artist || '', '-', t.album || '', '-', t.disc || '', '-', t.track || '', t.title || '').toLocaleLowerCase(); - }; - - return tokenize(a).localeCompare(tokenize(b)); + if (a.artist != b.artist) + return (a.artist || '').localeCompare(b.artist || ''); + if (a.album != b.album) + return (a.album || '').localeCompare(b.album || ''); + 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; @@ -177,6 +198,16 @@ Vue.component('music-mpd-search', { 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) + }, }, }); diff --git a/platypush/backend/http/templates/plugins/music.mpd/index.html b/platypush/backend/http/templates/plugins/music.mpd/index.html index 5675c85c..d46192e2 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/index.html +++ b/platypush/backend/http/templates/plugins/music.mpd/index.html @@ -55,6 +55,36 @@ + +
+
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+
+
+
diff --git a/platypush/backend/http/templates/plugins/music.mpd/search.html b/platypush/backend/http/templates/plugins/music.mpd/search.html index 25101554..cbc00036 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/search.html +++ b/platypush/backend/http/templates/plugins/music.mpd/search.html @@ -30,7 +30,7 @@ diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 601cabd3..487e4d8e 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -564,6 +564,24 @@ class MusicMpdPlugin(MusicPlugin): return sorted(self._exec('listplaylists', return_status=False), 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 def lsinfo(self, uri=None): """