Newer
Older
Eliot Berriot
committed
<template>
<div class="ui inverted segment player-wrapper" :style="style">
<div class="player">
Eliot Berriot
committed
<keep-alive>
<audio-track
ref="currentAudio"
v-if="renderAudio && currentTrack"
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:track="currentTrack">
</audio-track>
</keep-alive>
<div v-if="currentTrack" class="track-area ui unstackable items">
<div class="ui inverted item">
<div class="ui tiny image">
<img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover)">
<img v-else src="../../assets/audio/default-cover.png">
Eliot Berriot
committed
</div>
<div class="middle aligned content">
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
{{ currentTrack.title }}
</router-link>
<div class="meta">
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
{{ currentTrack.artist.name }}
</router-link> /
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
{{ currentTrack.album.title }}
</router-link>
</div>
<div class="description">
<track-favorite-icon
v-if="$store.state.auth.authenticated"
:track="currentTrack"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
:track="currentTrack"></track-playlist-icon>
</div>
Eliot Berriot
committed
</div>
</div>
</div>
<div class="progress-area" v-if="currentTrack">
<div class="ui grid">
<div class="left floated four wide column">
<p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
</div>
Eliot Berriot
committed
<div class="right floated four wide column">
<p class="timer total">{{durationFormatted}}</p>
</div>
</div>
<div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
Eliot Berriot
committed
</div>
</div>
<div class="two wide column controls ui grid">
<div
class="two wide column control"
:disabled="emptyQueue">
<i @click="previous" :class="['ui', 'backward', {'disabled': emptyQueue}, 'big', 'icon']"></i>
</div>
<div
v-if="!playing"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
v-else
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
class="two wide column control"
:disabled="!hasNext">
<i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
</div>
<div class="two wide column control volume-control">
<i :title="$gettext('Unmute')" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
<i :title="$gettext('Mute')" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
<i :title="$gettext('Mute')" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
</div>
<div class="two wide column control looping">
<i
:title="$gettext('Looping disabled. Click to switch to single-track looping.')"
v-if="looping === 0"
@click="$store.commit('player/looping', 1)"
:disabled="!currentTrack"
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
<i
:title="$gettext('Looping on a single track. Click to switch to whole queue looping.')"
v-if="looping === 1"
@click="$store.commit('player/looping', 2)"
:disabled="!currentTrack"
class="repeat secondary icon">
<span class="ui circular tiny orange label">1</span>
</i>
<i
:title="$gettext('Looping on whole queue. Click to disable looping.')"
v-if="looping === 2"
@click="$store.commit('player/looping', 0)"
:disabled="!currentTrack"
class="repeat orange secondary icon">
</i>
</div>
<div
:disabled="queue.tracks.length === 0"
class="two wide column control">
<div v-if="isShuffling" class="ui inline shuffling inverted small active loader"></div>
<i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
<div class="one wide column"></div>
<div
:disabled="queue.tracks.length === 0"
class="two wide column control">
<i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
Eliot Berriot
committed
</div>
<GlobalEvents
@keydown.space.prevent.exact="togglePlay"
@keydown.ctrl.left.prevent.exact="previous"
@keydown.ctrl.right.prevent.exact="next"
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
@keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
@keydown.s.prevent.exact="shuffle"
/>
Eliot Berriot
committed
</div>
</div>
</template>
<script>
import {mapState, mapGetters, mapActions} from 'vuex'
import GlobalEvents from '@/components/utils/global-events'
import ColorThief from '@/vendor/color-thief'
import AudioTrack from '@/components/audio/Track'
Eliot Berriot
committed
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
Eliot Berriot
committed
export default {
name: 'player',
components: {
GlobalEvents,
AudioTrack
Eliot Berriot
committed
},
data () {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
Eliot Berriot
committed
return {
Eliot Berriot
committed
renderAudio: true,
sliderVolume: this.volume,
defaultAmbiantColors: defaultAmbiantColors,
ambiantColors: defaultAmbiantColors
Eliot Berriot
committed
}
},
mounted () {
// we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume
Eliot Berriot
committed
},
methods: {
...mapActions({
togglePlay: 'player/togglePlay',
clean: 'queue/clean',
updateProgress: 'player/updateProgress'
}),
shuffle () {
if (this.isShuffling) {
return
}
let self = this
let msg = this.$gettext('Queue shuffled!')
this.isShuffling = true
setTimeout(() => {
self.$store.dispatch('queue/shuffle', () => {
self.isShuffling = false
self.$store.commit('ui/addMessage', {
date: new Date()
})
})
}, 100)
},
next () {
let self = this
this.$store.dispatch('queue/next').then(() => {
self.$emit('next')
})
},
previous () {
let self = this
this.$store.dispatch('queue/previous').then(() => {
self.$emit('previous')
})
},
Eliot Berriot
committed
touchProgress (e) {
let time
let target = this.$refs.progress
time = e.layerX / target.offsetWidth * this.duration
this.$refs.currentAudio.setCurrentTime(time)
},
updateBackground () {
if (!this.currentTrack.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
return
}
let image = this.$refs.cover
this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
Eliot Berriot
committed
}
},
computed: {
...mapState({
currentIndex: state => state.queue.currentIndex,
playing: state => state.player.playing,
volume: state => state.player.volume,
looping: state => state.player.looping,
duration: state => state.player.duration,
queue: state => state.queue
}),
...mapGetters({
currentTrack: 'queue/currentTrack',
hasNext: 'queue/hasNext',
durationFormatted: 'player/durationFormatted',
currentTimeFormatted: 'player/currentTimeFormatted',
progress: 'player/progress'
}),
style: function () {
let style = {
'background': this.ambiantGradiant
}
return style
},
ambiantGradiant: function () {
let indexConf = [
{orientation: 330, percent: 100, opacity: 0.7},
{orientation: 240, percent: 90, opacity: 0.7},
{orientation: 150, percent: 80, opacity: 0.7},
{orientation: 60, percent: 70, opacity: 0.7}
]
let gradients = this.ambiantColors.map((e, i) => {
let [r, g, b] = e
let conf = indexConf[i]
return `linear-gradient(${conf.orientation}deg, rgba(${r}, ${g}, ${b}, ${conf.opacity}) 10%, rgba(255, 255, 255, 0) ${conf.percent}%)`
}).join(', ')
return gradients
}
Eliot Berriot
committed
},
watch: {
currentTrack (newValue) {
Eliot Berriot
committed
if (!newValue || !newValue.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
}
},
Eliot Berriot
committed
currentIndex (newValue, oldValue) {
if (newValue !== oldValue) {
// why this? to ensure the audio tag is deleted and fully
// rerendered, so we don't have any issues with cached position
// or whatever
this.renderAudio = false
this.$nextTick(() => {
this.renderAudio = true
})
}
},
volume (newValue) {
Eliot Berriot
committed
this.sliderVolume = newValue
},
sliderVolume (newValue) {
this.$store.commit('player/volume', newValue)
Eliot Berriot
committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.ui.progress {
margin: 0.5rem 0 1rem;
}
.progress {
cursor: pointer;
.bar {
min-width: 0 !important;
}
}
.ui.inverted.item > .content > .description {
color: rgba(255, 255, 255, 0.9) !important;
}
.ui.item {
.meta {
font-size: 90%;
line-height: 1.2
}
}
.timer.total {
text-align: right;
}
.timer.start {
cursor: pointer
}
.track-area {
Eliot Berriot
committed
.header, .meta, .artist, .album {
color: white !important;
}
}
.controls .icon.big {
cursor: pointer;
font-size: 2em !important;
}
.controls .icon {
cursor: pointer;
vertical-align: middle;
}
.secondary.icon {
font-size: 1.5em;
}
.progress-area .actions {
text-align: center;
}
.volume-control {
position: relative;
.icon {
Eliot Berriot
committed
}
[type="range"] {
Eliot Berriot
committed
position: absolute;
bottom: 5px;
left: 10%;
cursor: pointer;
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
input[type=range] {
-webkit-appearance: none;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
cursor: pointer;
background: white;
}
input[type=range]::-webkit-slider-thumb {
background: white;
cursor: pointer;
-webkit-appearance: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #white;
}
input[type=range]::-moz-range-track {
cursor: pointer;
background: white;
}
input[type=range]::-moz-range-thumb {
background: white;
cursor: pointer;
}
input[type=range]::-ms-track {
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: white;
}
input[type=range]::-ms-fill-upper {
background: white;
}
input[type=range]::-ms-thumb {
background: white;
cursor: pointer;
}
input[type=range]:focus::-ms-fill-lower {
background: white;
}
input[type=range]:focus::-ms-fill-upper {
background: #white;
}
&:hover {
[type="range"] {
display: block;
}
}
}
.looping.control {
i {
position: relative;
}
.label {
position: absolute;
font-size: 0.7rem;
bottom: -0.7rem;
right: -0.7rem;
Eliot Berriot
committed
}
}
.ui.feed.icon {
margin: 0;
}
.shuffling.loader.inline {
margin: 0;
}