diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss
index 9c597551..3433d461 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 00000000..e09836bb
--- /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 973b5fc8..d52bdda5 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 31df3137..61c1a1f8 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 00000000..2079e289
--- /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 68bd9b5c..d278275a 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 5db4af81..76c0cdc6 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 56184d98..b7d2648f 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 b9975a03..a7f1a026 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 871ebd62..a9f50751 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 00000000..1e2e8646
--- /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 cb27abc2..4d587c8a 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 00000000..b8626383
--- /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 2614c228..08658df8 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 00000000..34a33399
--- /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 37d23fc9..90fd9654 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 3339a9e5..7ceaa63e 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: