koel/resources/assets/js/remote/app.vue

500 lines
11 KiB
Vue
Raw Normal View History

2017-08-25 00:37:21 +00:00
<template>
2017-09-08 22:15:27 +00:00
<div id="app" :class="{ 'standalone' : inStandaloneMode }">
2017-08-28 10:58:09 +00:00
<template v-if="authenticated">
<div class="translucent" v-if="song" :style="{ backgroundImage: 'url('+song.album.cover+')' }">
</div>
<div id="main">
<template v-if="connected">
<div class="details" v-if="song">
<div class="cover" :style="{ backgroundImage: 'url('+song.album.cover+')' }"></div>
<div class="info">
<div class="wrap">
<p class="title text">{{ song.title }}</p>
<p class="artist text">{{ song.artist.name }}</p>
<p class="album text">{{ song.album.name }}</p>
</div>
</div>
</div>
2017-10-02 18:51:15 +00:00
<div class="none" v-else>
<p>No song is playing.</p>
</div>
2017-08-28 10:58:09 +00:00
<footer>
<a class="favorite" @click.prevent="toggleFavorite">
<i class="fa fa-heart yep" v-if="song && song.liked"></i>
<i class="fa fa-heart-o" v-else></i>
</a>
<a class="prev" @click="playPrev">
<i class="fa fa-step-backward"></i>
</a>
<a class="play-pause" @click.prevent="togglePlayback">
<i class="fa fa-pause" v-if="playing"></i>
<i class="fa fa-play" v-else></i>
</a>
2017-09-08 22:15:27 +00:00
<a class="next" @click.prevent="playNext">
2017-08-28 10:58:09 +00:00
<i class="fa fa-step-forward"></i>
</a>
2017-09-08 22:15:27 +00:00
<span class="volume">
<span id="volumeSlider" v-show="showingVolumeSlider"></span>
<span class="icon" @click.prevent="toggleVolumeSlider">
<i class="fa fa-volume-off" v-if="muted"></i>
<i class="fa fa-volume-up" v-else></i>
</span>
</span>
2017-08-28 10:58:09 +00:00
</footer>
</template>
<div v-else class="loader">
2017-10-02 18:39:00 +00:00
<div v-if="!maxRetriesReached">
<p><span>Searching for Koel</span></p>
<div class="signal"></div>
</div>
<div v-else>
<p>No active Koel instance found.
<a @click.prevent="rescan" class="rescan">Rescan</a>
</p>
</div>
2017-08-25 00:37:21 +00:00
</div>
</div>
2017-08-28 10:58:09 +00:00
</template>
<div class="login-wrapper" v-else>
<login-form @loggedin="onUserLoggedIn"/>
2017-08-25 00:37:21 +00:00
</div>
</div>
</template>
<script>
2017-09-08 22:15:27 +00:00
import nouislider from 'nouislider'
import { socket, ls } from '../services'
import { userStore } from '../stores'
import loginForm from '../components/auth/login-form.vue'
2017-08-25 00:37:21 +00:00
2017-09-08 22:15:27 +00:00
let volumeSlider
2017-10-02 18:39:00 +00:00
const MAX_RETRIES = 10
2017-09-08 22:15:27 +00:00
2017-08-25 00:37:21 +00:00
export default {
components: { loginForm },
2017-08-25 00:37:21 +00:00
data () {
return {
authenticated: false,
song: null,
2017-08-27 21:38:59 +00:00
lastActiveTime: new Date().getTime(),
2017-09-08 22:15:27 +00:00
inStandaloneMode: false,
connected: false,
muted: false,
2017-10-02 18:39:00 +00:00
showingVolumeSlider: false,
retries: 0
2017-09-08 22:15:27 +00:00
}
},
watch: {
connected () {
this.$nextTick(() => {
volumeSlider = document.getElementById('volumeSlider')
nouislider.create(volumeSlider, {
orientation: 'vertical',
connect: [true, false],
start: this.volume,
range: { min: 0, max: 10 },
direction: 'rtl'
})
volumeSlider.noUiSlider.on('change', (values, handle) => {
const volume = parseFloat(values[handle])
this.muted = !volume
socket.broadcast('volume:set', { volume })
})
})
},
volume (value) {
volumeSlider.noUiSlider.set(value)
2017-08-25 00:37:21 +00:00
}
},
methods: {
onUserLoggedIn () {
this.authenticated = true
this.init()
},
2017-08-28 11:13:00 +00:00
async init () {
try {
const user = await userStore.getProfile()
userStore.init([], user)
await socket.init()
socket.listen('song', ({ song }) => {
this.song = song
}).listen('playback:stopped', () => {
if (this.song) {
this.song.playbackState = 'stopped'
}
2017-09-08 22:15:27 +00:00
}).listen('status', ({ song, volume }) => {
this.song = song
this.volume = volume
this.connected = true
}).listen('volume:changed', volume => {
volumeSlider.noUiSlider.set(volume)
})
2017-08-28 11:05:41 +00:00
this.scan()
} catch (e) {
this.authenticated = false
}
},
2017-09-08 22:15:27 +00:00
toggleVolumeSlider () {
this.showingVolumeSlider = !this.showingVolumeSlider
},
toggleFavorite () {
if (!this.song) {
2017-08-28 11:13:00 +00:00
return
}
this.song.liked = !this.song.liked
socket.broadcast('favorite:toggle')
},
2017-08-25 00:37:21 +00:00
togglePlayback () {
if (this.song) {
this.song.playbackState = this.song.playbackState === 'playing' ? 'paused' : 'playing'
2017-08-25 00:37:21 +00:00
}
socket.broadcast('playback:toggle')
},
playNext () {
socket.broadcast('playback:next')
},
playPrev () {
socket.broadcast('playback:prev')
},
2017-08-28 11:13:00 +00:00
getStatus () {
2017-09-08 22:15:27 +00:00
socket.broadcast('status:get')
},
/**
2017-08-28 11:13:00 +00:00
* As iOS will put a web app into standby/sleep mode (and halt all JS execution),
* this method will keep track of the last active time and keep the status always fresh.
*/
2017-08-27 21:13:49 +00:00
heartbeat () {
const now = new Date().getTime()
2017-08-27 21:13:49 +00:00
if (now - this.lastActiveTime > 2000) {
this.getStatus()
}
this.lastActiveTime = now
2017-08-28 11:05:41 +00:00
},
/**
* Scan for an active (desktop) Koel instance.
*/
scan () {
if (!this.connected) {
2017-10-02 18:39:00 +00:00
if (!this.maxRetriesReached) {
this.getStatus()
this.retries++
window.setTimeout(this.scan, 1000)
2017-10-02 18:44:44 +00:00
}
2017-10-02 18:39:00 +00:00
} else {
this.retries = 0
2017-08-28 11:05:41 +00:00
}
2017-10-02 18:39:00 +00:00
},
2017-10-02 18:44:44 +00:00
rescan () {
2017-10-02 18:39:00 +00:00
this.retries = 0
this.scan()
2017-08-25 00:37:21 +00:00
}
},
computed: {
playing () {
return this.song && this.song.playbackState === 'playing'
2017-10-02 18:39:00 +00:00
},
maxRetriesReached () {
return this.retries >= MAX_RETRIES
2017-08-25 00:37:21 +00:00
}
},
created () {
2017-08-27 21:13:49 +00:00
window.setInterval(this.heartbeat, 500)
2017-09-08 22:15:27 +00:00
this.inStandaloneMode = window.navigator.standalone
},
mounted () {
// The app has just been initialized, check if we can get the user data with an already existing token
const token = ls.get('jwt-token')
if (token) {
this.authenticated = true
this.init()
}
2017-08-25 00:37:21 +00:00
}
}
</script>
<style lang="scss">
2017-08-25 00:37:21 +00:00
@import "resources/assets/sass/partials/_vars.scss";
@import "resources/assets/sass/partials/_mixins.scss";
@import "resources/assets/sass/partials/_shared.scss";
#app {
height: 100%;
2017-08-25 00:37:21 +00:00
background: $colorMainBgr;
.login-wrapper {
display: flex;
min-height: 100vh;
flex-direction: column;
@include vertical-center();
}
2017-08-27 16:25:01 +00:00
.translucent {
position: absolute;
top: -20px;
left: -20px;
right: -20px;
bottom: -20px;
filter: blur(20px);
opacity: .3;
z-index: 0;
overflow: hidden;
background-size: cover;
background-position: center;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000;
pointer-events: none;
}
2017-08-28 10:58:09 +00:00
.loader {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
position: relative;
p {
position: absolute;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
padding-bottom: 40px;
}
.signal {
border: 1px solid $colorOrange;
border-radius: 50%;
height: 0;
opacity: 0;
width: 50vw;
animation: pulsate 1.5s ease-out;
animation-iteration-count: infinite;
transform: translate(-50%, -50%);
}
2017-10-02 18:39:00 +00:00
.rescan {
margin-left: 5px;
color: $colorOrange;
}
2017-08-28 10:58:09 +00:00
@keyframes pulsate {
0% {
transform:scale(.1);
opacity: 0.0;
}
50% {
opacity:1;
}
100% {
transform:scale(1.2);
opacity:0;
}
}
}
}
2017-08-27 16:25:01 +00:00
#main {
height: 100%;
2017-08-25 00:37:21 +00:00
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: center;
2017-08-27 16:25:01 +00:00
z-index: 1;
position: relative;
2017-08-25 00:37:21 +00:00
2017-10-02 18:51:15 +00:00
.none, .details {
2017-08-25 00:37:21 +00:00
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
2017-08-27 16:25:01 +00:00
justify-content: space-around;
2017-10-02 18:51:15 +00:00
}
2017-08-25 00:37:21 +00:00
2017-10-02 18:51:15 +00:00
.details {
2017-08-25 00:37:21 +00:00
.info {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
2017-08-25 00:37:21 +00:00
}
.cover {
margin: 0 auto;
2017-08-27 21:13:37 +00:00
width: calc(70vw + 4px);
height: calc(70vw + 4px);
border-radius: 50%;
border: 2px solid #fff;
2017-08-27 21:13:37 +00:00
background-position: center center;
background-size: cover;
background-color: #2d2f2f;
2017-08-25 00:37:21 +00:00
}
.text {
max-width: 90%;
margin: 0 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.title {
font-size: 6vmin;
font-weight: bold;
margin: 0 auto 10px;
2017-08-25 00:37:21 +00:00
}
.artist {
font-size: 5vmin;
2017-08-25 00:37:21 +00:00
margin: 0 auto 6px;
font-weight: 100;
opacity: .5;
}
.album {
font-size: 4vmin;
2017-08-25 00:37:21 +00:00
font-weight: 100;
opacity: .5;
}
}
footer {
height: 18vh;
2017-08-25 00:37:21 +00:00
display: flex;
justify-content: space-around;
2017-08-25 00:37:21 +00:00
align-items: center;
border-top: 1px solid rgba(255, 255, 255, .1);
font-size: 5vmin;
a {
color: #fff;
&:active {
opacity: .8;
}
}
.favorite {
.yep {
color: #bf2043;
}
}
2017-08-25 00:37:21 +00:00
.prev, .next {
font-size: 6vmin;
}
.play-pause {
display: inline-block;
width: 16vmin;
height: 16vmin;
border: 1px solid #fff;
border-radius: 50%;
line-height: 16vmin;
font-size: 7vmin;
&.fa-play {
margin-left: 4px;
}
2017-08-25 00:37:21 +00:00
}
}
2017-08-27 21:38:59 +00:00
}
#app.standalone {
padding-top: 20px;
#main {
.details {
.cover {
width: calc(80vw - 4px);
height: calc(80vw - 4px);
}
}
.footer {
height: 20vh;
}
}
}
2017-09-08 22:15:27 +00:00
.volume {
position: relative;
.icon {
width: 20px;
display: inline-block;
text-align: center;
}
}
#volumeSlider {
height: 80px;
position: absolute;
bottom: calc(50% + 26px);
}
.noUi-target {
background: #fff;
border-radius: 4px;
border: 0;
box-shadow: none;
left: 7px;
}
.noUi-base {
height: calc(100% - 16px);
border-radius: 4px;
}
.noUi-vertical {
width: 8px;
}
.noUi-vertical .noUi-handle {
width: 16px;
height: 16px;
border-radius: 50%;
border: 0;
left: -4px;
top: 0;
&::after, &::before {
display: none;
}
}
.noUi-connect {
background: transparent;
box-shadow: none;
}
2017-08-25 00:37:21 +00:00
</style>