music.mpd vue.js refactoring WIP

This commit is contained in:
Fabio Manganiello 2019-06-02 00:54:49 +02:00
parent 0f3987aaf2
commit e1ddf7bb3b
18 changed files with 718 additions and 75 deletions

View File

@ -1,4 +1,4 @@
//// General purpose classes ///// //// General purpose classes and rules /////
.hidden { .hidden {
display: none !important; display: none !important;
} }
@ -11,10 +11,26 @@
text-align: right !important; 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 ///// //// UI elements definitions /////
@import 'common/elements/button'; @import 'common/elements/button';
@import 'common/elements/switch'; @import 'common/elements/switch';
@import 'common/elements/range-slider'; @import 'common/elements/range-slider';
@import 'common/elements/slider'; @import 'common/elements/slider';
@import 'common/elements/text';
@import 'common/elements/dropdown';

View File

@ -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;
}
}
}

View File

@ -35,20 +35,25 @@
transparent var(--low), var(--range-color) 0, transparent var(--low), var(--range-color) 0,
var(--range-color) var(--high), transparent 0 var(--range-color) var(--high), transparent 0
) no-repeat 0 45% / 100% 40%; ) no-repeat 0 45% / 100% 40%;
--range-color: $slider-thumb-bg; --range-color: $slider-progress-bg;
&::-webkit-slider-runnable-track {
background: var(--track-background);
}
&::-webkit-slider-runnable-track,
&::-moz-range-track { &::-moz-range-track {
background: var(--track-background); background: var(--track-background);
height: 15px;
} }
} }
&[disabled]::-webkit-slider-thumb { &[disabled]::-webkit-slider-thumb,
&[disabled]::-moz-range-thumb {
display: none; display: none;
} }
&::-webkit-progress-value,
&::-moz-range-progress {
@include appearance(none);
background: none;
}
} }
} }

View File

@ -1,3 +1,5 @@
@import 'common/mixins';
.slider { .slider {
@include appearance(none); @include appearance(none);
@include transition(opacity .2s); @include transition(opacity .2s);
@ -7,29 +9,40 @@
background: $slider-bg; background: $slider-bg;
outline: none; outline: none;
&::-webkit-slider-thumb { &::-webkit-slider-thumb,
-webkit-appearance: none; &::-moz-range-thumb {
appearance: none; @include appearance(none);
width: 25px; width: 25px;
height: 25px; height: 25px;
border-radius: 50%; border-radius: 50%;
border: 0;
background: $slider-thumb-bg; background: $slider-thumb-bg;
cursor: pointer; cursor: pointer;
} }
&[disabled]::-webkit-slider-thumb { &[disabled]::-webkit-slider-thumb,
&[disabled]::-moz-range-thumb {
display: none; display: none;
width: 0;
} }
&.disabled { &.disabled {
opacity: 0.3; opacity: 0.3;
} }
&::-moz-range-thumb { &::-moz-range-track {
width: 25px; @include appearance(none);
height: 25px; }
background: $slider-thumb-bg;
cursor: pointer; &::-webkit-progress-value,
&::-moz-range-progress {
background: $slider-progress-bg;
height: 15px;
}
&[disabled]::-webkit-progress-value,
&[disabled]::-moz-range-progress {
background: none;
} }
} }

View File

@ -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;
}
}

View File

