Skip to content
Snippets Groups Projects
Queue.vue 18.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Georg Krause's avatar
    Georg Krause committed
      <section
        class="main with-background component-queue"
        :aria-label="labels.queue"
      >
    
        <div :class="['ui vertical stripe queue segment', playerFocused ? 'player-focused' : '']">
          <div class="ui fluid container">
    
    Georg Krause's avatar
    Georg Krause committed
            <div
              id="queue-grid"
              class="ui stackable grid"
            >
    
    Agate's avatar
    Agate committed
              <div class="ui six wide column current-track">
    
    Georg Krause's avatar
    Georg Krause committed
                <div
                  id="player"
                  class="ui basic segment"
                >
    
                  <template v-if="currentTrack">
    
    Georg Krause's avatar
    Georg Krause committed
                    <img
                      v-if="currentTrack.cover && currentTrack.cover.urls.large_square_crop"
                      ref="cover"
                      alt=""
                      :src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.large_square_crop)"
                    >
                    <img
                      v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.large_square_crop"
                      ref="cover"
                      alt=""
                      :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.large_square_crop)"
                    >
                    <img
                      v-else
                      class="ui image"
                      alt=""
                      src="../assets/audio/default-cover.png"
                    >
    
    Ciarán Ainsworth's avatar
    Ciarán Ainsworth committed
                      <div class="content ellipsis">
    
    Georg Krause's avatar
    Georg Krause committed
                        <router-link
                          class="small header discrete link track"
                          :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}"
                        >
    
    Ciarán Ainsworth's avatar
    Ciarán Ainsworth committed
                          {{ currentTrack.title }}
    
    Ciarán Ainsworth's avatar
    Ciarán Ainsworth committed
                        <div class="sub header ellipsis">
    
    Georg Krause's avatar
    Georg Krause committed
                          <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 }}
                            </router-link>
    
                          </template>
    
    Georg Krause's avatar
    Georg Krause committed
                    <div
                      v-if="currentTrack && errored"
                      class="ui small warning message"
                    >
    
                      <h3 class="header">
    
    Georg Krause's avatar
    Georg Krause committed
                        <translate translate-context="Sidebar/Player/Error message.Title">
                          The track cannot be loaded
                        </translate>
    
                      <p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
    
    Georg Krause's avatar
    Georg Krause committed
                        <translate translate-context="Sidebar/Player/Error message.Paragraph">
                          The next track will play automatically in a few seconds…
                        </translate>
                        <i class="loading spinner icon" />
    
    Georg Krause's avatar
    Georg Krause committed
                        <translate translate-context="Sidebar/Player/Error message.Paragraph">
                          You may have a connectivity issue.
                        </translate>
    
                    <div class="additional-controls tablet-and-below">
    
                      <track-favorite-icon
                        v-if="$store.state.auth.authenticated"
    
    Georg Krause's avatar
    Georg Krause committed
                        :track="currentTrack"
                      />
    
                      <track-playlist-icon
                        v-if="$store.state.auth.authenticated"
    
    Georg Krause's avatar
    Georg Krause committed
                        :track="currentTrack"
                      />
    
                      <button
                        v-if="$store.state.auth.authenticated"
    
                        :class="['ui', 'really', 'basic', 'circular', 'icon', 'button']"
    
                        :aria-label="labels.addArtistContentFilter"
    
    Georg Krause's avatar
    Georg Krause committed
                        :title="labels.addArtistContentFilter"
                        @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
                      >
                        <i :class="['eye slash outline', 'basic', 'icon']" />
    
                      </button>
                    </div>
                    <div class="progress-wrapper">
    
    Georg Krause's avatar
    Georg Krause committed
                      <div
                        v-if="currentTrack && !errored"
                        class="progress-area"
                      >
    
    Agate's avatar
    Agate committed
                          :class="['ui', 'small', 'vibrant', {'indicating': isLoadingAudio}, 'progress']"
    
    Georg Krause's avatar
    Georg Krause committed
                          @click="touchProgress"
                        >
                          <div
                            class="buffer bar"
                            :data-percent="bufferProgress"
                            :style="{ 'width': bufferProgress + '%' }"
                          />
                          <div
                            class="position bar"
                            :data-percent="progress"
                            :style="{ 'width': progress + '%' }"
                          />
    
    Georg Krause's avatar
    Georg Krause committed
                      <div
                        v-else
                        class="progress-area"
                      >
    
    Georg Krause's avatar
    Georg Krause committed
                          :class="['ui', 'small', 'vibrant', 'progress']"
                        >
                          <div class="buffer bar" />
                          <div class="position bar" />
    
                        </div>
                      </div>
                      <div class="progress">
                        <template v-if="!isLoadingAudio">
    
    Georg Krause's avatar
    Georg Krause committed
                          <a
                            href=""
                            :aria-label="labels.restart"
                            class="left floated timer discrete start"
                            @click.prevent="setCurrentTime(0)"
                          >{{ currentTimeFormatted }}</a>
                          <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">
    
    Georg Krause's avatar
    Georg Krause committed
                      <span
                        role="button"
                        :title="labels.previousTrack"
                        :aria-label="labels.previousTrack"
                        class="control"
                        :disabled="emptyQueue"
                        @click.prevent.stop="$store.dispatch('queue/previous')"
                      >
                        <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']" />
                      </span>
    
    Georg Krause's avatar
    Georg Krause committed
                      <span
                        v-if="!playing"
                        role="button"
                        :title="labels.play"
                        :aria-label="labels.play"
                        class="control"
                        @click.prevent.stop="resumePlayback"
                      >
                        <i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']" />
                      </span>
                      <span
                        v-else
                        role="button"
                        :title="labels.pause"
                        :aria-label="labels.pause"
                        class="control"
                        @click.prevent.stop="pausePlayback"
                      >
                        <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']" />
                      </span>
                      <span
                        role="button"
                        :title="labels.next"
                        :aria-label="labels.next"
                        class="control"
                        :disabled="!hasNext"
                        @click.prevent.stop="$store.dispatch('queue/next')"
                      >
                        <i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" />
                      </span>
    
    Agate's avatar
    Agate committed
              <div class="ui ten wide column queue-column">
    
                <div class="ui basic clearing fixed-header segment">
                  <h2 class="ui header">
                    <div class="content">
                      <button
    
    rrrnld's avatar
    rrrnld committed
                        class="ui right floated basic button"
    
    Georg Krause's avatar
    Georg Krause committed
                        @click="$store.commit('ui/queueFocused', null)"
                      >
                        <translate translate-context="*/Queue/*/Verb">
                          Close
                        </translate>
    
    rrrnld's avatar
    rrrnld committed
                      </button>
                      <button
                        class="ui right floated basic button danger"
    
    Georg Krause's avatar
    Georg Krause committed
                        @click="$store.dispatch('queue/clean')"
                      >
                        <translate translate-context="*/Queue/*/Verb">
                          Clear
                        </translate>
    
                      </button>
                      {{ labels.queue }}
                      <div class="sub header">
                        <div>
    
    Georg Krause's avatar
    Georg Krause committed
                          <translate
                            translate-context="Sidebar/Queue/Text"
                            :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}"
                          >
    
                            Track %{ index } of %{ length }
    
    Georg Krause's avatar
    Georg Krause committed
                          </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">
    
    Georg Krause's avatar
    Georg Krause committed
                  <draggable
                    v-model="tracks"
                    tag="tbody"
                    handle=".handle"
                    @update="reorder"
                  >
    
                    <tr
                      v-for="(track, index) in tracks"
                      :key="index"
    
    Georg Krause's avatar
    Georg Krause committed
                      :class="['queue-item', {'active': index === queue.currentIndex}]"
                    >
    
    Georg Krause's avatar
    Georg Krause committed
                        <i class="grip lines icon" />
    
    Georg Krause's avatar
    Georg Krause committed
                      <td
                        class="image-cell"
                        @click="$store.dispatch('queue/currentIndex', index)"
                      >
                        <img
                          v-if="track.cover && track.cover.urls.original"
                          class="ui mini image"
                          alt=""
                          :src="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
                        >
                        <img
                          v-else-if="track.album && track.album.cover && track.album.cover.urls.original"
                          class="ui mini image"
                          alt=""
                          :src="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
                        >
                        <img
                          v-else
                          class="ui mini image"
                          alt=""
                          src="../assets/audio/default-cover.png"
                        >
    
    Georg Krause's avatar
    Georg Krause committed
                      <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)">
    
    Georg Krause's avatar
    Georg Krause committed
                          <i class="pink heart icon" />
    
    Georg Krause's avatar
    Georg Krause committed
                        <button
                          :aria-label="labels.removeFromQueue"
                          :title="labels.removeFromQueue"
                          :class="['ui', 'really', 'tiny', 'basic', 'circular', 'icon', 'button']"
                          @click.stop="cleanTrack(index)"
                        >
                          <i class="x icon" />
    
    Georg Krause's avatar
    Georg Krause committed
                <div
                  v-if="$store.state.radios.running"
                  class="ui info message"
                >
    
                  <div class="content">
    
                    <h3 class="header">
    
    Georg Krause's avatar
    Georg Krause committed
                      <i class="feed icon" /> <translate translate-context="Sidebar/Player/Title">
                        You have a radio playing
                      </translate>
    
    Georg Krause's avatar
    Georg Krause committed
                    <p>
                      <translate translate-context="Sidebar/Player/Paragraph">
                        New tracks will be appended here automatically.
                      </translate>
                    </p>
                    <button
                      class="ui basic primary button"
                      @click="$store.dispatch('radios/stop')"
                    >
                      <translate translate-context="*/Player/Button.Label/Short, Verb">
                        Stop radio
                      </translate>
                    </button>
    
              </div>
            </div>
          </div>
        </div>
      </section>
    </template>
    <script>
    
    import { mapState, mapGetters, mapActions } from 'vuex'
    
    import moment from 'moment'
    
    import lodash from '@/lodash'
    
    Ciaran Ainsworth's avatar
    Ciaran Ainsworth committed
    import time from '@/utils/time.js'
    
    import { createFocusTrap } from 'focus-trap'
    
    Georg Krause's avatar
    Georg Krause committed
        TrackFavoriteIcon: () =>
        import('@/components/favorites/TrackFavoriteIcon.vue'),
        TrackPlaylistIcon: () =>
        import('@/components/playlists/TrackPlaylistIcon.vue'),
        draggable: () => import('vuedraggable')
    
      },
      data () {
        return {
          showVolume: false,
          isShuffling: false,
          tracksChangeBuffer: null,
    
          time
        }
      },
      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'
    
            return this.$store.state.queue.tracks
          },
    
          set (value) {
    
            this.tracksChangeBuffer = value
          }
        },
        labels () {
          return {
            queue: this.$pgettext('*/*/*', 'Queue'),
            duration: this.$pgettext('*/*/*', 'Duration'),
    
            addArtistContentFilter: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…'),
    
            restart: this.$pgettext('*/*/*', 'Restart track')
    
          const 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'
        }
      },
    
    Georg Krause's avatar
    Georg Krause committed
      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)
        }
      },
      mounted () {
        this.focusTrap = createFocusTrap(this.$el, { allowOutsideClick: () => { return true } })
        this.focusTrap.activate()
        this.$nextTick(() => {
          setTimeout(() => {
            this.scrollToCurrent()
            // delay is to let transition work
          }, 400)
        })
      },
    
          cleanTrack: 'queue/cleanTrack',
          mute: 'player/mute',
          unmute: 'player/unmute',
          clean: 'queue/clean',
          toggleMute: 'player/toggleMute',
          resumePlayback: 'player/resumePlayback',
          pausePlayback: 'player/pausePlayback'
    
        reorder: function (event) {
          this.$store.commit('queue/reorder', {
    
            tracks: this.tracksChangeBuffer,
            oldIndex: event.oldIndex,
            newIndex: event.newIndex
          })
        },
    
        scrollToCurrent () {
          const current = $(this.$el).find('.queue-item.active')[0]
    
          const elementRect = current.getBoundingClientRect()
          const absoluteElementTop = elementRect.top + window.pageYOffset
          const middle = absoluteElementTop - (window.innerHeight / 2)
          window.scrollTo({ top: middle, behaviour: 'smooth' })
    
        touchProgress (e) {
          const target = this.$refs.progress
          const time = (e.layerX / target.offsetWidth) * this.duration
    
          this.$emit('touch-progress', time)
        },
    
        shuffle () {
          const disabled = this.queue.tracks.length === 0
    
          if (this.isShuffling || disabled) {
            return
          }
    
          const self = this
          const msg = this.$pgettext('Content/Queue/Message', 'Queue shuffled!')
    
          this.isShuffling = true
          setTimeout(() => {
    
            self.$store.dispatch('queue/shuffle', () => {
    
              self.$store.commit('ui/addMessage', {