From 7df0cec14e8ae9d2ed688ceee2d7fe68192b4771 Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <blacklight86@gmail.com>
Date: Thu, 6 Jun 2019 02:08:36 +0200
Subject: [PATCH] Implemented support for modals and music.mpd search and item
 info

---
 .../css/source/common/elements/text.scss      |   2 +
 .../http/static/css/source/common/modal.scss  |  46 ++--
 .../http/static/css/source/common/vars.scss   |   9 +-
 .../webpanel/plugins/music.mpd/index.scss     | 258 ++++++++++++------
 .../webpanel/plugins/music.mpd/vars.scss      |   5 +
 .../backend/http/static/js/elements/common.js |  15 +
 .../http/static/js/elements/dropdown.js       |  19 +-
 .../backend/http/static/js/elements/modal.js  |  84 ++++++
 .../http/static/js/plugins/music.mpd/index.js |  44 +--
 .../static/js/plugins/music.mpd/playlist.js   |   1 +
 .../static/js/plugins/music.mpd/search.js     | 161 +++++++++++
 .../http/static/js/plugins/music.mpd/utils.js |  26 ++
 .../backend/http/templates/elements.html      |   2 +
 .../http/templates/elements/common.html       |   2 +
 .../http/templates/elements/modal.html        |  13 +
 .../templates/plugins/music.mpd/index.html    |  56 +++-
 .../templates/plugins/music.mpd/playlist.html |   8 +-
 .../templates/plugins/music.mpd/search.html   |  97 +++++++
 18 files changed, 692 insertions(+), 156 deletions(-)
 create mode 100644 platypush/backend/http/static/js/elements/common.js
 create mode 100644 platypush/backend/http/static/js/elements/modal.js
 create mode 100644 platypush/backend/http/static/js/plugins/music.mpd/search.js
 create mode 100644 platypush/backend/http/static/js/plugins/music.mpd/utils.js
 create mode 100644 platypush/backend/http/templates/elements/common.html
 create mode 100644 platypush/backend/http/templates/elements/modal.html
 create mode 100644 platypush/backend/http/templates/plugins/music.mpd/search.html

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 b9fb2d0c1..e8bd0f731 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 da02356df..c9f5edd01 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 76c0cdc66..7f6a1b3d5 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 b24579716..b7e5e25da 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 2ce8bdb48..ea7803d9e 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 000000000..48b0505a3
--- /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 eb8f7f907..0f6524601 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 000000000..6d6007825
--- /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 8cce55b8c..c30687286 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 463381b46..99a0eca74 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 000000000..c9abb07dc
--- /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 000000000..6fc0c496c
--- /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 08658df84..a9a6dc3d3 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 000000000..23ee12611
--- /dev/null
+++ b/platypush/backend/http/templates/elements/common.html
@@ -0,0 +1,2 @@
+<script type="application/javascript" src="{{ url_for('static', filename='js/elements/common.js') }}"></script>
+
diff --git a/platypush/backend/http/templates/elements/modal.html b/platypush/backend/http/templates/elements/modal.html
new file mode 100644
index 000000000..299d28512
--- /dev/null
+++ b/platypush/backend/http/templates/elements/modal.html
@@ -0,0 +1,13 @@
+<script type="text/x-template" id="tmpl-modal">
+    <div class="modal-container fade-in" :id="id" :class="{hidden: !value}" :style="{'--z-index': zIndex}" @click="modalClose">
+        <div class="modal" :style="{'--width':width, '--height':height}">
+            <div class="header" v-text="title" v-if="title" @click="modalClicked"></div>
+            <div class="body" @click="modalClicked">
+                <slot></slot>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script type="application/javascript" src="{{ url_for('static', filename='js/elements/modal.js') }}"></script>
+
diff --git a/platypush/backend/http/templates/plugins/music.mpd/index.html b/platypush/backend/http/templates/plugins/music.mpd/index.html
index 36b30fd0c..fd735ebf8 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 @@
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/utils.js') }}"></script>
+
 {% include 'plugins/music.mpd/browser.html' %}
 {% include 'plugins/music.mpd/playlist.html' %}