@ -14,6 +14,14 @@
transition: $value; transition: $value;
} }
@mixin animation($value) {
-webkit-animation: $value;
-ms-animation: $value;
-o-animation: $value;
-ms-animation: $value;
animation: $value;
}
@mixin box-shadow($value) { @mixin box-shadow($value) {
-webkit-box-shadow: $value; -webkit-box-shadow: $value;
-o-box-shadow: $value; -o-box-shadow: $value;

View File

@ -10,6 +10,7 @@ $default-font-size: 1.5rem !default;
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default; $default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$default-border: 1px solid #e1e4e8 !default; $default-border: 1px solid #e1e4e8 !default;
$default-border-2: 1px solid #dddddd !default; $default-border-2: 1px solid #dddddd !default;
$default-border-3: 1px solid #cccccc !default;
$default-bottom: $default-border !default; $default-bottom: $default-border !default;
$default-link-fg: #5f7869 !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-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; $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-bg: #e4e4e4 !default;
$slider-thumb-bg: rgba(0,215,80,1.0) !default; $slider-thumb-bg: rgba(0,215,80,1.0) !default;
$slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default; $slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default;
$slider-hover-on-hover-bg: #d2d2d2 !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 style
$header-bottom: $default-bottom; $header-bottom: $default-bottom;
//// Dropdown element
$dropdown-bg: rgba(241,243,242,0.9) !default;
$dropdown-shadow: 1px 1px 1px #bbb;

View File

@ -1,21 +1,24 @@
@import 'common/vars'; @import 'common/vars';
@import 'common/mixins';
@import 'common/layout'; @import 'common/layout';
@import 'webpanel/plugins/music.mpd/vars'; @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 { .music-mpd-container {
line-height: 3rem; line-height: 3rem;
letter-spacing: .03rem; letter-spacing: .03rem;
overflow: hidden; overflow: hidden;
* > .item { * > .item {
display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
border-radius: 1rem; border-radius: 1rem;
padding: .5rem; padding: .5rem;
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); } &:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
&:nth-child(even) { background: $default-bg-3; } &:nth-child(even) { background: $default-bg-3; }
&:hover { background: $hover-bg !important; }
&.selected { background: $selected-bg; }
.artist { .artist {
font-size: $artist-font-size; font-size: $artist-font-size;
@ -27,6 +30,24 @@
font-size: $duration-font-size; font-size: $duration-font-size;
} }
* > button {
border: 0;
&:disabled {
background: none;
}
&.enabled {
color: $button-enabled-color;
}
&:hover {
.fa {
opacity: 0.75;
}
}
}
.panels { .panels {
display: flex; display: flex;
@ -43,6 +64,9 @@
.item { .item {
background: none; background: none;
&:nth-of-type(2) {
margin-top: 4.5rem;
}
} }
.fa { .fa {
@ -50,8 +74,72 @@
} }
} }
.browser,
.playlist { .playlist {
position: relative; // For the dropdown menu
padding: .5rem 1rem 6rem 1rem; 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; @extend .vertical-center;
position: fixed; position: fixed;
width: 100%; width: 100%;
min-height: 6rem;
bottom: 0; bottom: 0;
border-top: $default-border-2; border-top: $default-border-2;
padding: 1rem; padding: 1rem;
@ -78,35 +165,31 @@
} }
} }
button {
&:hover {
.fa {
color: $button-hover-color;
}
}
}
.playback-controls { .playback-controls {
.row { .row {
@extend .vertical-center; @extend .vertical-center;
justify-content: center; justify-content: center;
} }
} button {
padding: 0 1.5rem;
* > button { .fa-play, .fa-pause {
border: 0;
padding: 0 1.5rem;
&.enabled {
color: $button-enabled-color;
}
&:hover {
.fa {
color: $button-hover-color; color: $button-hover-color;
} font-size: $font-size * 2;
} margin-top: .3rem;
.fa-play, .fa-pause { &:hover {
color: $button-hover-color; color: $play-button-hover-color;
font-size: $font-size * 2; }
margin-top: .3rem;
&:hover {
color: $play-button-hover-color;
} }
} }
} }
@ -154,9 +237,26 @@
margin-left: 1.5rem; 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; }
}

View File

@ -15,3 +15,11 @@ $control-time-font-size: $font-size * 0.666666;
$browser-panel-bg: rgba(248,250,250,0.95); $browser-panel-bg: rgba(248,250,250,0.95);
$browser-font-size: $font-size * 0.8666; $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;

View File

@ -48,6 +48,7 @@ window.vm = new Vue({
initEvents(); initEvents();
}, },
updated: function() {}, updated: function() {},
destroyed: function() {}, destroyed: function() {},
}); });

View 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;
}

View File

