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 {
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';

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

View file

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

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

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

View file

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

View file

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

View file

@ -48,6 +48,7 @@ window.vm = new Vue({
initEvents();
},
updated: 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 {
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();
},
});

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

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')
@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: