Newer
Older
Eliot Berriot
committed
<template>
<div class="ui inverted segment player-wrapper" :style="style">
<div class="player">
Eliot Berriot
committed
<audio-track
ref="currentAudio"
v-if="currentTrack"
Eliot Berriot
committed
@errored="handleError"
Eliot Berriot
committed
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:key="audioKey"
:track="currentTrack">
</audio-track>
<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 && currentTrack.album.cover.original" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.medium_square_crop)">
<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"
Eliot Berriot
committed
:class="{'inverted': !$store.getters['favorites/isFavorite'](currentTrack.id)}"
:track="currentTrack"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
Eliot Berriot
committed
:class="['inverted']"
:track="currentTrack"></track-playlist-icon>
</div>
Eliot Berriot
committed
</div>
</div>
</div>
Eliot Berriot
committed
<div class="progress-area" v-if="currentTrack && !errored">
<div class="ui grid">
<div class="left floated four wide column">
<p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
</div>
Eliot Berriot
committed
Eliot Berriot
committed
<div v-if="!isLoadingAudio" class="right floated four wide column">
<p class="timer total">{{durationFormatted}}</p>
</div>
</div>
Eliot Berriot
committed
<div
ref="progress"
:class="['ui', 'small', 'orange', 'inverted', {'indicating': isLoadingAudio}, 'progress']"
@click="touchProgress">
<div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
Eliot Berriot
committed
</div>
</div>
Eliot Berriot
committed
<div class="ui small warning message" v-if="currentTrack && errored">
<div class="header">
<translate>We cannot load this track</translate>
</div>
<p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
<translate>The next track will play automatically in a few seconds...</translate>
<i class="loading spinner icon"></i>
</p>
<p>
<translate>You may have a connectivity issue.</translate>
</p>
</div>
<div class="two wide column controls ui grid">
Eliot Berriot
committed
<a
href
Eliot Berriot
committed
:aria-label="labels.previousTrack"
Eliot Berriot
committed
@click.prevent.stop="previous"
Eliot Berriot
committed
<i :class="['ui', 'backward', {'disabled': emptyQueue}, 'secondary', 'icon']"></i>
</a>
<a
href
v-if="!playing"
Eliot Berriot
committed
:aria-label="labels.play"
@click.prevent.stop="togglePlay"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</a>
<a
href
v-else
Eliot Berriot
committed
:aria-label="labels.pause"
@click.prevent.stop="togglePlay"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</a>
<a
href
Eliot Berriot
committed
:aria-label="labels.next"
class="two wide column control"
Eliot Berriot
committed
@click.prevent.stop="next"
:disabled="!hasNext">
<i :class="['ui', {'disabled': !hasNext}, 'forward', 'secondary', 'icon']" ></i>
Eliot Berriot
committed
</a>
<div
class="wide column control volume-control"
v-on:mouseover="showVolume = true"
v-on:mouseleave="showVolume = false"
v-bind:class="{ active : showVolume }">
Eliot Berriot
committed
<a
href
v-if="volume === 0"
Eliot Berriot
committed
:aria-label="labels.unmute"
@click.prevent.stop="unmute">
<i class="volume off secondary icon"></i>
</a>
<a
href
v-else-if="volume < 0.5"
Eliot Berriot
committed
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume down secondary icon"></i>
</a>
<a
href
v-else
Eliot Berriot
committed
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume up secondary icon"></i>
</a>
<input
type="range"
step="0.05"
min="0"
max="1"
v-model="sliderVolume"
v-if="showVolume" />
</div>
<div class="two wide column control looping" v-if="!showVolume">
Eliot Berriot
committed
<a
href
v-if="looping === 0"
Eliot Berriot
committed
:title="labels.loopingDisabled"
:aria-label="labels.loopingDisabled"
@click.prevent.stop="$store.commit('player/looping', 1)"
:disabled="!currentTrack">
<i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
</a>
<a
href
@click.prevent.stop="$store.commit('player/looping', 2)"
Eliot Berriot
committed
:aria-label="labels.loopingSingle"
v-if="looping === 1"
Eliot Berriot
committed
:disabled="!currentTrack">
<i
class="repeat secondary icon">
<span class="ui circular tiny orange label">1</span>
</i>
</a>
<a
href
Eliot Berriot
committed
:aria-label="labels.loopingWhole"
v-if="looping === 2"
:disabled="!currentTrack"
Eliot Berriot
committed
@click.prevent.stop="$store.commit('player/looping', 0)">
<i
class="repeat orange secondary icon">
</i>
</a>
</div>
Eliot Berriot
committed
<a
href
:disabled="queue.tracks.length === 0"
Eliot Berriot
committed
:aria-label="labels.shuffle"
Eliot Berriot
committed
@click.prevent.stop="shuffle()"
class="two wide column control">
<div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
Eliot Berriot
committed
<i v-else :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</a>
<div class="one wide column" v-if="!showVolume"></div>
Eliot Berriot
committed
<a
href
:disabled="queue.tracks.length === 0"
Eliot Berriot
committed
:aria-label="labels.clear"
Eliot Berriot
committed
@click.prevent.stop="clean()"
class="two wide column control">
Eliot Berriot
committed
<i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</a>
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.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'
Eliot Berriot
committed
import {Howl} from 'howler'
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 {
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 {
sliderVolume: this.volume,
defaultAmbiantColors: defaultAmbiantColors,
Eliot Berriot
committed
ambiantColors: defaultAmbiantColors,
audioKey: String(new Date()),
dummyAudio: null
Eliot Berriot
committed
}
},
mounted () {
// we trigger the watcher explicitely it does not work otherwise
this.sliderVolume = this.volume
Eliot Berriot
committed
// this is needed to unlock audio playing under some browsers,
// cf https://github.com/goldfire/howler.js#mobilechrome-playback
// but we never actually load those audio files
this.dummyAudio = new Howl({
preload: false,
autoplay: false,
src: ['noop.webm', 'noop.mp3']
})
},
destroyed () {
this.dummyAudio.unload()
Eliot Berriot
committed
},
methods: {
...mapActions({
togglePlay: 'player/togglePlay',
mute: 'player/mute',
unmute: 'player/unmute',
clean: 'queue/clean',
updateProgress: 'player/updateProgress'
}),
let disabled = this.queue.tracks.length === 0
if (this.isShuffling || disabled) {
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
},
handleError ({sound, error}) {
this.$store.commit('player/isLoadingAudio', false)
this.$store.dispatch('player/trackErrored')
Eliot Berriot
committed
}
},
computed: {
...mapState({
currentIndex: state => state.queue.currentIndex,
playing: state => state.player.playing,
Eliot Berriot
committed
isLoadingAudio: state => state.player.isLoadingAudio,
volume: state => state.player.volume,
looping: state => state.player.looping,
duration: state => state.player.duration,
Eliot Berriot
committed
bufferProgress: state => state.player.bufferProgress,
Eliot Berriot
committed
errored: state => state.player.errored,
queue: state => state.queue
}),
...mapGetters({
currentTrack: 'queue/currentTrack',
hasNext: 'queue/hasNext',
durationFormatted: 'player/durationFormatted',
currentTimeFormatted: 'player/currentTimeFormatted',
progress: 'player/progress'
}),
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
labels () {
let previousTrack = this.$gettext('Previous track')
let play = this.$gettext('Play track')
let pause = this.$gettext('Pause track')
let next = this.$gettext('Next track')
let unmute = this.$gettext('Unmute')
let mute = this.$gettext('Mute')
let loopingDisabled = this.$gettext('Looping disabled. Click to switch to single-track looping.')
let loopingSingle = this.$gettext('Looping on a single track. Click to switch to whole queue looping.')
let loopingWhole = this.$gettext('Looping on whole queue. Click to disable looping.')
let shuffle = this.$gettext('Shuffle your queue')
let clear = this.$gettext('Clear your queue')
return {
previousTrack,
play,
pause,
next,
unmute,
mute,
loopingDisabled,
loopingSingle,
loopingWhole,
shuffle,
clear
}
},
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: {
Eliot Berriot
committed
currentTrack (newValue, oldValue) {
if (!this.isShuffling && newValue != oldValue) {
Eliot Berriot
committed
this.audioKey = String(new Date())
}
Eliot Berriot
committed
if (!newValue || !newValue.album.cover) {
this.ambiantColors = this.defaultAmbiantColors
}
},
volume (newValue) {
Eliot Berriot
committed
this.sliderVolume = newValue
},
sliderVolume (newValue) {
this.$store.commit('player/volume', newValue)
Eliot Berriot
committed
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
}
}
}
</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;
}
}
Eliot Berriot
committed
.controls a {
color: white;
}
Eliot Berriot
committed
.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;
Eliot Berriot
committed
[type="range"] {
Eliot Berriot
committed
position: absolute;
Eliot Berriot
committed
cursor: pointer;
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
background: white;
cursor: pointer;
-webkit-appearance: none;
}
input[type=range]::-moz-range-track {
cursor: pointer;
background: white;
input[type=range]::-moz-focus-outer {
border: 0;
}
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 {
Eliot Berriot
committed
background: white;
}
.active.volume-control {
width: 60% !important;
}
.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;
}
Eliot Berriot
committed
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
@keyframes MOVE-BG {
from {
transform: translateX(0px);
}
to {
transform: translateX(46px);
}
}
.indicating.progress {
overflow: hidden;
}
.ui.progress .bar {
transition: none;
}
.ui.inverted.progress .buffer.bar {
position: absolute;
background-color:rgba(255, 255, 255, 0.15);
}
.indicating.progress .bar {
left: -46px;
width: 200% !important;
color: grey;
background: repeating-linear-gradient(
-55deg,
grey 1px,
grey 10px,
transparent 10px,
transparent 20px,
) !important;
animation-name: MOVE-BG;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}