@ -5,17 +5,65 @@ Vue.component('music-mpd', {
return { return {
track: {}, track: {},
status: {}, status: {},
playlist: [],
timer: null, timer: null,
playlist: [],
playlistFilter: '',
browserFilter: '',
browserPath: [], browserPath: [],
browserItems: [], browserItems: [],
selectionMode: {
playlist: false,
browser: false,
},
selectedPlaylistItems: {},
selectedBrowserItems: {},
syncTime: { syncTime: {
timestamp: null, timestamp: null,
elapsed: 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: { methods: {
refresh: async function() { refresh: async function() {
const getStatus = request('music.mpd.status'); const getStatus = request('music.mpd.status');
@ -152,6 +200,22 @@ Vue.component('music-mpd', {
method({ status: status }); 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() { stop: async function() {
await request('music.mpd.stop'); await request('music.mpd.stop');
this.onMusicStop({}); this.onMusicStop({});
@ -199,6 +263,49 @@ Vue.component('music-mpd', {
this.onVolumeChange({status: status}); 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) { onNewPlayingTrack: async function(event) {
var previousTrack = { var previousTrack = {
file: this.track.file, file: this.track.file,
@ -207,18 +314,23 @@ Vue.component('music-mpd', {
}; };
this.status.state = 'play'; this.status.state = 'play';
this.status.elapsed = 0; Vue.set(this.status, 'elapsed', 0);
this.track = {}; this.track = {};
let status = await request('music.mpd.status');
this._parseStatus(status);
this._parseTrack(event.track); this._parseTrack(event.track);
let status = event.status ? event.status : await request('music.mpd.status');
this._parseStatus(status);
this.startTimer(); this.startTimer();
if (this.track.file != previousTrack.file if (this.track.file != previousTrack.file
|| this.track.artist != previousTrack.artist || this.track.artist != previousTrack.artist
|| this.track.title != previousTrack.title) { || this.track.title != previousTrack.title) {
this.showNewTrackNotification(); this.showNewTrackNotification();
const self = this;
setTimeout(function() {
self.scrollToActiveTrack();
}, 100);
} }
}, },
@ -234,6 +346,7 @@ Vue.component('music-mpd', {
onMusicStop: function(event) { onMusicStop: function(event) {
this.status.state = 'stop'; this.status.state = 'stop';
Vue.set(this.status, 'elapsed', 0);
this._parseStatus(event.status); this._parseStatus(event.status);
this._parseTrack(event.track); this._parseTrack(event.track);
this.stopTimer(); this.stopTimer();
@ -251,24 +364,29 @@ Vue.component('music-mpd', {
this._parseStatus(event.status); this._parseStatus(event.status);
this._parseTrack(event.track); this._parseTrack(event.track);
this.syncTime.timestamp = new Date(); Vue.set(this.syncTime, 'timestamp', new Date());
this.syncTime.elapsed = this.status.elapsed; Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
}, },
onSeekChange: function(event) { onSeekChange: function(event) {
if (event.position != null) if (event.position != null)
this.status.elapsed = parseFloat(event.position); Vue.set(this.status, 'elapsed', parseFloat(event.position));
if (event.status) if (event.status)
this._parseStatus(event.status); this._parseStatus(event.status);
if (event.track) if (event.track)
this._parseTrack(event.track); this._parseTrack(event.track);
this.syncTime.timestamp = new Date(); Vue.set(this.syncTime, 'timestamp', new Date());
this.syncTime.elapsed = this.status.elapsed; Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
}, },
onPlaylistChange: function(event) { onPlaylistChange: async function(event) {
console.log(event); if (event.changes) {
this.playlist = event.changes;
} else {
const playlist = await request('music.mpd.playlistinfo');
this._parsePlaylist(playlist);
}
}, },
onVolumeChange: function(event) { onVolumeChange: function(event) {
@ -293,8 +411,8 @@ Vue.component('music-mpd', {
this.stopTimer(); this.stopTimer();
} }
this.syncTime.timestamp = new Date(); Vue.set(this.syncTime, 'timestamp', new Date());
this.syncTime.elapsed = this.status.elapsed; Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
this.timer = setInterval(this.timerFunc, 1000); this.timer = setInterval(this.timerFunc, 1000);
}, },
@ -310,8 +428,63 @@ Vue.component('music-mpd', {
return; return;
} }
this.status.elapsed = this.syncTime.elapsed + Vue.set(this.status, 'elapsed', this.syncTime.elapsed +
((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000); ((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.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent'); registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
}, },
mounted: function() {
this.scrollToActiveTrack();
},
}); });

View File

@ -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,
},
},
});

View File

@ -1,3 +1,4 @@
{% include 'elements/switch.html' %} {% include 'elements/switch.html' %}
{% include 'elements/range-slider.html' %} {% include 'elements/range-slider.html' %}
{% include 'elements/dropdown.html' %}

View 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>

View File

@ -1,24 +1,103 @@
{% include 'plugins/music.mpd/browser.html' %} {% include 'plugins/music.mpd/browser.html' %}
{% include 'plugins/music.mpd/playlist.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">
<div class="row panels"> <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 <music-mpd-browser-item
v-for="item in browserItems" v-for="item in browserItems"
v-if="matchesBrowserFilter(item)"
:key="item.type + '-' + item.name" :key="item.type + '-' + item.name"
:type="item.type" :type="item.type"
:name="item.name"> :name="item.name">
</music-mpd-browser-item> </music-mpd-browser-item>
</div> </div>
<div class="col-no-margin-s-12 col-no-margin-m-12 col-no-margin-l-9 playlist"> <!-- Playlist section -->
<div class="row track item" <div class="col-s-12 col-no-margin-m-8 col-no-margin-l-9 playlist">
v-for="track in playlist"> <div class="row empty" v-if="playlist.length === 0">
<div class="col-5 artist" v-text="track.artist"></div> <i class="fa fa-list"></i>
<div class="col-5 title" v-text="track.title"></div>
<div class="col-2 pull-right duration" v-text="convertTime(track.time)"></div>
</div> </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>
</div> </div>
@ -32,17 +111,17 @@
<div class="col-6 playback-controls"> <div class="col-6 playback-controls">
<div class="row"> <div class="row">
<button @click="previous"> <button @click="previous" title="Play previous track">
<i class="fa fa-step-backward"></i> <i class="fa fa-step-backward"></i>
</button> </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-pause" v-if="status.state == 'play'"></i>
<i class="fa fa-play" v-else></i> <i class="fa fa-play" v-else></i>
</button> </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> <i class="fa fa-stop"></i>
</button> </button>
<button @click="next"> <button @click="next" title="Play next track">
<i class="fa fa-step-forward"></i> <i class="fa fa-step-forward"></i>
</button> </button>
</div> </div>
@ -62,10 +141,10 @@
<div class="col-3 pull-right"> <div class="col-3 pull-right">
<div class="row"> <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> <i class="fa fa-random"></i>
</button> </button>
<button @click="repeat" :class="{enabled: status.repeat}"> <button @click="repeat" :class="{enabled: status.repeat}" title="Toggle repeat">
<i class="fa fa-repeat"></i> <i class="fa fa-repeat"></i>
</button> </button>
</div> </div>

View File

@ -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>

View File

@ -248,12 +248,18 @@ class MusicMpdPlugin(MusicPlugin):
return self._exec('shuffle') return self._exec('shuffle')
@action @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 Add a resource (track, album, artist, folder etc.) to the current playlist
:param resource: Resource path or URI :param resource: Resource path or URI
:type resource: str :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): if isinstance(resource, list):
@ -272,9 +278,32 @@ class MusicMpdPlugin(MusicPlugin):
r = self._parse_resource(resource) r = self._parse_resource(resource)
if position is None: if position is None:
return self._exec('add', r) return self._exec('insert' if queue else 'add', r)
return self._exec('addid', r, position) 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 @classmethod
def _parse_resource(cls, resource): def _parse_resource(cls, resource):
if not resource: if not resource: