forked from platypush/platypush
Implemented support for modals and music.mpd search and item info
This commit is contained in:
parent
611a137ff6
commit
7df0cec14e
18 changed files with 692 additions and 156 deletions
|
@ -8,6 +8,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
|
border-radius: 5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: $border-hover;
|
border: $border-hover;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
.modal {
|
.modal-container {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
z-index: var(--z-index);
|
||||||
z-index: 999;
|
background: rgba(10,10,10,0.9);
|
||||||
background-color: rgba(10,10,10,0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
.modal {
|
||||||
margin: 5% auto auto auto;
|
--width: auto;
|
||||||
width: 70%;
|
--height: auto;
|
||||||
background: white;
|
width: var(--width);
|
||||||
border-radius: 10px;
|
height: var(--height);
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
div:first-child { border-radius: 1rem 1rem 0 0; }
|
||||||
border-bottom: 1px solid #ccc;
|
div:last-child { border-radius: 0 0 1rem 1rem; }
|
||||||
margin: 0.5rem auto;
|
|
||||||
padding: 0.5rem;
|
.header {
|
||||||
|
border-bottom: $modal-header-border;
|
||||||
|
padding: .5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $modal-bg;
|
background: $modal-header-bg;
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: .1rem;
|
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;
|
padding: 2.5rem 2rem 1.5rem 2rem;
|
||||||
|
background: $modal-body-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,6 @@ $nav-bg: #e8e8e8 !default;
|
||||||
$nav-fg: $default-link-fg;
|
$nav-fg: $default-link-fg;
|
||||||
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
|
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
|
||||||
|
|
||||||
$modal-bg: #f0f0f0 !default;
|
|
||||||
|
|
||||||
//// Animations defaults
|
//// Animations defaults
|
||||||
$transition-duration: .5s !default;
|
$transition-duration: .5s !default;
|
||||||
$fade-transition-duration: .5s !default;
|
$fade-transition-duration: .5s !default;
|
||||||
|
@ -88,5 +86,10 @@ $header-bottom: $default-bottom;
|
||||||
|
|
||||||
//// Dropdown element
|
//// Dropdown element
|
||||||
$dropdown-bg: rgba(241,243,242,0.9) !default;
|
$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;
|
||||||
|
|
||||||
|
|
|
@ -49,13 +49,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panels {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.browser, .playlist {
|
.browser, .playlist {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
@ -76,11 +77,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser,
|
.browser,
|
||||||
|
.search,
|
||||||
.playlist {
|
.playlist {
|
||||||
position: relative; // For the dropdown menu
|
position: relative; // For the dropdown menu
|
||||||
padding: .5rem 1rem 6rem 1rem;
|
padding: .5rem 1rem 6rem 1rem;
|
||||||
|
|
||||||
.browser-controls,
|
.browser-controls,
|
||||||
|
.results-controls,
|
||||||
.playlist-controls {
|
.playlist-controls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
|
@ -91,7 +94,6 @@
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* > button {
|
* > button {
|
||||||
|
@ -141,7 +143,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
@extend .vertical-center;
|
@extend .vertical-center;
|
||||||
|
@ -237,12 +238,114 @@
|
||||||
margin-left: 1.5rem;
|
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 {
|
.dropdown {
|
||||||
width: 20rem;
|
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 {
|
@keyframes active-track {
|
||||||
0% { background: $active-track-bg-1; }
|
0% { background: $active-track-bg-1; }
|
||||||
50% { background: $active-track-bg-2; }
|
50% { background: $active-track-bg-2; }
|
||||||
|
@ -260,3 +363,4 @@
|
||||||
50% { background: $active-track-bg-2; }
|
50% { background: $active-track-bg-2; }
|
||||||
100% { background: $active-track-bg-1; }
|
100% { background: $active-track-bg-1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,8 @@ $active-track-bg-2: #9cdfb0;
|
||||||
$move-mode-track-border: 3px dotted rgb(216,156,136);
|
$move-mode-track-border: 3px dotted rgb(216,156,136);
|
||||||
$move-mode-track-bg: rgba(216,156,136,0.3);
|
$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;
|
||||||
|
|
||||||
|
|
15
platypush/backend/http/static/js/elements/common.js
Normal file
15
platypush/backend/http/static/js/elements/common.js
Normal file
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -29,21 +29,6 @@ Vue.component('dropdown', {
|
||||||
|
|
||||||
var openedDropdown;
|
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) {
|
var clickHndl = function(event) {
|
||||||
if (!openedDropdown) {
|
if (!openedDropdown) {
|
||||||
return;
|
return;
|
||||||
|
@ -53,7 +38,7 @@ var clickHndl = function(event) {
|
||||||
|
|
||||||
while (element) {
|
while (element) {
|
||||||
if (element == openedDropdown) {
|
if (element == openedDropdown) {
|
||||||
return; // TODO dropdown click
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
element = element.parentElement;
|
element = element.parentElement;
|
||||||
|
@ -78,7 +63,7 @@ function closeDropdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDropdown(element) {
|
function openDropdown(element) {
|
||||||
element = _parseElement(element);
|
element = parseElement(element);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
console.error('Invalid dropdown element');
|
console.error('Invalid dropdown element');
|
||||||
return;
|
return;
|
||||||
|
|
84
platypush/backend/http/static/js/elements/modal.js
Normal file
84
platypush/backend/http/static/js/elements/modal.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Vue.component('music-mpd', {
|
Vue.component('music-mpd', {
|
||||||
template: '#tmpl-music-mpd',
|
template: '#tmpl-music-mpd',
|
||||||
props: ['config'],
|
props: ['config'],
|
||||||
|
mixins: [utils],
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
track: {},
|
track: {},
|
||||||
|
@ -22,6 +23,11 @@ Vue.component('music-mpd', {
|
||||||
editor: false,
|
editor: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
infoItem: {},
|
||||||
|
modalVisible: {
|
||||||
|
info: false,
|
||||||
|
},
|
||||||
|
|
||||||
selectedPlaylistItems: {},
|
selectedPlaylistItems: {},
|
||||||
selectedBrowserItems: {},
|
selectedBrowserItems: {},
|
||||||
|
|
||||||
|
@ -78,6 +84,10 @@ Vue.component('music-mpd', {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'View track info',
|
text: 'View track info',
|
||||||
icon: '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) {
|
if (Object.keys(this.selectedBrowserItems).length === 1) {
|
||||||
items.push(
|
items.push(
|
||||||
{
|
{
|
||||||
text: 'Add and play',
|
text: 'Play',
|
||||||
icon: 'play',
|
icon: 'play',
|
||||||
click: async function() {
|
click: async function() {
|
||||||
const item = Object.values(self.selectedBrowserItems)[0];
|
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() {
|
previous: async function() {
|
||||||
await request('music.mpd.previous');
|
await request('music.mpd.previous');
|
||||||
let track = await request('music.mpd.currentsong');
|
let track = await request('music.mpd.currentsong');
|
||||||
|
@ -697,18 +685,16 @@ Vue.component('music-mpd', {
|
||||||
if (this.playlistFilter.length === 0)
|
if (this.playlistFilter.length === 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return [track.artist || '', track.title || '', track.album || '']
|
const filter = this.playlistFilter.split(' ').filter(_ => _.length > 0).map(_ => _.toLocaleLowerCase()).join(' ');
|
||||||
.join(' ').toLocaleLowerCase().indexOf(
|
return [track.artist || '', track.title || '', track.album || ''].join(' ').toLocaleLowerCase().indexOf() >= 0;
|
||||||
this.playlistFilter.split(' ').filter(_ => _.length > 0).map(_ => _.toLocaleLowerCase()).join(' ')
|
|
||||||
) >= 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
matchesBrowserFilter: function(item) {
|
matchesBrowserFilter: function(item) {
|
||||||
if (this.browserFilter.length === 0)
|
if (this.browserFilter.length === 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return item.name.toLocaleLowerCase().indexOf(
|
const filter = this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ');
|
||||||
this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ')) >= 0;
|
return item.name.toLocaleLowerCase().indexOf(filter) >= 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
onPlaylistItemClick: async function(track) {
|
onPlaylistItemClick: async function(track) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
Vue.component('music-mpd-playlist-item', {
|
Vue.component('music-mpd-playlist-item', {
|
||||||
template: '#tmpl-music-mpd-playlist-item',
|
template: '#tmpl-music-mpd-playlist-item',
|
||||||
|
mixins: [utils],
|
||||||
props: {
|
props: {
|
||||||
track: {
|
track: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
161
platypush/backend/http/static/js/plugins/music.mpd/search.js
Normal file
161
platypush/backend/http/static/js/plugins/music.mpd/search.js
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
26
platypush/backend/http/static/js/plugins/music.mpd/utils.js
Normal file
26
platypush/backend/http/static/js/plugins/music.mpd/utils.js
Normal file
|
@ -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(':');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
{% include 'elements/common.html' %}
|
||||||
{% include 'elements/switch.html' %}
|
{% include 'elements/switch.html' %}
|
||||||
{% include 'elements/range-slider.html' %}
|
{% include 'elements/range-slider.html' %}
|
||||||
{% include 'elements/dropdown.html' %}
|
{% include 'elements/dropdown.html' %}
|
||||||
|
{% include 'elements/modal.html' %}
|
||||||
|
|
||||||
|
|
2
platypush/backend/http/templates/elements/common.html
Normal file
2
platypush/backend/http/templates/elements/common.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/common.js') }}"></script>
|
||||||
|
|
13
platypush/backend/http/templates/elements/modal.html
Normal file
13
platypush/backend/http/templates/elements/modal.html
Normal file
|
@ -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>
|
||||||
|
|
|
@ -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/browser.html' %}
|
||||||
{% include 'plugins/music.mpd/playlist.html' %}
|
{% include 'plugins/music.mpd/playlist.html' %}
|
||||||
|
{% include 'plugins/music.mpd/search.html' %}
|
||||||
|
|
||||||
<script type="text/x-template" id="tmpl-music-mpd">
|
<script type="text/x-template" id="tmpl-music-mpd">
|
||||||
<div class="row music-mpd-container">
|
<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">
|
<div class="row panels">
|
||||||
<!-- Browser section -->
|
<!-- Browser section -->
|
||||||
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
||||||
|
@ -65,17 +109,13 @@
|
||||||
|
|
||||||
<!-- Playlist section -->
|
<!-- Playlist section -->
|
||||||
<div class="col-s-12 col-no-margin-m-9 col-no-margin-l-9 panel playlist">
|
<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">
|
<div class="col-s-12 col-m-9 col-l-9 playlist-controls">
|
||||||
<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-7 filter-container">
|
<div class="col-7 filter-container">
|
||||||
<i class="fa fa-filter input-icon"></i>
|
<i class="fa fa-filter input-icon"></i>
|
||||||
<input type="text" class="with-icon" v-model="playlistFilter">
|
<input type="text" class="with-icon" v-model="playlistFilter">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-5 buttons pull-right">
|
<div class="col-5 buttons pull-right">
|
||||||
<button title="Search">
|
<button title="Search" @click="$refs.search.visible = true">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
<button title="Add item" @click="addToPlaylistPrompt">
|
<button title="Add item" @click="addToPlaylistPrompt">
|
||||||
|
@ -105,6 +145,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row empty" v-if="playlist.length === 0">
|
||||||
|
<i class="fa fa-list"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<dropdown id="music-mpd-playlist-dropdown"
|
<dropdown id="music-mpd-playlist-dropdown"
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
<div class="row item playlist-item"
|
<div class="row item playlist-item"
|
||||||
:class="{selected: selected, active: active, move: move}"
|
:class="{selected: selected, active: active, move: move}"
|
||||||
@click="$emit('input', track)">
|
@click="$emit('input', track)">
|
||||||
<div class="col-5 artist" v-text="track.artist"></div>
|
<div class="col-5 artist" v-text="track.artist" v-if="track.artist"></div>
|
||||||
<div class="col-5 title" v-text="track.title"></div>
|
<div class="col-5 artist empty" v-else>[No Artist]</div>
|
||||||
<div class="col-2 pull-right duration" v-text="$parent.convertTime(track.time)"></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>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue