Player.vue 29.8 KB
Newer Older
1
<template>
2
  <section class="ui inverted segment player-wrapper" :aria-label="labels.audioPlayer" :style="style">
3
4
5
6
    <div class="player">
      <div v-if="currentTrack" class="track-area ui unstackable items">
        <div class="ui inverted item">
          <div class="ui tiny image">
7
            <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)">
8
            <img v-else src="../../assets/audio/default-cover.png">
9
          </div>
10
11
12
13
14
15
16
17
18
19
20
21
22
          <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">
23
24
              <track-favorite-icon
                v-if="$store.state.auth.authenticated"
25
                :class="{'inverted': !$store.getters['favorites/isFavorite'](currentTrack.id)}"
26
27
28
                :track="currentTrack"></track-favorite-icon>
              <track-playlist-icon
                v-if="$store.state.auth.authenticated"
29
                :class="['inverted']"
30
                :track="currentTrack"></track-playlist-icon>
31
32
33
34
35
36
37
38
              <button
                v-if="$store.state.auth.authenticated"
                @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
                :class="['ui', 'really', 'basic', 'circular', 'inverted', 'icon', 'button']"
                :aria-label="labels.addArtistContentFilter"
                :title="labels.addArtistContentFilter">
                <i :class="['eye slash outline', 'basic', 'icon']"></i>
              </button>
39
            </div>
40
41
42
          </div>
        </div>
      </div>
43
      <div class="progress-area" v-if="currentTrack && !errored">
44
45
        <div class="ui grid">
          <div class="left floated four wide column">
46
            <p class="timer start" @click="setCurrentTime(0)">{{currentTimeFormatted}}</p>
47
          </div>
48

49
          <div v-if="!isLoadingAudio" class="right floated four wide column">
50
51
52
            <p class="timer total">{{durationFormatted}}</p>
          </div>
        </div>
53
54
55
56
57
        <div
          ref="progress"
          :class="['ui', 'small', 'orange', 'inverted', {'indicating': isLoadingAudio}, 'progress']"
          @click="touchProgress">
          <div class="buffer bar" :data-percent="bufferProgress" :style="{ 'width': bufferProgress + '%' }"></div>
58
          <div class="position bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
59
60
        </div>
      </div>
61
62
      <div class="ui small warning message" v-if="currentTrack && errored">
        <div class="header">
63
          <translate translate-context="Sidebar/Player/Error message.Title">The track cannot be loaded</translate>
64
65
        </div>
        <p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
66
          <translate translate-context="Sidebar/Player/Error message.Paragraph">The next track will play automatically in a few seconds…</translate>
67
68
69
          <i class="loading spinner icon"></i>
        </p>
        <p>
70
          <translate translate-context="Sidebar/Player/Error message.Paragraph">You may have a connectivity issue.</translate>
71
72
        </p>
      </div>
73
      <div class="two wide column controls ui grid">
74
75
        <span
          role="button"
Eliot Berriot's avatar
Eliot Berriot committed
76
          :title="labels.previousTrack"
77
          :aria-label="labels.previousTrack"
78
          class="two wide column control"
79
          @click.prevent.stop="previous"
80
          :disabled="emptyQueue">
81
            <i :class="['ui', 'backward step', {'disabled': emptyQueue}, 'icon']"></i>
82
83
84
        </span>
        <span
          role="button"
85
          v-if="!playing"
Eliot Berriot's avatar
Eliot Berriot committed
86
          :title="labels.play"
87
88
          :aria-label="labels.play"
          @click.prevent.stop="togglePlay"
89
          class="two wide column control">
90
            <i :class="['ui', 'play', {'disabled': !currentTrack}, 'icon']"></i>
91
92
93
        </span>
        <span
          role="button"
94
          v-else
Eliot Berriot's avatar
Eliot Berriot committed
95
          :title="labels.pause"
96
97
          :aria-label="labels.pause"
          @click.prevent.stop="togglePlay"
98
          class="two wide column control">
99
            <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'icon']"></i>
100
101
102
        </span>
        <span
          role="button"
Eliot Berriot's avatar
Eliot Berriot committed
103
          :title="labels.next"
104
          :aria-label="labels.next"
105
          class="two wide column control"
106
          @click.prevent.stop="next"
107
          :disabled="!hasNext">
108
            <i :class="['ui', {'disabled': !hasNext}, 'forward step', 'icon']" ></i>
109
        </span>
RenonDis's avatar
RenonDis committed
110
111
112
113
114
        <div
          class="wide column control volume-control"
          v-on:mouseover="showVolume = true"
          v-on:mouseleave="showVolume = false"
          v-bind:class="{ active : showVolume }">
115
116
          <span
            role="button"
117
            v-if="volume === 0"
Eliot Berriot's avatar
Eliot Berriot committed
118
            :title="labels.unmute"
119
120
            :aria-label="labels.unmute"
            @click.prevent.stop="unmute">
121
            <i class="volume off icon"></i>
122
123
124
          </span>
          <span
            role="button"
125
            v-else-if="volume < 0.5"
Eliot Berriot's avatar
Eliot Berriot committed
126
            :title="labels.mute"
127
128
            :aria-label="labels.mute"
            @click.prevent.stop="mute">
129
            <i class="volume down icon"></i>
130
131
132
          </span>
          <span
            role="button"
133
            v-else
Eliot Berriot's avatar
Eliot Berriot committed
134
            :title="labels.mute"
135
136
            :aria-label="labels.mute"
            @click.prevent.stop="mute">
137
            <i class="volume up icon"></i>
138
          </span>
RenonDis's avatar
RenonDis committed
139
140
141
142
143
144
145
          <input
            type="range"
            step="0.05"
            min="0"
            max="1"
            v-model="sliderVolume"
            v-if="showVolume" />
146
        </div>
RenonDis's avatar
RenonDis committed
147
        <div class="two wide column control looping" v-if="!showVolume">
148
149
          <span
            role="button"
150
            v-if="looping === 0"
151
152
153
154
            :title="labels.loopingDisabled"
            :aria-label="labels.loopingDisabled"
            @click.prevent.stop="$store.commit('player/looping', 1)"
            :disabled="!currentTrack">
155
            <i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'icon']"></i>
156
157
158
          </span>
          <span
            role="button"
159
            @click.prevent.stop="$store.commit('player/looping', 2)"
Eliot Berriot's avatar
Eliot Berriot committed
160
            :title="labels.loopingSingle"
161
            :aria-label="labels.loopingSingle"
162
            v-if="looping === 1"
163
164
            :disabled="!currentTrack">
            <i
165
              class="repeat icon">
166
167
              <span class="ui circular tiny orange label">1</span>
            </i>
168
169
170
          </span>
          <span
            role="button"
Eliot Berriot's avatar
Eliot Berriot committed
171
            :title="labels.loopingWhole"
172
            :aria-label="labels.loopingWhole"
173
174
            v-if="looping === 2"
            :disabled="!currentTrack"
175
176
            @click.prevent.stop="$store.commit('player/looping', 0)">
            <i
177
              class="repeat orange icon">
178
            </i>
179
          </span>
180
        </div>
181
182
        <span
          role="button"
183
          :disabled="queue.tracks.length === 0"
Eliot Berriot's avatar
Eliot Berriot committed
184
          :title="labels.shuffle"
185
          :aria-label="labels.shuffle"
RenonDis's avatar
RenonDis committed
186
          v-if="!showVolume"
187
          @click.prevent.stop="shuffle()"
188
          class="two wide column control">
RenonDis's avatar
RenonDis committed
189
          <div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
190
          <i v-else :class="['ui', 'random', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
191
        </span>
RenonDis's avatar
RenonDis committed
192
        <div class="one wide column" v-if="!showVolume"></div>
193
194
        <span
          role="button"
195
          :disabled="queue.tracks.length === 0"
Eliot Berriot's avatar
Eliot Berriot committed
196
          :title="labels.clear"
197
          :aria-label="labels.clear"
RenonDis's avatar
RenonDis committed
198
          v-if="!showVolume"
199
          @click.prevent.stop="clean()"
200
          class="two wide column control">
201
          <i class="icons">
202
203
            <i :class="['ui', 'trash', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
            <i :class="['ui corner inverted', 'list', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
204
          </i>
205
        </span>
206
      </div>
207
208
      <GlobalEvents
        @keydown.space.prevent.exact="togglePlay"
209
210
211
212
213
214
215
216
217
        @keydown.ctrl.shift.left.prevent.exact="previous"
        @keydown.ctrl.shift.right.prevent.exact="next"
        @keydown.shift.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
        @keydown.shift.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
        @keydown.right.prevent.exact="seek (5)"
        @keydown.left.prevent.exact="seek (-5)"
        @keydown.shift.right.prevent.exact="seek (30)"
        @keydown.shift.left.prevent.exact="seek (-30)"
        @keydown.m.prevent.exact="toggleMute"
218
219
        @keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
        @keydown.s.prevent.exact="shuffle"
220
221
        @keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
        @keydown.q.prevent.exact="clean"
222
        />
223
    </div>
224
  </section>
225
226
227
</template>

<script>
228
229
230
231
import { mapState, mapGetters, mapActions } from "vuex"
import GlobalEvents from "@/components/utils/global-events"
import ColorThief from "@/vendor/color-thief"
import { Howl } from "howler"
232
import $ from 'jquery'
233
234
235
import _ from '@/lodash'
import url from '@/utils/url'
import axios from 'axios'
236

237
238
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
239
240
241

export default {
  components: {
242
    TrackFavoriteIcon,
243
    TrackPlaylistIcon,
244
    GlobalEvents,
245
  },
246
247
248
249
250
251
252
  data() {
    let defaultAmbiantColors = [
      [46, 46, 46],
      [46, 46, 46],
      [46, 46, 46],
      [46, 46, 46]
    ]
253
    return {
254
      isShuffling: false,
255
      sliderVolume: this.volume,
256
      defaultAmbiantColors: defaultAmbiantColors,
RenonDis's avatar
RenonDis committed
257
      showVolume: false,
258
      ambiantColors: defaultAmbiantColors,
259
260
261
262
263
264
265
266
      currentSound: null,
      dummyAudio: null,
      isUpdatingTime: false,
      sourceErrors: 0,
      progressInterval: null,
      maxPreloaded: 3,
      preloadDelay: 15,
      soundsCache: [],
267
      soundId: null,
268
      playTimeout: null,
269
270
      nextTrackPreloaded: false,
      nowPlayingTimeout: null,
271
272
    }
  },
273
  mounted() {
274
275
276
    this.$store.dispatch('player/updateProgress', 0)
    this.$store.commit('player/playing', false)
    this.$store.commit("player/isLoadingAudio", false)
Eliot Berriot's avatar
Eliot Berriot committed
277
    Howler.unload()  // clear existing cache, if any
278
    this.nextTrackPreloaded = false
279
    // we trigger the watcher explicitely it does not work otherwise
280
    this.sliderVolume = this.volume
281
282
283
284
285
286
    // 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,
287
      src: ["noop.webm", "noop.mp3"]
288
    })
289
290
291
    if (this.currentTrack) {
      this.getSound(this.currentTrack)
    }
292
  },
293
  beforeDestroy () {
294
    this.dummyAudio.unload()
295
    this.observeProgress(false)
296
  },
297
298
  destroyed() {
  },
299
  methods: {
300
    ...mapActions({
301
302
303
304
      togglePlay: "player/togglePlay",
      mute: "player/mute",
      unmute: "player/unmute",
      clean: "queue/clean",
305
      toggleMute: "player/toggleMute",
306
    }),
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
    async getTrackData (trackData) {
      let data = null
      if (!trackData.uploads.length || trackData.uploads.length === 0) {
        // we don't have upload informations for this track, we need to fetch it
        await axios.get(`tracks/${trackData.id}/`).then((response) => {
          data = response.data
        }, error => {
          data = null
        })
      } else {
        return trackData
      }
      if (data === null) {
        return
      }
      return data
    },
324
    shuffle() {
RenonDis's avatar
RenonDis committed
325
326
      let disabled = this.queue.tracks.length === 0
      if (this.isShuffling || disabled) {
327
328
329
        return
      }
      let self = this
Jo Vuit's avatar
Jo Vuit committed
330
      let msg = this.$pgettext('Content/Queue/Message', "Queue shuffled!")
331
332
      this.isShuffling = true
      setTimeout(() => {
333
        self.$store.dispatch("queue/shuffle", () => {
334
          self.isShuffling = false
335
          self.$store.commit("ui/addMessage", {
336
            content: msg,
337
338
339
340
341
            date: new Date()
          })
        })
      }, 100)
    },
342
    next() {
343
      let self = this
344
345
      this.$store.dispatch("queue/next").then(() => {
        self.$emit("next")
346
347
      })
    },
348
    previous() {
349
      let self = this
350
351
      this.$store.dispatch("queue/previous").then(() => {
        self.$emit("previous")
352
353
      })
    },
354
    touchProgress(e) {
355
356
      let time
      let target = this.$refs.progress
357
      time = (e.layerX / target.offsetWidth) * this.duration
358
      this.setCurrentTime(time)
359
    },
360
    updateBackground() {
361
362
      // delete existing canvas, if any
      $('canvas.color-thief').remove()
363
364
365
366
367
      if (!this.currentTrack.album.cover) {
        this.ambiantColors = this.defaultAmbiantColors
        return
      }
      let image = this.$refs.cover
368
369
370
371
372
      try {
        this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
      } catch (e) {
        console.log('Cannot generate player background from cover image, likely a cross-origin tainted canvas issue')
      }
373
    },
374
375
376
    handleError({ sound, error }) {
      this.$store.commit("player/isLoadingAudio", false)
      this.$store.dispatch("player/trackErrored")
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
    },
    getSound (trackData) {
      let cached = this.getSoundFromCache(trackData)
      if (cached) {
        return cached.sound
      }
      let srcs = this.getSrcs(trackData)
      let self = this
      let sound = new Howl({
        src: srcs.map((s) => { return s.url }),
        format: srcs.map((s) => { return s.type }),
        autoplay: false,
        loop: false,
        html5: true,
        preload: true,
        volume: this.volume,
        onend: function () {
          self.ended()
        },
        onunlock: function () {
397
          if (self.$store.state.player.playing) {
Eliot Berriot's avatar
Eliot Berriot committed
398
            self.soundId = self.sound.play(self.soundId)
399
400
401
402
403
404
405
406
407
408
409
410
411
          }
        },
        onload: function () {
          let sound = this
          let node = this._sounds[0]._node;
          node.addEventListener('progress', () => {
            if (sound != self.currentSound) {
              return
            }
            self.updateBuffer(node)
          })
        },
        onplay: function () {
412
413
414
415
416
          if (trackData.id === self.currentTrack.id) {
            self.nowPlayingTimeout = setTimeout(() => {
              self.$store.dispatch('player/nowPlaying', trackData)
            }, 5000)
          }
417
418
419
420
421
422
          self.$store.commit('player/isLoadingAudio', false)
          self.$store.commit('player/resetErrorCount')
          self.$store.commit('player/errored', false)
          self.$store.commit('player/duration', this.duration())
        },
        onloaderror: function (sound, error) {
423
          self.removeFromCache(this)
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
          if (this != self.currentSound) {
            return
          }
          console.log('Error while playing:', sound, error)
          self.handleError({sound, error})
        },
      })
      this.addSoundToCache(sound, trackData)
      return sound
    },
    getSrcs: function (trackData) {
      let sources = trackData.uploads.map(u => {
        return {
          type: u.extension,
          url: this.$store.getters['instance/absoluteUrl'](u.listen_url),
        }
      })
      // We always add a transcoded MP3 src at the end
      // because transcoding is expensive, but we want browsers that do
      // not support other codecs to be able to play it :)
      sources.push({
        type: 'mp3',
        url: url.updateQueryString(
          this.$store.getters['instance/absoluteUrl'](trackData.listen_url),
          'to',
          'mp3'
        )
      })
      if (this.$store.state.auth.authenticated) {
        // we need to send the token directly in url
        // so authentication can be checked by the backend
        // because for audio files we cannot use the regular Authentication
        // header
        sources.forEach(e => {
          e.url = url.updateQueryString(e.url, 'jwt', this.$store.state.auth.token)
        })
      }
      return sources
    },

    updateBuffer (node) {
      // from https://github.com/goldfire/howler.js/issues/752#issuecomment-372083163
      let range = 0;
      let bf = node.buffered;
      let time = node.currentTime;
      try {
        while(!(bf.start(range) <= time && time <= bf.end(range))) {
          range += 1;
        }
      } catch (IndexSizeError) {
        return
      }
      let loadPercentage
      let start =  bf.start(range)
      let end =  bf.end(range)
      if (range === 0) {
        // easy case, no user-seek
        let loadStartPercentage = start / node.duration;
        let loadEndPercentage = end / node.duration;
        loadPercentage = loadEndPercentage - loadStartPercentage;
      } else {
        let loaded = end - start
        let remainingToLoad = node.duration - start
        // user seeked a specific position in the audio, our progress must be
        // computed based on the remaining portion of the track
        loadPercentage = loaded / remainingToLoad;
      }
      if (loadPercentage * 100 === this.bufferProgress) {
        return
      }
      this.$store.commit('player/bufferProgress', loadPercentage * 100)
    },
    updateProgress: function () {
      this.isUpdatingTime = true
      if (this.currentSound && this.currentSound.state() === 'loaded') {
        let t = this.currentSound.seek()
        let d = this.currentSound.duration()
        this.$store.dispatch('player/updateProgress', t)
        this.updateBuffer(this.currentSound._sounds[0]._node)
        let toPreload = this.$store.state.queue.tracks[this.currentIndex + 1]
504
        if (!this.nextTrackPreloaded && toPreload && !this.getSoundFromCache(toPreload) && (t > this.preloadDelay || d - t < 30)) {
505
          this.getSound(toPreload)
506
          this.nextTrackPreloaded = true
507
508
509
        }
      }
    },
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
    seek (step) {
      if (step > 0) {
        // seek right
        if (this.currentTime + step < this.duration) {
        this.$store.dispatch('player/updateProgress', (this.currentTime + step))
        } else {
        this.next() // parenthesis where missing here
        }
      }
      else {
        // seek left
        let position = Math.max(this.currentTime + step, 0)
        this.$store.dispatch('player/updateProgress', position)
      }
    },
525
526
527
528
529
530
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
    observeProgress: function (enable) {
      let self = this
      if (enable) {
        if (self.progressInterval) {
          clearInterval(self.progressInterval)
        }
        self.progressInterval = setInterval(() => {
          self.updateProgress()
        }, 1000)
      } else {
        clearInterval(self.progressInterval)
      }
    },
    setCurrentTime (t) {
      if (t < 0 | t > this.duration) {
        return
      }
      if (!this.currentSound || !this.currentSound._sounds[0]) {
        return
      }
      if (t === this.currentSound.seek()) {
        return
      }
      if (t === 0) {
        this.updateProgressThrottled.cancel()
      }
      this.currentSound.seek(t)
    },
    ended: function () {
      let onlyTrack = this.$store.state.queue.tracks.length === 1
      if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
        this.currentSound.seek(0)
Eliot Berriot's avatar
Eliot Berriot committed
557
558
        this.$store.dispatch('player/updateProgress', 0)
        this.soundId = this.currentSound.play(this.soundId)
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
587
588
589
590
591
592
593
      } else {
        this.$store.dispatch('player/trackEnded', this.currentTrack)
      }
    },
    getSoundFromCache (trackData) {
      return this.soundsCache.filter((d) => {
        if (d.track.id !== trackData.id) {
          return false
        }

        return true
      })[0]
    },
    addSoundToCache (sound, trackData) {
      let data = {
        date: new Date(),
        track: trackData,
        sound: sound
      }
      this.soundsCache.push(data)
      this.checkCache()
    },
    checkCache () {
      let self = this
      let toKeep = []
      _.reverse(this.soundsCache).forEach((e) => {
        if (toKeep.length < self.maxPreloaded) {
          toKeep.push(e)
        } else {
          let src = e.sound._src
          e.sound.unload()
        }
      })
      this.soundsCache = _.reverse(toKeep)
    },
594
595
596
597
598
599
600
601
602
603
604
    removeFromCache (sound) {
      let toKeep = []
      this.soundsCache.forEach((e) => {
        if (e.sound === sound) {
          e.sound.unload()
        } else {
          toKeep.push(e)
        }
      })
      this.soundsCache = toKeep
    },
605
606
607
608
    async loadSound (newValue, oldValue) {
      let trackData = newValue
      let oldSound = this.currentSound
      if (oldSound && trackData !== oldValue) {
Eliot Berriot's avatar
Eliot Berriot committed
609
610
        oldSound.stop(this.soundId)
        this.soundId = null
611
612
613
614
615
616
617
618
619
620
621
622
      }
      if (!trackData) {
        return
      }
      if (!this.isShuffling && trackData != oldValue) {
        trackData = await this.getTrackData(trackData)
        if (trackData === null) {
          this.handleError({})
        }
        this.currentSound = this.getSound(trackData)
        this.$store.commit('player/isLoadingAudio', true)
        if (this.playing) {
Eliot Berriot's avatar
Eliot Berriot committed
623
          this.soundId = this.currentSound.play()
624
          this.$store.commit('player/errored', false)
625
          this.$store.commit('player/playing', true)
626
          this.$store.dispatch('player/updateProgress', 0)
627
628
629
          this.observeProgress(true)
        }
      }
630
631
632
    }
  },
  computed: {
633
634
635
    ...mapState({
      currentIndex: state => state.queue.currentIndex,
      playing: state => state.player.playing,
636
      isLoadingAudio: state => state.player.isLoadingAudio,
637
638
639
      volume: state => state.player.volume,
      looping: state => state.player.looping,
      duration: state => state.player.duration,
640
      bufferProgress: state => state.player.bufferProgress,
641
      errored: state => state.player.errored,
642
      currentTime: state => state.player.currentTime,
643
644
645
      queue: state => state.queue
    }),
    ...mapGetters({
646
647
648
649
650
651
      currentTrack: "queue/currentTrack",
      hasNext: "queue/hasNext",
      emptyQueue: "queue/isEmpty",
      durationFormatted: "player/durationFormatted",
      currentTimeFormatted: "player/currentTimeFormatted",
      progress: "player/progress"
652
    }),
653
654
655
    updateProgressThrottled () {
      return _.throttle(this.updateProgress, 250)
    },
656
    labels() {
Jo Vuit's avatar
Jo Vuit committed
657
658
659
660
661
662
663
      let audioPlayer = this.$pgettext('Sidebar/Player/Hidden text', "Media player")
      let previousTrack = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Previous track")
      let play = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Play track")
      let pause = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Pause track")
      let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track")
      let unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute")
      let mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute")
664
      let loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip',
665
666
        "Looping disabled. Click to switch to single-track looping."
      )
667
      let loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip',
668
669
        "Looping on a single track. Click to switch to whole queue looping."
      )
670
      let loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip',
671
672
        "Looping on whole queue. Click to disable looping."
      )
Jo Vuit's avatar
Jo Vuit committed
673
674
      let shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Shuffle your queue")
      let clear = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Clear your queue")
675
      let addArtistContentFilter = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…')
Eliot Berriot's avatar
Eliot Berriot committed
676
      return {
677
        audioPlayer,
Eliot Berriot's avatar
Eliot Berriot committed
678
679
680
681
682
683
684
685
686
687
        previousTrack,
        play,
        pause,
        next,
        unmute,
        mute,
        loopingDisabled,
        loopingSingle,
        loopingWhole,
        shuffle,
688
689
        clear,
        addArtistContentFilter,
Eliot Berriot's avatar
Eliot Berriot committed
690
691
      }
    },
692
    style: function() {
693
      let style = {
694
        background: this.ambiantGradiant
695
696
697
      }
      return style
    },
698
    ambiantGradiant: function() {
699
      let indexConf = [
700
701
702
703
        { 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 }
704
      ]
705
706
707
708
709
710
711
712
713
714
715
      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(", ")
716
      return gradients
717
    },
718
719
  },
  watch: {
720
721
    currentTrack: {
      async handler (newValue, oldValue) {
722
723
724
        if (this.nowPlayingTimeout) {
          clearTimeout(this.nowPlayingTimeout)
        }
Eliot Berriot's avatar
Eliot Berriot committed
725
726
727
        if (newValue === oldValue) {
          return
        }
728
        this.nextTrackPreloaded = false
729
730
731
732
        clearTimeout(this.playTimeout)
        let self = this
        if (this.currentSound) {
          this.currentSound.pause()
733
        }
734
735
736
737
738
739
740
        this.$store.commit("player/isLoadingAudio", true)
        this.playTimeout = setTimeout(async () => {
          await self.loadSound(newValue, oldValue)
          if (!newValue || !newValue.album.cover) {
            self.ambiantColors = self.defaultAmbiantColors
          }
        }, 500);
741
742
      },
      immediate: false
743
    },
744
    volume(newValue) {
745
      this.sliderVolume = newValue
746
747
748
      if (this.currentSound) {
        this.currentSound.volume(newValue)
      }
749
    },
750
751
    sliderVolume(newValue) {
      this.$store.commit("player/volume", newValue)
752
753
754
755
    },
    playing: async function (newValue) {
      if (this.currentSound) {
        if (newValue === true) {
Eliot Berriot's avatar
Eliot Berriot committed
756
          this.soundId = this.currentSound.play(this.soundId)
757
        } else {
758
759
760
          if (this.nowPlayingTimeout) {
            clearTimeout(this.nowPlayingTimeout)
          }
Eliot Berriot's avatar
Eliot Berriot committed
761
          this.currentSound.pause(this.soundId)
762
763
764
765
766
767
768
769
770
771
772
773
        }
      } else {
        await this.loadSound(this.currentTrack, null)
      }

      this.observeProgress(newValue)
    },
    currentTime (newValue) {
      if (!this.isUpdatingTime) {
        this.setCurrentTime(newValue)
      }
      this.isUpdatingTime = false
Eliot Berriot's avatar
Eliot Berriot committed
774
775
776
777
778
    },
    emptyQueue (newValue) {
      if (newValue) {
        Howler.unload()
      }
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
    }
  }
}
</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%;
803
    line-height: 1.2;
804
805
806
  }
}
.timer.total {
807
  text-align: right;
808
809
}
.timer.start {
810
  cursor: pointer;
811
812
}
.track-area {
813
  margin-top: 0;
814
815
816
817
  .header,
  .meta,
  .artist,
  .album {
818
819
820
    color: white !important;
  }
}
821
822
823
.controls a {
  color: white;
}
824
825
826
827
828
829
830
831
832
833
834

.controls .icon.big {
  cursor: pointer;
  font-size: 2em !important;
}

.controls .icon {
  cursor: pointer;
  vertical-align: middle;
}

835
.control .icon {
836
837
838
839
840
  font-size: 1.5em;
}
.progress-area .actions {
  text-align: center;
}
841
842
843
844
.ui.progress:not([data-percent]):not(.indeterminate)
  .bar.position:not(.buffer) {
  background: #ff851b;
}
845
846
.volume-control {
  position: relative;
RenonDis's avatar
RenonDis committed
847
  width: 12.5% !important;
848
  [type="range"] {
RenonDis's avatar
RenonDis committed
849
    max-width: 70%;
850
    position: absolute;
RenonDis's avatar
RenonDis committed
851
852
    bottom: 1.1rem;
    left: 25%;
853
    cursor: pointer;
854
    background-color: transparent;
Eliot Berriot's avatar
Eliot Berriot committed
855
  }
856
  input[type="range"]:focus {
857
858
    outline: none;
  }
859
  input[type="range"]::-webkit-slider-runnable-track {
860
861
    cursor: pointer;
  }
862
  input[type="range"]::-webkit-slider-thumb {
863
864
865
    background: white;
    cursor: pointer;
    -webkit-appearance: none;
RenonDis's avatar
RenonDis committed
866
867
    border-radius: 3px;
    width: 10px;
868
  }
869
  input[type="range"]::-moz-range-track {
870
871
    cursor: pointer;
    background: white;
RenonDis's avatar
RenonDis committed
872
    opacity: 0.3;
873
  }
874
  input[type="range"]::-moz-focus-outer {
Cherry's avatar
Cherry committed
875
876
    border: 0;
  }
877
  input[type="range"]::-moz-range-thumb {
878
879
    background: white;
    cursor: pointer;
RenonDis's avatar
RenonDis committed
880
881
    border-radius: 3px;
    width: 10px;
882
  }
883
  input[type="range"]::-ms-track {
884
885
886
887
888
    cursor: pointer;
    background: transparent;
    border-color: transparent;
    color: transparent;
  }
889
  input[type="range"]::-ms-fill-lower {
890
    background: white;
RenonDis's avatar
RenonDis committed
891
    opacity: 0.3;
892
  }
893
  input[type="range"]::-ms-fill-upper {
894
    background: white;
RenonDis's avatar
RenonDis committed
895
    opacity: 0.3;
896
  }
897
  input[type="range"]::-ms-thumb {
898
899
    background: white;
    cursor: pointer;
RenonDis's avatar
RenonDis committed
900
901
    border-radius: 3px;
    width: 10px;
902
  }
903
  input[type="range"]:focus::-ms-fill-lower {
904
905
    background: white;
  }
906
  input[type="range"]:focus::-ms-fill-upper {
907
    background: white;
908
  }
RenonDis's avatar
RenonDis committed
909
910
911
912
}

.active.volume-control {
  width: 60% !important;
Eliot Berriot's avatar
Eliot Berriot committed
913
914
915
916
917
918
919
920
921
922
923
}

.looping.control {
  i {
    position: relative;
  }
  .label {
    position: absolute;
    font-size: 0.7rem;
    bottom: -0.7rem;
    right: -0.7rem;
924
925
926
927
928
  }
}
.ui.feed.icon {
  margin: 0;
}
929
930
931
.shuffling.loader.inline {
  margin: 0;
}
932

933
@keyframes MOVE-BG {
934
935
936
937
938
939
  from {
    transform: translateX(0px);
  }
  to {
    transform: translateX(46px);
  }
940
941
942
943
944
945
946
947
948
949
950
951
}

.indicating.progress {
  overflow: hidden;
}

.ui.progress .bar {
  transition: none;
}

.ui.inverted.progress .buffer.bar {
  position: absolute;
952
  background-color: rgba(255, 255, 255, 0.15);
953
954
955
956
957
958
959
960
961
962
}
.indicating.progress .bar {
  left: -46px;
  width: 200% !important;
  color: grey;
  background: repeating-linear-gradient(
    -55deg,
    grey 1px,
    grey 10px,
    transparent 10px,
963
964
    transparent 20px
  ) !important;
965
966

  animation-name: MOVE-BG;
967
968
969
  animation-duration: 2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
970
}
971
972
973
974
975

.icons {
  position: absolute;
}

976
977
978
979
i.icons .corner.icon {
  font-size: 1em;
  right: -0.3em;
}
980
</style>