diff --git a/platypush/backend/http/static/css/source/common/elements/text.scss b/platypush/backend/http/static/css/source/common/elements/text.scss index b9fb2d0c..e8bd0f73 100644 --- a/platypush/backend/http/static/css/source/common/elements/text.scss +++ b/platypush/backend/http/static/css/source/common/elements/text.scss @@ -8,6 +8,8 @@ } input[type=text] { + border-radius: 5rem; + &:hover { border: $border-hover; } diff --git a/platypush/backend/http/static/css/source/common/modal.scss b/platypush/backend/http/static/css/source/common/modal.scss index da02356d..c9f5edd0 100644 --- a/platypush/backend/http/static/css/source/common/modal.scss +++ b/platypush/backend/http/static/css/source/common/modal.scss @@ -1,36 +1,40 @@ -.modal { - display: none; +.modal-container { position: absolute; + display: flex; + align-items: center; + justify-content: center; top: 0; left: 0; width: 100%; height: 100%; - overflow: hidden; - z-index: 999; - background-color: rgba(10,10,10,0.85); -} + z-index: var(--z-index); + background: rgba(10,10,10,0.9); - .modal-container { - margin: 5% auto auto auto; - width: 70%; - background: white; - border-radius: 10px; - } + .modal { + --width: auto; + --height: auto; + width: var(--width); + height: var(--height); - .modal-header { - border-bottom: 1px solid #ccc; - margin: 0.5rem auto; - padding: 0.5rem; + div:first-child { border-radius: 1rem 1rem 0 0; } + div:last-child { border-radius: 0 0 1rem 1rem; } + + .header { + border-bottom: $modal-header-border; + padding: .5rem; text-align: center; - background-color: $modal-bg; - border-radius: 10px 10px 0 0; + background: $modal-header-bg; text-transform: uppercase; letter-spacing: .1rem; - line-height: 38px; + line-height: 3.8rem; } - .modal-body { + .body { + max-height: 75vh; + overflow: auto; padding: 2.5rem 2rem 1.5rem 2rem; + background: $modal-body-bg; } - + } +} diff --git a/platypush/backend/http/static/css/source/common/vars.scss b/platypush/backend/http/static/css/source/common/vars.scss index 76c0cdc6..7f6a1b3d 100644 --- a/platypush/backend/http/static/css/source/common/vars.scss +++ b/platypush/backend/http/static/css/source/common/vars.scss @@ -23,8 +23,6 @@ $nav-bg: #e8e8e8 !default; $nav-fg: $default-link-fg; $nav-date-time-shadow: 2px 2px 2px #ccc !default; -$modal-bg: #f0f0f0 !default; - //// Animations defaults $transition-duration: .5s !default; $fade-transition-duration: .5s !default; @@ -88,5 +86,10 @@ $header-bottom: $default-bottom; //// Dropdown element $dropdown-bg: rgba(241,243,242,0.9) !default; -$dropdown-shadow: 1px 1px 1px #bbb; +$dropdown-shadow: 1px 1px 1px #bbb !default; + +//// Modal element +$modal-header-bg: #f0f0f0 !default; +$modal-header-border: 1px solid #ccc !default; +$modal-body-bg: white !default; 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 b2457971..b7e5e25d 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 @@ -49,96 +49,97 @@ } } + .spacer { + height: 5rem; + } + .panels { display: flex; + } - .spacer { + .browser, .playlist { + 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; + } + } + + .browser, + .search, + .playlist { + position: relative; // For the dropdown menu + padding: .5rem 1rem 6rem 1rem; + + .browser-controls, + .results-controls, + .playlist-controls { + position: fixed; height: 5rem; - } + background: $playlist-controls-bg; + border-bottom: $playlist-controls-border; + margin: -.5rem 0 0 -1rem; + padding: .5rem; - .browser, .playlist { - 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; + input[type=text] { + width: 100%; } - .fa { - color: #666; + * > 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; + } } - .browser, - .playlist { - position: relative; // For the dropdown menu - padding: .5rem 1rem 6rem 1rem; + .empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 5rem; + color: $empty-playlist-color; + text-shadow: $empty-playlist-shadow; + } - .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; - - } + .item { + &.active { + height: 4rem; + @include animation(active-track 5s infinite); } - .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); - } - - &.move:hover { - background: $move-mode-track-bg !important; - border-top: $move-mode-track-border; - border-bottom: $move-mode-track-border; - cursor: move; - } + &.move:hover { + background: $move-mode-track-bg !important; + border-top: $move-mode-track-border; + border-bottom: $move-mode-track-border; + cursor: move; } } } @@ -237,12 +238,114 @@ margin-left: 1.5rem; } } + + .search { + --width: 90vw; + position: relative; + padding: 0; + + form { + margin-bottom: 0; + + .row { + padding: .5rem; + } + + .footer { + padding-top: 1.5rem; + margin-top: 1.5rem; + border-top: $search-modal-footer-border; + + .left { + display: flex; + justify-content: left; + } + } + + button, input[type=submit] { + border-radius: 5rem; + } + } + + .dropdown { + z-index: 503; + } + + .results-controls { + position: fixed; + padding: 0; + margin: -2.45rem auto 0 -2rem; + border-bottom: $default-border-2; + width: var(--width); + height: 3em; + display: flex; + align-items: center; + z-index: 502; + } + + .results { + padding-top: 2.7rem; + } + + form, .results { + position: relative; + } + } } .dropdown { width: 20rem; } +#music-mpd-info { + .modal { + .body { + .row { + margin: .5rem; + padding: .5rem; + border-bottom: $info-modal-row-border; + + &:hover { + border-radius: 1rem; + background: $hover-bg; + } + + .attr { + color: $info-modal-attr-color; + } + + .value { + text-align: right; + } + } + } + } +} + +@media #{map-get($widths, 's')} { + #music-mpd-info { + .modal { + width: 80vw; + } + } +} + +@media #{map-get($widths, 'm')} { + #music-mpd-info { + .modal { + width: 70vw; + } + } +} + +@media #{map-get($widths, 'l')} { + #music-mpd-info { + .modal { + width: 45vw; + } + } +} + @keyframes active-track { 0% { background: $active-track-bg-1; } 50% { background: $active-track-bg-2; } @@ -260,3 +363,4 @@ 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 2ce8bdb4..ea7803d9 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,3 +26,8 @@ $active-track-bg-2: #9cdfb0; $move-mode-track-border: 3px dotted rgb(216,156,136); $move-mode-track-bg: rgba(216,156,136,0.3); +$search-modal-footer-border: 1px solid #ccc; + +$info-modal-row-border: 1px solid #ddd; +$info-modal-attr-color: #777; + diff --git a/platypush/backend/http/static/js/elements/common.js b/platypush/backend/http/static/js/elements/common.js new file mode 100644 index 00000000..48b0505a --- /dev/null +++ b/platypush/backend/http/static/js/elements/common.js @@ -0,0 +1,15 @@ +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 DOM element'); + return; + } + + return element; +}; + diff --git a/platypush/backend/http/static/js/elements/dropdown.js b/platypush/backend/http/static/js/elements/dropdown.js index eb8f7f90..0f652460 100644 --- a/platypush/backend/http/static/js/elements/dropdown.js +++ b/platypush/backend/http/static/js/elements/dropdown.js @@ -29,21 +29,6 @@ Vue.component('dropdown', { 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; @@ -53,7 +38,7 @@ var clickHndl = function(event) { while (element) { if (element == openedDropdown) { - return; // TODO dropdown click + return; } element = element.parentElement; @@ -78,7 +63,7 @@ function closeDropdown() { } function openDropdown(element) { - element = _parseElement(element); + element = parseElement(element); if (!element) { console.error('Invalid dropdown element'); return; diff --git a/platypush/backend/http/static/js/elements/modal.js b/platypush/backend/http/static/js/elements/modal.js new file mode 100644 index 00000000..6d600782 --- /dev/null +++ b/platypush/backend/http/static/js/elements/modal.js @@ -0,0 +1,84 @@ +Vue.component('modal', { + template: '#tmpl-modal', + props: { + id: { + type: String, + }, + + title: { + type: String, + }, + + width: { + type: [Number, String], + }, + + height: { + type: [Number, String], + }, + + // Modal visibility value + value: { + type: Boolean, + default: false, + }, + + timeout: { + type: [Number, String], + }, + + level: { + type: Number, + default: 1, + }, + }, + + data: function() { + return { + timeoutId: undefined, + prevValue: this.value, + }; + }, + + computed: { + zIndex: function() { + return 500 + this.level; + }, + }, + + methods: { + modalClicked: function(event) { + // Close any opened dropdowns before stopping the click propagation + const dropdowns = this.$el.querySelectorAll('.dropdown:not(.hidden)'); + for (const dropdown of dropdowns) { + closeDropdown(dropdown); + } + + event.stopPropagation(); + }, + + modalClose: function() { + event.stopPropagation(); + this.$emit('input', false); + }, + }, + + updated: function() { + if (this.value != this.prevValue) { + this.$emit((this.value ? 'open' : 'close'), this); + this.prevValue = this.value; + } + + if (this.value && this.timeout && !this.timeoutId) { + var handler = (self) => { + return () => { + self.modalClose(); + self.timeoutId = undefined; + }; + }; + + this.timeoutId = setTimeout(handler(this), 0+this.timeout); + } + }, +}); + 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 8cce55b8..c3068728 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/index.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js @@ -1,6 +1,7 @@ Vue.component('music-mpd', { template: '#tmpl-music-mpd', props: ['config'], + mixins: [utils], data: function() { return { track: {}, @@ -22,6 +23,11 @@ Vue.component('music-mpd', { editor: false, }, + infoItem: {}, + modalVisible: { + info: false, + }, + selectedPlaylistItems: {}, selectedBrowserItems: {}, @@ -78,6 +84,10 @@ Vue.component('music-mpd', { items.push({ text: 'View track info', icon: 'info', + click: async function() { + self.infoItem = Object.values(self.selectedPlaylistItems)[0]; + self.modalVisible.info = true; + }, }); } @@ -103,7 +113,7 @@ Vue.component('music-mpd', { if (Object.keys(this.selectedBrowserItems).length === 1) { items.push( { - text: 'Add and play', + text: 'Play', icon: 'play', click: async function() { const item = Object.values(self.selectedBrowserItems)[0]; @@ -340,28 +350,6 @@ Vue.component('music-mpd', { } }, - 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'); @@ -697,18 +685,16 @@ Vue.component('music-mpd', { 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; + const filter = this.playlistFilter.split(' ').filter(_ => _.length > 0).map(_ => _.toLocaleLowerCase()).join(' '); + return [track.artist || '', track.title || '', track.album || ''].join(' ').toLocaleLowerCase().indexOf() >= 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; + const filter = this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' '); + return item.name.toLocaleLowerCase().indexOf(filter) >= 0; }, onPlaylistItemClick: async function(track) { diff --git a/platypush/backend/http/static/js/plugins/music.mpd/playlist.js b/platypush/backend/http/static/js/plugins/music.mpd/playlist.js index 463381b4..99a0eca7 100644 --- a/platypush/backend/http/static/js/plugins/music.mpd/playlist.js +++ b/platypush/backend/http/static/js/plugins/music.mpd/playlist.js @@ -1,5 +1,6 @@ Vue.component('music-mpd-playlist-item', { template: '#tmpl-music-mpd-playlist-item', + mixins: [utils], props: { track: { type: Object, diff --git a/platypush/backend/http/static/js/plugins/music.mpd/search.js b/platypush/backend/http/static/js/plugins/music.mpd/search.js new file mode 100644 index 00000000..c9abb07d --- /dev/null +++ b/platypush/backend/http/static/js/plugins/music.mpd/search.js @@ -0,0 +1,161 @@ +Vue.component('music-mpd-search', { + template: '#tmpl-music-mpd-search', + props: ['mpd'], + data: function() { + return { + visible: false, + showResults: false, + results: false, + filter: '', + selectionMode: false, + selectedItems: {}, + + query: { + any: '', + artist: '', + title: '', + album: '', + }, + }; + }, + + computed: { + dropdownItems: function() { + var self = this; + var items = []; + + if (Object.keys(this.selectedItems).length === 1) { + items.push( + { + text: 'Play', + icon: 'play', + click: async function() { + const item = Object.values(self.selectedItems)[0]; + await self.mpd.add(item.file, position=0); + await self.mpd.playpos(0); + self.selectedItems = {}; + }, + }, + { + text: 'Replace and play', + icon: 'play', + click: async function() { + await self.mpd.clear(); + + const item = Object.values(self.selectedItems)[0]; + await self.mpd.add(item.file, position=0); + await self.mpd.playpos(0); + self.selectedItems = {}; + }, + } + ); + } + + items.push( + { + text: 'Add to queue', + icon: 'plus', + click: async function() { + const items = Object.values(self.selectedItems); + const promises = items.map(item => self.mpd.add(item.file)); + + await Promise.all(promises); + self.selectedItems = {}; + }, + }, + ); + + if (Object.keys(this.selectedItems).length === 1) { + items.push({ + text: 'View info', + icon: 'info', + }); + } + + return items; + }, + }, + + methods: { + search: async function() { + const filter = Object.keys(this.query).reduce((items, key) => { + if (this.query[key].length) { + items.push(key, this.query[key]); + } + + return items; + }, []); + + 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)); + }); + + this.showResults = true; + }, + + matchesFilter: function(item) { + if (this.filter.length === 0) + return true; + + const filter = this.filter.split(' ').filter(_ => _.length > 0).map(_ => _.toLocaleLowerCase()).join(' '); + return [item.file || '', item.artist || '', item.title || '', item.album || ''].join(' ').toLocaleLowerCase().indexOf(filter) >= 0; + }, + + onItemClick: function(item) { + if (this.selectionMode) { + if (item.file in this.selectedItems) { + Vue.delete(this.selectedItems, item.file); + } else { + Vue.set(this.selectedItems, item.file, item); + } + } else if (item.file in this.selectedItems) { + Vue.delete(this.selectedItems, item.file); + } else { + this.selectedItems = {}; + Vue.set(this.selectedItems, item.file, item); + openDropdown(this.$refs.dropdown.$el); + } + }, + + toggleSelectionMode: function() { + if (this.selectionMode && Object.keys(this.selectedItems).length) { + openDropdown(this.$refs.dropdown.$el); + } + + this.selectionMode = !this.selectionMode; + }, + + selectAll: function() { + this.selectedItems = {}; + this.selectionMode = true; + + for (var item of this.results) { + this.selectedItems[item.id] = item; + } + + openDropdown(this.$refs.dropdown.$el); + }, + }, +}); + +Vue.component('music-mpd-search-item', { + template: '#tmpl-music-mpd-search-item', + mixins: [utils], + props: { + item: { + type: Object, + default: {}, + }, + + selected: { + type: Boolean, + default: false, + }, + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/music.mpd/utils.js b/platypush/backend/http/static/js/plugins/music.mpd/utils.js new file mode 100644 index 00000000..6fc0c496 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/music.mpd/utils.js @@ -0,0 +1,26 @@ +var utils = { + methods: { + 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(':'); + }, + }, +}; + diff --git a/platypush/backend/http/templates/elements.html b/platypush/backend/http/templates/elements.html index 08658df8..a9a6dc3d 100644 --- a/platypush/backend/http/templates/elements.html +++ b/platypush/backend/http/templates/elements.html @@ -1,4 +1,6 @@ +{% include 'elements/common.html' %} {% include 'elements/switch.html' %} {% include 'elements/range-slider.html' %} {% include 'elements/dropdown.html' %} +{% include 'elements/modal.html' %} diff --git a/platypush/backend/http/templates/elements/common.html b/platypush/backend/http/templates/elements/common.html new file mode 100644 index 00000000..23ee1261 --- /dev/null +++ b/platypush/backend/http/templates/elements/common.html @@ -0,0 +1,2 @@ + + diff --git a/platypush/backend/http/templates/elements/modal.html b/platypush/backend/http/templates/elements/modal.html new file mode 100644 index 00000000..299d2851 --- /dev/null +++ b/platypush/backend/http/templates/elements/modal.html @@ -0,0 +1,13 @@ + + + + diff --git a/platypush/backend/http/templates/plugins/music.mpd/index.html b/platypush/backend/http/templates/plugins/music.mpd/index.html index 36b30fd0..fd735ebf 100644 --- a/platypush/backend/http/templates/plugins/music.mpd/index.html +++ b/platypush/backend/http/templates/plugins/music.mpd/index.html @@ -1,8 +1,52 @@ + + {% include 'plugins/music.mpd/browser.html' %} {% include 'plugins/music.mpd/playlist.html' %} +{% include 'plugins/music.mpd/search.html' %} diff --git a/platypush/backend/http/templates/plugins/music.mpd/search.html b/platypush/backend/http/templates/plugins/music.mpd/search.html new file mode 100644 index 00000000..3cb06128 --- /dev/null +++ b/platypush/backend/http/templates/plugins/music.mpd/search.html @@ -0,0 +1,97 @@ + + + + + +