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"
: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>
<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>
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>
<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">
Eliot Berriot
committed
<i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'secondary', 'icon']" ></i>
</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
}
},
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,
queue: state => state.queue
}),
...mapGetters({
currentTrack: 'queue/currentTrack',
hasNext: 'queue/hasNext',
durationFormatted: 'player/durationFormatted',
currentTimeFormatted: 'player/currentTimeFormatted',
progress: 'player/progress'
}),
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
}
}
}
</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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
@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;
}