Merge branch 'develop'
This commit is contained in:
@ -42,7 +42,14 @@ RUN git clone https://github.com/jaedb/Iris.git /iris \
|
||||
&& echo "mopidy ALL=NOPASSWD: /iris/mopidy_iris/system.sh" >> /etc/sudoers
|
||||
|
||||
# Install additional Python dependencies
|
||||
RUN python3.7 -m pip install --no-cache tox mopidy-mpd mopidy-spotify mopidy-local
|
||||
RUN python3.7 -m pip install --no-cache \
|
||||
tox \
|
||||
mopidy-mpd \
|
||||
mopidy-spotify \
|
||||
mopidy-local \
|
||||
Mopidy-GMusic \
|
||||
# pip not up-to-date for Mopidy-Tidal (https://github.com/tehkillerbee/mopidy-tidal/issues/14)
|
||||
git+https://github.com/tehkillerbee/mopidy-tidal.git@master
|
||||
|
||||
# Start helper script.
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
@ -1 +1 @@
|
||||
3.47.0
|
||||
3.48.0
|
||||
@ -3,7 +3,7 @@ import pathlib
|
||||
|
||||
from mopidy import config, ext
|
||||
|
||||
__version__ = "3.47.0"
|
||||
__version__ = "3.48.0"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -2733,7 +2733,7 @@ main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 60px;
|
||||
bottom: 50px;
|
||||
right: 0;
|
||||
padding-left: 220px;
|
||||
perspective: 1000px;
|
||||
@ -3532,8 +3532,8 @@ select {
|
||||
background: transparent; }
|
||||
@media (max-width: 799px) {
|
||||
.button {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px; }
|
||||
padding-left: 28px;
|
||||
padding-right: 28px; }
|
||||
.button--large {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@ -4588,7 +4588,7 @@ select {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
top: 42%;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
margin-top: -3px; }
|
||||
@ -4664,125 +4664,157 @@ select {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 60px;
|
||||
transform-style: preserve-3d;
|
||||
border-left: 230px solid transparent; }
|
||||
.dark-theme .playback-controls {
|
||||
background: #383734;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
|
||||
border-color: #383734; }
|
||||
.light-theme .playback-controls {
|
||||
background: #FFFFFF;
|
||||
color: #121212;
|
||||
border-color: #FFFFFF;
|
||||
box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.1); }
|
||||
.playback-controls .current-track {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
align-content: center; }
|
||||
.playback-controls__background {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: -1; }
|
||||
.dark-theme .playback-controls {
|
||||
color: #FFFFFF; }
|
||||
.dark-theme .playback-controls__background {
|
||||
background: #383734;
|
||||
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6); }
|
||||
.light-theme .playback-controls {
|
||||
color: #121212; }
|
||||
.light-theme .playback-controls__background {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.1); }
|
||||
.playback-controls .current-track__wrapper {
|
||||
width: 210px;
|
||||
left: -230px;
|
||||
padding-right: 20px;
|
||||
z-index: 1;
|
||||
text-decoration: none; }
|
||||
|
||||
@keyframes slide_left {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
transform: translateX(0);
|
||||
opacity: 1; }
|
||||
100% {
|
||||
transform: translateX(-15px);
|
||||
transform: translateX(-15vw);
|
||||
opacity: 0; } }
|
||||
|
||||
@keyframes slide_right {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
transform: translateX(0);
|
||||
opacity: 1; }
|
||||
100% {
|
||||
transform: translateX(15px);
|
||||
transform: translateX(15vw);
|
||||
opacity: 0; } }
|
||||
.playback-controls .current-track__transition {
|
||||
z-index: 2;
|
||||
background: #383734; }
|
||||
.light-theme .playback-controls .current-track__transition {
|
||||
background: white; }
|
||||
.playback-controls .current-track__transition .text {
|
||||
-webkit-transition: all 0.15s ease-in-out;
|
||||
-moz-transition: all 0.15s ease-in-out;
|
||||
-o-transition: all 0.15s ease-in-out;
|
||||
transition: all 0.15s ease-in-out;
|
||||
opacity: 0; }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls .current-track__transition {
|
||||
animation-duration: 0.25s;
|
||||
animation-timing-function: ease-out; }
|
||||
.playback-controls .current-track__transition--previous {
|
||||
animation-name: slide_right; }
|
||||
.playback-controls .current-track__transition--next {
|
||||
animation-name: slide_left; } }
|
||||
.playback-controls .current-track .thumbnail {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
margin: 8px;
|
||||
margin-right: 12px; }
|
||||
.playback-controls .current-track .thumbnail__image {
|
||||
background-color: rgba(128, 128, 128, 0.5); }
|
||||
.playback-controls .current-track .thumbnail:before {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: block;
|
||||
content: "\F065";
|
||||
font-family: "FontAwesome";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: -9px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
z-index: 3;
|
||||
opacity: 0; }
|
||||
.playback-controls .current-track .thumbnail:after {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
content: "";
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
|
||||
@keyframes slide_from_left {
|
||||
0% {
|
||||
transform: translateX(15vw);
|
||||
opacity: 0; }
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1; } }
|
||||
|
||||
@keyframes slide_from_right {
|
||||
0% {
|
||||
transform: translateX(-15vw);
|
||||
opacity: 0; }
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1; } }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls .current-track__wrapper .current-track__incoming, .playback-controls .current-track__wrapper .current-track__outgoing {
|
||||
animation-duration: .25s;
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(0); }
|
||||
.playback-controls .current-track__wrapper[direction="previous"] .current-track__incoming {
|
||||
animation-name: slide_from_right; }
|
||||
.playback-controls .current-track__wrapper[direction="previous"] .current-track__outgoing {
|
||||
animation-name: slide_right; }
|
||||
.playback-controls .current-track__wrapper[direction="next"] .current-track__incoming {
|
||||
animation-name: slide_from_left; }
|
||||
.playback-controls .current-track__wrapper[direction="next"] .current-track__outgoing {
|
||||
animation-name: slide_left; } }
|
||||
.playback-controls .current-track__wrapper .current-track {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center; }
|
||||
.playback-controls .current-track__wrapper .current-track__outgoing {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
z-index: 2; }
|
||||
.playback-controls .current-track .thumbnail:hover:after, .playback-controls .current-track .thumbnail:hover:before {
|
||||
opacity: 1; }
|
||||
.playback-controls .current-track .text {
|
||||
opacity: 1;
|
||||
line-height: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
padding-top: 1.05rem; }
|
||||
.playback-controls .current-track .text .title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.playback-controls .current-track .text .links-sentence {
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.playback-controls .current-track .text .links-sentence__item {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent; }
|
||||
bottom: 0;
|
||||
left: 0; }
|
||||
.playback-controls .current-track__wrapper .current-track__outgoing .text {
|
||||
-webkit-transition: all 0.15s ease-in-out;
|
||||
-moz-transition: all 0.15s ease-in-out;
|
||||
-o-transition: all 0.15s ease-in-out;
|
||||
transition: all 0.15s ease-in-out;
|
||||
opacity: 0; }
|
||||
.playback-controls .current-track__wrapper .current-track .thumbnail {
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
margin: 8px;
|
||||
margin-right: 12px; }
|
||||
.playback-controls .current-track__wrapper .current-track .thumbnail__image {
|
||||
background-color: rgba(128, 128, 128, 0.5); }
|
||||
.playback-controls .current-track__wrapper .current-track .thumbnail:before {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: block;
|
||||
content: "\F065";
|
||||
font-family: "FontAwesome";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: -9px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
z-index: 3;
|
||||
opacity: 0; }
|
||||
.playback-controls .current-track__wrapper .current-track .thumbnail:after {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
content: "";
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
z-index: 2; }
|
||||
.playback-controls .current-track__wrapper .current-track .thumbnail:hover:after, .playback-controls .current-track__wrapper .current-track .thumbnail:hover:before {
|
||||
opacity: 1; }
|
||||
.playback-controls .current-track__wrapper .current-track .text {
|
||||
opacity: 1;
|
||||
line-height: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: 0; }
|
||||
.playback-controls .current-track__wrapper .current-track .text .title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.playback-controls .current-track__wrapper .current-track .text .links-sentence {
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
.playback-controls .current-track__wrapper .current-track .text .links-sentence__item {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent; }
|
||||
.playback-controls .control {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
@ -4840,155 +4872,138 @@ select {
|
||||
color: #08d58f !important; }
|
||||
.playback-controls .control:hover, .playback-controls .control:focus {
|
||||
opacity: 1; }
|
||||
.playback-controls section {
|
||||
position: absolute;
|
||||
bottom: 10px; }
|
||||
.playback-controls section.playback {
|
||||
bottom: 7px;
|
||||
left: 0;
|
||||
width: 10rem; }
|
||||
.playback-controls section.playback .control {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px; }
|
||||
.playback-controls section.playback .control .icon {
|
||||
font-size: 1.3rem; }
|
||||
.playback-controls section.playback .control.play {
|
||||
margin-bottom: -0.8rem; }
|
||||
.light-theme .playback-controls section.playback .control.play {
|
||||
color: #08d58f; }
|
||||
.playback-controls section.playback .control.play .icon {
|
||||
font-size: 3rem; }
|
||||
.playback-controls section.progress {
|
||||
bottom: 13px;
|
||||
left: 10rem;
|
||||
right: 25rem;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px; }
|
||||
.playback-controls section.progress .slider {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 60px;
|
||||
right: 60px; }
|
||||
.playback-controls section.progress .slider__track__progress {
|
||||
-webkit-transition: all 0.2s linear;
|
||||
-moz-transition: all 0.2s linear;
|
||||
-o-transition: all 0.2s linear;
|
||||
transition: all 0.2s linear; }
|
||||
.playback-controls section.progress .current {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 0;
|
||||
width: 50px;
|
||||
text-align: right; }
|
||||
.playback-controls section.progress .total {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 0;
|
||||
width: 50px; }
|
||||
.playback-controls section.settings {
|
||||
right: 14rem;
|
||||
width: 10rem;
|
||||
text-align: center; }
|
||||
.playback-controls section.volume {
|
||||
right: 2rem;
|
||||
width: 10rem;
|
||||
font-size: 18px; }
|
||||
.playback-controls section.volume .slider__wrapper {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 0;
|
||||
left: 30px; }
|
||||
.playback-controls section.triggers {
|
||||
.playback-controls section.playback {
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.playback-controls section.playback .control {
|
||||
padding: 4px; }
|
||||
.playback-controls section.playback .control .icon {
|
||||
font-size: 1.3rem; }
|
||||
.light-theme .playback-controls section.playback .control.play {
|
||||
color: #08d58f; }
|
||||
.playback-controls section.playback .control.play .icon {
|
||||
font-size: 3rem; }
|
||||
.playback-controls section.progress {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.playback-controls section.progress .slider {
|
||||
flex-grow: 1; }
|
||||
.playback-controls section.progress .slider__track__progress {
|
||||
-webkit-transition: all 0.2s linear;
|
||||
-moz-transition: all 0.2s linear;
|
||||
-o-transition: all 0.2s linear;
|
||||
transition: all 0.2s linear; }
|
||||
.playback-controls section.progress .time {
|
||||
font-size: 11px;
|
||||
width: 40px; }
|
||||
.playback-controls section.progress .time--current {
|
||||
text-align: right;
|
||||
padding-right: 10px; }
|
||||
.playback-controls section.progress .time--total {
|
||||
padding-left: 10px; }
|
||||
.playback-controls section.settings {
|
||||
text-align: center;
|
||||
padding: 0 30px; }
|
||||
.playback-controls section.volume {
|
||||
font-size: 18px;
|
||||
width: 10rem;
|
||||
padding-right: 30px;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.playback-controls section.volume .slider__wrapper {
|
||||
flex-grow: 1; }
|
||||
.playback-controls section.triggers {
|
||||
display: none; }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls--touch-enabled .control.next {
|
||||
display: none; }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls {
|
||||
border-left: none; }
|
||||
.playback-controls--touch-enabled .control.next {
|
||||
display: none; }
|
||||
.playback-controls .current-track {
|
||||
width: auto;
|
||||
bottom: 0.65rem;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2em;
|
||||
margin: auto; }
|
||||
.playback-controls .current-track .title,
|
||||
.playback-controls .current-track .artist {
|
||||
.playback-controls .current-track__wrapper {
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex; }
|
||||
.playback-controls .current-track__wrapper .text {
|
||||
padding: 0;
|
||||
flex-grow: 1; }
|
||||
.playback-controls .current-track__wrapper .text .title,
|
||||
.playback-controls .current-track__wrapper .text .artist {
|
||||
padding-top: 0; }
|
||||
.playback-controls .current-track .thumbnail {
|
||||
display: none; }
|
||||
.playback-controls section.playback {
|
||||
left: 0;
|
||||
width: 6rem; }
|
||||
.playback-controls section.playback .control.play {
|
||||
margin-bottom: -1rem; }
|
||||
.playback-controls section.playback .control.previous {
|
||||
display: none; }
|
||||
.playback-controls section.playback .control.stop {
|
||||
display: none; }
|
||||
.playback-controls section.settings {
|
||||
.playback-controls .current-track__wrapper .thumbnail-wrapper {
|
||||
display: none; }
|
||||
.playback-controls section.volume {
|
||||
.playback-controls section.playback {
|
||||
order: 1;
|
||||
width: 6rem; }
|
||||
.playback-controls section.playback .control.play {
|
||||
padding: 4px; }
|
||||
.playback-controls section.playback .control.previous, .playback-controls section.playback .control.stop {
|
||||
display: none; }
|
||||
.playback-controls section.progress {
|
||||
display: none; }
|
||||
.playback-controls section.triggers {
|
||||
display: block;
|
||||
right: 10px; }
|
||||
.playback-controls section.triggers .icon {
|
||||
font-size: 1.2rem; } }
|
||||
@media only screen and (max-width: 799px) and (-webkit-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min--moz-device-pixel-ratio: 2), only screen and (max-width: 799px) and (-o-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-resolution: 192dpi), only screen and (max-width: 799px) and (min-resolution: 2dppx) {
|
||||
.playback-controls section.triggers {
|
||||
bottom: 4px; }
|
||||
.playback-controls section.triggers .icon {
|
||||
font-size: 1.4rem; } }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls section.triggers .sidebar-toggle.open {
|
||||
opacity: 1 !important; }
|
||||
.playback-controls section.triggers .sidebar-toggle.open:before {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
content: "";
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 99; }
|
||||
.playback-controls--expanded {
|
||||
height: 140px;
|
||||
max-height: 100%; }
|
||||
.playback-controls--expanded section.settings {
|
||||
display: block;
|
||||
right: 0;
|
||||
bottom: 90px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
width: 100px; }
|
||||
.playback-controls--expanded section.volume {
|
||||
width: auto;
|
||||
display: block;
|
||||
right: 100px;
|
||||
left: 10px;
|
||||
bottom: 88px; } }
|
||||
@media only screen and (max-width: 799px) and (-webkit-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min--moz-device-pixel-ratio: 2), only screen and (max-width: 799px) and (-o-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-resolution: 192dpi), only screen and (max-width: 799px) and (min-resolution: 2dppx) {
|
||||
.playback-controls--expanded section.volume {
|
||||
right: 115px; }
|
||||
.playback-controls--expanded section.volume .slider__wrapper {
|
||||
bottom: 9px; } }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls--expanded section.progress {
|
||||
display: block;
|
||||
bottom: 55px;
|
||||
left: -15px;
|
||||
right: -15px; } }
|
||||
.playback-controls section.settings, .playback-controls section.volume, .playback-controls section.progress {
|
||||
display: none; }
|
||||
.playback-controls section.triggers {
|
||||
display: block;
|
||||
width: 6rem;
|
||||
order: 3;
|
||||
text-align: center; }
|
||||
.playback-controls section.triggers .icon {
|
||||
font-size: 1.2rem; } }
|
||||
@media only screen and (max-width: 799px) and (-webkit-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min--moz-device-pixel-ratio: 2), only screen and (max-width: 799px) and (-o-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-resolution: 192dpi), only screen and (max-width: 799px) and (min-resolution: 2dppx) {
|
||||
.playback-controls .control {
|
||||
padding: 14px 8px; } }
|
||||
.playback-controls section.triggers {
|
||||
bottom: 4px; }
|
||||
.playback-controls section.triggers .icon {
|
||||
font-size: 1.4rem; } }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls section.triggers .sidebar-toggle.open {
|
||||
opacity: 1 !important; }
|
||||
.playback-controls section.triggers .sidebar-toggle.open:before {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
content: "";
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 99; }
|
||||
.playback-controls--expanded {
|
||||
max-height: 100%; }
|
||||
.playback-controls--expanded .playback-controls__background {
|
||||
height: 230px; }
|
||||
.playback-controls--expanded section.settings {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
right: 0;
|
||||
bottom: 90px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
width: 100px; }
|
||||
.playback-controls--expanded section.volume {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
display: flex;
|
||||
right: 100px;
|
||||
left: 10px;
|
||||
bottom: 88px; } }
|
||||
@media only screen and (max-width: 799px) and (-webkit-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min--moz-device-pixel-ratio: 2), only screen and (max-width: 799px) and (-o-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-resolution: 192dpi), only screen and (max-width: 799px) and (min-resolution: 2dppx) {
|
||||
.playback-controls--expanded section.volume {
|
||||
right: 115px; }
|
||||
.playback-controls--expanded section.volume .slider__wrapper {
|
||||
bottom: 9px; } }
|
||||
@media (max-width: 799px) {
|
||||
.playback-controls--expanded section.progress {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 60px;
|
||||
left: 0;
|
||||
right: 0; } }
|
||||
|
||||
@media only screen and (max-width: 799px) and (-webkit-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min--moz-device-pixel-ratio: 2), only screen and (max-width: 799px) and (-o-min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-device-pixel-ratio: 2), only screen and (max-width: 799px) and (min-resolution: 192dpi), only screen and (max-width: 799px) and (min-resolution: 2dppx) {
|
||||
.playback-controls .control {
|
||||
padding: 14px 8px; } }
|
||||
|
||||
.output-control {
|
||||
position: relative; }
|
||||
@ -6208,6 +6223,102 @@ main header {
|
||||
.sub-views__option {
|
||||
margin-right: 15px; } }
|
||||
|
||||
.sub-tabs__menu::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block; }
|
||||
|
||||
.sub-tabs__menu .menu__inner {
|
||||
display: flex;
|
||||
margin: 0 0 0 -5px; }
|
||||
|
||||
.sub-tabs__menu .menu-item {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
width: 20%;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
padding: 0 5px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none !important;
|
||||
cursor: pointer; }
|
||||
.sub-tabs__menu .menu-item__inner {
|
||||
position: relative;
|
||||
padding: 30px 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
box-sizing: border-box; }
|
||||
.sub-tabs__menu .menu-item__thumbnail {
|
||||
margin-bottom: 15px;
|
||||
display: inline-block; }
|
||||
.sub-tabs__menu .menu-item__icon {
|
||||
font-size: 40px;
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
display: inline-block; }
|
||||
.sub-tabs__menu .menu-item__icon.icon--svg {
|
||||
opacity: 0.8; }
|
||||
.light-theme .sub-tabs__menu .menu-item__icon.icon--svg {
|
||||
-webkit-filter: invert(1);
|
||||
filter: invert(1); }
|
||||
.sub-tabs__menu .menu-item__title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
font-size: 16px; }
|
||||
.sub-tabs__menu .menu-item:hover .menu-item__inner {
|
||||
background: #222222; }
|
||||
.light-theme .sub-tabs__menu .menu-item:hover .menu-item__inner {
|
||||
background: #ebe8e4; }
|
||||
.sub-tabs__menu .menu-item:active {
|
||||
-moz-transform: scale(0.98);
|
||||
-webkit-transform: scale(0.98);
|
||||
transform: scale(0.98); }
|
||||
.sub-tabs__menu .menu-item--active .menu-item__inner {
|
||||
background: #222222;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-bottom: 40px;
|
||||
margin-bottom: 0; }
|
||||
.light-theme .sub-tabs__menu .menu-item--active .menu-item__inner {
|
||||
background: #ebe8e4; }
|
||||
|
||||
.sub-tabs__content {
|
||||
padding: 20px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #222222; }
|
||||
.sub-tabs__content .field:last-child {
|
||||
padding-bottom: 0; }
|
||||
.light-theme .sub-tabs__content {
|
||||
background: #ebe8e4; }
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.sub-tabs {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px; }
|
||||
.sub-tabs__menu {
|
||||
overflow-x: auto; }
|
||||
.sub-tabs__menu::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px; }
|
||||
.sub-tabs__menu__inner {
|
||||
padding: 0 20px; }
|
||||
.sub-tabs__menu .menu-item {
|
||||
width: 140px; }
|
||||
.sub-tabs__menu .menu-item__inner {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px; }
|
||||
.sub-tabs__menu .menu-item--active .menu-item__inner {
|
||||
padding-bottom: 20px; }
|
||||
.sub-tabs__content {
|
||||
margin-top: 0; } }
|
||||
|
||||
.debug-info {
|
||||
font-size: 10px;
|
||||
position: fixed;
|
||||
@ -6287,101 +6398,6 @@ main header {
|
||||
white-space: wrap;
|
||||
max-width: 200px; }
|
||||
|
||||
.services .menu::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block; }
|
||||
|
||||
.services .menu__inner {
|
||||
display: flex;
|
||||
margin: 0 0 0 -5px; }
|
||||
|
||||
.services .menu .menu-item {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
width: 20%;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
padding: 0 5px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none !important; }
|
||||
.services .menu .menu-item__inner {
|
||||
position: relative;
|
||||
padding: 30px 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
box-sizing: border-box; }
|
||||
.services .menu .menu-item__thumbnail {
|
||||
margin-bottom: 15px;
|
||||
display: inline-block; }
|
||||
.services .menu .menu-item__icon {
|
||||
font-size: 40px;
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
display: inline-block; }
|
||||
.services .menu .menu-item__icon.icon--svg {
|
||||
opacity: 0.8; }
|
||||
.light-theme .services .menu .menu-item__icon.icon--svg {
|
||||
-webkit-filter: invert(1);
|
||||
filter: invert(1); }
|
||||
.services .menu .menu-item__title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
font-size: 16px; }
|
||||
.services .menu .menu-item:hover .menu-item__inner {
|
||||
background: #222222; }
|
||||
.light-theme .services .menu .menu-item:hover .menu-item__inner {
|
||||
background: #ebe8e4; }
|
||||
.services .menu .menu-item:active {
|
||||
-moz-transform: scale(0.98);
|
||||
-webkit-transform: scale(0.98);
|
||||
transform: scale(0.98); }
|
||||
.services .menu .menu-item--active .menu-item__inner {
|
||||
background: #222222;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-bottom: 40px;
|
||||
margin-bottom: 0; }
|
||||
.light-theme .services .menu .menu-item--active .menu-item__inner {
|
||||
background: #ebe8e4; }
|
||||
|
||||
.services .service {
|
||||
padding: 20px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #222222; }
|
||||
.services .service .field:last-child {
|
||||
padding-bottom: 0; }
|
||||
.light-theme .services .service {
|
||||
background: #ebe8e4; }
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.services {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px; }
|
||||
.services .menu {
|
||||
overflow-x: auto; }
|
||||
.services .menu::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px; }
|
||||
.services .menu__inner {
|
||||
padding: 0 20px; }
|
||||
.services .menu .menu-item {
|
||||
width: 140px; }
|
||||
.services .menu .menu-item__inner {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px; }
|
||||
.services .menu .menu-item--active .menu-item__inner {
|
||||
padding-bottom: 20px; }
|
||||
.services .service {
|
||||
margin-top: 0; } }
|
||||
|
||||
.snapcast button {
|
||||
margin-bottom: 0px; }
|
||||
|
||||
@ -7527,14 +7543,21 @@ main .track-view {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 99; }
|
||||
z-index: 99;
|
||||
display: flex; }
|
||||
.modal .controls .control {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: block;
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
2
mopidy_iris/static/app.min.css
vendored
2
mopidy_iris/static/app.min.css
vendored
File diff suppressed because one or more lines are too long
46
mopidy_iris/static/app.min.js
vendored
46
mopidy_iris/static/app.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
mopidy_iris/static/assets/backgrounds/browse-tidal.jpg
Normal file
BIN
mopidy_iris/static/assets/backgrounds/browse-tidal.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@ -100,8 +100,8 @@
|
||||
|
||||
// Release details
|
||||
// These are automatically injected to built HTML
|
||||
var build = "1587329101";
|
||||
var version = "3.47.0";
|
||||
var build = "1588218286";
|
||||
var version = "3.48.0";
|
||||
|
||||
// Construct the script tag
|
||||
var js = document.createElement("script");
|
||||
|
||||
6510
package-lock.json
generated
6510
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file → Executable file
43
package.json
Normal file → Executable file
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mopidy-iris",
|
||||
"version": "3.47.0",
|
||||
"version": "3.48.0",
|
||||
"description": "Mopidy HTTP interface",
|
||||
"repository": "https://github.com/jaedb/iris",
|
||||
"author": "James Barnsley <james@barnsley.nz>",
|
||||
@ -10,25 +10,26 @@
|
||||
},
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"fast-average-color": "^6.0.2",
|
||||
"history": "^4.10.1",
|
||||
"jquery": "^3.4.1",
|
||||
"logrocket": "^1.0.3",
|
||||
"jquery": "^3.5.0",
|
||||
"logrocket": "^1.0.7",
|
||||
"mopidy": "^0.5",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-fontawesome": "^1.7.1",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-howler": "^3.7.4",
|
||||
"react-input-range": "1.3.0",
|
||||
"react-redux": "^6",
|
||||
"react-redux": "^7",
|
||||
"react-router": "^5",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-sortablejs": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-react": "^7.7.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"babel-core": "^6",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-jest": "^24.9.0",
|
||||
@ -39,30 +40,30 @@
|
||||
"babel-preset-stage-2": "^6",
|
||||
"copy-dir": "^0.3.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-airbnb": "^18.0.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"expose-loader": "0.7.3",
|
||||
"file-loader": "^3",
|
||||
"fs-copy-file": "2.1.2",
|
||||
"jest": "^24.9.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
"redux": "^4.0.4",
|
||||
"node-sass": "^4.14.0",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"sortablejs": "^1.10.1",
|
||||
"sortablejs": "^1.10.2",
|
||||
"style-loader": "^0.23",
|
||||
"url-loader": "^1",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack-strip": "0.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = Mopidy-Iris
|
||||
version = 3.47.0
|
||||
version = 3.48.0
|
||||
url = https://github.com/jaedb/iris
|
||||
author = James Barnsley
|
||||
author_email = james@barnsley.nz
|
||||
|
||||
BIN
src/assets/backgrounds/browse-tidal.jpg
Executable file
BIN
src/assets/backgrounds/browse-tidal.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@ -160,7 +160,7 @@ class ContextMenu extends React.Component {
|
||||
}
|
||||
|
||||
handleTouchStart = (e) => {
|
||||
const { uiActions: { hideContextMenu } } = this.props;
|
||||
const { menu, uiActions: { hideContextMenu } } = this.props;
|
||||
|
||||
if (menu && $(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0) {
|
||||
hideContextMenu();
|
||||
|
||||
@ -7,7 +7,7 @@ export default class TextField extends React.Component {
|
||||
|
||||
this.state = {
|
||||
in_focus: false,
|
||||
value: (this.props.value ? this.props.value : ''),
|
||||
value: props.value || '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,31 +20,40 @@ export default class TextField extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
handleChange = (e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
handleFocus = () => {
|
||||
this.setState({ in_focus: true });
|
||||
}
|
||||
|
||||
handleBlur(e) {
|
||||
handleBlur = () => {
|
||||
const { value, onChange } = this.props;
|
||||
const { value: stateValue } = this.state;
|
||||
this.setState({ in_focus: false });
|
||||
if (this.state.value !== this.props.value) {
|
||||
this.props.onChange(this.state.value);
|
||||
if (stateValue !== value) {
|
||||
onChange(stateValue);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render = () => {
|
||||
const {
|
||||
className,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
} = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
return (
|
||||
<input
|
||||
className={this.props.className ? this.props.className : ''}
|
||||
type={this.props.type ? this.props.type : 'text'}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : null}
|
||||
className={className}
|
||||
type={type}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,49 +66,49 @@ class Hotkeys extends React.Component {
|
||||
prevent = true;
|
||||
break;
|
||||
|
||||
case 'escape':
|
||||
if (dragging) {
|
||||
uiActions.dragEnd();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
|
||||
} else if (modal) {
|
||||
window.history.back();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
|
||||
case 's': // Stop
|
||||
mopidyActions.stop();
|
||||
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Stop' });
|
||||
break;
|
||||
|
||||
case 'b': // Seek backwards 30 sec
|
||||
var new_position = play_time_position - 30000;
|
||||
if (new_position < 0) {
|
||||
new_position = 0;
|
||||
}
|
||||
mopidyActions.setTimePosition(new_position);
|
||||
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
|
||||
break;
|
||||
|
||||
case 's':
|
||||
history.push('/search');
|
||||
case 'f': // Seek forwards 30 sec
|
||||
mopidyActions.setTimePosition(play_time_position + 30000);
|
||||
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
history.push('/queue');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
history.push('/kiosk-mode');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
|
||||
break;
|
||||
|
||||
case ',':
|
||||
window.history.back();
|
||||
case '<': // Previous track
|
||||
mopidyActions.previous();
|
||||
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Back' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
|
||||
break;
|
||||
|
||||
case '.':
|
||||
window.history.forward();
|
||||
case '>': // Next track
|
||||
mopidyActions.next();
|
||||
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Forward' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
|
||||
break;
|
||||
|
||||
case '=':
|
||||
case '+': // Volume up
|
||||
if (volume !== 'false') {
|
||||
volume += 5;
|
||||
if (volume > 100) {
|
||||
@ -124,7 +124,7 @@ class Hotkeys extends React.Component {
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
|
||||
break;
|
||||
|
||||
case '-':
|
||||
case '-': // Volume down
|
||||
if (volume !== 'false') {
|
||||
volume -= 5;
|
||||
if (volume < 0) {
|
||||
@ -140,7 +140,7 @@ class Hotkeys extends React.Component {
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
|
||||
break;
|
||||
|
||||
case '0':
|
||||
case '0': // Mute
|
||||
if (mute) {
|
||||
mopidyActions.setMute(false);
|
||||
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
|
||||
@ -153,36 +153,34 @@ class Hotkeys extends React.Component {
|
||||
prevent = true;
|
||||
break;
|
||||
|
||||
case ';':
|
||||
var new_position = play_time_position - 30000;
|
||||
if (new_position < 0) {
|
||||
new_position = 0;
|
||||
case 'escape': // Cancel current action/context
|
||||
if (dragging) {
|
||||
uiActions.dragEnd();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
|
||||
} else if (modal) {
|
||||
window.history.back();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
|
||||
}
|
||||
mopidyActions.setTimePosition(new_position);
|
||||
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
|
||||
break;
|
||||
|
||||
case "'":
|
||||
mopidyActions.setTimePosition(play_time_position + 30000);
|
||||
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
|
||||
case '1': // Navigation: Queue
|
||||
history.push('/queue');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
|
||||
break;
|
||||
|
||||
case '[':
|
||||
mopidyActions.previous();
|
||||
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
|
||||
case '2': // Navigation: Search
|
||||
history.push('/search');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
|
||||
break;
|
||||
|
||||
case ']':
|
||||
mopidyActions.next();
|
||||
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
|
||||
case '3': // Navigation: Kiosk mode
|
||||
history.push('/kiosk-mode');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@ -94,7 +94,6 @@ class PlaybackControls extends React.Component {
|
||||
|
||||
setTransition(direction) {
|
||||
this.setState({
|
||||
current_track: null,
|
||||
transition_track: this.state.current_track,
|
||||
transition_direction: direction,
|
||||
});
|
||||
@ -104,7 +103,6 @@ class PlaybackControls extends React.Component {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
transition_track: null,
|
||||
transition_direction: null,
|
||||
});
|
||||
},
|
||||
250);
|
||||
@ -173,53 +171,81 @@ class PlaybackControls extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { next_track, touch_enabled, time_position } = this.props;
|
||||
const { current_track, expanded } = this.state;
|
||||
|
||||
let images = false;
|
||||
if (current_track && current_track.images) {
|
||||
images = current_track.images;
|
||||
}
|
||||
const {
|
||||
next_track,
|
||||
touch_enabled,
|
||||
time_position,
|
||||
} = this.props;
|
||||
const {
|
||||
current_track,
|
||||
expanded,
|
||||
transition_track,
|
||||
transition_direction,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={`playback-controls${expanded ? ' playback-controls--expanded' : ''}${touch_enabled ? ' playback-controls--touch-enabled' : ''}`}>
|
||||
|
||||
<div className="playback-controls__background" />
|
||||
|
||||
<Stream />
|
||||
|
||||
{next_track && next_track.images ? <Thumbnail className="hide" size="large" images={next_track.images} /> : null}
|
||||
|
||||
{this.state.transition_track && this.state.transition_direction ? (
|
||||
<div
|
||||
className={`current-track current-track__transition current-track__transition--${this.state.transition_direction}`}
|
||||
>
|
||||
<div className="text">
|
||||
<div className="title">
|
||||
{this.state.transition_track.name}
|
||||
</div>
|
||||
<div className="artist">
|
||||
<LinksSentence items={this.state.transition_track.artists} nolinks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={this.state.transition_track && this.state.transition_direction ? 'current-track current-track--transitioning' : 'current-track'}
|
||||
onTouchStart={this.handleTouchStart}
|
||||
onTouchEnd={this.handleTouchEnd}
|
||||
tabIndex="-1"
|
||||
className="current-track__wrapper"
|
||||
transition={transition_track}
|
||||
direction={transition_direction}
|
||||
>
|
||||
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
|
||||
<Thumbnail size="small" images={images} type="track" />
|
||||
</Link>
|
||||
<div className="text">
|
||||
<div className="title">
|
||||
{current_track ? current_track.name : <span>-</span>}
|
||||
{transition_track && transition_direction && (
|
||||
<div className={`current-track current-track__outgoing`}>
|
||||
<div className="text">
|
||||
<div className="title">
|
||||
{transition_track.name}
|
||||
</div>
|
||||
<div className="artist">
|
||||
<LinksSentence items={transition_track.artists} nolinks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="artist">
|
||||
{current_track ? <LinksSentence items={current_track.artists} /> : <LinksSentence />}
|
||||
)}
|
||||
|
||||
{current_track && (!transition_track || transition_track.tlid !== current_track.tlid) ? (
|
||||
<div
|
||||
className="current-track current-track__incoming"
|
||||
onTouchStart={this.handleTouchStart}
|
||||
onTouchEnd={this.handleTouchEnd}
|
||||
tabIndex="-1"
|
||||
key={current_track.tlid}
|
||||
>
|
||||
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
|
||||
<Thumbnail size="small" images={current_track.images} type="track" />
|
||||
</Link>
|
||||
<div className="text">
|
||||
<div className="title">
|
||||
{current_track ? current_track.name : <span>-</span>}
|
||||
</div>
|
||||
<div className="artist">
|
||||
{current_track ? <LinksSentence items={current_track.artists} /> : <LinksSentence />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="current-track"
|
||||
onTouchStart={this.handleTouchStart}
|
||||
onTouchEnd={this.handleTouchEnd}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
|
||||
<Thumbnail size="small" type="track" />
|
||||
</Link>
|
||||
<div className="text">
|
||||
<div className="title"> </div>
|
||||
<div className="artist"> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className="playback">
|
||||
@ -233,9 +259,13 @@ class PlaybackControls extends React.Component {
|
||||
</section>
|
||||
|
||||
<section className="progress">
|
||||
<div className="time time--current">
|
||||
{time_position ? <Dater type="length" data={time_position} /> : '-'}
|
||||
</div>
|
||||
<ProgressSlider />
|
||||
<span className="current">{ time_position ? <Dater type="length" data={time_position} /> : '-' }</span>
|
||||
<span className="total">{ current_track ? <Dater type="length" data={current_track.duration} /> : '-' }</span>
|
||||
<div className="time time--total">
|
||||
{current_track ? <Dater type="length" data={current_track.duration} /> : '-'}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings">
|
||||
|
||||
183
src/js/components/Servers.js
Executable file
183
src/js/components/Servers.js
Executable file
@ -0,0 +1,183 @@
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useStore, useDispatch } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Link from './Link';
|
||||
import Icon from './Icon';
|
||||
import URILink from './URILink';
|
||||
import TextField from './Fields/TextField';
|
||||
import { indexToArray } from '../util/arrays';
|
||||
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
import { iconFromKeyword } from '../util/helpers';
|
||||
|
||||
const Servers = ({
|
||||
match: { params: { id: serverId } },
|
||||
history,
|
||||
}) => {
|
||||
const store = useStore();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
mopidy: {
|
||||
servers,
|
||||
current_server,
|
||||
ssl,
|
||||
connected: mopidyConnected,
|
||||
connecting: mopidyConnecting,
|
||||
},
|
||||
pusher: {
|
||||
connected: pusherConnected,
|
||||
connecting: pusherConnecting,
|
||||
},
|
||||
} = store.getState();
|
||||
|
||||
const addServer = () => {
|
||||
const action = mopidyActions.addServer();
|
||||
dispatch(action);
|
||||
history.push(`/settings/servers/${action.server.id}`);
|
||||
};
|
||||
|
||||
const renderMenu = () => (
|
||||
<div className="sub-tabs__menu" id="servers-menu">
|
||||
<div className="menu__inner">
|
||||
{indexToArray(servers).map((server) => {
|
||||
let status = <span className="status mid_grey-text">Inactive</span>;
|
||||
if (server.id === current_server) {
|
||||
if (mopidyConnecting || pusherConnecting) {
|
||||
status = <span className="status mid_grey-text">Connecting</span>
|
||||
} else if (!mopidyConnected || !pusherConnected) {
|
||||
status = <span className="status red-text">Disconnected</span>
|
||||
} else if (mopidyConnected && pusherConnected) {
|
||||
status = <span className="status green-text">Connected</span>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
history={history}
|
||||
className="menu-item"
|
||||
activeClassName="menu-item--active"
|
||||
to={`/settings/servers/${server.id}`}
|
||||
scrollTo="#servers-menu"
|
||||
key={server.id}
|
||||
>
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name={iconFromKeyword(server.name) || 'dns'} />
|
||||
<div className="menu-item__title">
|
||||
{server.name}
|
||||
</div>
|
||||
{status}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<span
|
||||
className="menu-item menu-item--add"
|
||||
onClick={addServer}
|
||||
>
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name="add" />
|
||||
<div className="menu-item__title">
|
||||
Add
|
||||
</div>
|
||||
<span className="status mid_grey-text">New server</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderServer = () => {
|
||||
if (!serverId) return null;
|
||||
const server = servers[serverId];
|
||||
if (!server) return null;
|
||||
|
||||
const remove = () => {
|
||||
dispatch(mopidyActions.removeServer(server.id));
|
||||
};
|
||||
|
||||
const setAsCurrent = () => {
|
||||
dispatch(mopidyActions.setCurrentServer(server));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sub-tabs__content">
|
||||
<label className="field">
|
||||
<div className="name">Name</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.name}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, name: value }))}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Host</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={server.host}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, host: value }))}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Port</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.port}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, port: value }))}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div className="field checkbox">
|
||||
<div className="name">Encryption</div>
|
||||
<div className="input">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="ssl"
|
||||
value={server.ssl}
|
||||
checked={server.ssl}
|
||||
onChange={() => dispatch(mopidyActions.updateServer({ id: server.id, ssl: !server.ssl }))}
|
||||
/>
|
||||
<span className="label tooltip">
|
||||
Use SSL
|
||||
<span className="tooltip__content">Requires a SSL proxy</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="button button--primary"
|
||||
onClick={setAsCurrent}
|
||||
>
|
||||
Switch to this server
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button button--destructive"
|
||||
disabled={server.id === current_server}
|
||||
onClick={remove}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sub-tabs sub-tabs--servers">
|
||||
{renderMenu()}
|
||||
{renderServer()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Servers;
|
||||
@ -7,6 +7,7 @@ import Link from './Link';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import Icon from './Icon';
|
||||
import URILink from './URILink';
|
||||
import TextField from './Fields/TextField';
|
||||
import SpotifyAuthenticationFrame from './Fields/SpotifyAuthenticationFrame';
|
||||
import LastfmAuthenticationFrame from './Fields/LastfmAuthenticationFrame';
|
||||
import GeniusAuthenticationFrame from './Fields/GeniusAuthenticationFrame';
|
||||
@ -109,11 +110,8 @@ class Services extends React.Component {
|
||||
<label className="field">
|
||||
<div className="name">Country</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => this.setState({ country: e.target.value })}
|
||||
onFocus={(e) => this.setState({ input_in_focus: 'country' })}
|
||||
onBlur={(e) => this.props.spotifyActions.set({ country: e.target.value })}
|
||||
<TextField
|
||||
onChange={value => this.props.spotifyActions.set({ country: value })}
|
||||
value={this.state.country}
|
||||
/>
|
||||
<div className="description">
|
||||
@ -322,9 +320,9 @@ class Services extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="menu" id="services-menu">
|
||||
<div className="sub-tabs__menu menu" id="services-menu">
|
||||
<div className="menu__inner">
|
||||
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/spotify" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/services/spotify" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{spotify_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -333,7 +331,7 @@ class Services extends React.Component {
|
||||
{this.props.spotify.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/lastfm" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/services/lastfm" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{lastfm_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -342,7 +340,7 @@ class Services extends React.Component {
|
||||
{this.props.lastfm.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/genius" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/services/genius" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{genius_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -351,7 +349,7 @@ class Services extends React.Component {
|
||||
{this.props.genius.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Unauthorized</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/snapcast" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/services/snapcast" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name="devices" />
|
||||
<div className="menu-item__title">
|
||||
@ -368,7 +366,7 @@ class Services extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/icecast" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/services/icecast" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name="wifi_tethering" />
|
||||
<div className="menu-item__title">
|
||||
@ -386,15 +384,15 @@ class Services extends React.Component {
|
||||
const { match } = this.props;
|
||||
switch (match.params.service) {
|
||||
case 'spotify':
|
||||
return <div className="service">{this.renderSpotify()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderSpotify()}</div>;
|
||||
case 'lastfm':
|
||||
return <div className="service">{this.renderLastfm()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderLastfm()}</div>;
|
||||
case 'genius':
|
||||
return <div className="service">{this.renderGenius()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderGenius()}</div>;
|
||||
case 'icecast':
|
||||
return <div className="service">{this.renderIcecast()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderIcecast()}</div>;
|
||||
case 'snapcast':
|
||||
return <div className="service">{<Snapcast match={this.props.match} />}</div>;
|
||||
return <div className="sub-tabs__content">{<Snapcast match={this.props.match} />}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -402,7 +400,7 @@ class Services extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="services">
|
||||
<div className="sub-tabs sub-tabs--services">
|
||||
{this.renderMenu()}
|
||||
{this.renderService()}
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { collate } from '../util/format';
|
||||
import { sortItems } from '../util/arrays';
|
||||
import { iconFromKeyword } from '../util/helpers';
|
||||
import VolumeControl from './Fields/VolumeControl';
|
||||
import MuteControl from './Fields/MuteControl';
|
||||
import TextField from './Fields/TextField';
|
||||
@ -106,42 +107,17 @@ const SnapcastGroups = (props) => {
|
||||
!group.clients.filter((client) => client.connected).length
|
||||
)
|
||||
);
|
||||
const icon = () => {
|
||||
const iconWords = [
|
||||
{ icon: 'business', words: ['office', 'work'] },
|
||||
{ icon: 'king_bed', words: ['bed'] },
|
||||
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
|
||||
{ icon: 'directions_car', words: ['garage', 'laundry'] },
|
||||
{ icon: 'fitness_center', words: ['gym'] },
|
||||
{ icon: 'kitchen', words: ['kitchen'] },
|
||||
{ icon: 'deck', words: ['deck', 'outside'] },
|
||||
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
|
||||
{ icon: 'laptop', words: ['laptop'] },
|
||||
{ icon: 'bug_report', words: ['test', 'debug'] },
|
||||
{ icon: 'child_care', words: ['kids', 'baby'] },
|
||||
{ icon: 'smartphone', words: ['phone', 'mobile'] },
|
||||
];
|
||||
const name = group.name.toLowerCase();
|
||||
for (let item of iconWords) {
|
||||
for (let word of item.words) {
|
||||
if (name.match(new RegExp(`(${word})`, 'gi'))) {
|
||||
return item.icon;
|
||||
}
|
||||
}
|
||||
};
|
||||
return 'speaker_group';
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
className={`snapcast__groups__menu-item menu-item${anyClients ? ' menu-item--no-clients' : ''}`}
|
||||
activeClassName="menu-item--active"
|
||||
key={group.id}
|
||||
history={history}
|
||||
to={`/settings/snapcast/${group.id}`}
|
||||
to={`/settings/services/snapcast/${group.id}`}
|
||||
scrollTo="#services-snapcast-groups"
|
||||
>
|
||||
<div className="snapcast__groups__menu-item__inner menu-item__inner">
|
||||
<Icon className="menu-item__icon" name={icon()} />
|
||||
<Icon className="menu-item__icon" name={iconFromKeyword(group.name.toLowerCase()) || 'speaker_group'} />
|
||||
<div className="menu-item__title">
|
||||
{group.name}
|
||||
{group.mute && (
|
||||
@ -154,8 +130,8 @@ const SnapcastGroups = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="snapcast__groups" id="services-snapcast-groups">
|
||||
<div className="snapcast__groups__menu menu">
|
||||
<div className="sub-tabs snapcast__groups" id="services-snapcast-groups">
|
||||
<div className="sub-tabs__menu snapcast__groups__menu menu">
|
||||
<div className="menu__inner">
|
||||
{sortItems(groupsArray, 'name').map((group) => renderMenuItem(group))}
|
||||
</div>
|
||||
|
||||
@ -264,8 +264,7 @@ export function getAlbum(uri, artist, album, mbid = false) {
|
||||
(response) => {
|
||||
if (response.album) {
|
||||
const existing_album = getState().core.albums[uri];
|
||||
|
||||
const album = {
|
||||
const album = {
|
||||
uri,
|
||||
images: response.album.image,
|
||||
listeners: parseInt(response.album.listeners),
|
||||
@ -342,8 +341,7 @@ export function getImages(context, uri) {
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.album) {
|
||||
record = { ...record, images: response.album.image };
|
||||
dispatch(coreActions.albumLoaded(record));
|
||||
dispatch(coreActions.albumLoaded({ uri, images: response.album.image }));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -63,6 +63,15 @@ const localstorageMiddleware = (function () {
|
||||
);
|
||||
break;
|
||||
|
||||
case 'MOPIDY_UPDATE_SERVERS':
|
||||
storage.set(
|
||||
'mopidy',
|
||||
{
|
||||
servers: action.servers,
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTED':
|
||||
if (action.authorization !== undefined) {
|
||||
var { authorization } = action;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
|
||||
import { createRange } from '../../util/arrays';
|
||||
import { generateGuid } from '../../util/helpers';
|
||||
|
||||
export function set(data) {
|
||||
return {
|
||||
@ -8,6 +9,47 @@ export function set(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateServer(server) {
|
||||
return {
|
||||
type: 'MOPIDY_UPDATE_SERVER',
|
||||
server,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateServers(servers) {
|
||||
return {
|
||||
type: 'MOPIDY_UPDATE_SERVERS',
|
||||
servers,
|
||||
};
|
||||
}
|
||||
|
||||
export function addServer() {
|
||||
return {
|
||||
type: 'MOPIDY_UPDATE_SERVER',
|
||||
server: {
|
||||
id: generateGuid(),
|
||||
name: 'New server',
|
||||
host: window.location.hostname,
|
||||
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
|
||||
ssl: window.location.protocol === 'https:',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setCurrentServer(server) {
|
||||
return {
|
||||
type: 'MOPIDY_SET_CURRENT_SERVER',
|
||||
server,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeServer(id) {
|
||||
return {
|
||||
type: 'MOPIDY_REMOVE_SERVER',
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function setConnection(data) {
|
||||
return {
|
||||
type: 'MOPIDY_SET_CONNECTION',
|
||||
|
||||
@ -249,7 +249,7 @@ const MopidyMiddleware = (function () {
|
||||
var state = store.getState();
|
||||
|
||||
socket = new Mopidy({
|
||||
webSocketUrl: `ws${window.location.protocol === 'https:' ? 's' : ''}://${state.mopidy.host}:${state.mopidy.port}/mopidy/ws/`,
|
||||
webSocketUrl: `ws${state.mopidy.ssl ? 's' : ''}://${state.mopidy.host}:${state.mopidy.port}/mopidy/ws/`,
|
||||
callingConvention: 'by-position-or-by-name',
|
||||
});
|
||||
|
||||
@ -284,10 +284,23 @@ const MopidyMiddleware = (function () {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'MOPIDY_SET_CONNECTION':
|
||||
store.dispatch(mopidyActions.set(action.data));
|
||||
case 'MOPIDY_UPDATE_SERVER':
|
||||
let servers = store.getState().mopidy.servers;
|
||||
servers[action.server.id] = { ...servers[action.server.id], ...action.server };
|
||||
store.dispatch(mopidyActions.updateServers(servers));
|
||||
break;
|
||||
|
||||
// Wait 250 ms and then retry connection
|
||||
case 'MOPIDY_SET_CURRENT_SERVER':
|
||||
store.dispatch(mopidyActions.set({
|
||||
current_server: action.server.id,
|
||||
host: action.server.host,
|
||||
port: action.server.port,
|
||||
ssl: action.server.ssl,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
}));
|
||||
|
||||
// Wait a moment for store to update, then attempt connection
|
||||
setTimeout(
|
||||
() => {
|
||||
store.dispatch(mopidyActions.connect());
|
||||
@ -297,6 +310,12 @@ const MopidyMiddleware = (function () {
|
||||
);
|
||||
break;
|
||||
|
||||
case 'MOPIDY_REMOVE_SERVER':
|
||||
let remaining_servers = store.getState().mopidy.servers;
|
||||
delete remaining_servers[action.id];
|
||||
store.dispatch(mopidyActions.updateServers(remaining_servers));
|
||||
break;
|
||||
|
||||
case 'SET_WINDOW_FOCUS':
|
||||
|
||||
// Focus has just been regained
|
||||
@ -2355,11 +2374,13 @@ const MopidyMiddleware = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
const action_data = {
|
||||
type: (`${action.context}_LOADED`).toUpperCase(),
|
||||
};
|
||||
action_data[action.context] = records;
|
||||
store.dispatch(action_data);
|
||||
if (records.length) {
|
||||
const action_data = {
|
||||
type: (`${action.context}_LOADED`).toUpperCase(),
|
||||
};
|
||||
action_data[action.context] = records;
|
||||
store.dispatch(action_data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -6,12 +6,15 @@ export default function reducer(mopidy = {}, action) {
|
||||
case 'MOPIDY_SET':
|
||||
return { ...mopidy, ...action.data };
|
||||
|
||||
case 'MOPIDY_UPDATE_SERVERS':
|
||||
return { ...mopidy, servers: action.servers };
|
||||
|
||||
case 'MOPIDY_CONNECT':
|
||||
case 'MOPIDY_CONNECTING':
|
||||
return {
|
||||
...mopidy,
|
||||
connected: false,
|
||||
connecting: true,
|
||||
connecting: true,
|
||||
};
|
||||
|
||||
case 'MOPIDY_CONNECTED':
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
|
||||
import { createStore, applyMiddleware, combineReducers } from 'redux';
|
||||
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { generateGuid } from '../util/helpers';
|
||||
@ -60,7 +59,17 @@ let state = {
|
||||
connected: false,
|
||||
host: window.location.hostname,
|
||||
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
|
||||
ssl: (window.location.protocol === 'https:'),
|
||||
ssl: window.location.protocol === 'https:',
|
||||
current_server: 'default',
|
||||
servers: {
|
||||
default: {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
host: window.location.hostname,
|
||||
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
|
||||
ssl: window.location.protocol === 'https:',
|
||||
},
|
||||
},
|
||||
mute: false,
|
||||
volume: 0,
|
||||
progress: 0,
|
||||
|
||||
@ -410,6 +410,33 @@ const titleCase = function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use a keyword to return an icon name
|
||||
*/
|
||||
const iconFromKeyword = (name) => {
|
||||
const iconWords = [
|
||||
{ icon: 'business', words: ['office', 'work'] },
|
||||
{ icon: 'king_bed', words: ['bed'] },
|
||||
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
|
||||
{ icon: 'directions_car', words: ['garage', 'basement'] },
|
||||
{ icon: 'local_laundry_service', words: ['laundry'] },
|
||||
{ icon: 'fitness_center', words: ['gym'] },
|
||||
{ icon: 'kitchen', words: ['kitchen'] },
|
||||
{ icon: 'deck', words: ['deck', 'outside'] },
|
||||
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
|
||||
{ icon: 'laptop', words: ['laptop'] },
|
||||
{ icon: 'bug_report', words: ['test', 'debug'] },
|
||||
{ icon: 'child_care', words: ['kids', 'baby'] },
|
||||
{ icon: 'smartphone', words: ['phone', 'mobile'] },
|
||||
];
|
||||
for (let item of iconWords) {
|
||||
for (let word of item.words) {
|
||||
if (name.match(new RegExp(`(${word})`, 'gi'))) {
|
||||
return item.icon;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the top of the page
|
||||
@ -512,6 +539,7 @@ export {
|
||||
scrollTo,
|
||||
upgradeSpotifyPlaylistUris,
|
||||
upgradeSpotifyPlaylistUri,
|
||||
iconFromKeyword,
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -536,4 +564,5 @@ export default {
|
||||
scrollTo,
|
||||
upgradeSpotifyPlaylistUris,
|
||||
upgradeSpotifyPlaylistUri,
|
||||
iconFromKeyword,
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import TextField from '../components/Fields/TextField';
|
||||
import Header from '../components/Header';
|
||||
import Icon from '../components/Icon';
|
||||
import Services from '../components/Services';
|
||||
import Servers from '../components/Servers';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as pusherActions from '../services/pusher/actions';
|
||||
@ -99,39 +100,6 @@ class Settings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderServerStatus() {
|
||||
const {
|
||||
mopidy,
|
||||
pusher,
|
||||
} = this.props;
|
||||
let colour = 'grey';
|
||||
let icon = 'help';
|
||||
let status = 'Unknown';
|
||||
let className = null;
|
||||
|
||||
if (mopidy.connecting || pusher.connecting) {
|
||||
icon = 'autorenew';
|
||||
status = 'Connecting...';
|
||||
className = 'icon--spin';
|
||||
} else if (!mopidy.connected || !pusher.connected) {
|
||||
colour = 'red';
|
||||
icon = 'close';
|
||||
status = 'Disconnected';
|
||||
} else if (mopidy.connected && pusher.connected) {
|
||||
colour = 'green';
|
||||
icon = 'check';
|
||||
status = 'Connected';
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`${colour}-text`}>
|
||||
<Icon className={className} name={icon} />
|
||||
{' '}
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocalScanButton = () => {
|
||||
const { processes } = this.props.ui;
|
||||
const loading = processes.local_scan && processes.local_scan.status === 'running';
|
||||
@ -183,15 +151,6 @@ class Settings extends React.Component {
|
||||
<a name="server" />
|
||||
</h4>
|
||||
|
||||
<div className="field">
|
||||
<div className="name">Status</div>
|
||||
<div className="input">
|
||||
<div className="text">
|
||||
{this.renderServerStatus()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
<div className="name">Username</div>
|
||||
<div className="input">
|
||||
@ -205,34 +164,14 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<form onSubmit={(e) => this.setConfig(e)}>
|
||||
<label className="field">
|
||||
<div className="name">Host</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={mopidy.host}
|
||||
onChange={value => mopidyActions.setConnection({ host: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Port</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={mopidy.port}
|
||||
onChange={value => mopidyActions.setConnection({ port: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
<Route path="/settings/:server?/:id?" component={Servers} />
|
||||
|
||||
<h4 className="underline">
|
||||
Services
|
||||
<a name="services" />
|
||||
</h4>
|
||||
|
||||
<Route path="/settings/:service?/:id?" component={Services} />
|
||||
<Route path="/settings/:services?/:service?/:id?" component={Services} />
|
||||
|
||||
<h4 className="underline">
|
||||
Interface
|
||||
@ -433,7 +372,10 @@ class Settings extends React.Component {
|
||||
<div className="input">
|
||||
<span className="text">
|
||||
{this.props.pusher.version.current}
|
||||
{' installed'}
|
||||
{' '}
|
||||
<span className="mid_grey-text">
|
||||
{build}
|
||||
</span>
|
||||
{this.props.pusher.version.upgrade_available ? (
|
||||
<span className="flag flag--dark">
|
||||
<Icon name="cloud_download" className="blue-text" />
|
||||
@ -444,7 +386,7 @@ class Settings extends React.Component {
|
||||
<Icon name="check" className="green-text" />
|
||||
{' Up-to-date'}
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,6 +71,16 @@ class LibraryBrowse extends React.Component {
|
||||
subdirectory.icons = ['/iris/assets/backgrounds/browse-somafm.jpg'];
|
||||
break;
|
||||
|
||||
case 'Tidal':
|
||||
subdirectory.icons = ['/iris/assets/backgrounds/browse-tidal.jpg'];
|
||||
break;
|
||||
|
||||
case 'Google':
|
||||
case 'Google Play':
|
||||
case 'Google Play Music':
|
||||
subdirectory.icons = ['/iris/assets/backgrounds/browse-google.jpg'];
|
||||
break;
|
||||
|
||||
default:
|
||||
subdirectory.icons = ['/iris/assets/backgrounds/browse-default.jpg'];
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import DropdownField from '../../components/Fields/DropdownField';
|
||||
import Icon from '../../components/Icon';
|
||||
import URILink from '../../components/URILink';
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import LazyLoadListener from '../../components/LazyLoadListener';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
@ -20,7 +21,25 @@ import {
|
||||
import { arrayOf, sortItems } from '../../util/arrays';
|
||||
|
||||
class LibraryBrowseDirectory extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: '',
|
||||
limit: 50,
|
||||
per_page: 50,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Restore any limit defined in our location state
|
||||
const state = (this.props.location.state ? this.props.location.state : {});
|
||||
if (state.limit) {
|
||||
this.setState({
|
||||
limit: state.limit,
|
||||
});
|
||||
}
|
||||
|
||||
this.props.uiActions.setWindowTitle('Browse');
|
||||
this.loadDirectory();
|
||||
}
|
||||
@ -48,6 +67,17 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
const new_limit = this.state.limit + this.state.per_page;
|
||||
|
||||
this.setState({ limit: new_limit });
|
||||
|
||||
// Set our pagination to location state
|
||||
const state = (this.props.location && this.props.location.state ? this.props.location.state : {});
|
||||
state.limit = new_limit;
|
||||
this.props.history.replace({ state });
|
||||
}
|
||||
|
||||
playAll(e, tracks) {
|
||||
const tracks_uris = arrayOf('uri', tracks);
|
||||
this.props.mopidyActions.playURIs(tracks_uris, `iris:browse:${this.props.uri}`);
|
||||
@ -98,7 +128,7 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
}
|
||||
|
||||
renderSubdirectories(subdirectories) {
|
||||
if (this.props.view == 'list') {
|
||||
if (this.props.view === 'list') {
|
||||
return (
|
||||
<List
|
||||
nocontext
|
||||
@ -143,12 +173,20 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let tracks = (this.props.directory.tracks && this.props.directory.tracks.length > 0 ? this.props.directory.tracks : null);
|
||||
tracks = sortItems(tracks, 'name');
|
||||
|
||||
let subdirectories = (this.props.directory.subdirectories && this.props.directory.subdirectories.length > 0 ? this.props.directory.subdirectories : null);
|
||||
subdirectories = sortItems(subdirectories, 'name');
|
||||
|
||||
const total_items = (tracks ? tracks.length : 0) + (subdirectories ? subdirectories.length : 0);
|
||||
subdirectories = subdirectories.slice(0, this.state.limit);
|
||||
let all_tracks = null;
|
||||
let tracks = null;
|
||||
let limit_remaining = this.state.limit - subdirectories;
|
||||
if (limit_remaining > 0) {
|
||||
all_tracks = (this.props.directory.tracks && this.props.directory.tracks.length > 0 ? this.props.directory.tracks : null);
|
||||
all_tracks = sortItems(all_tracks, 'name');
|
||||
tracks = all_tracks.slice(0, limit_remaining);
|
||||
}
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
label: 'Thumbnails',
|
||||
@ -171,7 +209,7 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
handleChange={(value) => { this.props.uiActions.set({ library_directory_view: value }); this.props.uiActions.hideContextMenu(); }}
|
||||
/>
|
||||
{tracks && (
|
||||
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.playAll(e, tracks); }}>
|
||||
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.playAll(e, all_tracks); }}>
|
||||
<Icon name="play_circle_filled" />
|
||||
Play all
|
||||
</a>
|
||||
@ -198,12 +236,18 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
|
||||
{tracks && (
|
||||
<TrackList
|
||||
tracks={this.props.directory.tracks}
|
||||
tracks={tracks}
|
||||
uri={`iris:browse:${this.props.uri}`}
|
||||
className="library-local-track-list"
|
||||
/>
|
||||
)}
|
||||
|
||||
<LazyLoadListener
|
||||
loadKey={total_items > this.state.limit ? this.state.limit : total_items}
|
||||
showLoader={this.state.limit < total_items}
|
||||
loadMore={() => this.loadMore()}
|
||||
/>
|
||||
|
||||
</ErrorBoundary>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -169,8 +169,12 @@ class KioskMode extends React.Component {
|
||||
}
|
||||
|
||||
const extraControls = (
|
||||
<div className="control" onClick={this.toggleLyrics} style={show_lyrics ? { opacity: 1 } : {}}>
|
||||
<Icon name="queue_music" className={show_lyrics ? 'turquoise-text' : null} />
|
||||
<div className="control" onClick={this.toggleLyrics}>
|
||||
{show_lyrics ? <Icon name="toggle_on" className="turquoise-text" />
|
||||
: <Icon name="toggle_off" />}
|
||||
<div style={{ paddingLeft: '6px', fontWeight: 'bold' }}>
|
||||
Lyrics
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@ -24,12 +24,12 @@ class Modal extends React.Component {
|
||||
<div className={`modal ${className}`}>
|
||||
|
||||
<div className="controls">
|
||||
{extraControls}
|
||||
{!noclose && (
|
||||
<div className="control close" onClick={(e) => window.history.back()}>
|
||||
<Icon name="close" className="white" />
|
||||
</div>
|
||||
)}
|
||||
{extraControls}
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
|
||||
@ -27,9 +27,9 @@
|
||||
@import 'components/filter-field';
|
||||
@import 'components/sources-priority-field';
|
||||
@import 'components/sub-views';
|
||||
@import 'components/sub-tabs';
|
||||
@import 'components/debug';
|
||||
@import 'components/tooltip';
|
||||
@import 'components/services';
|
||||
@import 'components/snapcast';
|
||||
@import 'components/popularity';
|
||||
@import 'components/colour-field';
|
||||
|
||||
@ -4,152 +4,206 @@
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 60px;
|
||||
transform-style: preserve-3d;
|
||||
border-left: 230px solid transparent;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
||||
&__background {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dark-theme & {
|
||||
background: colour(grey);
|
||||
color: colour(white);
|
||||
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
|
||||
border-color: colour(grey);
|
||||
|
||||
&__background {
|
||||
background: colour(grey);
|
||||
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.light-theme & {
|
||||
background: colour(white);
|
||||
color: colour(darkest_grey);
|
||||
border-color: colour(white);
|
||||
box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&__background {
|
||||
background: colour(white);
|
||||
box-shadow: 0 -1px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.current-track {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
.current-track__wrapper {
|
||||
width: 210px;
|
||||
left: -230px;
|
||||
padding-right: 20px;
|
||||
z-index: 1;
|
||||
text-decoration: none;
|
||||
|
||||
@keyframes slide_left {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-15px);
|
||||
transform: translateX(-15vw);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide_right {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(15px);
|
||||
transform: translateX(15vw);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__transition {
|
||||
z-index: 2;
|
||||
background: colour(grey);
|
||||
|
||||
.light-theme & {
|
||||
background: lighten(colour(white), 15%);
|
||||
}
|
||||
|
||||
.text {
|
||||
@include animate(0.15s);
|
||||
@keyframes slide_from_left {
|
||||
0% {
|
||||
transform: translateX(15vw);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive($bp_medium) {
|
||||
animation-duration: 0.25s;
|
||||
animation-timing-function: ease-out;
|
||||
@keyframes slide_from_right {
|
||||
0% {
|
||||
transform: translateX(-15vw);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--previous {
|
||||
@include responsive($bp_medium) {
|
||||
.current-track {
|
||||
&__incoming,
|
||||
&__outgoing {
|
||||
animation-duration: .25s;
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
&[direction="previous"] {
|
||||
.current-track__incoming {
|
||||
animation-name: slide_from_right;
|
||||
}
|
||||
.current-track__outgoing {
|
||||
animation-name: slide_right;
|
||||
}
|
||||
}
|
||||
|
||||
&--next {
|
||||
&[direction="next"] {
|
||||
.current-track__incoming {
|
||||
animation-name: slide_from_left;
|
||||
}
|
||||
.current-track__outgoing {
|
||||
animation-name: slide_left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
margin: 8px;
|
||||
margin-right: 12px;
|
||||
.current-track {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
&__image {
|
||||
background-color: colour(middle_grey);
|
||||
}
|
||||
|
||||
&:before {
|
||||
@include animate();
|
||||
display: block;
|
||||
content: "\f065";
|
||||
font-family: "FontAwesome";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: -9px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: colour(white);
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@include animate();
|
||||
content: "";
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
&__outgoing {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&:hover {
|
||||
&:after,
|
||||
&:before {
|
||||
opacity: 1;
|
||||
.text {
|
||||
@include animate(0.15s);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
opacity: 1;
|
||||
line-height: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
padding-top: 1.05rem;
|
||||
.thumbnail {
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
margin: 8px;
|
||||
margin-right: 12px;
|
||||
|
||||
.title {
|
||||
@include one_line_text;
|
||||
&__image {
|
||||
background-color: colour(middle_grey);
|
||||
}
|
||||
|
||||
&:before {
|
||||
@include animate();
|
||||
display: block;
|
||||
content: "\f065";
|
||||
font-family: "FontAwesome";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-top: -9px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: colour(white);
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@include animate();
|
||||
content: "";
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:after,
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.links-sentence {
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
@include one_line_text;
|
||||
.text {
|
||||
opacity: 1;
|
||||
line-height: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: 0;
|
||||
|
||||
&__item {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent;
|
||||
.title {
|
||||
@include one_line_text;
|
||||
}
|
||||
|
||||
.links-sentence {
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
@include one_line_text;
|
||||
|
||||
&__item {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,25 +240,17 @@
|
||||
}
|
||||
|
||||
section {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
|
||||
&.playback {
|
||||
bottom: 7px;
|
||||
left: 0;
|
||||
width: 10rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.control {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding: 4px;
|
||||
|
||||
.icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
&.play {
|
||||
margin-bottom: -0.8rem;
|
||||
|
||||
.light-theme & {
|
||||
color: colour(turquoise);
|
||||
}
|
||||
@ -217,18 +263,12 @@
|
||||
}
|
||||
|
||||
&.progress {
|
||||
bottom: 13px;
|
||||
left: 10rem;
|
||||
right: 25rem;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
|
||||
flex-grow: 1;
|
||||
&__track {
|
||||
&__progress {
|
||||
@include animate(0.2s, linear);
|
||||
@ -236,40 +276,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
.current {
|
||||
.time {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 0;
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
width: 40px;
|
||||
|
||||
.total {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
&--current {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&--total {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
right: 14rem;
|
||||
width: 10rem;
|
||||
text-align: center;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
right: 2rem;
|
||||
width: 10rem;
|
||||
font-size: 18px;
|
||||
width: 10rem;
|
||||
padding-right: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.slider__wrapper {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 0;
|
||||
left: 30px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,67 +314,63 @@
|
||||
}
|
||||
|
||||
@include responsive($bp_medium) {
|
||||
border-left: none;
|
||||
|
||||
&--touch-enabled .control.next {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.current-track {
|
||||
width: auto;
|
||||
bottom: 0.65rem;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
.current-track__wrapper {
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2em;
|
||||
margin: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
|
||||
.title,
|
||||
.artist {
|
||||
padding-top: 0;
|
||||
.text {
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
.title,
|
||||
.artist {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
.thumbnail-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
&.playback {
|
||||
left: 0;
|
||||
order: 1;
|
||||
width: 6rem;
|
||||
|
||||
.control {
|
||||
&.play {
|
||||
margin-bottom: -1rem;
|
||||
}
|
||||
|
||||
&.previous {
|
||||
display: none;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&.previous,
|
||||
&.stop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.settings,
|
||||
&.volume,
|
||||
&.progress {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.triggers {
|
||||
display: block;
|
||||
right: 10px;
|
||||
width: 6rem;
|
||||
order: 3;
|
||||
text-align: center;
|
||||
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
@ -372,12 +403,16 @@
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
height: 140px;
|
||||
max-height: 100%;
|
||||
|
||||
.playback-controls__background {
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
section {
|
||||
&.settings {
|
||||
display: block;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
right: 0;
|
||||
bottom: 90px;
|
||||
padding-right: 10px;
|
||||
@ -386,8 +421,9 @@
|
||||
}
|
||||
|
||||
&.volume {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
display: block;
|
||||
display: flex;
|
||||
right: 100px;
|
||||
left: 10px;
|
||||
bottom: 88px;
|
||||
@ -402,10 +438,11 @@
|
||||
}
|
||||
|
||||
&.progress {
|
||||
display: block;
|
||||
bottom: 55px;
|
||||
left: -15px;
|
||||
right: -15px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
width: 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 1px;
|
||||
border: 0;
|
||||
@ -45,7 +45,7 @@
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
top: 42%;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
margin-top: -3px;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
|
||||
.services {
|
||||
.sub-tabs {
|
||||
|
||||
.menu {
|
||||
&__menu {
|
||||
@include clearfix();
|
||||
|
||||
&__inner {
|
||||
.menu__inner {
|
||||
display: flex;
|
||||
margin: 0 0 0 -5px;
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none !important;
|
||||
cursor: pointer;
|
||||
|
||||
&__inner {
|
||||
position: relative;
|
||||
@ -89,7 +90,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.service {
|
||||
&__content {
|
||||
padding: 20px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: lighten(colour(dark_grey), 4%);
|
||||
@ -107,7 +108,7 @@
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
|
||||
.menu {
|
||||
&__menu {
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar{
|
||||
@ -135,7 +136,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.service {
|
||||
&__content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@ -93,7 +93,7 @@ main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 60px;
|
||||
bottom: 50px;
|
||||
right: 0;
|
||||
padding-left: 220px;
|
||||
perspective: 1000px;
|
||||
|
||||
@ -398,9 +398,9 @@ select {
|
||||
}
|
||||
|
||||
@include responsive($bp_medium) {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
|
||||
&--large {
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
@ -22,11 +22,13 @@
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
|
||||
.control {
|
||||
@include noselect();
|
||||
@include animate();
|
||||
display: block;
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user