+{% include 'plugins/music.mpd/search.html' %}
 
 <script type="text/x-template" id="tmpl-music-mpd">
     <div class="row music-mpd-container">
+        <music-mpd-search ref="search" :mpd="this"></music-mpd-search>
+
+        <modal id="music-mpd-info" title="Info" v-model="modalVisible.info" ref="modal">
+            <div class="info-container">
+                <div class="row">
+                    <div class="col-4 attr">File</div>
+                    <div class="col-8 value" v-text="infoItem.file"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.artist">
+                    <div class="col-4 attr">Artist</div>
+                    <div class="col-8 value" v-text="infoItem.artist"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.title">
+                    <div class="col-4 attr">Title</div>
+                    <div class="col-8 value" v-text="infoItem.title"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.album">
+                    <div class="col-4 attr">Album</div>
+                    <div class="col-8 value" v-text="infoItem.album"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.disc">
+                    <div class="col-4 attr">Disc</div>
+                    <div class="col-8 value" v-text="infoItem.disc"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.track">
+                    <div class="col-4 attr">Track</div>
+                    <div class="col-8 value" v-text="infoItem.track"></div>
+                </div>
+
+                <div class="row" v-if="infoItem.time && infoItem.time > 0">
+                    <div class="col-4 attr">Time</div>
+                    <div class="col-8 value" v-text="convertTime(infoItem.time)"></div>
+                </div>
+            </div>
+        </modal>
+
         <div class="row panels">
             <!-- Browser section -->
             <div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
@@ -65,17 +109,13 @@
 
             <!-- Playlist section -->
             <div class="col-s-12 col-no-margin-m-9 col-no-margin-l-9 panel playlist">
-                <div class="row empty" v-if="playlist.length === 0">
-                    <i class="fa fa-list"></i>
-                </div>
-
-                <div class="col-s-12 col-m-9 col-l-9 playlist-controls" v-else>
+                <div class="col-s-12 col-m-9 col-l-9 playlist-controls">
                     <div class="col-7 filter-container">
                         <i class="fa fa-filter input-icon"></i>
                         <input type="text" class="with-icon" v-model="playlistFilter">
                     </div>
                     <div class="col-5 buttons pull-right">
-                        <button title="Search">
+                        <button title="Search" @click="$refs.search.visible = true">
                             <i class="fa fa-search"></i>
                         </button>
                         <button title="Add item" @click="addToPlaylistPrompt">
@@ -105,6 +145,10 @@
                     </div>
                 </div>
 
+                <div class="row empty" v-if="playlist.length === 0">
+                    <i class="fa fa-list"></i>
+                </div>
+
                 <div class="spacer"></div>
 
                 <dropdown id="music-mpd-playlist-dropdown"
diff --git a/platypush/backend/http/templates/plugins/music.mpd/playlist.html b/platypush/backend/http/templates/plugins/music.mpd/playlist.html
index 97774d77f..b24cb7c9b 100644
--- a/platypush/backend/http/templates/plugins/music.mpd/playlist.html
+++ b/platypush/backend/http/templates/plugins/music.mpd/playlist.html
@@ -4,9 +4,11 @@
     <div class="row item playlist-item"
             :class="{selected: selected, active: active, move: move}"
             @click="$emit('input', track)">
