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 {
|
.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';
|
||||||
|
|
||||||
|
|
|
@ -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,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ window.vm = new Vue({
|
||||||
|
|
||||||
initEvents();
|
initEvents();
|
||||||
},
|
},
|
||||||
|
|
||||||
updated: function() {},
|
updated: function() {},
|
||||||
destroyed: 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 {
|
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();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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/switch.html' %}
|
||||||
{% include 'elements/range-slider.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/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>
|
||||||
|
|
|
@ -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')
|
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:
|
||||||
|
|
Loading…
Reference in a new issue