forked from platypush/platypush
- 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,
|
scripts=enabled_scripts, styles=enabled_styles,
|
||||||
utils=HttpUtils, token=Config.get('token'),
|
utils=HttpUtils, token=Config.get('token'),
|
||||||
websocket_port=get_websocket_port(),
|
websocket_port=get_websocket_port(),
|
||||||
|
plugins=Config.get_plugins(), backends=Config.get_backends(),
|
||||||
has_ssl=http_conf.get('ssl_cert') is not None)
|
has_ssl=http_conf.get('ssl_cert') is not None)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
.fade-in {
|
.fade-in {
|
||||||
--duration: $fade-in-transition-duration;
|
--duration: $fade-in-transition-duration;
|
||||||
opacity: 1;
|
animation-name: fadeIn;
|
||||||
animation-name: fadeInOpacity;
|
|
||||||
animation-iteration-count: 1;
|
|
||||||
animation-timing-function: ease-in;
|
animation-timing-function: ease-in;
|
||||||
animation-duration: var(--duration);
|
animation-duration: var(--duration);
|
||||||
|
animation-fill-mode:both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInOpacity {
|
@keyframes fadeIn {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -16,22 +15,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-out {
|
.roll-in {
|
||||||
--duration: $fade-out-transition-duration;
|
--duration: $roll-in-transition-duration;
|
||||||
opacity: 1;
|
animation-name: rollIn;
|
||||||
animation-name: fadeOutOpacity;
|
animation-timing-function: ease-in;
|
||||||
animation-iteration-count: 1;
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
animation-duration: var(--duration);
|
animation-duration: var(--duration);
|
||||||
|
animation-fill-mode:both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeOutOpacity {
|
@keyframes rollIn {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 0;
|
||||||
|
transform:translateX(-100%);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 1;
|
||||||
display: none;
|
transform:translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,15 +23,20 @@ $selected-bg: #c8ffd0 !default;
|
||||||
$hover-bg: #def6ea !default;
|
$hover-bg: #def6ea !default;
|
||||||
$header-bg: $default_bg !default;
|
$header-bg: $default_bg !default;
|
||||||
|
|
||||||
|
$nav-height: 4.5rem !default;
|
||||||
$nav-bg: #e8e8e8 !default;
|
$nav-bg: #e8e8e8 !default;
|
||||||
$nav-fg: $default-link-fg;
|
$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
|
//// Animations defaults
|
||||||
$transition-duration: .5s !default;
|
$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-in-transition-duration: $fade-transition-duration !default;
|
||||||
$fade-out-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
|
//// Notifications
|
||||||
$notification-bg: rgba(185, 255, 193, 0.9) !default;
|
$notification-bg: rgba(185, 255, 193, 0.9) !default;
|
||||||
|
|
|
@ -11,17 +11,42 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-family: $default-font-family;
|
font-family: $default-font-family;
|
||||||
font-size: $default-font-size;
|
font-size: $default-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
#app {
|
||||||
padding: 4.9rem 0;
|
display: flex;
|
||||||
margin: 0;
|
flex-flow: column;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
a {
|
main {
|
||||||
color: $default-link-fg;
|
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 {
|
nav {
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 10;
|
height: var(--nav-height);
|
||||||
opacity: 0.95;
|
margin-bottom: $nav-margin;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
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';
|
@import 'webpanel/plugins/music.mpd/vars';
|
||||||
|
|
||||||
.music-mpd-container {
|
.music-mpd-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
letter-spacing: .03rem;
|
letter-spacing: .03rem;
|
||||||
|
height: inherit;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
* > .item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -25,12 +28,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .duration {
|
.duration {
|
||||||
color: $duration-color;
|
color: $duration-color;
|
||||||
font-size: .7em;
|
font-size: .7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
@ -55,16 +58,17 @@
|
||||||
|
|
||||||
.panels {
|
.panels {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
flex: 0 1 auto;
|
||||||
.browser, .playlist {
|
order: 0;
|
||||||
overflow: auto;
|
height: calc(100% - 10.1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser {
|
.browser {
|
||||||
|
width: 40%;
|
||||||
|
min-width: 20rem;
|
||||||
|
max-width: 35rem;
|
||||||
background: $browser-panel-bg;
|
background: $browser-panel-bg;
|
||||||
border-right: $default-border-2;
|
|
||||||
padding: .3rem 1rem 6rem 1rem;
|
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
@ -103,28 +107,27 @@
|
||||||
.browser,
|
.browser,
|
||||||
.search,
|
.search,
|
||||||
.playlist {
|
.playlist {
|
||||||
position: relative; // For the dropdown menu
|
.results {
|
||||||
padding: .5rem 1rem 6rem 1rem;
|
position: relative; // For the dropdown menu
|
||||||
|
height: calc(100% - 5.1rem);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.browser-controls,
|
.browser-controls,
|
||||||
.results-controls,
|
.results-controls,
|
||||||
.playlist-controls {
|
.playlist-controls {
|
||||||
position: fixed;
|
width: 100%;
|
||||||
height: 5rem;
|
height: 4rem;
|
||||||
background: $playlist-controls-bg;
|
background: $playlist-controls-bg;
|
||||||
border-bottom: $playlist-controls-border;
|
border-bottom: $playlist-controls-border;
|
||||||
margin: -.5rem 0 0 -1rem;
|
padding: .5rem 0;
|
||||||
padding: .5rem;
|
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > button {
|
|
||||||
@extend %ctrl-button;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
@extend %ctrl-button;
|
||||||
padding: 0 .75rem;
|
padding: 0 .75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,6 +160,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.playlist-add,
|
.playlist-add,
|
||||||
.editor {
|
.editor {
|
||||||
.editor-controls,
|
.editor-controls,
|
||||||
|
@ -202,32 +208,32 @@
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
@extend .vertical-center;
|
@extend .vertical-center;
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 0;
|
|
||||||
border-top: $default-border-2;
|
border-top: $default-border-2;
|
||||||
padding: 1rem;
|
|
||||||
background: $control-panel-bg;
|
background: $control-panel-bg;
|
||||||
box-shadow: $control-panel-shadow;
|
box-shadow: $control-panel-shadow;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
order: 1;
|
||||||
|
flex: 0 0 $control-panel-height;
|
||||||
|
|
||||||
.track-container {
|
.track-container {
|
||||||
@extend .vertical-center;
|
@extend .vertical-center;
|
||||||
|
padding-left: 1rem;
|
||||||
line-height: 2.6rem;
|
line-height: 2.6rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: initial;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $track-info-hover-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.track-info {
|
.track-info {
|
||||||
.artist {
|
.artist {
|
||||||
font-weight: bold;
|
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%;
|
width: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .volume-slider {
|
.volume-slider {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .elapsed-time,
|
.elapsed-time,
|
||||||
* > .total-time {
|
.total-time {
|
||||||
font-size: .7em;
|
font-size: .7em;
|
||||||
color: .7em;
|
color: .7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .elapsed-time {
|
.elapsed-time {
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
* > .total-time {
|
.total-time {
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
--width: 90vw;
|
--width: 90vw;
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
form {
|
form {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
padding: 2.7rem;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
@ -318,7 +324,7 @@
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
margin-top: 1.5rem;
|
margin: 2.5rem 0;
|
||||||
border-top: $search-modal-footer-border;
|
border-top: $search-modal-footer-border;
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
|
@ -333,24 +339,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-controls {
|
.results-controls {
|
||||||
position: fixed;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -2.45rem auto 0 -2rem;
|
|
||||||
border-bottom: $default-border-2;
|
border-bottom: $default-border-2;
|
||||||
width: var(--width);
|
width: var(--width);
|
||||||
height: 3em;
|
height: 4.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 502;
|
z-index: 502;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results {
|
|
||||||
padding-top: 2.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form, .results {
|
form, .results {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
height: calc(100% - 4.7rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
|
@ -394,6 +398,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#music-mpd-search-modal {
|
||||||
|
.header {
|
||||||
|
height: 3.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#music-mpd-playlist-add {
|
#music-mpd-playlist-add {
|
||||||
.modal {
|
.modal {
|
||||||
min-width: 50rem;
|
min-width: 50rem;
|
||||||
|
|
|
@ -4,6 +4,7 @@ $play-button-hover-color: #64ef4a;
|
||||||
|
|
||||||
$duration-color: #666;
|
$duration-color: #666;
|
||||||
|
|
||||||
|
$control-panel-height: 10rem !default;
|
||||||
$control-panel-bg: rgba(245,245,245,0.95);
|
$control-panel-bg: rgba(245,245,245,0.95);
|
||||||
$control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
|
$control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
|
||||||
$control-time-color: #666;
|
$control-time-color: #666;
|
||||||
|
|
|
@ -39,7 +39,6 @@ window.vm = new Vue({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {},
|
|
||||||
created: function() {
|
created: function() {
|
||||||
const self = this;
|
const self = this;
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
@ -48,8 +47,5 @@ window.vm = new Vue({
|
||||||
|
|
||||||
initEvents();
|
initEvents();
|
||||||
},
|
},
|
||||||
|
|
||||||
updated: function() {},
|
|
||||||
destroyed: function() {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -100,5 +100,13 @@ function openDropdown(element) {
|
||||||
element.style.top = (parseFloat(element.style.top) - parseFloat(getComputedStyle(element).height)) + 'px';
|
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)();
|
// adjust(this)();
|
||||||
setInterval(adjust(this), 2000);
|
// setInterval(adjust(this), 2000);
|
||||||
},
|
},
|
||||||
|
|
||||||
_parseStatus: async function(status) {
|
_parseStatus: async function(status) {
|
||||||
|
@ -1200,18 +1200,6 @@ Vue.component('music-mpd', {
|
||||||
} else {
|
} else {
|
||||||
return;
|
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() {
|
addToPlaylistPrompt: async function() {
|
||||||
|
|
|
@ -27,11 +27,15 @@
|
||||||
|
|
||||||
window.config = { ...window.config,
|
window.config = { ...window.config,
|
||||||
websocket_port: {{ websocket_port }},
|
websocket_port: {{ websocket_port }},
|
||||||
has_ssl: {% print('true' if has_ssl else 'false') %},
|
has_ssl: {{ 'true' if has_ssl else 'false' }},
|
||||||
templates: JSON.parse('{% print(utils.to_json(templates))|safe %}'),
|
templates: JSON.parse('{{ utils.to_json(templates)|safe }}'),
|
||||||
scripts: JSON.parse('{% print(utils.to_json(scripts))|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 %}
|
{% if token %}
|
||||||
window.config.token = '{{ token }}';
|
window.config.token = '{{ token }}';
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{% include 'plugins/light.hue/animations.html' %}
|
{% include 'plugins/light.hue/animations.html' %}
|
||||||
|
|
||||||
<script type="text/x-template" id="tmpl-light-hue">
|
<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="groups col-no-margin-3 col-s-12">
|
||||||
<div class="title">Rooms</div>
|
<div class="title">Rooms</div>
|
||||||
<light-hue-group
|
<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' %}
|
{% include 'plugins/music.mpd/search.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 plugin music-mpd-container">
|
||||||
<music-mpd-search ref="search" @info="info" :mpd="this">
|
<music-mpd-search ref="search" @info="info" :mpd="this">
|
||||||
</music-mpd-search>
|
</music-mpd-search>
|
||||||
|
|
||||||
|
@ -136,8 +136,8 @@
|
||||||
|
|
||||||
<div class="row panels">
|
<div class="row panels">
|
||||||
<!-- Browser section -->
|
<!-- Browser section -->
|
||||||
<div class="col-no-margin-l-3 col-no-margin-m-3 s-hidden panel browser">
|
<div class="s-hidden panel browser">
|
||||||
<div class="col-s-12 col-no-margin-m-3 col-no-margin-l-3 browser-controls">
|
<div class="browser-controls">
|
||||||
<div class="col-7 filter-container">
|
<div class="col-7 filter-container">
|
||||||
<i class="fa fa-filter input-icon"></i>
|
<i class="fa fa-filter input-icon"></i>
|
||||||
<input type="text" class="with-icon" v-model="browserFilter">
|
<input type="text" class="with-icon" v-model="browserFilter">
|
||||||
|
@ -155,48 +155,48 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dropdown id="music-mpd-browser-dropdown"
|
<div class="results">
|
||||||
v-if="browserItems.length > 0"
|
<dropdown id="music-mpd-browser-dropdown"
|
||||||
ref="browserDropdown"
|
v-if="browserItems.length > 0"
|
||||||
:items="browserDropdownItems">
|
ref="browserDropdown"
|
||||||
</dropdown>
|
: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
|
<music-mpd-browser-item
|
||||||
v-if="browserPath.length > 0"
|
v-for="item in browserItems"
|
||||||
key=".."
|
v-if="matchesBrowserFilter(item)"
|
||||||
id="directory:.."
|
:key="item.id"
|
||||||
type="directory"
|
:id="item.id"
|
||||||
name=".."
|
:type="item.type"
|
||||||
:selected="'directory:..' in selectedBrowserItems"
|
:name="item.name"
|
||||||
@input="onBrowserItemClick">
|
:file="item.file"
|
||||||
</music-mpd-browser-item>
|
:time="item.time"
|
||||||
|
:artist="item.artist"
|
||||||
<music-mpd-browser-item
|
:title="item.title"
|
||||||
v-for="item in browserItems"
|
:date="item.date"
|
||||||
v-if="matchesBrowserFilter(item)"
|
:track="item.track"
|
||||||
:key="item.id"
|
:genre="item.genre"
|
||||||
:id="item.id"
|
:lastModified="item['last-modified']"
|
||||||
:type="item.type"
|
:albumUri="item['x-albumuri']"
|
||||||
:name="item.name"
|
:selected="item.id in selectedBrowserItems"
|
||||||
:file="item.file"
|
@input="onBrowserItemClick">
|
||||||
:time="item.time"
|
</music-mpd-browser-item>
|
||||||
:artist="item.artist"
|
</div>
|
||||||
: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 -->
|
<!-- Playlist section -->
|
||||||
<div class="col-s-12 col-no-margin-m-9 col-no-margin-l-9 panel playlist">
|
<div class="panel playlist">
|
||||||
<div class="col-s-12 col-m-9 col-l-9 playlist-controls">
|
<div class="playlist-controls">
|
||||||
<div class="col-7 filter-container">
|
<div class="col-7 filter-container">
|
||||||
<i class="fa fa-filter input-icon"></i>
|
<i class="fa fa-filter input-icon"></i>
|
||||||
<input type="text" class="with-icon" v-model="playlistFilter">
|
<input type="text" class="with-icon" v-model="playlistFilter">
|
||||||
|
@ -232,29 +232,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row empty" v-if="playlist.length === 0">
|
<div class="results">
|
||||||
<i class="fa fa-list"></i>
|
<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>
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,6 @@
|
||||||
<modal id="music-mpd-search-modal" title="Search" v-model="visible"
|
<modal id="music-mpd-search-modal" title="Search" v-model="visible"
|
||||||
:width="showResults ? '90vw' : 'initial'" ref="modal"
|
:width="showResults ? '90vw' : 'initial'" ref="modal"
|
||||||
@open="$refs.form.querySelector('input[type=text]:first-child').focus()">
|
@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">
|
<div class="search">
|
||||||
<form ref="form" @submit.prevent="search" :class="{hidden: showResults}">
|
<form ref="form" @submit.prevent="search" :class="{hidden: showResults}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -67,17 +61,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="results" :class="{hidden: !showResults}">
|
<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 class="no-results" v-if="results.length === 0">No results</div>
|
||||||
<div v-else>
|
<music-mpd-search-item
|
||||||
<music-mpd-search-item
|
v-for="item in results"
|
||||||
v-for="item in results"
|
v-if="matchesFilter(item)"
|
||||||
v-if="matchesFilter(item)"
|
:key="item.file"
|
||||||
:key="item.file"
|
:item="item"
|
||||||
:item="item"
|
:selected="item.file in selectedItems"
|
||||||
:selected="item.file in selectedItems"
|
@input="onItemClick">
|
||||||
@input="onItemClick">
|
</music-mpd-search-item>
|
||||||
</music-mpd-search-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% include 'plugins/music.snapcast/host.html' %}
|
{% include 'plugins/music.snapcast/host.html' %}
|
||||||
|
|
||||||
<script type="text/x-template" id="tmpl-music-snapcast">
|
<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">
|
<modal id="music-snapcast-host-info" title="Server info" v-model="modal.host.visible" ref="modalHost">
|
||||||
{% include 'plugins/music.snapcast/modals/host.html' %}
|
{% include 'plugins/music.snapcast/modals/host.html' %}
|
||||||
</modal>
|
</modal>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="row tts-container">
|
<div class="row plugin tts-container">
|
||||||
<form @submit="talk">
|
<form @submit="talk">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<input type="text" name="text" placeholder="Text to say">
|
<input type="text" name="text" placeholder="Text to say">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -7,6 +8,8 @@ from platypush.backend.http.app import template_folder
|
||||||
|
|
||||||
|
|
||||||
class HttpUtils(object):
|
class HttpUtils(object):
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def widget_columns_to_html_class(columns):
|
def widget_columns_to_html_class(columns):
|
||||||
if not isinstance(columns, int):
|
if not isinstance(columns, int):
|
||||||
|
@ -85,10 +88,18 @@ class HttpUtils(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_json(cls, data):
|
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())):
|
if isinstance(data, type({}.keys())):
|
||||||
# Convert dict_keys to list before serializing
|
# Convert dict_keys to list before serializing
|
||||||
data = list(data)
|
data = list(data)
|
||||||
return json.dumps(data)
|
return json.dumps(data, default=json_parse)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data):
|
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'))
|
input_path = path(os.path.join(base_path,'source'))
|
||||||
output_path = path(os.path.join(base_path,'dist'))
|
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')
|
scss_file = os.path.join(root, 'index.scss')
|
||||||
if os.path.isfile(scss_file):
|
if os.path.isfile(scss_file):
|
||||||
css_path = os.path.split(scss_file[len(input_path):])[0][1:] + '.css'
|
css_path = os.path.split(scss_file[len(input_path):])[0][1:] + '.css'
|
||||||
|
|
Loading…
Reference in a new issue