diff --git a/platypush/backend/http/static/css/source/common/layout.scss b/platypush/backend/http/static/css/source/common/layout.scss
index 2e6cc4cae..beaad3c5c 100644
--- a/platypush/backend/http/static/css/source/common/layout.scss
+++ b/platypush/backend/http/static/css/source/common/layout.scss
@@ -26,6 +26,7 @@ $widths: (
float: left;
box-sizing: border-box;
width: ((100%/12)*$i);
+ margin: 0;
}
@if $i < 12 {
diff --git a/platypush/backend/http/static/css/source/common/notifications.scss b/platypush/backend/http/static/css/source/common/notifications.scss
index e568208cd..989a55b3d 100644
--- a/platypush/backend/http/static/css/source/common/notifications.scss
+++ b/platypush/backend/http/static/css/source/common/notifications.scss
@@ -3,6 +3,7 @@
bottom: 0;
right: 0;
width: 25em;
+ z-index: 1000;
.notification {
background: $notification-bg;
diff --git a/platypush/backend/http/static/css/source/common/vars.scss b/platypush/backend/http/static/css/source/common/vars.scss
index 0ec0872ae..5db4af812 100644
--- a/platypush/backend/http/static/css/source/common/vars.scss
+++ b/platypush/backend/http/static/css/source/common/vars.scss
@@ -1,7 +1,11 @@
//// Common defaults
-$default-bg: #f4f5f6 !default;
+$default-bg: white !default;
+$default-bg-2: #f4f5f6 !default;
+$default-bg-3: #f1f3f2 !default;
+$default-bg-4: #edf0ee !default;
$default-fg: black !default;
$default-fg-2: #333333 !default;
+$default-font-size: 1.5rem !default;
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$default-border: 1px solid #e1e4e8 !default;
@@ -9,6 +13,7 @@ $default-border-2: 1px solid #dddddd !default;
$default-bottom: $default-border !default;
$default-link-fg: #5f7869 !default;
+$font-size: $default-font-size !default;
$selected-bg: #c8ffd0 !default;
$hover-bg: #def6ea !default;
$header-bg: $default_bg !default;
@@ -26,12 +31,12 @@ $fade-in-transition-duration: $fade-transition-duration !default;
$fade-out-transition-duration: $fade-transition-duration !default;
//// Notifications
-$notification-bg: rgba(185, 255, 193, 0.85) !default;
-$notification-hover-bg: rgba(160,245,178,0.9) !default;
-$notification-warning-bg: rgba(228, 255, 78, 0.85) !default;
-$notification-warning-hover-bg: rgba(218, 245, 68, 0.9) !default;
-$notification-error-bg: rgba(255, 100, 100, 0.85) !default;
-$notification-error-hover-bg: rgba(245, 90, 90, 0.9) !default;
+$notification-bg: rgba(185, 255, 193, 0.9) !default;
+$notification-hover-bg: rgba(160,245,178,0.95) !default;
+$notification-warning-bg: rgba(228, 255, 78, 0.9) !default;
+$notification-warning-hover-bg: rgba(218, 245, 68, 0.95) !default;
+$notification-error-bg: rgba(255, 100, 100, 0.9) !default;
+$notification-error-hover-bg: rgba(245, 90, 90, 0.95) !default;
$notification-border: 1px solid rgba(109, 205, 134, 0.62) !default;
$notification-warning-border: 1px solid rgba(205, 205, 109, 0.62) !default;
$notification-error-border: 1px solid rgba(205, 109, 109, 0.62) !default;
diff --git a/platypush/backend/http/static/css/source/webpanel/header.scss b/platypush/backend/http/static/css/source/webpanel/header.scss
deleted file mode 100644
index 8e0f14d74..000000000
--- a/platypush/backend/http/static/css/source/webpanel/header.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-header {
- .row {
- width: 100%;
- background: $header-bg;
- padding: 1rem 2.5rem;
- // margin: 0 1rem 3.5rem -1rem;
- border-bottom: $header-bottom;
- display: flex;
- align-items: center;
-
- .logo {
- font-size: 25px;
-
- .logo-1 {
- font-weight: bold;
- }
- }
-
- .date-time {
- text-align: right;
- padding-right: 3rem;
-
- .date {
- color: #666;
- }
-
- .time {
- font-weight: bold;
- font-size: 25px;
- }
- }
- }
-}
-
diff --git a/platypush/backend/http/static/css/source/webpanel/index.scss b/platypush/backend/http/static/css/source/webpanel/index.scss
index f9d3a09f2..5f65e8bf8 100644
--- a/platypush/backend/http/static/css/source/webpanel/index.scss
+++ b/platypush/backend/http/static/css/source/webpanel/index.scss
@@ -7,27 +7,34 @@
@import 'common/modal';
@import 'common/notifications';
-@import 'header';
@import 'nav';
body {
width: 100%;
overflow-x: hidden;
font-family: $default-font-family;
+ font-size: $default-font-size;
+}
+
+body::-webkit-scrollbar {
+ width: 1em;
+}
+
+body::-webkit-scrollbar-track {
+ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
+}
+
+body::-webkit-scrollbar-thumb {
+ background-color: darkgrey;
+ outline: 1px solid slategrey;
}
main {
- margin: 6rem auto;
+ padding: 4.9rem 0;
+ margin: 0;
}
a {
color: $default-link-fg;
}
-.plugin-container {
- border: $default-border-2;
- border-radius: 1rem;
- margin: 1.5rem;
- box-shadow: 8px 8px 6px -1px rgba(187,187,187,0.75);
-}
-
diff --git a/platypush/backend/http/static/css/source/webpanel/nav.scss b/platypush/backend/http/static/css/source/webpanel/nav.scss
index bb902ac21..f485f1f75 100644
--- a/platypush/backend/http/static/css/source/webpanel/nav.scss
+++ b/platypush/backend/http/static/css/source/webpanel/nav.scss
@@ -4,7 +4,7 @@ nav {
top: 0;
width: 100%;
z-index: 10;
- opacity: 0.9;
+ opacity: 0.95;
ul {
position: relative;
@@ -23,9 +23,9 @@ nav {
margin: 0;
text-transform: uppercase;
letter-spacing: .1rem;
+ border-radius: 2rem;
&:hover {
- border-radius: 2rem;
background: $hover-bg;
letter-spacing: .4rem;
}
diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss
index da02f79f0..4e3aadd59 100644
--- a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/index.scss
@@ -8,6 +8,8 @@
font-weight: 400;
line-height: 3.8rem;
letter-spacing: .1rem;
+ border-bottom: $default-border-2;
+ border-radius: 0 0 1em 1em;
%panel {
margin: 1.5rem auto;
@@ -28,7 +30,7 @@
.title {
padding: .75rem;
- background: $default-bg;
+ background: $default-bg-2;
border-bottom: $default-border-2;
&:last-child {
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
new file mode 100644
index 000000000..56184d982
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/index.scss
@@ -0,0 +1,162 @@
+@import 'common/vars';
+@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 {
+ cursor: pointer;
+ border-radius: 1rem;
+ padding: .5rem;
+
+ &:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
+ &:nth-child(even) { background: $default-bg-3; }
+
+ .artist {
+ font-size: $artist-font-size;
+ }
+ }
+
+ * > .duration {
+ color: $duration-color;
+ font-size: $duration-font-size;
+ }
+
+ .panels {
+ display: flex;
+
+ .browser, .playlist {
+ height: 100vh - 16rem;
+ overflow: auto;
+ }
+
+ .browser {
+ background: $browser-panel-bg;
+ border-right: $default-border-2;
+ padding: .3rem 1rem 6rem 1rem;
+ font-size: $browser-font-size;
+
+ .item {
+ background: none;
+ }
+
+ .fa {
+ color: #666;
+ }
+ }
+
+ .playlist {
+ padding: .5rem 1rem 6rem 1rem;
+ }
+ }
+
+ .controls {
+ @extend .vertical-center;
+ position: fixed;
+ width: 100%;
+ min-height: 6rem;
+ bottom: 0;
+ border-top: $default-border-2;
+ padding: 1rem;
+ background: $control-panel-bg;
+ box-shadow: $control-panel-shadow;
+ z-index: 2;
+
+ .track-container {
+ @extend .vertical-center;
+ line-height: 2.6rem;
+
+ .track-info {
+ .artist {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .playback-controls {
+ .row {
+ @extend .vertical-center;
+ justify-content: center;
+ }
+
+ }
+
+ * > button {
+ border: 0;
+ padding: 0 1.5rem;
+
+ &.enabled {
+ color: $button-enabled-color;
+ }
+
+ &:hover {
+ .fa {
+ color: $button-hover-color;
+ }
+ }
+
+ .fa-play, .fa-pause {
+ color: $button-hover-color;
+ font-size: $font-size * 2;
+ margin-top: .3rem;
+
+ &:hover {
+ color: $play-button-hover-color;
+ }
+ }
+ }
+
+ .pull-right {
+ padding-right: 2.5rem;
+ button {
+ &:not(last-child) {
+ padding: 0 .7rem;
+ }
+
+ &:last-child {
+ padding: 0;
+ }
+ }
+
+ .volume-container {
+ button {
+ padding: 0 .3rem 0 0;
+ background: none;
+ }
+ }
+ }
+
+ * > .seek-slider {
+ width: 75%;
+ }
+
+ * > .volume-slider {
+ width: 75%;
+ margin-right: 1rem;
+ }
+
+ * > .elapsed-time,
+ * > .total-time {
+ font-size: $control-time-font-size;
+ color: $control-time-color;
+ }
+
+ * > .elapsed-time {
+ margin-right: 1.5rem;
+ }
+
+ * > .total-time {
+ margin-left: 1.5rem;
+ }
+ }
+
+ * > .item:hover {
+ background: $hover-bg !important;
+ }
+}
+
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
new file mode 100644
index 000000000..b9975a035
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/music.mpd/vars.scss
@@ -0,0 +1,17 @@
+$button-enabled-color: #59df3e;
+$button-hover-color: $button-enabled-color;
+$play-button-hover-color: #64ef4a;
+
+$artist-font-size: $font-size * 0.9333;
+
+$duration-color: #666;
+$duration-font-size: $font-size * 0.86666;
+
+$control-panel-bg: rgba(245,245,245,0.95);
+$control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
+$control-time-color: #666;
+$control-time-font-size: $font-size * 0.666666;
+
+$browser-panel-bg: rgba(248,250,250,0.95);
+$browser-font-size: $font-size * 0.8666;
+
diff --git a/platypush/backend/http/static/js/plugins/music.mpd/browser.js b/platypush/backend/http/static/js/plugins/music.mpd/browser.js
new file mode 100644
index 000000000..539e60d76
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/music.mpd/browser.js
@@ -0,0 +1,8 @@
+Vue.component('music-mpd-browser-item', {
+ template: '#tmpl-music-mpd-browser-item',
+ props: ['type','name'],
+
+ methods: {
+ },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/music.mpd/index.js b/platypush/backend/http/static/js/plugins/music.mpd/index.js
new file mode 100644
index 000000000..cb27abc22
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js
@@ -0,0 +1,331 @@
+Vue.component('music-mpd', {
+ template: '#tmpl-music-mpd',
+ props: ['config'],
+ data: function() {
+ return {
+ track: {},
+ status: {},
+ playlist: [],
+ timer: null,
+ browserPath: [],
+ browserItems: [],
+ syncTime: {
+ timestamp: null,
+ elapsed: null,
+ },
+ };
+ },
+
+ methods: {
+ refresh: async function() {
+ const getStatus = request('music.mpd.status');
+ const getTrack = request('music.mpd.currentsong');
+ const getPlaylist = request('music.mpd.playlistinfo');
+ const getBrowserItems = request('music.mpd.lsinfo');
+
+ let [status, track, playlist, browserItems] = await Promise.all([getStatus, getTrack, getPlaylist, getBrowserItems]);
+
+ this._parseStatus(status);
+ this._parseTrack(track);
+ this._parsePlaylist(playlist);
+ this._parseBrowserItems(browserItems);
+
+ if (this.status.state === 'play') {
+ this.startTimer();
+ }
+ },
+
+ _parseStatus: async function(status) {
+ if (!status || status.length === 0) {
+ status = await request('music.mpd.status');
+ }
+
+ for (const [attr, value] of Object.entries(status)) {
+ if (['consume','random','repeat','single','bitrate'].indexOf(attr) >= 0) {
+ Vue.set(this.status, attr, !!parseInt(value));
+ } else if (['nextsong','nextsongid','playlist','playlistlength',
+ 'volume','xfade','song','songid'].indexOf(attr) >= 0) {
+ Vue.set(this.status, attr, parseInt(value));
+ } else if (['elapsed'].indexOf(attr) >= 0) {
+ Vue.set(this.status, attr, parseFloat(value));
+ } else {
+ Vue.set(this.status, attr, value);
+ }
+ }
+ },
+
+ _parseTrack: async function(track) {
+ if (!track || track.length === 0) {
+ track = await request('music.mpd.currentsong');
+ }
+
+ for (const [attr, value] of Object.entries(track)) {
+ if (['id','pos','time'].indexOf(attr) >= 0) {
+ Vue.set(this.track, attr, parseInt(value));
+ } else {
+ Vue.set(this.track, attr, value);
+ }
+ }
+ },
+
+ _parsePlaylist: function(playlist) {
+ if (!playlist || playlist.length === 0) {
+ return;
+ }
+
+ this.playlist = [];
+
+ for (var track of playlist) {
+ for (const [attr, value] of Object.entries(track)) {
+ if (['time','pos','id'].indexOf(attr) >= 0) {
+ track[attr] = parseInt(value);
+ } else {
+ track[attr] = value;
+ }
+ }
+
+ this.playlist.push(track);
+ }
+ },
+
+ _parseBrowserItems: function(browserItems) {
+ if (!browserItems || browserItems.length === 0) {
+ return;
+ }
+
+ this.browserItems = [];
+
+ for (var item of browserItems) {
+ if (item.directory) {
+ this.browserItems.push({
+ type: 'directory',
+ name: item.directory,
+ });
+ } else if (item.playlist) {
+ this.browserItems.push({
+ type: 'playlist',
+ name: item.playlist,
+ 'last-modified': item['last-modified'],
+ });
+ }
+ }
+ },
+
+ convertTime: function(time) {
+ time = parseFloat(time); // Normalize strings
+ var t = {};
+ t.h = '' + parseInt(time/3600);
+ t.m = '' + parseInt(time/60 - t.h*60);
+ t.s = '' + parseInt(time - t.m*60);
+
+ for (var attr of ['m','s']) {
+ if (parseInt(t[attr]) < 10) {
+ t[attr] = '0' + t[attr];
+ }
+ }
+
+ var ret = [];
+ if (parseInt(t.h)) {
+ ret.push(t.h);
+ }
+
+ ret.push(t.m, t.s);
+ return ret.join(':');
+ },
+
+ previous: async function() {
+ await request('music.mpd.previous');
+ let track = await request('music.mpd.currentsong');
+ this.onNewPlayingTrack({track: track});
+ },
+
+ repeat: async function() {
+ await request('music.mpd.repeat');
+ let status = await request('music.mpd.status');
+ this._parseStatus(status);
+ },
+
+ playPause: async function() {
+ await request('music.mpd.pause');
+ let status = await request('music.mpd.status');
+ const method = status.state === 'play' ? this.onMusicPlay : this.onMusicPause;
+ method({ status: status });
+ },
+
+ stop: async function() {
+ await request('music.mpd.stop');
+ this.onMusicStop({});
+ },
+
+ random: async function() {
+ await request('music.mpd.random');
+ let status = await request('music.mpd.status');
+ this._parseStatus(status);
+ },
+
+ next: async function() {
+ await request('music.mpd.next');
+ let track = await request('music.mpd.currentsong');
+ this.onNewPlayingTrack({track: track});
+ },
+
+ seek: async function(event) {
+ var value;
+
+ if (event.target) {
+ value = parseFloat(event.target.value);
+ } else if (event.value) {
+ value = parseFloat(event.value);
+ } else {
+ value = parseFloat(event);
+ }
+
+ const status = await request('music.mpd.seekcur', {value: value});
+ this.onSeekChange({status: status});
+ },
+
+ volume: async function(event) {
+ var value;
+
+ if (event.target) {
+ value = parseFloat(event.target.value);
+ } else if (event.value) {
+ value = parseFloat(event.value);
+ } else {
+ value = parseFloat(event);
+ }
+
+ const status = await request('music.mpd.setvol', {vol: value});
+ this.onVolumeChange({status: status});
+ },
+
+ onNewPlayingTrack: async function(event) {
+ var previousTrack = {
+ file: this.track.file,
+ artist: this.track.artist,
+ title: this.track.title,
+ };
+
+ this.status.state = 'play';
+ this.status.elapsed = 0;
+ this.track = {};
+
+ let status = await request('music.mpd.status');
+ this._parseStatus(status);
+ this._parseTrack(event.track);
+ this.startTimer();
+
+ if (this.track.file != previousTrack.file
+ || this.track.artist != previousTrack.artist
+ || this.track.title != previousTrack.title) {
+ this.showNewTrackNotification();
+ }
+ },
+
+ showNewTrackNotification: function() {
+ createNotification({
+ html: '' + (this.track.artist || '[No Artist]') + '
' +
+ (this.track.title || '[No Title]'),
+ image: {
+ icon: 'play',
+ }
+ });
+ },
+
+ onMusicStop: function(event) {
+ this.status.state = 'stop';
+ this._parseStatus(event.status);
+ this._parseTrack(event.track);
+ this.stopTimer();
+ },
+
+ onMusicPlay: function(event) {
+ this.status.state = 'play';
+ this._parseStatus(event.status);
+ this._parseTrack(event.track);
+ this.startTimer();
+ },
+
+ onMusicPause: function(event) {
+ this.status.state = 'pause';
+ this._parseStatus(event.status);
+ this._parseTrack(event.track);
+
+ this.syncTime.timestamp = new Date();
+ this.syncTime.elapsed = this.status.elapsed;
+ },
+
+ onSeekChange: function(event) {
+ if (event.position != null)
+ 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;
+ },
+
+ onPlaylistChange: function(event) {
+ console.log(event);
+ },
+
+ onVolumeChange: function(event) {
+ if (event.volume != null)
+ this.status.volume = parseFloat(event.volume);
+ if (event.status)
+ this._parseStatus(event.status);
+ if (event.track)
+ this._parseTrack(event.track);
+ },
+
+ onRepeatChange: function(event) {
+ this.status.repeat = event.state;
+ },
+
+ onRandomChange: function(event) {
+ this.status.random = event.state;
+ },
+
+ startTimer: function() {
+ if (this.timer != null) {
+ this.stopTimer();
+ }
+
+ this.syncTime.timestamp = new Date();
+ this.syncTime.elapsed = this.status.elapsed;
+ this.timer = setInterval(this.timerFunc, 1000);
+ },
+
+ stopTimer: function() {
+ if (this.timer == null) {
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+ },
+
+ timerFunc: function() {
+ if (this.status.state !== 'play' || this.status.elapsed == null) {
+ return;
+ }
+
+ this.status.elapsed = this.syncTime.elapsed +
+ ((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000);
+ },
+ },
+
+ created: function() {
+ this.refresh();
+ registerEventHandler(this.onNewPlayingTrack, 'platypush.message.event.music.NewPlayingTrackEvent');
+ registerEventHandler(this.onMusicStop, 'platypush.message.event.music.MusicStopEvent');
+ registerEventHandler(this.onMusicPlay, 'platypush.message.event.music.MusicPlayEvent');
+ registerEventHandler(this.onMusicPause, 'platypush.message.event.music.MusicPauseEvent');
+ registerEventHandler(this.onSeekChange, 'platypush.message.event.music.SeekChangeEvent');
+ registerEventHandler(this.onPlaylistChange, 'platypush.message.event.music.PlaylistChangeEvent');
+ registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent');
+ registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
+ registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
+ },
+});
+
diff --git a/platypush/backend/http/templates/header.html b/platypush/backend/http/templates/header.html
deleted file mode 100644
index 24e7af4bf..000000000
--- a/platypush/backend/http/templates/header.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-