-        <div class="col-5 artist" v-text="track.artist"></div>
-        <div class="col-5 title" v-text="track.title"></div>
-        <div class="col-2 pull-right duration" v-text="$parent.convertTime(track.time)"></div>
+        <div class="col-5 artist" v-text="track.artist" v-if="track.artist"></div>
+        <div class="col-5 artist empty" v-else>[No Artist]</div>
+
+        <div class="col-5 title" v-text="track.title || track.file"></div>
+        <div class="col-2 pull-right duration" v-text="convertTime(track.time)"></div>
     </div>
 </script>
 
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 000000000..3cb061280
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/music.mpd/search.html
@@ -0,0 +1,97 @@
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/search.js') }}"></script>
+
+<script type="text/x-template" id="tmpl-music-mpd-search">
+    <modal id="music-mpd-search-modal" title="Search" v-model="visible"
+           :width="showResults ? '90vw' : 'initial'" ref="modal"
+           @open="$refs.form.querySelector('input[type=text]:first-child').focus()">
+        <div class="search">
+            <form ref="form" @submit.prevent="search" :class="{hidden: showResults}">
+                <div class="row">
+                    <input type="text" v-model.lazy.trim="query.any" placeholder="Free-text search">
+                </div>
+                <div class="row">
+                    <input type="text" v-model.lazy.trim="query.artist" placeholder="Artist">
+                </div>
+                <div class="row">
+                    <input type="text" v-model.lazy.trim="query.title" placeholder="Title">
+                </div>
+                <div class="row">
+                    <input type="text" v-model.lazy.trim="query.album" placeholder="Album">
+                </div>
+
+                <div class="footer">
+                    <div class="left col-6">
+                        <button class="btn-default" v-if="results.length" @click="showResults = true" title="Show results">
+                            <i class="fa fa-list"></i>
+                        </button>
+                    </div>
+                    <div class="pull-right" :class="{'col-6': results.length > 0, 'col-12': results.length == 0}">
+                        <button class="btn-primary" type="submit">
+                            <i class="fa fa-search"></i>
+                        </button>
+                    </div>
+                </div>
+            </form>
+
+            <div class="results-controls" :class="{hidden: !showResults}">
+                <div class="col-6">
+                    <i class="fa fa-filter input-icon"></i>
+                    <input type="text" class="with-icon" v-model="filter">
+                </div>
+                <div class="col-6 pull-right">
+                    <button :class="{enabled: selectionMode}"
+                            :title="selectionMode ? 'End selection' : 'Start selection'"
+                            v-if="results.length > 0"
+                            @click="toggleSelectionMode">
+                        <i class="fa fa-check"></i>
+                    </button>
+                    <button title="Select all"
+                            v-if="results.length > 0"
+                            @click="selectAll">
+                        <i class="fa fa-check-double"></i>
+                    </button>
+                    <button class="btn-default" @click="showResults = false" title="Show search form">
+                        <i class="fa fa-search"></i>
+                    </button>
+                </div>
+            </div>
+
+            <dropdown id="music-mpd-search-dropdown"
+                      v-if="results.length > 0"
+                      ref="dropdown"
+                      :items="dropdownItems">
+            </dropdown>
+
+            <div class="results" :class="{hidden: !showResults}">
+                <div class="no-results" v-if="results.length === 0">No results</div>
+                <div v-else>
+                    <music-mpd-search-item
+                            v-for="item in results"
+                            v-if="matchesFilter(item)"
+                            :key="item.file"
+                            :item="item"
+                            :selected="item.file in selectedItems"
+                            @input="onItemClick">
+                    </music-mpd-search-item>
+                </div>
+            </div>
+        </div>
+    </modal>
+</script>
+
+<script type="text/x-template" id="tmpl-music-mpd-search-item">
+    <div class="row item search-item"
+         :class="{selected: selected}"
+         @click="$emit('input', item)">
+        <div class="col-3 artist" v-text="item.artist" v-if="item.artist"></div>
+        <div class="col-3 artist empty" v-else>[No Artist]</div>
+
+        <div class="col-4 title" v-text="item.title || item.file"></div>
+
+        <div class="col-3 album" v-text="item.album" v-if="item.album"></div>
+        <div class="col-3 album empty" v-else>-</div>
+
+        <div class="col-2 pull-right duration" v-text="item.time && item.time != 0 ? convertTime(item.time) : '-:--'"></div>
+    </div>
+</script>
+