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] {
|
||||
border-radius: 5rem;
|
||||
|
||||
&:hover {
|
||||
border: $border-hover;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
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 _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;
|
||||
|
|
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', {
|
||||
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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
Vue.component('music-mpd-playlist-item', {
|
||||
template: '#tmpl-music-mpd-playlist-item',
|
||||
mixins: [utils],
|
||||
props: {
|
||||
track: {
|
||||
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/range-slider.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/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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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