2019-12-26 10:38:26 +00:00
< template >
2020-05-15 12:12:36 +00:00
< section class = "main with-background component-queue" :aria-label ="labels.queue" >
2019-12-26 10:38:26 +00:00
< div : class = "['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']" >
< div class = "ui fluid container" >
< div class = "ui stackable grid" id = "queue-grid" >
2020-05-15 12:12:36 +00:00
< div class = "ui six wide column current-track" >
2019-12-26 10:38:26 +00:00
< div class = "ui basic segment" id = "player" >
< template v-if ="currentTrack" >
2020-08-28 18:59:54 +00:00
< img ref = "cover" alt = "" v-if ="currentTrack.cover && currentTrack.cover.urls.large_square_crop" :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)" >
< img ref = "cover" alt = "" v -else -if = " currentTrack.album & & currentTrack.album.cover & & currentTrack.album.cover.urls.large_square_crop " :src ="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)" >
2020-08-01 17:07:06 +00:00
< img class = "ui image" alt = "" v -else src = "../assets/audio/default-cover.png" >
2019-12-26 10:38:26 +00:00
< h1 class = "ui header" >
2020-07-27 09:05:34 +00:00
< div class = "content ellipsis" >
< router -link class = "small header discrete link track" : to = "{name: 'library.tracks.detail', params: {id: currentTrack.id }}" >
{ { currentTrack . title } }
2019-12-26 10:38:26 +00:00
< / r o u t e r - l i n k >
2020-07-27 09:05:34 +00:00
< div class = "sub header ellipsis" >
< router -link class = "discrete link artist" : to = "{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}" >
{ { currentTrack . artist . name } } < /router-link> <template v-if="currentTrack.album">/ < router -link class = "discrete link album" : to = "{name: 'library.albums.detail', params: {id: currentTrack.album.id }}" >
{ { currentTrack . album . title } }
2020-02-05 14:06:07 +00:00
< / r o u t e r - l i n k > < / t e m p l a t e >
2019-12-26 10:38:26 +00:00
< / div >
< / div >
< / h1 >
< div class = "ui small warning message" v-if ="currentTrack && errored" >
2020-07-03 12:20:47 +00:00
< h3 class = "header" >
2019-12-26 10:38:26 +00:00
< translate translate -context = " Sidebar / Player / Error message.Title " > The track cannot be loaded < / translate >
2020-07-03 12:20:47 +00:00
< / h3 >
2019-12-26 10:38:26 +00:00
< p v-if ="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors" >
< translate translate -context = " Sidebar / Player / Error message.Paragraph " > The next track will play automatically in a few seconds … < / translate >
< i class = "loading spinner icon" > < / i >
< / p >
< p >
< translate translate -context = " Sidebar / Player / Error message.Paragraph " > You may have a connectivity issue . < / translate >
< / p >
< / div >
2020-08-31 15:16:48 +00:00
< div class = "additional-controls tablet-and-below" >
2019-12-26 10:38:26 +00:00
< track -favorite -icon
v - if = "$store.state.auth.authenticated"
: track = "currentTrack" > < / t r a c k - f a v o r i t e - i c o n >
< track -playlist -icon
v - if = "$store.state.auth.authenticated"
: track = "currentTrack" > < / t r a c k - p l a y l i s t - i c o n >
< button
v - if = "$store.state.auth.authenticated"
@ click = "$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
2020-08-31 15:16:48 +00:00
: class = "['ui', 'really', 'basic', 'circular', 'icon', 'button']"
2019-12-26 10:38:26 +00:00
: aria - label = "labels.addArtistContentFilter"
: title = "labels.addArtistContentFilter" >
< i : class = "['eye slash outline', 'basic', 'icon']" > < / i >
< / button >
< / div >
< div class = "progress-wrapper" >
< div class = "progress-area" v-if ="currentTrack && !errored" >
< div
ref = "progress"
2020-05-15 12:12:36 +00:00
: class = "['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
2019-12-26 10:38:26 +00:00
@ click = "touchProgress" >
< div class = "buffer bar" :data-percent ="bufferProgress" : style = "{ 'width': bufferProgress + '%' }" > < / div >
< div class = "position bar" :data-percent ="progress" : style = "{ 'width': progress + '%' }" > < / div >
< / div >
< / div >
< div class = "progress-area" v-else >
< div
ref = "progress"
2020-05-15 12:12:36 +00:00
: class = "['ui', 'small', 'vibrant', 'progress']" >
2019-12-26 10:38:26 +00:00
< div class = "buffer bar" > < / div >
< div class = "position bar" > < / div >
< / div >
< / div >
< div class = "progress" >
< template v-if ="!isLoadingAudio" >
2020-08-11 12:07:06 +00:00
< a href = "" :aria-label ="labels.restart" class = "left floated timer discrete start" @click.prevent ="setCurrentTime(0)" > { { currentTimeFormatted } } < / a >
2019-12-26 10:38:26 +00:00
< span class = "right floated timer total" > { { durationFormatted } } < / span >
< / template >
< template v-else >
< span class = "left floated timer" > 00 : 00 < / span >
< span class = "right floated timer" > 00 : 00 < / span >
< / template >
< / div >
< / div >
< div class = "player-controls tablet-and-below" >
< template >
< span
role = "button"
: title = "labels.previousTrack"
: aria - label = "labels.previousTrack"
class = "control"
@ click . prevent . stop = "$store.dispatch('queue/previous')"
: disabled = "emptyQueue" >
< i : class = "['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" > < / i >
< / span >
< span
role = "button"
v - if = "!playing"
: title = "labels.play"
: aria - label = "labels.play"
@ click . prevent . stop = "togglePlay"
class = "control" >
< i : class = "['ui', 'play', {'disabled': !currentTrack}, 'icon']" > < / i >
< / span >
< span
role = "button"
v - else
: title = "labels.pause"
: aria - label = "labels.pause"
@ click . prevent . stop = "togglePlay"
class = "control" >
< i : class = "['ui', 'pause', {'disabled': !currentTrack}, 'icon']" > < / i >
< / span >
< span
role = "button"
: title = "labels.next"
: aria - label = "labels.next"
class = "control"
@ click . prevent . stop = "$store.dispatch('queue/next')"
: disabled = "!hasNext" >
< i : class = "['ui', {'disabled': !hasNext}, 'forward step', 'icon']" > < / i >
< / span >
< / template >
< / div >
< / template >
< / div >
< / div >
2020-05-15 12:12:36 +00:00
< div class = "ui ten wide column queue-column" >
2019-12-26 10:38:26 +00:00
< div class = "ui basic clearing fixed-header segment" >
< h2 class = "ui header" >
< div class = "content" >
< button
class = "ui right floated basic icon button"
@ click = "$store.dispatch('queue/clean')" >
< translate translate -context = " * / Queue / * / Verb " > Clear < / translate >
< / button >
{ { labels . queue } }
< div class = "sub header" >
< div >
< translate translate -context = " Sidebar / Queue / Text " : translate -params = " { index : queue.currentIndex + 1 , length : queue.tracks.length } " >
Track % { index } of % { length }
< / translate > < template v-if ="!$store.state.radios.running" > -
< span :title ="labels.duration" >
{ { timeLeft } }
< / span >
< / template >
< / div >
< / div >
< / div >
< / h2 >
< / div >
< table class = "ui compact very basic fixed single line selectable unstackable table" >
< draggable v-model ="tracks" tag="tbody" @update="reorder" handle=".handle" >
< tr
v - for = "(track, index) in tracks"
: key = "index"
: class = "['queue-item', {'active': index === queue.currentIndex}]" >
< td class = "handle" >
2020-05-15 12:12:36 +00:00
< i class = "grip lines icon" > < / i >
2019-12-26 10:38:26 +00:00
< / td >
< td class = "image-cell" @ click = "$store.dispatch('queue/currentIndex', index)" >
2020-08-03 13:47:14 +00:00
< img class = "ui mini image" alt = "" v-if ="track.cover && track.cover.urls.original" :src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)" >
< img class = "ui mini image" alt = "" v -else -if = " track.album & & track.album.cover & & track.album.cover.urls.original " :src ="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)" >
2020-08-01 17:07:06 +00:00
< img class = "ui mini image" alt = "" v -else src = "../assets/audio/default-cover.png" >
2019-12-26 10:38:26 +00:00
< / td >
< td colspan = "3" @ click = "$store.dispatch('queue/currentIndex', index)" >
< button class = "title reset ellipsis" :title ="track.title" :aria-label ="labels.selectTrack" >
< strong > { { track . title } } < / strong > < br / >
< span >
{ { track . artist . name } }
< / span >
< / button >
< / td >
< td class = "duration-cell" >
< template v-if ="track.uploads.length > 0" >
{ { time . durationFormatted ( track . uploads [ 0 ] . duration ) } }
< / template >
< / td >
< td class = "controls" >
< template v-if ="$store.getters['favorites/isFavorite'](track.id)" >
< i class = "pink heart icon" > < / i >
< / template >
2020-08-01 09:11:51 +00:00
< button :aria-label ="labels.removeFromQueue" :title ="labels.removeFromQueue" @click.stop ="cleanTrack(index)" : class = "['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']" >
2019-12-26 10:38:26 +00:00
< i class = "x icon" > < / i >
< / button >
< / td >
< / tr >
< / draggable >
< / table >
2020-03-26 08:44:31 +00:00
< div v-if ="$store.state.radios.running" class="ui info message" >
< div class = "content" >
2020-07-03 12:20:47 +00:00
< h3 class = "header" >
2020-03-26 08:44:31 +00:00
< i class = "feed icon" > < / i > < translate translate -context = " Sidebar / Player / Title " > You have a radio playing < / translate >
2020-07-03 12:20:47 +00:00
< / h3 >
2020-03-26 08:44:31 +00:00
< p > < translate translate -context = " Sidebar / Player / Paragraph " > New tracks will be appended here automatically . < / translate > < / p >
2020-08-04 11:22:31 +00:00
< button @click ="$store.dispatch('radios/stop')" class = "ui basic primary button" > < translate translate -context = " * / Player / Button.Label / Short , Verb " > Stop radio < / translate > < / button >
2020-03-26 08:44:31 +00:00
< / div >
< / div >
2019-12-26 10:38:26 +00:00
< / div >
< / div >
< / div >
< / div >
< / section >
< / template >
< script >
import { mapState , mapGetters , mapActions } from "vuex"
import $ from 'jquery'
import moment from "moment"
import lodash from '@/lodash'
import time from "@/utils/time"
2020-07-03 14:07:44 +00:00
import createFocusTrap from 'focus-trap'
2019-12-26 10:38:26 +00:00
import store from "@/store"
export default {
components : {
TrackFavoriteIcon : ( ) => import ( /* webpackChunkName: "auth-audio" */ "@/components/favorites/TrackFavoriteIcon" ) ,
TrackPlaylistIcon : ( ) => import ( /* webpackChunkName: "auth-audio" */ "@/components/playlists/TrackPlaylistIcon" ) ,
VolumeControl : ( ) => import ( /* webpackChunkName: "audio" */ "@/components/audio/VolumeControl" ) ,
draggable : ( ) => import ( /* webpackChunkName: "draggable" */ "vuedraggable" ) ,
} ,
data ( ) {
return {
showVolume : false ,
isShuffling : false ,
tracksChangeBuffer : null ,
2020-07-03 14:07:44 +00:00
focusTrap : null ,
2019-12-26 10:38:26 +00:00
time
}
} ,
mounted ( ) {
let self = this
2020-07-03 14:07:44 +00:00
this . focusTrap = createFocusTrap ( this . $el , { allowOutsideClick : ( ) => { return true } } )
this . focusTrap . activate ( )
2019-12-26 10:38:26 +00:00
this . $nextTick ( ( ) => {
setTimeout ( ( ) => {
this . scrollToCurrent ( )
// delay is to let transition work
} , 400 ) ;
} )
} ,
computed : {
... mapState ( {
currentIndex : state => state . queue . currentIndex ,
playing : state => state . player . playing ,
isLoadingAudio : state => state . player . isLoadingAudio ,
volume : state => state . player . volume ,
looping : state => state . player . looping ,
duration : state => state . player . duration ,
bufferProgress : state => state . player . bufferProgress ,
errored : state => state . player . errored ,
currentTime : state => state . player . currentTime ,
queue : state => state . queue
} ) ,
... mapGetters ( {
currentTrack : "queue/currentTrack" ,
hasNext : "queue/hasNext" ,
emptyQueue : "queue/isEmpty" ,
durationFormatted : "player/durationFormatted" ,
currentTimeFormatted : "player/currentTimeFormatted" ,
progress : "player/progress"
} ) ,
tracks : {
get ( ) {
return this . $store . state . queue . tracks
} ,
set ( value ) {
this . tracksChangeBuffer = value
}
} ,
labels ( ) {
return {
queue : this . $pgettext ( '*/*/*' , 'Queue' ) ,
duration : this . $pgettext ( '*/*/*' , 'Duration' ) ,
2020-08-11 12:07:06 +00:00
addArtistContentFilter : this . $pgettext ( 'Sidebar/Player/Icon.Tooltip/Verb' , 'Hide content from this artist…' ) ,
restart : this . $pgettext ( '*/*/*' , 'Restart track' ) ,
2019-12-26 10:38:26 +00:00
}
} ,
timeLeft ( ) {
let seconds = lodash . sum (
this . queue . tracks . slice ( this . queue . currentIndex ) . map ( ( t ) => {
return ( t . uploads || [ ] ) . map ( ( u ) => {
return u . duration || 0
} ) [ 0 ] || 0
} )
)
return moment ( this . $store . state . ui . lastDate ) . add ( seconds , 'seconds' ) . fromNow ( true )
} ,
sliderVolume : {
get ( ) {
return this . volume
} ,
set ( v ) {
this . $store . commit ( "player/volume" , v )
}
} ,
playerFocused ( ) {
return this . $store . state . ui . queueFocused === 'player'
}
} ,
methods : {
... mapActions ( {
cleanTrack : "queue/cleanTrack" ,
mute : "player/mute" ,
unmute : "player/unmute" ,
clean : "queue/clean" ,
toggleMute : "player/toggleMute" ,
togglePlay : "player/togglePlay" ,
} ) ,
reorder : function ( event ) {
this . $store . commit ( "queue/reorder" , {
tracks : this . tracksChangeBuffer ,
oldIndex : event . oldIndex ,
newIndex : event . newIndex
} )
} ,
scrollToCurrent ( ) {
let current = $ ( this . $el ) . find ( '.queue-item.active' ) [ 0 ]
if ( ! current ) {
return
}
const elementRect = current . getBoundingClientRect ( ) ;
const absoluteElementTop = elementRect . top + window . pageYOffset ;
const middle = absoluteElementTop - ( window . innerHeight / 2 ) ;
window . scrollTo ( { top : middle , behaviour : 'smooth' } ) ;
} ,
touchProgress ( e ) {
let time
let target = this . $refs . progress
time = ( e . layerX / target . offsetWidth ) * this . duration
this . $emit ( 'touch-progress' , time )
} ,
shuffle ( ) {
let disabled = this . queue . tracks . length === 0
if ( this . isShuffling || disabled ) {
return
}
let self = this
let msg = this . $pgettext ( 'Content/Queue/Message' , "Queue shuffled!" )
this . isShuffling = true
setTimeout ( ( ) => {
self . $store . dispatch ( "queue/shuffle" , ( ) => {
self . isShuffling = false
self . $store . commit ( "ui/addMessage" , {
content : msg ,
date : new Date ( )
} )
} )
} , 100 )
} ,
} ,
watch : {
"$store.state.ui.queueFocused" : {
handler ( v ) {
if ( v === 'queue' ) {
this . $nextTick ( ( ) => {
this . scrollToCurrent ( )
} )
}
} ,
immediate : true
} ,
'$store.state.queue.currentIndex' : {
handler ( ) {
this . $nextTick ( ( ) => {
this . scrollToCurrent ( )
} )
} ,
} ,
'$store.state.queue.tracks' : {
handler ( v ) {
if ( ! v || v . length === 0 ) {
this . $store . commit ( 'ui/queueFocused' , null )
}
} ,
immediate : true
} ,
"$route.fullPath" ( ) {
this . $store . commit ( 'ui/queueFocused' , null )
}
}
}
< / script >