forked from platypush/platypush
music.mpd vue.js refactoring WIP
This commit is contained in:
parent
0f3987aaf2
commit
e1ddf7bb3b
18 changed files with 718 additions and 75 deletions
|
@ -1,4 +1,4 @@
|
|||
//// General purpose classes /////
|
||||
//// General purpose classes and rules /////
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -11,10 +11,26 @@
|
|||
text-align: right !important;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-moz-focus-outer,
|
||||
::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
//// UI elements definitions /////
|
||||
|
||||
@import 'common/elements/button';
|
||||
@import 'common/elements/switch';
|
||||
@import 'common/elements/range-slider';
|
||||
@import 'common/elements/slider';
|
||||
@import 'common/elements/text';
|
||||
@import 'common/elements/dropdown';
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
@import 'common/vars';
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
background: $default-bg-3;
|
||||
border-radius: .75rem;
|
||||
border: $default-border-3;
|
||||
box-shadow: $dropdown-shadow;
|
||||
min-width: 15rem;
|
||||
|
||||
.item {
|
||||
margin: 0 !important;
|
||||
padding: 1rem;
|
||||
|
||||
.icon {
|
||||
margin: 0 .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,20 +35,25 @@
|
|||
transparent var(--low), var(--range-color) 0,
|
||||
var(--range-color) var(--high), transparent 0
|
||||
) no-repeat 0 45% / 100% 40%;
|
||||
--range-color: $slider-thumb-bg;
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: var(--track-background);
|
||||
}
|
||||
--range-color: $slider-progress-bg;
|
||||
|
||||
&::-webkit-slider-runnable-track,
|
||||
&::-moz-range-track {
|
||||
background: var(--track-background);
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-slider-thumb {
|
||||
&[disabled]::-webkit-slider-thumb,
|
||||
&[disabled]::-moz-range-thumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value,
|
||||
&::-moz-range-progress {
|
||||
@include appearance(none);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import 'common/mixins';
|
||||
|
||||
.slider {
|
||||
@include appearance(none);
|
||||
@include transition(opacity .2s);
|
||||
|
@ -7,29 +9,40 @@
|
|||
background: $slider-bg;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
&::-webkit-slider-thumb,
|
||||
&::-moz-range-thumb {
|
||||
@include appearance(none);
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: 0;
|
||||
background: $slider-thumb-bg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-slider-thumb {
|
||||
&[disabled]::-webkit-slider-thumb,
|
||||
&[disabled]::-moz-range-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background: $slider-thumb-bg;
|
||||
cursor: pointer;
|
||||
&::-moz-range-track {
|
||||
@include appearance(none);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value,
|
||||
&::-moz-range-progress {
|
||||
background: $slider-progress-bg;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
&[disabled]::-webkit-progress-value,
|
||||
&[disabled]::-moz-range-progress {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
@import 'common/vars';
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
min-width: 3rem;
|
||||
padding: 1rem;
|
||||
color: $text-icon-color;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
&:hover {
|
||||
border: $border-hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: $border-focus;
|
||||
box-shadow: $text-shadow;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,14 @@
|
|||
transition: $value;
|
||||
}
|
||||
|
||||
@mixin animation($value) {
|
||||
-webkit-animation: $value;
|
||||
-ms-animation: $value;
|
||||
-o-animation: $value;
|
||||
-ms-animation: $value;
|
||||
animation: $value;
|
||||
}
|
||||
|
||||
@mixin box-shadow($value) {
|
||||
-webkit-box-shadow: $value;
|
||||
-o-box-shadow: $value;
|
||||
|
|
|
@ -10,6 +10,7 @@ $default-font-size: 1.5rem !default;
|
|||
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
|
||||
$default-border: 1px solid #e1e4e8 !default;
|
||||
$default-border-2: 1px solid #dddddd !default;
|
||||
$default-border-3: 1px solid #cccccc !default;
|
||||
$default-bottom: $default-border !default;
|
||||
$default-link-fg: #5f7869 !default;
|
||||
|
||||
|
@ -69,12 +70,23 @@ $switch-shadow-glow-hover: inset 0 0 0 5px #fff, inset 0 0 0 14px #fff !default;
|
|||
$switch-shadow-glow-checked-1: 0 0px 8px 0 #00ad72, 0 0 0 3px #00e094, 0 0 30px 0 #00e094, 0 0 0 6px #fff !default;
|
||||
$switch-shadow-glow-checked-2: inset 0 0 0 5px #00e094, inset 0 0 0 14px #fff !default;
|
||||
|
||||
//// Slier element
|
||||
//// Slider element
|
||||
$slider-bg: #e4e4e4 !default;
|
||||
$slider-thumb-bg: rgba(0,215,80,1.0) !default;
|
||||
$slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default;
|
||||
$slider-hover-on-hover-bg: #d2d2d2 !default;
|
||||
$slider-progress-bg: rgba(0,215,80,0.2) !default;
|
||||
|
||||
//// Input element
|
||||
$text-icon-color: #888;
|
||||
$border-focus: 1px solid rgba(127, 216, 95, 0.83);
|
||||
$border-hover: 1px solid rgba(159, 180, 152, 0.83);
|
||||
$text-shadow: 2px 2px 2px #d4d4d4;
|
||||
|
||||
//// Header style
|
||||
$header-bottom: $default-bottom;
|
||||
|
||||
//// Dropdown element
|
||||
$dropdown-bg: rgba(241,243,242,0.9) !default;
|
||||
$dropdown-shadow: 1px 1px 1px #bbb;
|
||||
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
@import 'common/vars';
|
||||
@import 'common/mixins';
|
||||
@import 'common/layout';
|
||||
@import 'webpanel/plugins/music.mpd/vars';
|
||||
|
||||
// background-image: linear-gradient(to right bottom, rgb(123, 84, 30), rgb(0, 0, 0)), linear-gradient(transparent, rgb(0, 0, 0) 70%);
|
||||
|
||||
.music-mpd-container {
|
||||
line-height: 3rem;
|
||||
letter-spacing: .03rem;
|
||||
overflow: hidden;
|
||||
|
||||
* > .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 1rem;
|
||||
padding: .5rem;
|
||||
|
||||
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
|
||||
&:nth-child(even) { background: $default-bg-3; }
|
||||
&:hover { background: $hover-bg !important; }
|
||||
&.selected { background: $selected-bg; }
|
||||
|
||||
.artist {
|
||||
font-size: $artist-font-size;
|
||||
|
@ -27,6 +30,24 @@
|
|||
font-size: $duration-font-size;
|
||||
}
|
||||
|
||||
* > button {
|
||||
border: 0;
|
||||
|
||||
&:disabled {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
color: $button-enabled-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panels {
|
||||
display: flex;
|
||||
|
||||
|
@ -43,6 +64,9 @@
|
|||
|
||||
.item {
|
||||
background: none;
|
||||
&:nth-of-type(2) {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
|
@ -50,8 +74,72 @@
|
|||
}
|
||||
}
|
||||
|
||||
.browser,
|
||||
.playlist {
|
||||
position: relative; // For the dropdown menu
|
||||
padding: .5rem 1rem 6rem 1rem;
|
||||
|
||||
.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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 4.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +147,6 @@
|
|||
@extend .vertical-center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
min-height: 6rem;
|
||||
bottom: 0;
|
||||
border-top: $default-border-2;
|
||||
padding: 1rem;
|
||||
|
@ -78,28 +165,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
.fa {
|
||||
color: $button-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
.row {
|
||||
@extend .vertical-center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
* > button {
|
||||
border: 0;
|
||||
button {
|
||||
padding: 0 1.5rem;
|
||||
|
||||
&.enabled {
|
||||
color: $button-enabled-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa {
|
||||
color: $button-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-play, .fa-pause {
|
||||
color: $button-hover-color;
|
||||
font-size: $font-size * 2;
|
||||
|
@ -110,6 +192,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
padding-right: 2.5rem;
|
||||
|
@ -154,9 +237,26 @@
|
|||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
* > .item:hover {
|
||||
background: $hover-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
#music-mpd-playlist-dropdown {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
@keyframes active-track {
|
||||
0% { background: $active-track-bg-1; }
|
||||
50% { background: $active-track-bg-2; }
|
||||
100% { background: $active-track-bg-1; }
|
||||
}
|
||||
|
||||
@-moz-keyframes active-track {
|
||||
0% { background: $active-track-bg-1; }
|
||||
50% { background: $active-track-bg-2; }
|
||||
100% { background: $active-track-bg-1; }
|
||||
}
|
||||
|
||||
@-webkit-keyframes active-track {
|
||||
0% { background: $active-track-bg-1; }
|
||||
50% { background: $active-track-bg-2; }
|
||||
100% { background: $active-track-bg-1; }
|
||||
}
|
||||
|
|
|
@ -15,3 +15,11 @@ $control-time-font-size: $font-size * 0.666666;
|
|||
$browser-panel-bg: rgba(248,250,250,0.95);
|
||||
$browser-font-size: $font-size * 0.8666;
|
||||
|
||||
$empty-playlist-color: rgba(200,200,200,0.7);
|
||||
$empty-playlist-shadow: 2px 1px rgb(235,235,235);
|
||||
$playlist-controls-bg: rgba(247,247,247,0.95);
|
||||
$playlist-controls-border: $default-border-2;
|
||||
|
||||
$active-track-bg-1: #d4ffe3;
|
||||
$active-track-bg-2: #9cdfb0;
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ window.vm = new Vue({
|
|||
|
||||
initEvents();
|
||||
},
|
||||
|
||||
updated: function() {},
|
||||
destroyed: function() {},
|
||||
});
|
||||
|
|
104
platypush/backend/http/static/js/elements/dropdown.js
Normal file
104
platypush/backend/http/static/js/elements/dropdown.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
Vue.component('dropdown', {
|
||||
template: '#tmpl-dropdown',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
items: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clicked: function(item) {
|
||||
if (item.click) {
|
||||
item.click();
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var element = event.target;
|
||||
|
||||
while (element) {
|
||||
if (element == openedDropdown) {
|
||||
return; // TODO dropdown click
|
||||
}
|
||||
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
// Click outside the dropdown, close it
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
function closeDropdown() {
|
||||
if (!openedDropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.removeEventListener('click', clickHndl);
|
||||
|
||||
if (openedDropdown.className.indexOf('hidden') < 0) {
|
||||
openedDropdown.className = (openedDropdown.className + ' hidden').trim();
|
||||
}
|
||||
|
||||
openedDropdown = undefined;
|
||||
}
|
||||
|
||||
function openDropdown(element) {
|
||||
element = _parseElement(element);
|
||||
if (!element) {
|
||||
console.error('Invalid dropdown element');
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
closeDropdown();
|
||||
|
||||
if (getComputedStyle(element.parentElement).position === 'relative') {
|
||||
// Position the dropdown relatively to the parent
|
||||
element.style.left = (window.event.clientX - element.parentElement.offsetLeft + element.parentElement.scrollLeft) + 'px';
|
||||
element.style.top = (window.event.clientY - element.parentElement.offsetTop + element.parentElement.scrollTop) + 'px';
|
||||
} else {
|
||||
// Position the dropdown absolutely on the window
|
||||
element.style.left = (window.event.clientX + window.scrollX) + 'px';
|
||||
element.style.top = (window.event.clientY + window.scrollY) + 'px';
|
||||
}
|
||||
|
||||
document.addEventListener('click', clickHndl);
|
||||
element.className = element.className.split(' ').filter(c => c !== 'hidden').join(' ');
|
||||
openedDropdown = element;
|
||||
}
|
||||
|
|
@ -5,17 +5,65 @@ Vue.component('music-mpd', {
|
|||
return {
|
||||
track: {},
|
||||
status: {},
|
||||
playlist: [],
|
||||
timer: null,
|
||||
playlist: [],
|
||||
playlistFilter: '',
|
||||
browserFilter: '',
|
||||
browserPath: [],
|
||||
browserItems: [],
|
||||
|
||||
selectionMode: {
|
||||
playlist: false,
|
||||
browser: false,
|
||||
},
|
||||
|
||||
selectedPlaylistItems: {},
|
||||
selectedBrowserItems: {},
|
||||
|
||||
syncTime: {
|
||||
timestamp: null,
|
||||
elapsed: null,
|
||||
},
|
||||
|
||||
newTrackLock: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
playlistDropdownItems: function() {
|
||||
var self = this;
|
||||
|
||||
return [
|
||||
{
|
||||
text: 'Play',
|
||||
icon: 'play',
|
||||
click: async function() {
|
||||
await self.playpos();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Add to playlist',
|
||||
icon: 'list',
|
||||
},
|
||||
{
|
||||
text: 'Move',
|
||||
icon: 'retweet',
|
||||
},
|
||||
{
|
||||
text: 'Remove from queue',
|
||||
icon: 'trash',
|
||||
click: async function() {
|
||||
await self.del();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'View track info',
|
||||
icon: 'info',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh: async function() {
|
||||
const getStatus = request('music.mpd.status');
|
||||
|
@ -152,6 +200,22 @@ Vue.component('music-mpd', {
|
|||
method({ status: status });
|
||||
},
|
||||
|
||||
playpos: async function(pos) {
|
||||
if (pos == null) {
|
||||
if (!Object.keys(this.selectedPlaylistItems).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
pos = Object.keys(this.selectedPlaylistItems)[0];
|
||||
}
|
||||
|
||||
let status = await request('music.mpd.play_pos', {pos: pos});
|
||||
this._parseStatus(status);
|
||||
|
||||
let track = await request('music.mpd.currentsong');
|
||||
this._parseTrack(track);
|
||||
},
|
||||
|
||||
stop: async function() {
|
||||
await request('music.mpd.stop');
|
||||
this.onMusicStop({});
|
||||
|
@ -199,6 +263,49 @@ Vue.component('music-mpd', {
|
|||
this.onVolumeChange({status: status});
|
||||
},
|
||||
|
||||
clear: async function() {
|
||||
if (!confirm('Are you sure that you want to clear the playlist?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('music.mpd.clear');
|
||||
this.stopTimer();
|
||||
this.track = {};
|
||||
this.playlist = [];
|
||||
|
||||
let status = await request('music.mpd.status');
|
||||
this._parseStatus(status);
|
||||
},
|
||||
|
||||
del: async function() {
|
||||
const positions = Object.keys(this.selectedPlaylistItems);
|
||||
if (!positions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let status = await request('music.mpd.delete', {'positions': positions});
|
||||
this._parseStatus(status);
|
||||
|
||||
for (const pos in positions) {
|
||||
Vue.delete(this.selectedPlaylistItems, pos);
|
||||
}
|
||||
},
|
||||
|
||||
swap: async function() {
|
||||
if (Object.keys(this.selectedPlaylistItems).length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = Object.keys(this.selectedPlaylistItems).sort();
|
||||
await request('music.mpd.move', {from_pos: positions[1], to_pos: positions[0]});
|
||||
|
||||
let status = await request('music.mpd.move', {from_pos: positions[0]+1, to_pos: positions[1]});
|
||||
this._parseStatus(status);
|
||||
|
||||
const playlist = await request('music.mpd.playlistinfo');
|
||||
this._parsePlaylist(playlist);
|
||||
},
|
||||
|
||||
onNewPlayingTrack: async function(event) {
|
||||
var previousTrack = {
|
||||
file: this.track.file,
|
||||
|
@ -207,18 +314,23 @@ Vue.component('music-mpd', {
|
|||
};
|
||||
|
||||
this.status.state = 'play';
|
||||
this.status.elapsed = 0;
|
||||
Vue.set(this.status, 'elapsed', 0);
|
||||
this.track = {};
|
||||
|
||||
let status = await request('music.mpd.status');
|
||||
this._parseStatus(status);
|
||||
this._parseTrack(event.track);
|
||||
|
||||
let status = event.status ? event.status : await request('music.mpd.status');
|
||||
this._parseStatus(status);
|
||||
this.startTimer();
|
||||
|
||||
if (this.track.file != previousTrack.file
|
||||
|| this.track.artist != previousTrack.artist
|
||||
|| this.track.title != previousTrack.title) {
|
||||
this.showNewTrackNotification();
|
||||
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
self.scrollToActiveTrack();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -234,6 +346,7 @@ Vue.component('music-mpd', {
|
|||
|
||||
onMusicStop: function(event) {
|
||||
this.status.state = 'stop';
|
||||
Vue.set(this.status, 'elapsed', 0);
|
||||
this._parseStatus(event.status);
|
||||
this._parseTrack(event.track);
|
||||
this.stopTimer();
|
||||
|
@ -251,24 +364,29 @@ Vue.component('music-mpd', {
|
|||
this._parseStatus(event.status);
|
||||
this._parseTrack(event.track);
|
||||
|
||||
this.syncTime.timestamp = new Date();
|
||||
this.syncTime.elapsed = this.status.elapsed;
|
||||
Vue.set(this.syncTime, 'timestamp', new Date());
|
||||
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
|
||||
},
|
||||
|
||||
onSeekChange: function(event) {
|
||||
if (event.position != null)
|
||||
this.status.elapsed = parseFloat(event.position);
|
||||
Vue.set(this.status, 'elapsed', parseFloat(event.position));
|
||||
if (event.status)
|
||||
this._parseStatus(event.status);
|
||||
if (event.track)
|
||||
this._parseTrack(event.track);
|
||||
|
||||
this.syncTime.timestamp = new Date();
|
||||
this.syncTime.elapsed = this.status.elapsed;
|
||||
Vue.set(this.syncTime, 'timestamp', new Date());
|
||||
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
|
||||
},
|
||||
|
||||
onPlaylistChange: function(event) {
|
||||
console.log(event);
|
||||
onPlaylistChange: async function(event) {
|
||||
if (event.changes) {
|
||||
this.playlist = event.changes;
|
||||
} else {
|
||||
const playlist = await request('music.mpd.playlistinfo');
|
||||
this._parsePlaylist(playlist);
|
||||
}
|
||||
},
|
||||
|
||||
onVolumeChange: function(event) {
|
||||
|
@ -293,8 +411,8 @@ Vue.component('music-mpd', {
|
|||
this.stopTimer();
|
||||
}
|
||||
|
||||
this.syncTime.timestamp = new Date();
|
||||
this.syncTime.elapsed = this.status.elapsed;
|
||||
Vue.set(this.syncTime, 'timestamp', new Date());
|
||||
Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
|
||||
this.timer = setInterval(this.timerFunc, 1000);
|
||||
},
|
||||
|
||||
|
@ -310,8 +428,63 @@ Vue.component('music-mpd', {
|
|||
return;
|
||||
}
|
||||
|
||||
this.status.elapsed = this.syncTime.elapsed +
|
||||
((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000);
|
||||
Vue.set(this.status, 'elapsed', this.syncTime.elapsed +
|
||||
((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000));
|
||||
},
|
||||
|
||||
matchesPlaylistFilter: function(track) {
|
||||
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;
|
||||
},
|
||||
|
||||
matchesBrowserFilter: function(item) {
|
||||
if (this.browserFilter.length === 0)
|
||||
return true;
|
||||
|
||||
return item.name.toLocaleLowerCase().indexOf(
|
||||
this.browserFilter.toLocaleLowerCase().split(' ').filter(_ => _.length > 0).join(' ')) >= 0;
|
||||
},
|
||||
|
||||
onPlaylistItemClick: function(track) {
|
||||
if (this.selectionMode.playlist) {
|
||||
if (track.pos in this.selectedPlaylistItems) {
|
||||
Vue.delete(this.selectedPlaylistItems, track.pos);
|
||||
} else {
|
||||
Vue.set(this.selectedPlaylistItems, track.pos, track);
|
||||
}
|
||||
} else if (track.pos in this.selectedPlaylistItems) {
|
||||
// TODO when track clicked twice
|
||||
Vue.delete(this.selectedPlaylistItems, track.pos);
|
||||
} else {
|
||||
this.selectedPlaylistItems = {};
|
||||
Vue.set(this.selectedPlaylistItems, track.pos, track);
|
||||
openDropdown(this.$refs.playlistDropdown.$el);
|
||||
}
|
||||
},
|
||||
|
||||
togglePlaylistSelectionMode: function() {
|
||||
this.selectionMode.playlist = !this.selectionMode.playlist;
|
||||
if (!this.selectionMode.playlist) {
|
||||
this.selectedPlaylistItems = {};
|
||||
}
|
||||
},
|
||||
|
||||
toggleBrowserSelectionMode: function() {
|
||||
this.selectionMode.browser = !this.selectionMode.browser;
|
||||
if (!this.selectionMode.browser) {
|
||||
this.selectedBrowserItems = {};
|
||||
}
|
||||
},
|
||||
|
||||
scrollToActiveTrack: function() {
|
||||
if (this.$refs.activePlaylistTrack && this.$refs.activePlaylistTrack.length) {
|
||||
this.$refs.activePlaylistTrack[0].$el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -327,5 +500,9 @@ Vue.component('music-mpd', {
|
|||
registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
|
||||
registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.scrollToActiveTrack();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
Vue.component('music-mpd-playlist-item', {
|
||||
template: '#tmpl-music-mpd-playlist-item',
|
||||
props: {
|
||||
track: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
{% include 'elements/switch.html' %}
|
||||
{% include 'elements/range-slider.html' %}
|
||||
{% include 'elements/dropdown.html' %}
|
||||
|
||||
|
|
14
platypush/backend/http/templates/elements/dropdown.html
Normal file
14
platypush/backend/http/templates/elements/dropdown.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script type="text/x-template" id="tmpl-dropdown">
|
||||
<div class="dropdown" :id="id" :class="{hidden: !visible}">
|
||||
<div class="row item" v-for="item in items" @click="clicked(item)">
|
||||
<div class="col-1 icon">
|
||||
<i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
|
||||
<img src="item.img" v-else-if="item.image">
|
||||
</div>
|
||||
<div class="col-11 text" v-text="item.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/dropdown.js') }}"></script>
|
||||
|
|
@ -1,24 +1,103 @@
|
|||
{% include 'plugins/music.mpd/browser.html' %}
|
||||
{% include 'plugins/music.mpd/playlist.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-mpd">
|
||||
<div class="row music-mpd-container">
|
||||
<div class="row panels">
|
||||
<div class="col-no-margin-l-3 s-hidden m-hidden browser">
|
||||
<!-- Browser section -->
|
||||
<div class="col-no-margin-l-3 col-no-margin-m-4 s-hidden browser">
|
||||
<div class="col-s-12 col-m-4 col-l-3 browser-controls">
|
||||
<div class="col-8 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="browserFilter">
|
||||
</div>
|
||||
<div class="col-4 buttons pull-right">
|
||||
<button title="Add to queue">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button title="Remove tracks" v-if="selectionMode.playlist"
|
||||
:disabled="Object.keys(selectedPlaylistItems).length === 0"
|
||||
@click="del">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button :class="{enabled: selectionMode.browser}"
|
||||
:title="selectionMode.browser ? 'End selection' : 'Start selection'"
|
||||
@click="toggleBrowserSelectionMode">
|
||||
<i class="fa fa-check-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<music-mpd-browser-item
|
||||
v-for="item in browserItems"
|
||||
v-if="matchesBrowserFilter(item)"
|
||||
:key="item.type + '-' + item.name"
|
||||
:type="item.type"
|
||||
:name="item.name">
|
||||
</music-mpd-browser-item>
|
||||
</div>
|
||||
|
||||
<div class="col-no-margin-s-12 col-no-margin-m-12 col-no-margin-l-9 playlist">
|
||||
<div class="row track item"
|
||||
v-for="track in playlist">
|
||||
<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="convertTime(track.time)"></div>
|
||||
<!-- Playlist section -->
|
||||
<div class="col-s-12 col-no-margin-m-8 col-no-margin-l-9 playlist">
|
||||
<div class="row empty" v-if="playlist.length === 0">
|
||||
<i class="fa fa-list"></i>
|
||||
</div>
|
||||
|
||||
<div class="col-s-12 col-m-8 col-l-9 playlist-controls" v-else>
|
||||
<div class="col-8 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="playlistFilter">
|
||||
</div>
|
||||
<div class="col-4 buttons pull-right">
|
||||
<button title="Search">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button title="Add item">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button title="Save playlist" v-if="playlist.length">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
<button title="Swap tracks"
|
||||
v-if="selectionMode.playlist && playlist.length > 1"
|
||||
:disabled="Object.keys(selectedPlaylistItems).length !== 2"
|
||||
@click="swap">
|
||||
<i class="fa fa-retweet"></i>
|
||||
</button>
|
||||
<button title="Remove tracks" v-if="selectionMode.playlist"
|
||||
:disabled="Object.keys(selectedPlaylistItems).length === 0"
|
||||
@click="del">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button :class="{enabled: selectionMode.playlist}"
|
||||
:title="selectionMode.playlist ? 'End selection' : 'Start selection'"
|
||||
@click="togglePlaylistSelectionMode">
|
||||
<i class="fa fa-check-square"></i>
|
||||
</button>
|
||||
<button title="Clear playlist" @click="clear">
|
||||
<i class="fa fa-ban"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<dropdown id="music-mpd-playlist-dropdown"
|
||||
v-if="playlist.length > 0"
|
||||
ref="playlistDropdown"
|
||||
:items="playlistDropdownItems">
|
||||
</dropdown>
|
||||
|
||||
<music-mpd-playlist-item
|
||||
v-for="item in playlist"
|
||||
v-if="matchesPlaylistFilter(item)"
|
||||
:key="item.pos"
|
||||
:track="item"
|
||||
:active="track.file && status.state !== 'stop' && item.file === track.file"
|
||||
:selected="item.pos in selectedPlaylistItems"
|
||||
:ref="track.file && status.state !== 'stop' && item.file === track.file ? 'activePlaylistTrack' : undefined"
|
||||
@input="onPlaylistItemClick">
|
||||
</music-mpd-playlist-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -32,17 +111,17 @@
|
|||
|
||||
<div class="col-6 playback-controls">
|
||||
<div class="row">
|
||||
<button @click="previous">
|
||||
<button @click="previous" title="Play previous track">
|
||||
<i class="fa fa-step-backward"></i>
|
||||
</button>
|
||||
<button @click="playPause">
|
||||
<button @click="playPause" :title="status.state == 'play' ? 'Pause playback' : 'Start playback'">
|
||||
<i class="fa fa-pause" v-if="status.state == 'play'"></i>
|
||||
<i class="fa fa-play" v-else></i>
|
||||
</button>
|
||||
<button @click="stop" v-if="status.state != 'stop'">
|
||||
<button @click="stop" v-if="status.state != 'stop'" title="Stop playback">
|
||||
<i class="fa fa-stop"></i>
|
||||
</button>
|
||||
<button @click="next">
|
||||
<button @click="next" title="Play next track">
|
||||
<i class="fa fa-step-forward"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -62,10 +141,10 @@
|
|||
|
||||
<div class="col-3 pull-right">
|
||||
<div class="row">
|
||||
<button @click="random" :class="{enabled: status.random}">
|
||||
<button @click="random" :class="{enabled: status.random}" title="Toggle shuffle">
|
||||
<i class="fa fa-random"></i>
|
||||
</button>
|
||||
<button @click="repeat" :class="{enabled: status.repeat}">
|
||||
<button @click="repeat" :class="{enabled: status.repeat}" title="Toggle repeat">
|
||||
<i class="fa fa-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/music.mpd/playlist.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-mpd-playlist-item">
|
||||
<div class="row item"
|
||||
:class="{selected: selected, active: active}"
|
||||
@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>
|
||||
</script>
|
||||
|
|
@ -248,12 +248,18 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
return self._exec('shuffle')
|
||||
|
||||
@action
|
||||
def add(self, resource, position=None):
|
||||
def add(self, resource, queue=False, position=None):
|
||||
"""
|
||||
Add a resource (track, album, artist, folder etc.) to the current playlist
|
||||
|
||||
:param resource: Resource path or URI
|
||||
:type resource: str
|
||||
|
||||
:param queue: If true then the tracks will be queued after the currently playing track (default: False)
|
||||
:type queue: bool
|
||||
|
||||
:param position: Position where the track(s) will be inserted if queue is false (default: end of the playlist)
|
||||
:type position: int
|
||||
"""
|
||||
|
||||
if isinstance(resource, list):
|
||||
|
@ -272,9 +278,32 @@ class MusicMpdPlugin(MusicPlugin):
|
|||
r = self._parse_resource(resource)
|
||||
|
||||
if position is None:
|
||||
return self._exec('add', r)
|
||||
return self._exec('insert' if queue else 'add', r)
|
||||
return self._exec('addid', r, position)
|
||||
|
||||
@action
|
||||
def delete(self, positions):
|
||||
"""
|
||||
Delete the playlist item(s) in the specified position(s).
|
||||
|
||||
:param positions: Positions of the tracks to be removed
|
||||
:type positions: list[int]
|
||||
"""
|
||||
return self._exec('delete', *positions)
|
||||
|
||||
@action
|
||||
def move(self, from_pos, to_pos):
|
||||
"""
|
||||
Move the playlist item in position <from_pos> to position <to_pos>
|
||||
|
||||
:param from_pos: Track current position
|
||||
:type from_pos: int
|
||||
|
||||
:param to_pos: Track new position
|
||||
:type to_pos: int
|
||||
"""
|
||||
return self._exec('move', from_pos, to_pos)
|
||||
|
||||
@classmethod
|
||||
def _parse_resource(cls, resource):
|
||||
if not resource:
|
||||
|
|
Loading…
Reference in a new issue