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 61c1a1f87..efa4cd6d7 100644 --- a/platypush/backend/http/static/css/source/common/elements/slider.scss +++ b/platypush/backend/http/static/css/source/common/elements/slider.scss @@ -9,7 +9,17 @@ background: $slider-bg; outline: none; - &::-webkit-slider-thumb, + // Cursed be thy name Chrome for forcing designers to this hysterical redundancy + &::-webkit-slider-thumb { + @include appearance(none); + width: 25px; + height: 25px; + border-radius: 50%; + border: 0; + background: $slider-thumb-bg; + cursor: pointer; + } + &::-moz-range-thumb { @include appearance(none); width: 25px; @@ -20,27 +30,36 @@ cursor: pointer; } - &[disabled]::-webkit-slider-thumb, + &[disabled]::-webkit-slider-thumb { + display: none; + width: 0; + } + &[disabled]::-moz-range-thumb { display: none; width: 0; } - &.disabled { - opacity: 0.3; - } + &.disabled { opacity: 0.3; } &::-moz-range-track { @include appearance(none); } - &::-webkit-progress-value, + &::-webkit-progress-value { + background: $slider-progress-bg; + height: 15px; + } + &::-moz-range-progress { background: $slider-progress-bg; height: 15px; } - &[disabled]::-webkit-progress-value, + &[disabled]::-webkit-progress-value { + background: none; + } + &[disabled]::-moz-range-progress { background: none; } 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 4e3aadd59..0243f42c5 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 @@ -69,7 +69,8 @@ } * > .fa { - font-size: 3rem; + font-size: 2.5rem; + color: $light-hue-icon-color; } * > .color-logo { diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/vars.scss index ba8bff1d8..5422d35ed 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/vars.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/light.hue/vars.scss @@ -1,4 +1,5 @@ $light-hue-properties-bg: rgba(239,239,240,0.5); $light-hue-properties-hover-bg: white; $light-hue-properties-shadow: 0 0 4px 2px rgba(187,187,187,0.75); +$light-hue-icon-color: #555; 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 b7d2648f0..69705dc80 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 @@ -18,7 +18,7 @@ &: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; } + &.selected { background: $selected-bg !important; } .artist { font-size: $artist-font-size; @@ -38,7 +38,8 @@ } &.enabled { - color: $button-enabled-color; + color: $button-enabled-color !important; + .fa { color: $button-enabled-color !important; } } &:hover { @@ -51,8 +52,11 @@ .panels { display: flex; + .spacer { + height: 5rem; + } + .browser, .playlist { - height: 100vh - 16rem; overflow: auto; } @@ -64,9 +68,6 @@ .item { background: none; - &:nth-of-type(2) { - margin-top: 4.5rem; - } } .fa { @@ -116,10 +117,6 @@ } } - .spacer { - height: 5rem; - } - .empty { display: flex; align-items: center; @@ -135,10 +132,6 @@ height: 4rem; @include animation(active-track 5s infinite); } - - &:first-child { - margin-top: 4.5rem; - } } } } @@ -239,7 +232,7 @@ } } -#music-mpd-playlist-dropdown { +.dropdown { width: 20rem; } diff --git a/platypush/backend/http/static/font-awesome b/platypush/backend/http/static/font-awesome index bdfa9823c..3afe50bda 160000 --- a/platypush/backend/http/static/font-awesome +++ b/platypush/backend/http/static/font-awesome @@ -1 +1 @@ -Subproject commit bdfa9823c8b1e25a5c822f6c719ec0e38ead7f71 +Subproject commit 3afe50bda5308c27f7c8eee597663948ffbd084e diff --git a/platypush/backend/http/static/js/elements/dropdown.js b/platypush/backend/http/static/js/elements/dropdown.js index 1e2e86468..eb8f7f907 100644 --- a/platypush/backend/http/static/js/elements/dropdown.js +++ b/platypush/backend/http/static/js/elements/dropdown.js @@ -100,5 +100,20 @@ function openDropdown(element) { document.addEventListener('click', clickHndl); element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' '); openedDropdown = element; + + const maxLeft = Math.min(window.innerWidth, element.parentElement.clientWidth) + element.parentElement.scrollLeft; + const maxTop = Math.min(window.innerHeight, element.parentElement.clientHeight) + element.parentElement.scrollTop; + + if (element.parentElement.offsetLeft + element.offsetLeft + parseFloat(getComputedStyle(element).width) >= maxLeft) { + if (parseFloat(element.style.left) - parseFloat(getComputedStyle(element).width) >= 0) { + element.style.left = (parseFloat(element.style.left) - parseFloat(getComputedStyle(element).width)) + 'px'; + } + } + + if (element.parentElement.offsetTop + element.offsetTop + parseFloat(getComputedStyle(element).height) >= maxTop) { + if (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height) >= 0) { + element.style.top = (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height)) + 'px'; + } + } } diff --git a/platypush/backend/http/static/js/plugins/music.mpd/browser.js b/platypush/backend/http/static/js/plugins/music.mpd/browser.js index 539e60d76..e748fd134 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/browser.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/browser.js @@ -1,6 +1,24 @@ Vue.component('music-mpd-browser-item', { template: '#tmpl-music-mpd-browser-item', - props: ['type','name'], + props: { + id: { type: String, }, + type: { type: String, }, + name: { type: String, }, + file: { type: String, }, + time: { type: String, }, + artist: { type: String, }, + title: { type: String, }, + date: { type: String, }, + track: { type: String, }, + genre: { type: String, }, + lastModified: { type: String, }, + albumUri: { type: String, }, + + selected: { + type: Boolean, + default: false, + }, + }, 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 index 4d587c8a0..90ca033bb 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/index.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js @@ -32,15 +32,20 @@ Vue.component('music-mpd', { computed: { playlistDropdownItems: function() { var self = this; + var items = []; - return [ - { + if (Object.keys(this.selectedPlaylistItems).length === 1) { + items.push({ text: 'Play', icon: 'play', click: async function() { await self.playpos(); + self.selectedPlaylistItems = {}; }, - }, + }); + } + + items.push( { text: 'Add to playlist', icon: 'list', @@ -54,13 +59,141 @@ Vue.component('music-mpd', { icon: 'trash', click: async function() { await self.del(); + self.selectedPlaylistItems = {}; }, }, - { + ); + + if (Object.keys(this.selectedPlaylistItems).length === 1) { + items.push({ text: 'View track info', icon: 'info', + }); + } + + return items; + }, + + browserDropdownItems: function() { + var self = this; + var items = []; + + if (Object.keys(this.selectedBrowserItems).length === 1 && + Object.values(this.selectedBrowserItems)[0].type === 'directory') { + items.push({ + text: 'Open', + icon: 'folder', + click: async function() { + await self.cd(); + self.selectedBrowserItems = {}; + }, + }); + } + + if (Object.keys(this.selectedBrowserItems).length === 1) { + items.push( + { + text: 'Add and play', + icon: 'play', + click: async function() { + const item = Object.values(self.selectedBrowserItems)[0]; + var promise; + + switch (item.type) { + case 'playlist': + promise = self.load(item.name); + break; + case 'file': + promise = self.add(item.name, position=0); + break; + case 'directory': + promise = self.add(item.name); + break; + default: + console.warning('Unable to handle type: ' + item.type); + break; + } + + await promise; + await self.playpos(0); + self.selectedBrowserItems = {}; + }, + }, + { + text: 'Replace and play', + icon: 'play', + click: async function() { + await self.clear(); + + const item = Object.values(self.selectedBrowserItems)[0]; + var promise; + + switch (item.type) { + case 'playlist': + promise = self.load(item.name); + break; + case 'file': + promise = self.add(item.name, position=0); + break; + case 'directory': + promise = self.add(item.name); + break; + default: + console.warning('Unable to handle type: ' + item.type); + break; + } + + await promise; + await self.playpos(0); + self.selectedBrowserItems = {}; + }, + } + ); + } + + items.push( + { + text: 'Add to queue', + icon: 'plus', + click: async function() { + const items = Object.values(self.selectedBrowserItems); + const promises = items.map(item => item.type === 'playlist' ? self.load(item.name) : self.add(item.name)); + + await Promise.all(promises); + self.selectedBrowserItems = {}; + }, }, - ]; + ); + + if (Object.keys(this.selectedBrowserItems).length === 1 + && Object.values(this.selectedBrowserItems)[0].type === 'playlist') { + items.push({ + text: 'Edit', + icon: 'pen', + }); + } + + if (Object.values(this.selectedBrowserItems).filter(item => item.type === 'playlist').length === Object.values(this.selectedBrowserItems).length) { + items.push({ + text: 'Remove', + icon: 'trash', + click: async function() { + const items = Object.values(self.selectedBrowserItems); + await self.rm(playlists); + self.selectedBrowserItems = {}; + }, + }); + } + + if (Object.keys(this.selectedBrowserItems).length === 1 + && Object.values(this.selectedBrowserItems)[0].type === 'file') { + items.push({ + text: 'View info', + icon: 'info', + }); + } + + return items; }, }, @@ -83,6 +216,30 @@ Vue.component('music-mpd', { } }, + // Hack-ish workaround to get the browser and playlist panels to keep their height + // in sync with the nav and control bars, as both those elements are fixed. + adjustLayout: function() { + const adjust = (self) => { + const nav = document.querySelector('nav'); + const panels = document.querySelectorAll('.music-mpd-container .panels .panel'); + const controls = document.querySelector('.music-mpd-container .controls'); + + return () => { + const panelHeight = window.innerHeight - nav.clientHeight - controls.clientHeight - 5; + if (panelHeight >= 0) { + for (const panel of panels) { + if (panelHeight != parseFloat(panel.style.height)) { + panel.style.height = panelHeight + 'px'; + } + } + } + } + }; + + adjust(this)(); + setInterval(adjust(this), 2000); + }, + _parseStatus: async function(status) { if (!status || status.length === 0) { status = await request('music.mpd.status'); @@ -146,15 +303,24 @@ Vue.component('music-mpd', { for (var item of browserItems) { if (item.directory) { this.browserItems.push({ + id: 'directory:' + item.directory, type: 'directory', name: item.directory, }); } else if (item.playlist) { this.browserItems.push({ + id: 'playlist:' + item.playlist, type: 'playlist', name: item.playlist, 'last-modified': item['last-modified'], }); + } else if (item.file) { + this.browserItems.push({ + id: item.file, + type: 'file', + name: item.file, + ...item, + }); } } }, @@ -193,6 +359,18 @@ Vue.component('music-mpd', { this._parseStatus(status); }, + consume: async function() { + await request('music.mpd.consume'); + let status = await request('music.mpd.status'); + this._parseStatus(status); + }, + + single: async function() { + await request('music.mpd.single'); + 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'); @@ -277,6 +455,27 @@ Vue.component('music-mpd', { this._parseStatus(status); }, + add: async function(resource, position=null) { + var args = {resource: resource}; + if (position != null) { + args.position = position; + } + + let status = await request('music.mpd.add', args); + this._parseStatus(status); + + let playlist = await request('music.mpd.playlistinfo'); + this._parsePlaylist(playlist); + }, + + load: async function(item) { + let status = await request('music.mpd.load', {playlist:item}); + this._parseStatus(status); + + let playlist = await request('music.mpd.playlistinfo'); + this._parsePlaylist(playlist); + }, + del: async function() { const positions = Object.keys(this.selectedPlaylistItems); if (!positions.length) { @@ -291,6 +490,22 @@ Vue.component('music-mpd', { } }, + rm: async function(items) { + if (!items) { + items = Object.values(this.selectedBrowserItems); + } + + if (!items.length) { + return; + } + + let status = await request('music.mpd.rm', {resource: items.map(_ => _.name)}); + this._parseStatus(status); + + items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')}); + this._parseBrowserItems(items); + }, + swap: async function() { if (Object.keys(this.selectedPlaylistItems).length !== 2) { return; @@ -306,6 +521,21 @@ Vue.component('music-mpd', { this._parsePlaylist(playlist); }, + cd: async function() { + const item = Object.values(this.selectedBrowserItems)[0]; + + if (item.name === '..') { + if (this.browserPath.length) { + this.browserPath.pop(); + } + } else { + this.browserPath = item.name.split('/'); + } + + const items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')}); + this._parseBrowserItems(items); + }, + onNewPlayingTrack: async function(event) { var previousTrack = { file: this.track.file, @@ -406,6 +636,14 @@ Vue.component('music-mpd', { this.status.random = event.state; }, + onConsumeChange: function(event) { + this.status.consume = event.state; + }, + + onSingleChange: function(event) { + this.status.single = event.state; + }, + startTimer: function() { if (this.timer != null) { this.stopTimer(); @@ -458,7 +696,6 @@ Vue.component('music-mpd', { 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 = {}; @@ -467,18 +704,68 @@ Vue.component('music-mpd', { } }, - togglePlaylistSelectionMode: function() { - this.selectionMode.playlist = !this.selectionMode.playlist; - if (!this.selectionMode.playlist) { - this.selectedPlaylistItems = {}; + onBrowserItemClick: function(item) { + if (item.type === 'directory' && item.name === '..') { + this.selectedBrowserItems = {}; + this.selectedBrowserItems[item.id] = item; + this.cd(); + this.selectedBrowserItems = {}; + return; + } + + if (this.selectionMode.browser) { + if (item.id in this.selectedBrowserItems) { + Vue.delete(this.selectedBrowserItems, item.id); + } else { + Vue.set(this.selectedBrowserItems, item.id, item); + } + } else if (item.id in this.selectedBrowserItems) { + Vue.delete(this.selectedBrowserItems, item.id); + } else { + this.selectedBrowserItems = {}; + Vue.set(this.selectedBrowserItems, item.id, item); + openDropdown(this.$refs.browserDropdown.$el); } }, - toggleBrowserSelectionMode: function() { - this.selectionMode.browser = !this.selectionMode.browser; - if (!this.selectionMode.browser) { - this.selectedBrowserItems = {}; + togglePlaylistSelectionMode: function() { + if (this.selectionMode.playlist && Object.keys(this.selectedPlaylistItems).length) { + openDropdown(this.$refs.playlistDropdown.$el); } + + this.selectionMode.playlist = !this.selectionMode.playlist; + }, + + playlistSelectAll: function() { + this.selectedPlaylistItems = {}; + this.selectionMode.playlist = true; + + for (var track of this.playlist) { + this.selectedPlaylistItems[track.pos] = track; + } + + openDropdown(this.$refs.playlistDropdown.$el); + }, + + toggleBrowserSelectionMode: function() { + if (this.selectionMode.browser && Object.keys(this.selectedBrowserItems).length) { + openDropdown(this.$refs.browserDropdown.$el); + } + + this.selectionMode.browser = !this.selectionMode.browser; + }, + + browserSelectAll: function() { + this.selectedBrowserItems = {}; + this.selectionMode.browser = true; + + for (var item of this.browserItems) { + if (item.type !== 'directory' && item.name !== '..') { + this.selectedBrowserItems[item.id] = item; + } + } + + openDropdown(this.$refs.browserDropdown.$el); }, scrollToActiveTrack: function() { @@ -486,6 +773,28 @@ Vue.component('music-mpd', { this.$refs.activePlaylistTrack[0].$el.scrollIntoView({behavior: 'smooth'}); } }, + + addToPlaylistPrompt: async function() { + var resource = prompt('Path or URI of the resource to add'); + if (!resource.length) { + return; + } + + this.add(resource); + }, + + savePlaylistPrompt: async function() { + var name = prompt('Playlist name'); + if (!name.length) { + return; + } + + let status = await request('music.mpd.save', {name: name}); + this._parseStatus(status); + + let items = await request('music.mpd.lsinfo', {uri: this.browserPath.join('/')}); + this._parseBrowserItems(items); + }, }, created: function() { @@ -499,9 +808,12 @@ Vue.component('music-mpd', { registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent'); registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent'); registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent'); + registerEventHandler(this.onConsumeChange, 'platypush.message.event.music.PlaybackConsumeModeChangeEvent'); + registerEventHandler(this.onSingleChange, 'platypush.message.event.music.PlaybackSingleModeChangeEvent'); }, mounted: function() { + this.adjustLayout(); this.scrollToActiveTrack(); }, }); diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index 79dd02a0d..ac956f7ce 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -6,7 +6,7 @@ - + diff --git a/platypush/backend/http/templates/plugins/light.hue/elements.html b/platypush/backend/http/templates/plugins/light.hue/elements.html index fb476af88..357dccfd0 100644 --- a/platypush/backend/http/templates/plugins/light.hue/elements.html +++ b/platypush/backend/http/templates/plugins/light.hue/elements.html @@ -29,7 +29,7 @@
- +
diff --git a/platypush/backend/http/templates/plugins/music.mpd/browser.html b/platypush/backend/http/templates/plugins/music.mpd/browser.html index f256d4654..2720db411 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/browser.html +++ b/platypush/backend/http/templates/plugins/music.mpd/browser.html @@ -1,13 +1,16 @@ diff --git a/platypush/backend/http/templates/plugins/music.mpd/index.html b/platypush/backend/http/templates/plugins/music.mpd/index.html index 90fd96546..1c8afd536 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/index.html +++ b/platypush/backend/http/templates/plugins/music.mpd/index.html @@ -5,58 +5,84 @@
-
-
-
+
+
+
-
- - +
+
+ + + +
+ + + + + :name="item.name" + :file="item.file" + :time="item.time" + :artist="item.artist" + :title="item.title" + :date="item.date" + :track="item.track" + :genre="item.genre" + :lastModified="item['last-modified']" + :albumUri="item['x-albumuri']" + :selected="item.id in selectedBrowserItems" + @input="onBrowserItemClick">
-
+
-
-
+
+
-
+
- - + +
diff --git a/platypush/backend/http/templates/plugins/music.mpd/playlist.html b/platypush/backend/http/templates/plugins/music.mpd/playlist.html index 97ce1f2f5..47eb83c9c 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/playlist.html +++ b/platypush/backend/http/templates/plugins/music.mpd/playlist.html @@ -1,7 +1,7 @@