- Refactored webpanel style to use flex and dynamic element heights
instead of ugly fixed/absolute positioning. - New media webpanel plugin WIP
This commit is contained in:
parent
076d766745
commit
9d4511577f
48 changed files with 833 additions and 178 deletions
|
@ -58,6 +58,7 @@ def index():
|
|||
scripts=enabled_scripts, styles=enabled_styles,
|
||||
utils=HttpUtils, token=Config.get('token'),
|
||||
websocket_port=get_websocket_port(),
|
||||
plugins=Config.get_plugins(), backends=Config.get_backends(),
|
||||
has_ssl=http_conf.get('ssl_cert') is not None)
|
||||
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
.fade-in {
|
||||
--duration: $fade-in-transition-duration;
|
||||
opacity: 1;
|
||||
animation-name: fadeInOpacity;
|
||||
animation-iteration-count: 1;
|
||||
animation-name: fadeIn;
|
||||
animation-timing-function: ease-in;
|
||||
animation-duration: var(--duration);
|
||||
animation-fill-mode:both;
|
||||
}
|
||||
|
||||
@keyframes fadeInOpacity {
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -16,22 +15,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
--duration: $fade-out-transition-duration;
|
||||
opacity: 1;
|
||||
animation-name: fadeOutOpacity;
|
||||
animation-iteration-count: 1;
|
||||
animation-timing-function: ease-out;
|
||||
.roll-in {
|
||||
--duration: $roll-in-transition-duration;
|
||||
animation-name: rollIn;
|
||||
animation-timing-function: ease-in;
|
||||
animation-duration: var(--duration);
|
||||
animation-fill-mode:both;
|
||||
}
|
||||
|
||||
@keyframes fadeOutOpacity {
|
||||
@keyframes rollIn {
|
||||
0% {
|
||||
opacity: 1;
|
||||
opacity: 0;
|
||||
transform:translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
opacity: 1;
|
||||
transform:translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,15 +23,20 @@ $selected-bg: #c8ffd0 !default;
|
|||
$hover-bg: #def6ea !default;
|
||||
$header-bg: $default_bg !default;
|
||||
|
||||
$nav-height: 4.5rem !default;
|
||||
$nav-bg: #e8e8e8 !default;
|
||||
$nav-fg: $default-link-fg;
|
||||
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
|
||||
$nav-margin: .2rem;
|
||||
$nav-date-time-shadow: .2rem .2rem .2rem #ccc !default;
|
||||
|
||||
//// Animations defaults
|
||||
$transition-duration: .5s !default;
|
||||
$fade-transition-duration: .5s !default;
|
||||
$fade-transition-duration: $transition-duration !default;
|
||||
$roll-transition-duration: $transition-duration !default;
|
||||
$fade-in-transition-duration: $fade-transition-duration !default;
|
||||
$fade-out-transition-duration: $fade-transition-duration !default;
|
||||
$roll-in-transition-duration: $roll-transition-duration !default;
|
||||
$roll-out-transition-duration: $roll-transition-duration !default;
|
||||
|
||||
//// Notifications
|
||||
$notification-bg: rgba(185, 255, 193, 0.9) !default;
|
||||
|
|
|
@ -11,17 +11,42 @@
|
|||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: $default-font-family;
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 4.9rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $default-link-fg;
|
||||
#app {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
|
||||
main {
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
// nav height hardcoded, calc won't support either CSS4 nor SASS vars
|
||||
height: calc(100vh - 4.8rem);
|
||||
|
||||
.plugins-container {
|
||||
height: inherit;
|
||||
|
||||
.plugin-container {
|
||||
overflow: auto;
|
||||
height: inherit;
|
||||
|
||||
.plugin {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $default-link-fg;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
:root {
|
||||
--nav-height: $nav-height;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-bottom: 1.2rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
opacity: 0.95;
|
||||
height: var(--nav-height);
|
||||
margin-bottom: $nav-margin;
|
||||
flex: 0 1 auto;
|
||||
z-index: 2;
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
media
|
|
@ -0,0 +1 @@
|
|||
media
|
|
@ -0,0 +1 @@
|
|||
media
|
|
@ -0,0 +1 @@
|
|||
media
|
|
@ -0,0 +1,87 @@
|
|||
.media-plugin {
|
||||
.controls {
|
||||
@extend .vertical-center;
|
||||
width: 100%;
|
||||
border-top: $default-border-2;
|
||||
box-shadow: $control-panel-shadow;
|
||||
flex: 0 0 $control-panel-height;
|
||||
|
||||
.item-container {
|
||||
@extend .vertical-center;
|
||||
padding-left: 1rem;
|
||||
line-height: 2.6rem;
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
.fa {
|
||||
color: $button-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
.row {
|
||||
@extend .vertical-center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.fa-play, .fa-pause {
|
||||
color: $button-hover-color;
|
||||
font-size: $font-size * 2;
|
||||
margin-top: .3rem;
|
||||
|
||||
&:hover {
|
||||
color: $play-button-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
button {
|
||||
&:not(last-child) {
|
||||
padding: 0 .7rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-container {
|
||||
button {
|
||||
padding: 0 .3rem 0 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.seek-slider {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 75%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.elapsed-time,
|
||||
.total-time {
|
||||
font-size: .7em;
|
||||
color: .7em;
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.total-time {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
.media-plugin {
|
||||
.devices {
|
||||
display: inline-block;
|
||||
|
||||
button {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@import 'common/vars';
|
||||
@import 'common/mixins';
|
||||
@import 'common/layout';
|
||||
@import 'common/animations';
|
||||
|
||||
@import 'webpanel/plugins/media/vars';
|
||||
@import 'webpanel/plugins/media/search';
|
||||
@import 'webpanel/plugins/media/devices';
|
||||
@import 'webpanel/plugins/media/results';
|
||||
@import 'webpanel/plugins/media/controls';
|
||||
|
||||
.media-plugin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: inherit;
|
||||
letter-spacing: .03rem;
|
||||
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
.media-plugin {
|
||||
.results {
|
||||
height: calc(100% - 16rem);
|
||||
overflow: auto;
|
||||
|
||||
.empty {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
letter-spacing: .1rem;
|
||||
color: $empty-results-color;
|
||||
}
|
||||
|
||||
.media-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 !important; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
.media-plugin {
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: $default-bg-3;
|
||||
border-bottom: $default-border-3;
|
||||
|
||||
input[type=text] {
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
[type=submit] {
|
||||
color: $default-hover-fg;
|
||||
font-size: 1.2em;
|
||||
|
||||
&:hover {
|
||||
border: $default-border-2;
|
||||
border-radius: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.types {
|
||||
.type {
|
||||
display: inline-block;
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0 2rem;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
$button-enabled-color: #59df3e;
|
||||
$button-hover-color: $button-enabled-color;
|
||||
$play-button-hover-color: #64ef4a;
|
||||
|
||||
$control-panel-bg: rgba(245,245,245,0.95);
|
||||
$control-panel-height: 10rem !default;
|
||||
$control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
|
||||
$control-time-color: #666;
|
||||
|
||||
$empty-results-color: #506050;
|
||||
|
|
@ -4,11 +4,14 @@
|
|||
@import 'webpanel/plugins/music.mpd/vars';
|
||||
|
||||
.music-mpd-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 3rem;
|
||||
letter-spacing: .03rem;
|
||||
height: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
* > .item {
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
@ -25,12 +28,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
* > .duration {
|
||||
.duration {
|
||||
color: $duration-color;
|
||||
font-size: .7em;
|
||||
}
|
||||
|
||||
* > button {
|
||||
button {
|
||||
border: 0;
|
||||
|
||||
&:disabled {
|
||||
|
@ -55,16 +58,17 @@
|
|||
|
||||
.panels {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.browser, .playlist {
|
||||
overflow: auto;
|
||||
flex-direction: row;
|
||||
flex: 0 1 auto;
|
||||
order: 0;
|
||||
height: calc(100% - 10.1rem);
|
||||
}
|
||||
|
||||
.browser {
|
||||
width: 40%;
|
||||
min-width: 20rem;
|
||||
max-width: 35rem;
|
||||
background: $browser-panel-bg;
|
||||
border-right: $default-border-2;
|
||||
padding: .3rem 1rem 6rem 1rem;
|
||||
font-size: .9em;
|
||||
|
||||
.item {
|
||||
|
@ -103,28 +107,27 @@
|
|||
.browser,
|
||||
.search,
|
||||
.playlist {
|
||||
position: relative; // For the dropdown menu
|
||||
padding: .5rem 1rem 6rem 1rem;
|
||||
.results {
|
||||
position: relative; // For the dropdown menu
|
||||
height: calc(100% - 5.1rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.browser-controls,
|
||||
.results-controls,
|
||||
.playlist-controls {
|
||||
position: fixed;
|
||||
height: 5rem;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
background: $playlist-controls-bg;
|
||||
border-bottom: $playlist-controls-border;
|
||||
margin: -.5rem 0 0 -1rem;
|
||||
padding: .5rem;
|
||||
padding: .5rem 0;
|
||||
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
* > button {
|
||||
@extend %ctrl-button;
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %ctrl-button;
|
||||
padding: 0 .75rem;
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +160,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.playlist {
|
||||
width: 100%;
|
||||
}
|
||||
.playlist-add,
|
||||
.editor {
|
||||
.editor-controls,
|
||||
|
@ -202,32 +208,32 @@
|
|||
|
||||
.controls {
|
||||
@extend .vertical-center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
border-top: $default-border-2;
|
||||
padding: 1rem;
|
||||
background: $control-panel-bg;
|
||||
box-shadow: $control-panel-shadow;
|
||||
z-index: 2;
|
||||
order: 1;
|
||||
flex: 0 0 $control-panel-height;
|
||||
|
||||
.track-container {
|
||||
@extend .vertical-center;
|
||||
padding-left: 1rem;
|
||||
line-height: 2.6rem;
|
||||
|
||||
a {
|
||||
color: initial;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $track-info-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.track-info {
|
||||
.artist {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a {
|
||||
color: initial;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $track-info-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,37 +286,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
* > .seek-slider {
|
||||
.seek-slider {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
* > .volume-slider {
|
||||
.volume-slider {
|
||||
width: 75%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
* > .elapsed-time,
|
||||
* > .total-time {
|
||||
.elapsed-time,
|
||||
.total-time {
|
||||
font-size: .7em;
|
||||
color: .7em;
|
||||
}
|
||||
|
||||
* > .elapsed-time {
|
||||
.elapsed-time {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
* > .total-time {
|
||||
.total-time {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
--width: 90vw;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
padding: 2.7rem;
|
||||
|
||||
.row {
|
||||
padding: .5rem;
|
||||
|
@ -318,7 +324,7 @@
|
|||
|
||||
.footer {
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
margin: 2.5rem 0;
|
||||
border-top: $search-modal-footer-border;
|
||||
|
||||
.left {
|
||||
|
@ -333,24 +339,22 @@
|
|||
}
|
||||
|
||||
.results-controls {
|
||||
position: fixed;
|
||||
padding: 0;
|
||||
margin: -2.45rem auto 0 -2rem;
|
||||
border-bottom: $default-border-2;
|
||||
width: var(--width);
|
||||
height: 3em;
|
||||
height: 4.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 502;
|
||||
}
|
||||
|
||||
.results {
|
||||
padding-top: 2.7rem;
|
||||
}
|
||||
|
||||
form, .results {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.results {
|
||||
height: calc(100% - 4.7rem);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
@ -394,6 +398,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
#music-mpd-search-modal {
|
||||
.header {
|
||||
height: 3.8rem;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#music-mpd-playlist-add {
|
||||
.modal {
|
||||
min-width: 50rem;
|
||||
|
|
|
@ -4,6 +4,7 @@ $play-button-hover-color: #64ef4a;
|
|||
|
||||
$duration-color: #666;
|
||||
|
||||
$control-panel-height: 10rem !default;
|
||||
$control-panel-bg: rgba(245,245,245,0.95);
|
||||
$control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
|
||||
$control-time-color: #666;
|
||||
|
|
|
@ -39,7 +39,6 @@ window.vm = new Vue({
|
|||
};
|
||||
},
|
||||
|
||||
mounted: function() {},
|
||||
created: function() {
|
||||
const self = this;
|
||||
setInterval(() => {
|
||||
|
@ -48,8 +47,5 @@ window.vm = new Vue({
|
|||
|
||||
initEvents();
|
||||
},
|
||||
|
||||
updated: function() {},
|
||||
destroyed: function() {},
|
||||
});
|
||||
|
||||
|
|
|
@ -100,5 +100,13 @@ function openDropdown(element) {
|
|||
element.style.top = (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height)) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
if (parseFloat(element.style.left) < 0) {
|
||||
element.style.left = 0;
|
||||
}
|
||||
|
||||
if (parseFloat(element.style.top) < 0) {
|
||||
element.style.top = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Vue.component('media-mplayer', {
|
||||
template: '#tmpl-media-mplayer',
|
||||
props: ['config'],
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Vue.component('media-mpv', {
|
||||
template: '#tmpl-media-mpv',
|
||||
props: ['config'],
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Vue.component('media-omxplayer', {
|
||||
template: '#tmpl-media-omxplayer',
|
||||
props: ['config'],
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Vue.component('media-vlc', {
|
||||
template: '#tmpl-media-vlc',
|
||||
props: ['config'],
|
||||
});
|
||||
|
14
platypush/backend/http/static/js/plugins/media/controls.js
vendored
Normal file
14
platypush/backend/http/static/js/plugins/media/controls.js
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
Vue.component('media-controls', {
|
||||
template: '#tmpl-media-controls',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
},
|
||||
});
|
||||
|
39
platypush/backend/http/static/js/plugins/media/devices.js
Normal file
39
platypush/backend/http/static/js/plugins/media/devices.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
Vue.component('media-devices', {
|
||||
template: '#tmpl-media-devices',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
showDevicesMenu: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
dropdownItems: function() {
|
||||
var items = [
|
||||
{
|
||||
text: 'Local player',
|
||||
icon: 'desktop',
|
||||
},
|
||||
{
|
||||
text: 'Browser',
|
||||
icon: 'laptop',
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openDevicesMenu: function() {
|
||||
openDropdown(this.$refs.menu);
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
},
|
||||
});
|
||||
|
46
platypush/backend/http/static/js/plugins/media/index.js
Normal file
46
platypush/backend/http/static/js/plugins/media/index.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
Vue.component('media', {
|
||||
template: '#tmpl-media',
|
||||
props: ['config','player'],
|
||||
data: function() {
|
||||
return {
|
||||
bus: new Vue({}),
|
||||
results: [],
|
||||
currentItem: {},
|
||||
loading: {
|
||||
results: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
types: function() {
|
||||
return {
|
||||
file: {},
|
||||
torrent: {},
|
||||
youtube: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refresh: async function() {
|
||||
},
|
||||
|
||||
onResultsLoading: function() {
|
||||
this.loading.results = true;
|
||||
},
|
||||
|
||||
onResultsReady: function(results) {
|
||||
this.loading.results = false;
|
||||
this.results = results;
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.refresh();
|
||||
|
||||
this.bus.$on('results-loading', this.onResultsLoading);
|
||||
this.bus.$on('results-ready', this.onResultsReady);
|
||||
},
|
||||
});
|
||||
|
11
platypush/backend/http/static/js/plugins/media/item.js
Normal file
11
platypush/backend/http/static/js/plugins/media/item.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
Vue.component('media-item', {
|
||||
template: '#tmpl-media-item',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
18
platypush/backend/http/static/js/plugins/media/results.js
Normal file
18
platypush/backend/http/static/js/plugins/media/results.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
Vue.component('media-results', {
|
||||
template: '#tmpl-media-results',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
results: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
},
|
||||
});
|
||||
|
44
platypush/backend/http/static/js/plugins/media/search.js
Normal file
44
platypush/backend/http/static/js/plugins/media/search.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
Vue.component('media-search', {
|
||||
template: '#tmpl-media-search',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
supportedTypes: { type: Object },
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
searching: false,
|
||||
showFilter: false,
|
||||
query: '',
|
||||
|
||||
types: Object.keys(this.supportedTypes).reduce((obj, type) => {
|
||||
obj[type] = true;
|
||||
return obj;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
search: async function(event) {
|
||||
const types = Object.entries(this.types).filter(t => t[1]).map(t => t[0]);
|
||||
var results = [];
|
||||
|
||||
this.searching = true;
|
||||
this.bus.$emit('results-loading');
|
||||
|
||||
try {
|
||||
results = await request('media.search', {
|
||||
query: this.query,
|
||||
types: types,
|
||||
});
|
||||
} finally {
|
||||
this.searching = false;
|
||||
this.bus.$emit('results-ready', results);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
},
|
||||
});
|
||||
|
|
@ -468,8 +468,8 @@ Vue.component('music-mpd', {
|
|||
}
|
||||
};
|
||||
|
||||
adjust(this)();
|
||||
setInterval(adjust(this), 2000);
|
||||
// adjust(this)();
|
||||
// setInterval(adjust(this), 2000);
|
||||
},
|
||||
|
||||
_parseStatus: async function(status) {
|
||||
|
@ -1200,18 +1200,6 @@ Vue.component('music-mpd', {
|
|||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
var parent = self.$refs.activePlaylistTrack[0].$el.parentElement;
|
||||
if (parent.clientHeight + parent.scrollTop < parent.scrollHeight) {
|
||||
if (parent.scrollTop-50 > 0) {
|
||||
parent.scrollTop -= 50;
|
||||
} else {
|
||||
parent.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
}, 750);
|
||||
},
|
||||
|
||||
addToPlaylistPrompt: async function() {
|
||||
|
|
|
@ -27,11 +27,15 @@
|
|||
|
||||
window.config = { ...window.config,
|
||||
websocket_port: {{ websocket_port }},
|
||||
has_ssl: {% print('true' if has_ssl else 'false') %},
|
||||
templates: JSON.parse('{% print(utils.to_json(templates))|safe %}'),
|
||||
scripts: JSON.parse('{% print(utils.to_json(scripts))|safe %}'),
|
||||
has_ssl: {{ 'true' if has_ssl else 'false' }},
|
||||
templates: JSON.parse('{{ utils.to_json(templates)|safe }}'),
|
||||
scripts: JSON.parse('{{ utils.to_json(scripts)|safe }}'),
|
||||
};
|
||||
|
||||
|
||||
var __plugins__ = JSON.parse('{{ utils.to_json(plugins)|safe }}');
|
||||
var __backends__ = JSON.parse('{{ utils.to_json(backends)|safe }}');
|
||||
|
||||
{% if token %}
|
||||
window.config.token = '{{ token }}';
|
||||
{% else %}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% include 'plugins/light.hue/animations.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-light-hue">
|
||||
<div class="row light-hue-container">
|
||||
<div class="row plugin light-hue-container">
|
||||
<div class="groups col-no-margin-3 col-s-12">
|
||||
<div class="title">Rooms</div>
|
||||
<light-hue-group
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/index.js') }}"></script>
|
||||
{% include 'plugins/media/index.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-mplayer">
|
||||
<media :config="config" player="mplayer"></media>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/index.js') }}"></script>
|
||||
{% include 'plugins/media/index.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-mpv">
|
||||
<media :config="config" player="mpv"></media>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/index.js') }}"></script>
|
||||
{% include 'plugins/media/index.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-omxplayer">
|
||||
<media :config="config" player="omxplayer"></media>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/index.js') }}"></script>
|
||||
{% include 'plugins/media/index.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-vlc">
|
||||
<media :config="config" player="vlc"></media>
|
||||
</script>
|
||||
|
44
platypush/backend/http/templates/plugins/media/controls.html
Normal file
44
platypush/backend/http/templates/plugins/media/controls.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/controls.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-controls">
|
||||
<div class="controls">
|
||||
<div class="col-3 item-container">
|
||||
<div class="item-info">
|
||||
<span v-text="item.name"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 playback-controls">
|
||||
<div class="row">
|
||||
<button>
|
||||
<i class="fa fa-play"></i>
|
||||
</button>
|
||||
<button>
|
||||
<i class="fa fa-stop"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<span class="elapsed-time" v-text="'-:--'"></span>
|
||||
<input type="range"
|
||||
class="slider seek-slider"
|
||||
min="0"
|
||||
max="100">
|
||||
<span class="total-time" v-text="'-:--'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3 pull-right">
|
||||
<div class="row volume-container">
|
||||
<button disabled>
|
||||
<i class="fa fa-volume-up"></i>
|
||||
</button>
|
||||
<input type="range"
|
||||
class="slider volume-slider"
|
||||
min="0"
|
||||
max="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
13
platypush/backend/http/templates/plugins/media/devices.html
Normal file
13
platypush/backend/http/templates/plugins/media/devices.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/devices.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-devices">
|
||||
<div class="devices">
|
||||
<button type="button" title="Select target player" @click="openDevicesMenu">
|
||||
<i class="fa fa-podcast"></i>
|
||||
</button>
|
||||
|
||||
<dropdown ref="menu" :items="dropdownItems">
|
||||
</dropdown>
|
||||
</div>
|
||||
</script>
|
||||
|
22
platypush/backend/http/templates/plugins/media/index.html
Normal file
22
platypush/backend/http/templates/plugins/media/index.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% include 'plugins/media/search.html' %}
|
||||
{% include 'plugins/media/controls.html' %}
|
||||
{% include 'plugins/media/results.html' %}
|
||||
{% include 'plugins/media/item.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-media">
|
||||
<div class="plugin media-plugin">
|
||||
<media-search :bus="bus"
|
||||
:supportedTypes="types">
|
||||
</media-search>
|
||||
|
||||
<media-results :bus="bus"
|
||||
:loading="loading.results"
|
||||
:results="results">
|
||||
</media-results>
|
||||
|
||||
<media-controls :bus="bus"
|
||||
:item="currentItem">
|
||||
</media-controls>
|
||||
</div>
|
||||
</script>
|
||||
|
8
platypush/backend/http/templates/plugins/media/item.html
Normal file
8
platypush/backend/http/templates/plugins/media/item.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/item.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-item">
|
||||
<div class="media-item">
|
||||
<span v-text="item.title"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
18
platypush/backend/http/templates/plugins/media/results.html
Normal file
18
platypush/backend/http/templates/plugins/media/results.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/results.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-results">
|
||||
<div class="results">
|
||||
<div class="empty" v-if="loading || !results.length">
|
||||
<div class="loading" v-if="loading">Loading</div>
|
||||
<div class="no-results" v-else-if="!results.length">No results</div>
|
||||
</div>
|
||||
|
||||
<media-item v-for="item in results"
|
||||
:key="item.url"
|
||||
:bus="bus"
|
||||
:item="item"
|
||||
v-else>
|
||||
</media-item>
|
||||
</div>
|
||||
</script>
|
||||
|
39
platypush/backend/http/templates/plugins/media/search.html
Normal file
39
platypush/backend/http/templates/plugins/media/search.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% include 'plugins/media/devices.html' %}
|
||||
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/search.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-search">
|
||||
<div class="search">
|
||||
<form @submit.prevent="search">
|
||||
<div class="row">
|
||||
<div class="col-11 query-container">
|
||||
<button type="button" title="Media type filter" class="filter" @click="showFilter = !showFilter">
|
||||
<i class="fa fa-filter"></i>
|
||||
</button>
|
||||
|
||||
<input type="text" name="query" v-model.lazy.trim="query"
|
||||
:disabled="searching" placeholder="Search query or video URL">
|
||||
|
||||
<button type="submit" :disabled="searching" title="Search">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-1 pull-right">
|
||||
<media-devices></media-devices>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row types fade-in" :class="{hidden: !showFilter}">
|
||||
<div class="type" v-for="config,type in types">
|
||||
<input type="checkbox"
|
||||
name="type"
|
||||
:id="'media-type-' + type"
|
||||
v-model.lazy="types[type]">
|
||||
<label :for="'media-type-' + type" v-text="type"></label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
{% include 'plugins/music.mpd/search.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-mpd">
|
||||
<div class="row music-mpd-container">
|
||||
<div class="row plugin music-mpd-container">
|
||||
<music-mpd-search ref="search" @info="info" :mpd="this">
|
||||
</music-mpd-search>
|
||||
|
||||
|
@ -136,8 +136,8 @@
|
|||
|
||||
<div class="row panels">
|
||||
<!-- Browser section -->
|
||||
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
||||
<div class="col-s-12 col-no-margin-m-3 col-no-margin-l-3 browser-controls">
|
||||
<div class="s-hidden panel browser">
|
||||
<div class="browser-controls">
|
||||
<div class="col-7 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="browserFilter">
|
||||
|
@ -155,48 +155,48 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<dropdown id="music-mpd-browser-dropdown"
|
||||
v-if="browserItems.length > 0"
|
||||
ref="browserDropdown"
|
||||
:items="browserDropdownItems">
|
||||
</dropdown>
|
||||
<div class="results">
|
||||
<dropdown id="music-mpd-browser-dropdown"
|
||||
v-if="browserItems.length > 0"
|
||||
ref="browserDropdown"
|
||||
:items="browserDropdownItems">
|
||||
</dropdown>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<music-mpd-browser-item
|
||||
v-if="browserPath.length > 0"
|
||||
key=".."
|
||||
id="directory:.."
|
||||
type="directory"
|
||||
name=".."
|
||||
:selected="'directory:..' in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
|
||||
<music-mpd-browser-item
|
||||
v-if="browserPath.length > 0"
|
||||
key=".."
|
||||
id="directory:.."
|
||||
type="directory"
|
||||
name=".."
|
||||
:selected="'directory:..' in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
|
||||
<music-mpd-browser-item
|
||||
v-for="item in browserItems"
|
||||
v-if="matchesBrowserFilter(item)"
|
||||
:key="item.id"
|
||||
:id="item.id"
|
||||
:type="item.type"
|
||||
:name="item.name"
|
||||
:file="item.file"
|
||||
:time="item.time"
|
||||
:artist="item.artist"
|
||||
:title="item.title"
|
||||
:date="item.date"
|
||||
:track="item.track"
|
||||
:genre="item.genre"
|
||||
:lastModified="item['last-modified']"
|
||||
:albumUri="item['x-albumuri']"
|
||||
:selected="item.id in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
<music-mpd-browser-item
|
||||
v-for="item in browserItems"
|
||||
v-if="matchesBrowserFilter(item)"
|
||||
:key="item.id"
|
||||
:id="item.id"
|
||||
:type="item.type"
|
||||
:name="item.name"
|
||||
:file="item.file"
|
||||
:time="item.time"
|
||||
:artist="item.artist"
|
||||
:title="item.title"
|
||||
:date="item.date"
|
||||
:track="item.track"
|
||||
:genre="item.genre"
|
||||
:lastModified="item['last-modified']"
|
||||
:albumUri="item['x-albumuri']"
|
||||
:selected="item.id in selectedBrowserItems"
|
||||
@input="onBrowserItemClick">
|
||||
</music-mpd-browser-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist section -->
|
||||
<div class="col-s-12 col-no-margin-m-9 col-no-margin-l-9 panel playlist">
|
||||
<div class="col-s-12 col-m-9 col-l-9 playlist-controls">
|
||||
<div class="panel playlist">
|
||||
<div class="playlist-controls">
|
||||
<div class="col-7 filter-container">
|
||||
<i class="fa fa-filter input-icon"></i>
|
||||
<input type="text" class="with-icon" v-model="playlistFilter">
|
||||
|
@ -232,29 +232,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row empty" v-if="playlist.length === 0">
|
||||
<i class="fa fa-list"></i>
|
||||
<div class="results">
|
||||
<div class="row empty" v-if="playlist.length === 0">
|
||||
<i class="fa fa-list"></i>
|
||||
</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"
|
||||
:move="moveMode.playlist"
|
||||
:ref="track.file && status.state !== 'stop' && item.file === track.file ? 'activePlaylistTrack' : undefined"
|
||||
@input="onPlaylistItemClick">
|
||||
</music-mpd-playlist-item>
|
||||
</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"
|
||||
:move="moveMode.playlist"
|
||||
:ref="track.file && status.state !== 'stop' && item.file === track.file ? 'activePlaylistTrack' : undefined"
|
||||
@input="onPlaylistItemClick">
|
||||
</music-mpd-playlist-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,12 +4,6 @@
|
|||
<modal id="music-mpd-search-modal" title="Search" v-model="visible"
|
||||
:width="showResults ? '90vw' : 'initial'" ref="modal"
|
||||
@open="$refs.form.querySelector('input[type=text]:first-child').focus()">
|
||||
<dropdown id="music-mpd-search-dropdown"
|
||||
v-if="results.length > 0"
|
||||
ref="dropdown"
|
||||
:items="dropdownItems">
|
||||
</dropdown>
|
||||
|
||||
<div class="search">
|
||||
<form ref="form" @submit.prevent="search" :class="{hidden: showResults}">
|
||||
<div class="row">
|
||||
|
@ -67,17 +61,21 @@
|
|||
</div>
|
||||
|
||||
<div class="results" :class="{hidden: !showResults}">
|
||||
<dropdown id="music-mpd-search-dropdown"
|
||||
v-if="results.length > 0"
|
||||
ref="dropdown"
|
||||
:items="dropdownItems">
|
||||
</dropdown>
|
||||
|
||||
<div class="no-results" v-if="results.length === 0">No results</div>
|
||||
<div v-else>
|
||||
<music-mpd-search-item
|
||||
v-for="item in results"
|
||||
v-if="matchesFilter(item)"
|
||||
:key="item.file"
|
||||
:item="item"
|
||||
:selected="item.file in selectedItems"
|
||||
@input="onItemClick">
|
||||
</music-mpd-search-item>
|
||||
</div>
|
||||
<music-mpd-search-item
|
||||
v-for="item in results"
|
||||
v-if="matchesFilter(item)"
|
||||
:key="item.file"
|
||||
:item="item"
|
||||
:selected="item.file in selectedItems"
|
||||
@input="onItemClick">
|
||||
</music-mpd-search-item>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% include 'plugins/music.snapcast/host.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-music-snapcast">
|
||||
<div class="row music-snapcast-container">
|
||||
<div class="row plugin music-snapcast-container">
|
||||
<modal id="music-snapcast-host-info" title="Server info" v-model="modal.host.visible" ref="modalHost">
|
||||
{% include 'plugins/music.snapcast/modals/host.html' %}
|
||||
</modal>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="row tts-container">
|
||||
<div class="row plugin tts-container">
|
||||
<form @submit="talk">
|
||||
<div class="col-8">
|
||||
<input type="text" name="text" placeholder="Text to say">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
|
@ -7,6 +8,8 @@ from platypush.backend.http.app import template_folder
|
|||
|
||||
|
||||
class HttpUtils(object):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@staticmethod
|
||||
def widget_columns_to_html_class(columns):
|
||||
if not isinstance(columns, int):
|
||||
|
@ -85,10 +88,18 @@ class HttpUtils(object):
|
|||
|
||||
@classmethod
|
||||
def to_json(cls, data):
|
||||
def json_parse(x):
|
||||
if type(x) == __import__('datetime').timedelta:
|
||||
return x.days * 24 * 60 * 60 + x.seconds + x.microseconds / 1e6
|
||||
|
||||
# Ignore non-serializable attributes
|
||||
cls.log.warning('Non-serializable attribute type "{}": {}'.format(type(x), x))
|
||||
return None
|
||||
|
||||
if isinstance(data, type({}.keys())):
|
||||
# Convert dict_keys to list before serializing
|
||||
data = list(data)
|
||||
return json.dumps(data)
|
||||
return json.dumps(data, default=json_parse)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
|
|
2
setup.py
2
setup.py
|
@ -25,7 +25,7 @@ class WebBuildCommand(distutils.cmd.Command):
|
|||
input_path = path(os.path.join(base_path,'source'))
|
||||
output_path = path(os.path.join(base_path,'dist'))
|
||||
|
||||
for root, dirs, files in os.walk(input_path):
|
||||
for root, dirs, files in os.walk(input_path, followlinks=True):
|
||||
scss_file = os.path.join(root, 'index.scss')
|
||||
if os.path.isfile(scss_file):
|
||||
css_path = os.path.split(scss_file[len(input_path):])[0][1:] + '.css'
|
||||
|
|
Loading…
Reference in a new issue