Skip to content
Snippets Groups Projects
Player.vue 17.6 KiB
Newer Older
  • Learn to ignore specific revisions
  •   <section class="ui inverted segment player-wrapper" :aria-label="labels.audioPlayer" :style="style">
    
            :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">
    
              <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"
    
                    :class="{'inverted': !$store.getters['favorites/isFavorite'](currentTrack.id)}"
    
                    :track="currentTrack"></track-favorite-icon>
                  <track-playlist-icon
                    v-if="$store.state.auth.authenticated"
    
                    :track="currentTrack"></track-playlist-icon>
    
          <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>
    
              <div v-if="!isLoadingAudio" class="right floated four wide column">
    
                <p class="timer total">{{durationFormatted}}</p>
              </div>
            </div>
    
            <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>
    
          <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's avatar
    Eliot Berriot committed
              :title="labels.previousTrack"
    
              class="two wide column control"
    
              :disabled="emptyQueue">
    
                <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'secondary', 'icon']"></i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
              :title="labels.play"
    
              :aria-label="labels.play"
              @click.prevent.stop="togglePlay"
    
                <i :class="['ui', 'play', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
              :title="labels.pause"
    
              :aria-label="labels.pause"
              @click.prevent.stop="togglePlay"
    
                <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
              :title="labels.next"
    
                <i :class="['ui', {'disabled': !hasNext}, 'forward step', 'secondary', 'icon']" ></i>
    
    R En's avatar
    R En committed
            <div
              class="wide column control volume-control"
              v-on:mouseover="showVolume = true"
              v-on:mouseleave="showVolume = false"
              v-bind:class="{ active : showVolume }">
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                :title="labels.unmute"
    
                :aria-label="labels.unmute"
                @click.prevent.stop="unmute">
                <i class="volume off secondary icon"></i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                :title="labels.mute"
    
                :aria-label="labels.mute"
                @click.prevent.stop="mute">
                <i class="volume down secondary icon"></i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                :title="labels.mute"
    
                :aria-label="labels.mute"
                @click.prevent.stop="mute">
                <i class="volume up secondary icon"></i>
    
    R En's avatar
    R En committed
              <input
                type="range"
                step="0.05"
                min="0"
                max="1"
                v-model="sliderVolume"
                v-if="showVolume" />
    
    R En's avatar
    R En committed
            <div class="two wide column control looping" v-if="!showVolume">
    
                :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>
    
                @click.prevent.stop="$store.commit('player/looping', 2)"
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                :title="labels.loopingSingle"
    
                :disabled="!currentTrack">
                <i
                  class="repeat secondary icon">
                  <span class="ui circular tiny orange label">1</span>
                </i>
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                :title="labels.loopingWhole"
    
                v-if="looping === 2"
                :disabled="!currentTrack"
    
                @click.prevent.stop="$store.commit('player/looping', 0)">
                <i
                  class="repeat orange secondary icon">
                </i>
    
              :disabled="queue.tracks.length === 0"
    
    Eliot Berriot's avatar
    Eliot Berriot committed
              :title="labels.shuffle"
    
    R En's avatar
    R En committed
              v-if="!showVolume"
    
    R En's avatar
    R En committed
              <div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
    
              <i v-else :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
    
    R En's avatar
    R En committed
            <div class="one wide column" v-if="!showVolume"></div>
    
              :disabled="queue.tracks.length === 0"
    
    Eliot Berriot's avatar
    Eliot Berriot committed
              :title="labels.clear"
    
    R En's avatar
    R En committed
              v-if="!showVolume"
    
              <i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
    
          <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"
            />
    
    import { mapState, mapGetters, mapActions } from "vuex"
    import GlobalEvents from "@/components/utils/global-events"
    import ColorThief from "@/vendor/color-thief"
    import { Howl } from "howler"
    
    import AudioTrack from "@/components/audio/Track"
    import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
    import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
    
        TrackFavoriteIcon,
    
      data() {
        let defaultAmbiantColors = [
          [46, 46, 46],
          [46, 46, 46],
          [46, 46, 46],
          [46, 46, 46]
        ]
    
          isShuffling: false,
    
          sliderVolume: this.volume,
    
          defaultAmbiantColors: defaultAmbiantColors,
    
    R En's avatar
    R En committed
          showVolume: false,
    
          ambiantColors: defaultAmbiantColors,
          audioKey: String(new Date()),
          dummyAudio: null
    
        // we trigger the watcher explicitely it does not work otherwise
    
        this.sliderVolume = this.volume
    
        // 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,
    
          togglePlay: "player/togglePlay",
          mute: "player/mute",
          unmute: "player/unmute",
          clean: "queue/clean",
          updateProgress: "player/updateProgress"
    
    R En's avatar
    R En committed
          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)
        },
    
          let self = this
    
          this.$store.dispatch("queue/next").then(() => {
            self.$emit("next")
    
          let self = this
    
          this.$store.dispatch("queue/previous").then(() => {
            self.$emit("previous")
    
          time = (e.layerX / target.offsetWidth) * this.duration
    
          this.$refs.currentAudio.setCurrentTime(time)
    
          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)
    
        handleError({ sound, error }) {
          this.$store.commit("player/isLoadingAudio", false)
          this.$store.dispatch("player/trackErrored")
    
        ...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,
    
          queue: state => state.queue
        }),
        ...mapGetters({
    
          currentTrack: "queue/currentTrack",
          hasNext: "queue/hasNext",
          emptyQueue: "queue/isEmpty",
          durationFormatted: "player/durationFormatted",
          currentTimeFormatted: "player/currentTimeFormatted",
          progress: "player/progress"
    
        labels() {
          let audioPlayer = this.$gettext("Media player")
          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")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
          return {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            previousTrack,
            play,
            pause,
            next,
            unmute,
            mute,
            loopingDisabled,
            loopingSingle,
            loopingWhole,
            shuffle,
            clear
          }
        },
    
            { 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(", ")
    
        currentTrack(newValue, oldValue) {
    
          if (!this.isShuffling && newValue != oldValue) {
    
            this.ambiantColors = this.defaultAmbiantColors
          }
        },
    
        sliderVolume(newValue) {
          this.$store.commit("player/volume", newValue)
    
        }
      }
    }
    </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%;
    
    
    .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;
    
    R En's avatar
    R En committed
      width: 12.5% !important;
    
    R En's avatar
    R En committed
        max-width: 70%;
    
    R En's avatar
    R En committed
        bottom: 1.1rem;
        left: 25%;
    
      input[type="range"]::-webkit-slider-runnable-track {
    
      input[type="range"]::-webkit-slider-thumb {
    
        background: white;
        cursor: pointer;
        -webkit-appearance: none;
    
    R En's avatar
    R En committed
        border-radius: 3px;
        width: 10px;
    
      input[type="range"]::-moz-range-track {
    
        cursor: pointer;
        background: white;
    
    R En's avatar
    R En committed
        opacity: 0.3;
    
      input[type="range"]::-moz-focus-outer {
    
        border: 0;
      }
    
      input[type="range"]::-moz-range-thumb {
    
        background: white;
        cursor: pointer;
    
    R En's avatar
    R En committed
        border-radius: 3px;
        width: 10px;
    
        cursor: pointer;
        background: transparent;
        border-color: transparent;
        color: transparent;
      }
    
      input[type="range"]::-ms-fill-lower {
    
    R En's avatar
    R En committed
        opacity: 0.3;
    
      input[type="range"]::-ms-fill-upper {
    
    R En's avatar
    R En committed
        opacity: 0.3;
    
        background: white;
        cursor: pointer;
    
    R En's avatar
    R En committed
        border-radius: 3px;
        width: 10px;
    
      input[type="range"]:focus::-ms-fill-lower {
    
      input[type="range"]:focus::-ms-fill-upper {
    
    R En's avatar
    R En committed
    }
    
    .active.volume-control {
      width: 60% !important;
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    }
    
    .looping.control {
      i {
        position: relative;
      }
      .label {
        position: absolute;
        font-size: 0.7rem;
        bottom: -0.7rem;
        right: -0.7rem;
    
    .shuffling.loader.inline {
      margin: 0;
    }
    
      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,
    
      animation-duration: 2s;
      animation-timing-function: linear;
      animation-iteration-count: infinite;