From e1ddf7bb3b0811cc5d6b9956e9931c48ff989aaf Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 2 Jun 2019 00:54:49 +0200 Subject: [PATCH] music.mpd vue.js refactoring WIP --- .../static/css/source/common/elements.scss | 18 +- .../css/source/common/elements/dropdown.scss | 20 ++ .../source/common/elements/range-slider.scss | 17 +- .../css/source/common/elements/slider.scss | 31 ++- .../css/source/common/elements/text.scss | 24 ++ .../http/static/css/source/common/mixins.scss | 8 + .../http/static/css/source/common/vars.scss | 14 +- .../webpanel/plugins/music.mpd/index.scss | 154 ++++++++++--- .../webpanel/plugins/music.mpd/vars.scss | 8 + .../backend/http/static/js/application.js | 1 + .../http/static/js/elements/dropdown.js | 104 +++++++++ .../http/static/js/plugins/music.mpd/index.js | 209 ++++++++++++++++-- .../static/js/plugins/music.mpd/playlist.js | 20 ++ .../backend/http/templates/elements.html | 1 + .../http/templates/elements/dropdown.html | 14 ++ .../templates/plugins/music.mpd/index.html | 105 +++++++-- .../templates/plugins/music.mpd/playlist.html | 12 + platypush/plugins/music/mpd/__init__.py | 33 ++- 18 files changed, 718 insertions(+), 75 deletions(-) create mode 100644 platypush/backend/http/static/css/source/common/elements/dropdown.scss create mode 100644 platypush/backend/http/static/css/source/common/elements/text.scss create mode 100644 platypush/backend/http/static/js/elements/dropdown.js create mode 100644 platypush/backend/http/static/js/plugins/music.mpd/playlist.js create mode 100644 platypush/backend/http/templates/elements/dropdown.html create mode 100644 platypush/backend/http/templates/plugins/music.mpd/playlist.html diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss index 9c597551b..3433d461e 100644 --- a/platypush/backend/http/static/css/source/common/elements.scss +++ b/platypush/backend/http/static/css/source/common/elements.scss @@ -1,4 +1,4 @@ -//// General purpose classes ///// +//// General purpose classes and rules ///// .hidden { display: none !important; } @@ -11,10 +11,26 @@ text-align: right !important; } +a:focus { + outline: none; +} + +::-moz-focus-outer, +::-moz-focus-inner { + border: 0; +} + +select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #000; +} + //// UI elements definitions ///// @import 'common/elements/button'; @import 'common/elements/switch'; @import 'common/elements/range-slider'; @import 'common/elements/slider'; +@import 'common/elements/text'; +@import 'common/elements/dropdown'; diff --git a/platypush/backend/http/static/css/source/common/elements/dropdown.scss b/platypush/backend/http/static/css/source/common/elements/dropdown.scss new file mode 100644 index 000000000..e09836bba --- /dev/null +++ b/platypush/backend/http/static/css/source/common/elements/dropdown.scss @@ -0,0 +1,20 @@ +@import 'common/vars'; + +.dropdown { + position: absolute; + background: $default-bg-3; + border-radius: .75rem; + border: $default-border-3; + box-shadow: $dropdown-shadow; + min-width: 15rem; + + .item { + margin: 0 !important; + padding: 1rem; + + .icon { + margin: 0 .75rem; + } + } +} + diff --git a/platypush/backend/http/static/css/source/common/elements/range-slider.scss b/platypush/backend/http/static/css/source/common/elements/range-slider.scss index 973b5fc87..d52bdda51 100644 --- a/platypush/backend/http/static/css/source/common/elements/range-slider.scss +++ b/platypush/backend/http/static/css/source/common/elements/range-slider.scss @@ -35,20 +35,25 @@ transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0 ) no-repeat 0 45% / 100% 40%; - --range-color: $slider-thumb-bg; - - &::-webkit-slider-runnable-track { - background: var(--track-background); - } + --range-color: $slider-progress-bg; + &::-webkit-slider-runnable-track, &::-moz-range-track { background: var(--track-background); + height: 15px; } } - &[disabled]::-webkit-slider-thumb { + &[disabled]::-webkit-slider-thumb, + &[disabled]::-moz-range-thumb { display: none; } + + &::-webkit-progress-value, + &::-moz-range-progress { + @include appearance(none); + background: none; + } } } diff --git a/platypush/backend/http/static/css/source/common/elements/slider.scss b/platypush/backend/http/static/css/source/common/elements/slider.scss index 31df3137d..61c1a1f87 100644 --- a/platypush/backend/http/static/css/source/common/elements/slider.scss +++ b/platypush/backend/http/static/css/source/common/elements/slider.scss @@ -1,3 +1,5 @@ +@import 'common/mixins'; + .slider { @include appearance(none); @include transition(opacity .2s); @@ -7,29 +9,40 @@ background: $slider-bg; outline: none; - &::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; + &::-webkit-slider-thumb, + &::-moz-range-thumb { + @include appearance(none); width: 25px; height: 25px; border-radius: 50%; + border: 0; background: $slider-thumb-bg; cursor: pointer; } - &[disabled]::-webkit-slider-thumb { + &[disabled]::-webkit-slider-thumb, + &[disabled]::-moz-range-thumb { display: none; + width: 0; } &.disabled { opacity: 0.3; } - &::-moz-range-thumb { - width: 25px; - height: 25px; - background: $slider-thumb-bg; - cursor: pointer; + &::-moz-range-track { + @include appearance(none); + } + + &::-webkit-progress-value, + &::-moz-range-progress { + background: $slider-progress-bg; + height: 15px; + } + + &[disabled]::-webkit-progress-value, + &[disabled]::-moz-range-progress { + background: none; } } diff --git a/platypush/backend/http/static/css/source/common/elements/text.scss b/platypush/backend/http/static/css/source/common/elements/text.scss new file mode 100644 index 000000000..2079e2899 --- /dev/null +++ b/platypush/backend/http/static/css/source/common/elements/text.scss @@ -0,0 +1,24 @@ +@import 'common/vars'; + +.input-icon { + position: absolute; + min-width: 3rem; + padding: 1rem; + color: $text-icon-color; +} + +input[type=text] { + &:hover { + border: $border-hover; + } + + &:focus { + border: $border-focus; + box-shadow: $text-shadow; + } + + &.with-icon { + padding-left: 3rem; + } +} + diff --git a/platypush/backend/http/static/css/source/common/mixins.scss b/platypush/backend/http/static/css/source/common/mixins.scss index 68bd9b5c2..d278275a8 100644 --- a/platypush/backend/http/static/css/source/common/mixins.scss +++ b/platypush/backend/http/static/css/source/common/mixins.scss @@ -14,6 +14,14 @@ transition: $value; } +@mixin animation($value) { + -webkit-animation: $value; + -ms-animation: $value; + -o-animation: $value; + -ms-animation: $value; + animation: $value; +} + @mixin box-shadow($value) { -webkit-box-shadow: $value; -o-box-shadow: $value; diff --git a/platypush/backend/http/static/css/source/common/vars.scss b/platypush/backend/http/static/css/source/common/vars.scss index 5db4af812..76c0cdc66 100644 --- a/platypush/backend/http/static/css/source/common/vars.scss +++ b/platypush/backend/http/static/css/source/common/vars.scss @@ -10,6 +10,7 @@ $default-font-size: 1.5rem !default; $default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default; $default-border: 1px solid #e1e4e8 !default; $default-border-2: 1px solid #dddddd !default; +$default-border-3: 1px solid #cccccc !default; $default-bottom: $default-border !default; $default-link-fg: #5f7869 !default; @@ -69,12 +70,23 @@ $switch-shadow-glow-hover: inset 0 0 0 5px #fff, inset 0 0 0 14px #fff !default; $switch-shadow-glow-checked-1: 0 0px 8px 0 #00ad72, 0 0 0 3px #00e094, 0 0 30px 0 #00e094, 0 0 0 6px #fff !default; $switch-shadow-glow-checked-2: inset 0 0 0 5px #00e094, inset 0 0 0 14px #fff !default; -//// Slier element +//// Slider element $slider-bg: #e4e4e4 !default; $slider-thumb-bg: rgba(0,215,80,1.0) !default; $slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default; $slider-hover-on-hover-bg: #d2d2d2 !default; +$slider-progress-bg: rgba(0,215,80,0.2) !default; + +//// Input element +$text-icon-color: #888; +$border-focus: 1px solid rgba(127, 216, 95, 0.83); +$border-hover: 1px solid rgba(159, 180, 152, 0.83); +$text-shadow: 2px 2px 2px #d4d4d4; //// Header style $header-bottom: $default-bottom; +//// Dropdown element +$dropdown-bg: rgba(241,243,242,0.9) !default; +$dropdown-shadow: 1px 1px 1px #bbb; + 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 56184d982..b7d2648f0 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 @@ -1,21 +1,24 @@ @import 'common/vars'; +@import 'common/mixins'; @import 'common/layout'; @import 'webpanel/plugins/music.mpd/vars'; -// background-image: linear-gradient(to right bottom, rgb(123, 84, 30), rgb(0, 0, 0)), linear-gradient(transparent, rgb(0, 0, 0) 70%); - .music-mpd-container { line-height: 3rem; letter-spacing: .03rem; overflow: hidden; * > .item { + display: flex; + align-items: center; cursor: pointer; border-radius: 1rem; padding: .5rem; &: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; } .artist { font-size: $artist-font-size; @@ -27,6 +30,24 @@ font-size: $duration-font-size; } + * > button { + border: 0; + + &:disabled { + background: none; + } + + &.enabled { + color: $button-enabled-color; + } + + &:hover { + .fa { + opacity: 0.75; + } + } + } + .panels { display: flex; @@ -43,6 +64,9 @@ .item { background: none; + &:nth-of-type(2) { + margin-top: 4.5rem; + } } .fa { @@ -50,8 +74,72 @@ } } + .browser, .playlist { + position: relative; // For the dropdown menu padding: .5rem 1rem 6rem 1rem; + + .browser-controls, + .playlist-controls { + position: fixed; + height: 5rem; + background: $playlist-controls-bg; + border-bottom: $playlist-controls-border; + margin: -.5rem 0 0 -1rem; + padding: .5rem; + + input[type=text] { + width: 100%; + border-radius: 5rem; + } + + * > button { + border: 0; + padding: 0 1.5rem; + + &:disabled { + background: none; + } + + &.enabled { + color: $button-enabled-color; + } + + .fa-search { + color: $button-hover-color; + } + } + + button { + padding: 0 .75rem; + + } + } + + .spacer { + height: 5rem; + } + + .empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 5rem; + color: $empty-playlist-color; + text-shadow: $empty-playlist-shadow; + } + + .item { + &.active { + height: 4rem; + @include animation(active-track 5s infinite); + } + + &:first-child { + margin-top: 4.5rem; + } + } } } @@ -59,7 +147,6 @@ @extend .vertical-center; position: fixed; width: 100%; - min-height: 6rem; bottom: 0; border-top: $default-border-2; padding: 1rem; @@ -78,35 +165,31 @@ } } + button { + &:hover { + .fa { + color: $button-hover-color; + } + } + } + .playback-controls { .row { @extend .vertical-center; justify-content: center; } - } + button { + padding: 0 1.5rem; - * > button { - border: 0; - padding: 0 1.5rem; - - &.enabled { - color: $button-enabled-color; - } - - &:hover { - .fa { + .fa-play, .fa-pause { color: $button-hover-color; - } - } + font-size: $font-size * 2; + margin-top: .3rem; - .fa-play, .fa-pause { - color: $button-hover-color; - font-size: $font-size * 2; - margin-top: .3rem; - - &:hover { - color: $play-button-hover-color; + &:hover { + color: $play-button-hover-color; + } } } } @@ -154,9 +237,26 @@ margin-left: 1.5rem; } } - - * > .item:hover { - background: $hover-bg !important; - } } +#music-mpd-playlist-dropdown { + width: 20rem; +} + +@keyframes active-track { + 0% { background: $active-track-bg-1; } + 50% { background: $active-track-bg-2; } + 100% { background: $active-track-bg-1; } +} + +@-moz-keyframes active-track { + 0% { background: $active-track-bg-1; } + 50% { background: $active-track-bg-2; } + 100% { background: $active-track-bg-1; } +} + +@-webkit-keyframes active-track { + 0% { background: $active-track-bg-1; } + 50% { background: $active-track-bg-2; } + 100% { background: $active-track-bg-1; } +} 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 b9975a035..a7f1a0262 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 @@ -15,3 +15,11 @@ $control-time-font-size: $font-size * 0.666666; $browser-panel-bg: rgba(248,250,250,0.95); $browser-font-size: $font-size * 0.8666; +$empty-playlist-color: rgba(200,200,200,0.7); +$empty-playlist-shadow: 2px 1px rgb(235,235,235); +$playlist-controls-bg: rgba(247,247,247,0.95); +$playlist-controls-border: $default-border-2; + +$active-track-bg-1: #d4ffe3; +$active-track-bg-2: #9cdfb0; + diff --git a/platypush/backend/http/static/js/application.js b/platypush/backend/http/static/js/application.js index 871ebd622..a9f50751a 100644 --- a/platypush/backend/http/static/js/application.js +++ b/platypush/backend/http/static/js/application.js @@ -48,6 +48,7 @@ window.vm = new Vue({ initEvents(); }, + updated: function() {}, destroyed: function() {}, }); diff --git a/platypush/backend/http/static/js/elements/dropdown.js b/platypush/backend/http/static/js/elements/dropdown.js new file mode 100644 index 000000000..1e2e86468 --- /dev/null +++ b/platypush/backend/http/static/js/elements/dropdown.js @@ -0,0 +1,104 @@ +Vue.component('dropdown', { + template: '#tmpl-dropdown', + props: { + id: { + type: String, + }, + + visible: { + type: Boolean, + default: false, + }, + + items: { + type: Array, + default: [], + }, + }, + + methods: { + clicked: function(item) { + if (item.click) { + item.click(); + } + + closeDropdown(); + }, + }, +}); + +var openedDropdown; + +var _parseElement = function(element) { + if (element instanceof Object) { + if (element.$el) { + element = element.$el; + } + } else if (element instanceof String || typeof(element) === 'string') { + element = document.getElementById(element); + } else { + console.error('Got unexpected type ' + typeof(element) + ' for dropdown element'); + return; + } + + return element; +}; + +var clickHndl = function(event) { + if (!openedDropdown) { + return; + } + + var element = event.target; + + while (element) { + if (element == openedDropdown) { + return; // TODO dropdown click + } + + element = element.parentElement; + } + + // Click outside the dropdown, close it + closeDropdown(); +}; + +function closeDropdown() { + if (!openedDropdown) { + return; + } + + document.removeEventListener('click', clickHndl); + + if (openedDropdown.className.indexOf('hidden') < 0) { + openedDropdown.className = (openedDropdown.className + ' hidden').trim(); + } + + openedDropdown = undefined; +} + +function openDropdown(element) { + element = _parseElement(element); + if (!element) { + console.error('Invalid dropdown element'); + return; + } + + event.stopPropagation(); + closeDropdown(); + + if (getComputedStyle(element.parentElement).position === 'relative') { + // Position the dropdown relatively to the parent + element.style.left = (window.event.clientX - element.parentElement.offsetLeft + element.parentElement.scrollLeft) + 'px'; + element.style.top = (window.event.clientY - element.parentElement.offsetTop + element.parentElement.scrollTop) + 'px'; + } else { + // Position the dropdown absolutely on the window + element.style.left = (window.event.clientX + window.scrollX) + 'px'; + element.style.top = (window.event.clientY + window.scrollY) + 'px'; + } + + document.addEventListener('click', clickHndl); + element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' '); + openedDropdown = element; +} + 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 cb27abc22..4d587c8a0 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/index.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js @@ -5,17 +5,65 @@ Vue.component('music-mpd', { return { track: {}, status: {}, - playlist: [], timer: null, + playlist: [], + playlistFilter: '', + browserFilter: '', browserPath: [], browserItems: [], + + selectionMode: { + playlist: false, + browser: false, + }, + + selectedPlaylistItems: {}, + selectedBrowserItems: {}, + syncTime: { timestamp: null, elapsed: null, }, + + newTrackLock: false, }; }, + computed: { + playlistDropdownItems: function() { + var self = this; + + return [ + { + text: 'Play', + icon: 'play', + click: async function() { + await self.playpos(); + }, + }, + { + text: 'Add to playlist', + icon: 'list', + }, + { + text: 'Move', + icon: 'retweet', + }, + { + text: 'Remove from queue', + icon: 'trash', + click: async function() { + await self.del(); + }, + }, + { + text: 'View track info', + icon: 'info', + }, + ]; + }, + }, + methods: { refresh: async function() { const getStatus = request('music.mpd.status'); @@ -152,6 +200,22 @@ Vue.component('music-mpd', { method({ status: status }); }, + playpos: async function(pos) { + if (pos == null) { + if (!Object.keys(this.selectedPlaylistItems).length) { + return; + } + + pos = Object.keys(this.selectedPlaylistItems)[0]; + } + + let status = await request('music.mpd.play_pos', {pos: pos}); + this._parseStatus(status); + + let track = await request('music.mpd.currentsong'); + this._parseTrack(track); + }, + stop: async function() { await request('music.mpd.stop'); this.onMusicStop({}); @@ -199,6 +263,49 @@ Vue.component('music-mpd', { this.onVolumeChange({status: status}); }, + clear: async function() { + if (!confirm('Are you sure that you want to clear the playlist?')) { + return; + } + + await request('music.mpd.clear'); + this.stopTimer(); + this.track = {}; + this.playlist = []; + + let status = await request('music.mpd.status'); + this._parseStatus(status); + }, + + del: async function() { + const positions = Object.keys(this.selectedPlaylistItems); + if (!positions.length) { + return; + } + + let status = await request('music.mpd.delete', {'positions': positions}); + this._parseStatus(status); + + for (const pos in positions) { + Vue.delete(this.selectedPlaylistItems, pos); + } + }, + + swap: async function() { + if (Object.keys(this.selectedPlaylistItems).length !== 2) { + return; + } + + const positions = Object.keys(this.selectedPlaylistItems).sort(); + await request('music.mpd.move', {from_pos: positions[1], to_pos: positions[0]}); + + let status = await request('music.mpd.move', {from_pos: positions[0]+1, to_pos: positions[1]}); + this._parseStatus(status); + + const playlist = await request('music.mpd.playlistinfo'); + this._parsePlaylist(playlist); + }, + onNewPlayingTrack: async function(event) { var previousTrack = { file: this.track.file, @@ -207,18 +314,23 @@ Vue.component('music-mpd', { }; this.status.state = 'play'; - this.status.elapsed = 0; + Vue.set(this.status, 'elapsed', 0); this.track = {}; - - let status = await request('music.mpd.status'); - this._parseStatus(status); this._parseTrack(event.track); + + let status = event.status ? event.status : await request('music.mpd.status'); + this._parseStatus(status); this.startTimer(); if (this.track.file != previousTrack.file || this.track.artist != previousTrack.artist || this.track.title != previousTrack.title) { this.showNewTrackNotification(); + + const self = this; + setTimeout(function() { + self.scrollToActiveTrack(); + }, 100); } }, @@ -234,6 +346,7 @@ Vue.component('music-mpd', { onMusicStop: function(event) { this.status.state = 'stop'; + Vue.set(this.status, 'elapsed', 0); this._parseStatus(event.status); this._parseTrack(event.track); this.stopTimer(); @@ -251,24 +364,29 @@ Vue.component('music-mpd', { this._parseStatus(event.status); this._parseTrack(event.track); - this.syncTime.timestamp = new Date(); - this.syncTime.elapsed = this.status.elapsed; + Vue.set(this.syncTime, 'timestamp', new Date()); + Vue.set(this.syncTime, 'elapsed', this.status.elapsed); }, onSeekChange: function(event) { if (event.position != null) - this.status.elapsed = parseFloat(event.position); + Vue.set(this.status, 'elapsed', parseFloat(event.position)); if (event.status) this._parseStatus(event.status); if (event.track) this._parseTrack(event.track); - this.syncTime.timestamp = new Date(); - this.syncTime.elapsed = this.status.elapsed; + Vue.set(this.syncTime, 'timestamp', new Date()); + Vue.set(this.syncTime, 'elapsed', this.status.elapsed); }, - onPlaylistChange: function(event) { - console.log(event); + onPlaylistChange: async function(event) { + if (event.changes) { + this.playlist = event.changes; + } else { + const playlist = await request('music.mpd.playlistinfo'); + this._parsePlaylist(playlist); + } }, onVolumeChange: function(event) { @@ -293,8 +411,8 @@ Vue.component('music-mpd', { this.stopTimer(); } - this.syncTime.timestamp = new Date(); - this.syncTime.elapsed = this.status.elapsed; + Vue.set(this.syncTime, 'timestamp', new Date()); + Vue.set(this.syncTime, 'elapsed', this.status.elapsed); this.timer = setInterval(this.timerFunc, 1000); }, @@ -310,8 +428,63 @@ Vue.component('music-mpd', { return; } - this.status.elapsed = this.syncTime.elapsed + - ((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000); + Vue.set(this.status, 'elapsed', this.syncTime.elapsed + + ((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000)); + }, + + matchesPlaylistFilter: function(track) { + if (this.playlistFilter.length === 0) + return true; + + return [track.artist || '', track.title || '', track.album || ''] + .join(' ').toLocaleLowerCase().indexOf( + this.playlistFilter.split(' ').filter(_ => _.length > 0).map(_ => _.toLocaleLowerCase()).join(' ') + ) >= 0; + }, + + matchesBrowserFilter: function(item) { + if (this.browserFilter.length === 0) + return true; + + return item.name.toLocaleLowerCase().indexOf( + this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ')) >= 0; + }, + + onPlaylistItemClick: function(track) { + if (this.selectionMode.playlist) { + if (track.pos in this.selectedPlaylistItems) { + Vue.delete(this.selectedPlaylistItems, track.pos); + } else { + 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 = {}; + Vue.set(this.selectedPlaylistItems, track.pos, track); + openDropdown(this.$refs.playlistDropdown.$el); + } + }, + + togglePlaylistSelectionMode: function() { + this.selectionMode.playlist = !this.selectionMode.playlist; + if (!this.selectionMode.playlist) { + this.selectedPlaylistItems = {}; + } + }, + + toggleBrowserSelectionMode: function() { + this.selectionMode.browser = !this.selectionMode.browser; + if (!this.selectionMode.browser) { + this.selectedBrowserItems = {}; + } + }, + + scrollToActiveTrack: function() { + if (this.$refs.activePlaylistTrack && this.$refs.activePlaylistTrack.length) { + this.$refs.activePlaylistTrack[0].$el.scrollIntoView({behavior: 'smooth'}); + } }, }, @@ -327,5 +500,9 @@ Vue.component('music-mpd', { registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent'); registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent'); }, + + mounted: function() { + this.scrollToActiveTrack(); + }, }); diff --git a/platypush/backend/http/static/js/plugins/music.mpd/playlist.js b/platypush/backend/http/static/js/plugins/music.mpd/playlist.js new file mode 100644 index 000000000..b86263838 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/music.mpd/playlist.js @@ -0,0 +1,20 @@ +Vue.component('music-mpd-playlist-item', { + template: '#tmpl-music-mpd-playlist-item', + props: { + track: { + type: Object, + default: {}, + }, + + selected: { + type: Boolean, + default: false, + }, + + active: { + type: Boolean, + default: false, + }, + }, +}); + diff --git a/platypush/backend/http/templates/elements.html b/platypush/backend/http/templates/elements.html index 2614c2285..08658df84 100644 --- a/platypush/backend/http/templates/elements.html +++ b/platypush/backend/http/templates/elements.html @@ -1,3 +1,4 @@ {% include 'elements/switch.html' %} {% include 'elements/range-slider.html' %} +{% include 'elements/dropdown.html' %} diff --git a/platypush/backend/http/templates/elements/dropdown.html b/platypush/backend/http/templates/elements/dropdown.html new file mode 100644 index 000000000..34a333991 --- /dev/null +++ b/platypush/backend/http/templates/elements/dropdown.html @@ -0,0 +1,14 @@ + + + + diff --git a/platypush/backend/http/templates/plugins/music.mpd/index.html b/platypush/backend/http/templates/plugins/music.mpd/index.html index 37d23fc92..90fd96546 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/index.html +++ b/platypush/backend/http/templates/plugins/music.mpd/index.html @@ -1,24 +1,103 @@ {% include 'plugins/music.mpd/browser.html' %} +{% include 'plugins/music.mpd/playlist.html' %} + + + diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 3339a9e56..7ceaa63ea 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -248,12 +248,18 @@ class MusicMpdPlugin(MusicPlugin): return self._exec('shuffle') @action - def add(self, resource, position=None): + def add(self, resource, queue=False, 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) + :type position: int """ if isinstance(resource, list): @@ -272,9 +278,32 @@ class MusicMpdPlugin(MusicPlugin): r = self._parse_resource(resource) if position is None: - return self._exec('add', r) + return self._exec('insert' if queue else 'add', r) return self._exec('addid', r, position) + @action + def delete(self, positions): + """ + Delete the playlist item(s) in the specified position(s). + + :param positions: Positions of the tracks to be removed + :type positions: list[int] + """ + return self._exec('delete', *positions) + + @action + def move(self, from_pos, to_pos): + """ + Move the playlist item in position to position + + :param from_pos: Track current position + :type from_pos: int + + :param to_pos: Track new position + :type to_pos: int + """ + return self._exec('move', from_pos, to_pos) + @classmethod def _parse_resource(cls, resource): if not resource: