mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			279 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						|
  <div class="w-full h-dvh max-h-dvh overflow-hidden" :style="{ backgroundColor: coverRgb }">
 | 
						|
    <div class="w-screen h-screen absolute inset-0 pointer-events-none" style="background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)"></div>
 | 
						|
    <div class="absolute inset-0 w-screen h-dvh flex items-center justify-center z-10">
 | 
						|
      <div class="w-full p-2 sm:p-4 md:p-8">
 | 
						|
        <div v-if="!isMobileLandscape" :style="{ width: coverWidth + 'px', height: coverHeight + 'px' }" class="mx-auto overflow-hidden rounded-xl my-2">
 | 
						|
          <img ref="coverImg" :src="coverUrl" class="object-contain w-full h-full" @load="coverImageLoaded" />
 | 
						|
        </div>
 | 
						|
        <p class="text-2xl lg:text-3xl font-semibold text-center mb-1 line-clamp-2">{{ mediaItemShare.playbackSession.displayTitle || 'No title' }}</p>
 | 
						|
        <p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p>
 | 
						|
 | 
						|
        <div class="w-full pt-16">
 | 
						|
          <player-ui ref="audioPlayer" :chapters="chapters" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  </div>
 | 
						|
</template>
 | 
						|
 | 
						|
<script>
 | 
						|
import LocalAudioPlayer from '../../players/LocalAudioPlayer'
 | 
						|
import { FastAverageColor } from 'fast-average-color'
 | 
						|
 | 
						|
export default {
 | 
						|
  layout: 'blank',
 | 
						|
  async asyncData({ params, error, app, query }) {
 | 
						|
    let endpoint = `/public/share/${params.slug}`
 | 
						|
    if (query.t && !isNaN(query.t)) {
 | 
						|
      endpoint += `?t=${query.t}`
 | 
						|
    }
 | 
						|
    const mediaItemShare = await app.$axios.$get(endpoint, { timeout: 10000 }).catch((error) => {
 | 
						|
      console.error('Failed', error)
 | 
						|
      return null
 | 
						|
    })
 | 
						|
    if (!mediaItemShare) {
 | 
						|
      return error({ statusCode: 404, message: 'Media item not found or expired' })
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      mediaItemShare: mediaItemShare
 | 
						|
    }
 | 
						|
  },
 | 
						|
  data() {
 | 
						|
    return {
 | 
						|
      localAudioPlayer: new LocalAudioPlayer(),
 | 
						|
      playerState: null,
 | 
						|
      playInterval: null,
 | 
						|
      hasLoaded: false,
 | 
						|
      totalDuration: 0,
 | 
						|
      windowWidth: 0,
 | 
						|
      windowHeight: 0,
 | 
						|
      listeningTimeSinceSync: 0,
 | 
						|
      coverRgb: null,
 | 
						|
      coverBgIsLight: false
 | 
						|
    }
 | 
						|
  },
 | 
						|
  computed: {
 | 
						|
    playbackSession() {
 | 
						|
      return this.mediaItemShare.playbackSession
 | 
						|
    },
 | 
						|
    coverUrl() {
 | 
						|
      if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
 | 
						|
      if (process.env.NODE_ENV === 'development') {
 | 
						|
        return `http://localhost:3333/public/share/${this.mediaItemShare.slug}/cover`
 | 
						|
      }
 | 
						|
      return `/public/share/${this.mediaItemShare.slug}/cover`
 | 
						|
    },
 | 
						|
    audioTracks() {
 | 
						|
      return (this.playbackSession.audioTracks || []).map((track) => {
 | 
						|
        if (process.env.NODE_ENV === 'development') {
 | 
						|
          track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
 | 
						|
        }
 | 
						|
        track.relativeContentUrl = track.contentUrl
 | 
						|
        return track
 | 
						|
      })
 | 
						|
    },
 | 
						|
    isPlaying() {
 | 
						|
      return this.playerState === 'PLAYING'
 | 
						|
    },
 | 
						|
    isPaused() {
 | 
						|
      return !this.isPlaying
 | 
						|
    },
 | 
						|
    chapters() {
 | 
						|
      return this.playbackSession.chapters || []
 | 
						|
    },
 | 
						|
    coverAspectRatio() {
 | 
						|
      const coverAspectRatio = this.playbackSession.coverAspectRatio
 | 
						|
      return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
 | 
						|
    },
 | 
						|
    isMobileLandscape() {
 | 
						|
      return this.windowWidth > this.windowHeight && this.windowHeight < 450
 | 
						|
    },
 | 
						|
    coverWidth() {
 | 
						|
      const availableCoverWidth = Math.min(450, this.windowWidth - 32)
 | 
						|
      const availableCoverHeight = Math.min(450, this.windowHeight - 250)
 | 
						|
 | 
						|
      const mostCoverHeight = availableCoverWidth * this.coverAspectRatio
 | 
						|
      if (mostCoverHeight > availableCoverHeight) {
 | 
						|
        return availableCoverHeight / this.coverAspectRatio
 | 
						|
      }
 | 
						|
      return availableCoverWidth
 | 
						|
    },
 | 
						|
    coverHeight() {
 | 
						|
      return this.coverWidth * this.coverAspectRatio
 | 
						|
    }
 | 
						|
  },
 | 
						|
  methods: {
 | 
						|
    async coverImageLoaded(e) {
 | 
						|
      if (!this.playbackSession.coverPath) return
 | 
						|
      const fac = new FastAverageColor()
 | 
						|
      fac
 | 
						|
        .getColorAsync(e.target)
 | 
						|
        .then((color) => {
 | 
						|
          this.coverRgb = color.rgba
 | 
						|
          this.coverBgIsLight = color.isLight
 | 
						|
 | 
						|
          document.body.style.backgroundColor = color.hex
 | 
						|
        })
 | 
						|
        .catch((e) => {
 | 
						|
          console.log(e)
 | 
						|
        })
 | 
						|
    },
 | 
						|
    playPause() {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
      this.localAudioPlayer.playPause()
 | 
						|
    },
 | 
						|
    jumpForward() {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
      const currentTime = this.localAudioPlayer.getCurrentTime()
 | 
						|
      const duration = this.localAudioPlayer.getDuration()
 | 
						|
      this.seek(Math.min(currentTime + 10, duration))
 | 
						|
    },
 | 
						|
    jumpBackward() {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
      const currentTime = this.localAudioPlayer.getCurrentTime()
 | 
						|
      this.seek(Math.max(currentTime - 10, 0))
 | 
						|
    },
 | 
						|
    setVolume(volume) {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
      this.localAudioPlayer.setVolume(volume)
 | 
						|
    },
 | 
						|
    setPlaybackRate(playbackRate) {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
      this.localAudioPlayer.setPlaybackRate(playbackRate)
 | 
						|
    },
 | 
						|
    seek(time) {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
 | 
						|
      this.localAudioPlayer.seek(time, this.isPlaying)
 | 
						|
      this.setCurrentTime(time)
 | 
						|
    },
 | 
						|
    setCurrentTime(time) {
 | 
						|
      if (!this.$refs.audioPlayer) return
 | 
						|
 | 
						|
      // Update UI
 | 
						|
      this.$refs.audioPlayer.setCurrentTime(time)
 | 
						|
    },
 | 
						|
    setDuration() {
 | 
						|
      if (!this.localAudioPlayer) return
 | 
						|
      this.totalDuration = this.localAudioPlayer.getDuration()
 | 
						|
      if (this.$refs.audioPlayer) {
 | 
						|
        this.$refs.audioPlayer.setDuration(this.totalDuration)
 | 
						|
      }
 | 
						|
    },
 | 
						|
    sendProgressSync(currentTime) {
 | 
						|
      console.log('Sending progress sync for time', currentTime)
 | 
						|
      const progress = {
 | 
						|
        currentTime
 | 
						|
      }
 | 
						|
      this.$axios.$patch(`/public/share/${this.mediaItemShare.slug}/progress`, progress, { progress: false }).catch((error) => {
 | 
						|
        console.error('Failed to send progress sync', error)
 | 
						|
      })
 | 
						|
    },
 | 
						|
    startPlayInterval() {
 | 
						|
      let lastTick = Date.now()
 | 
						|
      clearInterval(this.playInterval)
 | 
						|
      this.playInterval = setInterval(() => {
 | 
						|
        if (!this.localAudioPlayer) return
 | 
						|
 | 
						|
        const currentTime = this.localAudioPlayer.getCurrentTime()
 | 
						|
        this.setCurrentTime(currentTime)
 | 
						|
        const exactTimeElapsed = (Date.now() - lastTick) / 1000
 | 
						|
        lastTick = Date.now()
 | 
						|
        this.listeningTimeSinceSync += exactTimeElapsed
 | 
						|
        if (this.listeningTimeSinceSync >= 30) {
 | 
						|
          this.listeningTimeSinceSync = 0
 | 
						|
          this.sendProgressSync(currentTime)
 | 
						|
        }
 | 
						|
      }, 1000)
 | 
						|
    },
 | 
						|
    stopPlayInterval() {
 | 
						|
      clearInterval(this.playInterval)
 | 
						|
      this.playInterval = null
 | 
						|
    },
 | 
						|
    playerStateChange(state) {
 | 
						|
      this.playerState = state
 | 
						|
      if (state === 'LOADED' || state === 'PLAYING') {
 | 
						|
        this.setDuration()
 | 
						|
      }
 | 
						|
      if (state === 'LOADED') {
 | 
						|
        this.hasLoaded = true
 | 
						|
      }
 | 
						|
      if (state === 'PLAYING') {
 | 
						|
        this.startPlayInterval()
 | 
						|
      } else {
 | 
						|
        this.stopPlayInterval()
 | 
						|
      }
 | 
						|
    },
 | 
						|
    playerTimeUpdate(time) {
 | 
						|
      this.setCurrentTime(time)
 | 
						|
    },
 | 
						|
    getHotkeyName(e) {
 | 
						|
      var keyCode = e.keyCode || e.which
 | 
						|
      if (!this.$keynames[keyCode]) {
 | 
						|
        // Unused hotkey
 | 
						|
        return null
 | 
						|
      }
 | 
						|
 | 
						|
      var keyName = this.$keynames[keyCode]
 | 
						|
      var name = keyName
 | 
						|
      if (e.shiftKey) name = 'Shift-' + keyName
 | 
						|
      if (process.env.NODE_ENV !== 'production') {
 | 
						|
        console.log('Hotkey command', name)
 | 
						|
      }
 | 
						|
      return name
 | 
						|
    },
 | 
						|
    keyDown(e) {
 | 
						|
      if (!this.localAudioPlayer || !this.hasLoaded) return
 | 
						|
 | 
						|
      var name = this.getHotkeyName(e)
 | 
						|
      if (!name) return
 | 
						|
 | 
						|
      // Playing audiobook
 | 
						|
      if (Object.values(this.$hotkeys.AudioPlayer).includes(name)) {
 | 
						|
        this.$eventBus.$emit('player-hotkey', name)
 | 
						|
        e.preventDefault()
 | 
						|
      }
 | 
						|
    },
 | 
						|
    resize() {
 | 
						|
      this.windowWidth = window.innerWidth
 | 
						|
      this.windowHeight = window.innerHeight
 | 
						|
    },
 | 
						|
    playerError(error) {
 | 
						|
      console.error('Player error', error)
 | 
						|
      this.$toast.error('Failed to play audio on device')
 | 
						|
    },
 | 
						|
    playerFinished() {
 | 
						|
      console.log('Player finished')
 | 
						|
    }
 | 
						|
  },
 | 
						|
  mounted() {
 | 
						|
    this.resize()
 | 
						|
    window.addEventListener('resize', this.resize)
 | 
						|
    window.addEventListener('keydown', this.keyDown)
 | 
						|
 | 
						|
    if (process.env.NODE_ENV === 'development') {
 | 
						|
      console.log('Loaded media item share', this.mediaItemShare)
 | 
						|
    }
 | 
						|
 | 
						|
    const startTime = this.playbackSession.currentTime || 0
 | 
						|
    this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false)
 | 
						|
    this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this))
 | 
						|
    this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
 | 
						|
    this.localAudioPlayer.on('error', this.playerError.bind(this))
 | 
						|
    this.localAudioPlayer.on('finished', this.playerFinished.bind(this))
 | 
						|
  },
 | 
						|
  beforeDestroy() {
 | 
						|
    window.removeEventListener('resize', this.resize)
 | 
						|
    window.removeEventListener('keydown', this.keyDown)
 | 
						|
 | 
						|
    this.localAudioPlayer.off('stateChange', this.playerStateChange.bind(this))
 | 
						|
    this.localAudioPlayer.off('timeupdate', this.playerTimeUpdate.bind(this))
 | 
						|
    this.localAudioPlayer.off('error', this.playerError.bind(this))
 | 
						|
    this.localAudioPlayer.off('finished', this.playerFinished.bind(this))
 | 
						|
    this.localAudioPlayer.destroy()
 | 
						|
  }
 | 
						|
}
 | 
						|
</script>
 |