mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
fdfb07ff2c
Starting to use audiobookshelf, the function of some buttons weren't very clear to me and while some buttons have tooltips, others have not. This patch adds some additional tooltips to the user interface, further explaining some of the functionality.
452 lines
16 KiB
Vue
452 lines
16 KiB
Vue
<template>
|
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
|
<div id="videoDock" />
|
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
|
</nuxt-link>
|
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
|
<div>
|
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
|
{{ title }}
|
|
</nuxt-link>
|
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
|
<span class="material-icons text-sm">person</span>
|
|
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
|
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
|
</p>
|
|
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</p>
|
|
</div>
|
|
|
|
<div class="text-gray-400 flex items-center">
|
|
<span class="material-icons text-xs">schedule</span>
|
|
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow" />
|
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
|
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
|
</ui-tooltip>
|
|
</div>
|
|
<player-ui
|
|
ref="audioPlayer"
|
|
:chapters="chapters"
|
|
:paused="!isPlaying"
|
|
:loading="playerLoading"
|
|
:bookmarks="bookmarks"
|
|
:sleep-timer-set="sleepTimerSet"
|
|
:sleep-timer-remaining="sleepTimerRemaining"
|
|
:is-podcast="isPodcast"
|
|
@playPause="playPause"
|
|
@jumpForward="jumpForward"
|
|
@jumpBackward="jumpBackward"
|
|
@setVolume="setVolume"
|
|
@setPlaybackRate="setPlaybackRate"
|
|
@seek="seek"
|
|
@close="closePlayer"
|
|
@showBookmarks="showBookmarks"
|
|
@showSleepTimer="showSleepTimerModal = true"
|
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
|
/>
|
|
|
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
|
|
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
|
|
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import PlayerHandler from '@/players/PlayerHandler'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
playerHandler: new PlayerHandler(this),
|
|
totalDuration: 0,
|
|
showBookmarksModal: false,
|
|
bookmarkCurrentTime: 0,
|
|
playerLoading: false,
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
showSleepTimerModal: false,
|
|
showPlayerQueueItemsModal: false,
|
|
sleepTimerSet: false,
|
|
sleepTimerTime: 0,
|
|
sleepTimerRemaining: 0,
|
|
sleepTimer: null,
|
|
displayTitle: null,
|
|
initialPlaybackRate: 1,
|
|
syncFailedToast: null
|
|
}
|
|
},
|
|
computed: {
|
|
coverAspectRatio() {
|
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
|
},
|
|
bookCoverWidth() {
|
|
return 88
|
|
},
|
|
bookCoverPosTop() {
|
|
if (this.coverAspectRatio == 1) return -10
|
|
return -64
|
|
},
|
|
cover() {
|
|
if (this.media.coverPath) return this.media.coverPath
|
|
return 'Logo.png'
|
|
},
|
|
user() {
|
|
return this.$store.state.user.user
|
|
},
|
|
userMediaProgress() {
|
|
if (!this.libraryItemId) return
|
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
|
},
|
|
userItemCurrentTime() {
|
|
return this.userMediaProgress ? this.userMediaProgress.currentTime || 0 : 0
|
|
},
|
|
bookmarks() {
|
|
if (!this.libraryItemId) return []
|
|
return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId)
|
|
},
|
|
streamLibraryItem() {
|
|
return this.$store.state.streamLibraryItem
|
|
},
|
|
libraryItemId() {
|
|
return this.streamLibraryItem ? this.streamLibraryItem.id : null
|
|
},
|
|
media() {
|
|
return this.streamLibraryItem ? this.streamLibraryItem.media || {} : {}
|
|
},
|
|
isPodcast() {
|
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
|
},
|
|
mediaMetadata() {
|
|
return this.media.metadata || {}
|
|
},
|
|
chapters() {
|
|
return this.media.chapters || []
|
|
},
|
|
title() {
|
|
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
|
return this.mediaMetadata.title || 'No Title'
|
|
},
|
|
authors() {
|
|
return this.mediaMetadata.authors || []
|
|
},
|
|
libraryId() {
|
|
return this.streamLibraryItem ? this.streamLibraryItem.libraryId : null
|
|
},
|
|
totalDurationPretty() {
|
|
return this.$secondsToTimestamp(this.totalDuration)
|
|
},
|
|
podcastAuthor() {
|
|
if (!this.isPodcast) return null
|
|
return this.mediaMetadata.author || 'Unknown'
|
|
},
|
|
playerQueueItems() {
|
|
return this.$store.state.playerQueueItems || []
|
|
}
|
|
},
|
|
methods: {
|
|
mediaFinished(libraryItemId, episodeId) {
|
|
// Play next item in queue
|
|
if (!this.playerQueueItems.length || !this.$store.state.playerQueueAutoPlay) {
|
|
// TODO: Set media finished flag so play button will play next queue item
|
|
return
|
|
}
|
|
var currentQueueIndex = this.playerQueueItems.findIndex((i) => {
|
|
if (episodeId) return i.libraryItemId === libraryItemId && i.episodeId === episodeId
|
|
return i.libraryItemId === libraryItemId
|
|
})
|
|
if (currentQueueIndex < 0) {
|
|
console.error('Media finished not found in queue - using first in queue', this.playerQueueItems)
|
|
currentQueueIndex = -1
|
|
}
|
|
if (currentQueueIndex === this.playerQueueItems.length - 1) {
|
|
console.log('Finished last item in queue')
|
|
return
|
|
}
|
|
const nextItemInQueue = this.playerQueueItems[currentQueueIndex + 1]
|
|
if (nextItemInQueue) {
|
|
this.playLibraryItem({
|
|
libraryItemId: nextItemInQueue.libraryItemId,
|
|
episodeId: nextItemInQueue.episodeId || null,
|
|
queueItems: this.playerQueueItems
|
|
})
|
|
}
|
|
},
|
|
setPlaying(isPlaying) {
|
|
this.isPlaying = isPlaying
|
|
this.$store.commit('setIsPlaying', isPlaying)
|
|
this.updateMediaSessionPlaybackState()
|
|
},
|
|
setSleepTimer(seconds) {
|
|
this.sleepTimerSet = true
|
|
this.sleepTimerTime = seconds
|
|
this.sleepTimerRemaining = seconds
|
|
this.runSleepTimer()
|
|
this.showSleepTimerModal = false
|
|
},
|
|
runSleepTimer() {
|
|
var lastTick = Date.now()
|
|
clearInterval(this.sleepTimer)
|
|
this.sleepTimer = setInterval(() => {
|
|
var elapsed = Date.now() - lastTick
|
|
lastTick = Date.now()
|
|
this.sleepTimerRemaining -= elapsed / 1000
|
|
|
|
if (this.sleepTimerRemaining <= 0) {
|
|
this.clearSleepTimer()
|
|
this.playerHandler.pause()
|
|
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
|
}
|
|
}, 1000)
|
|
},
|
|
cancelSleepTimer() {
|
|
this.showSleepTimerModal = false
|
|
this.clearSleepTimer()
|
|
},
|
|
clearSleepTimer() {
|
|
clearInterval(this.sleepTimer)
|
|
this.sleepTimerRemaining = 0
|
|
this.sleepTimer = null
|
|
this.sleepTimerSet = false
|
|
},
|
|
incrementSleepTimer(amount) {
|
|
if (!this.sleepTimerSet) return
|
|
this.sleepTimerRemaining += amount
|
|
},
|
|
decrementSleepTimer(amount) {
|
|
if (this.sleepTimerRemaining < amount) {
|
|
this.sleepTimerRemaining = 3
|
|
return
|
|
}
|
|
this.sleepTimerRemaining = Math.max(0, this.sleepTimerRemaining - amount)
|
|
},
|
|
playPause() {
|
|
this.playerHandler.playPause()
|
|
},
|
|
jumpForward() {
|
|
this.playerHandler.jumpForward()
|
|
},
|
|
jumpBackward() {
|
|
this.playerHandler.jumpBackward()
|
|
},
|
|
setVolume(volume) {
|
|
this.playerHandler.setVolume(volume)
|
|
},
|
|
setPlaybackRate(playbackRate) {
|
|
this.initialPlaybackRate = playbackRate
|
|
this.playerHandler.setPlaybackRate(playbackRate)
|
|
},
|
|
seek(time) {
|
|
this.playerHandler.seek(time)
|
|
},
|
|
setCurrentTime(time) {
|
|
this.currentTime = time
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setCurrentTime(time)
|
|
}
|
|
},
|
|
setDuration(duration) {
|
|
this.totalDuration = duration
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setDuration(duration)
|
|
}
|
|
},
|
|
setBufferTime(buffertime) {
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setBufferTime(buffertime)
|
|
}
|
|
},
|
|
showBookmarks() {
|
|
this.bookmarkCurrentTime = this.currentTime
|
|
this.showBookmarksModal = true
|
|
},
|
|
selectBookmark(bookmark) {
|
|
this.seek(bookmark.time)
|
|
this.showBookmarksModal = false
|
|
},
|
|
closePlayer() {
|
|
this.playerHandler.closePlayer()
|
|
this.$store.commit('setMediaPlaying', null)
|
|
},
|
|
mediaSessionPlay() {
|
|
console.log('Media session play')
|
|
this.playerHandler.play()
|
|
},
|
|
mediaSessionPause() {
|
|
console.log('Media session pause')
|
|
this.playerHandler.pause()
|
|
},
|
|
mediaSessionStop() {
|
|
console.log('Media session stop')
|
|
this.playerHandler.pause()
|
|
},
|
|
mediaSessionSeekBackward() {
|
|
console.log('Media session seek backward')
|
|
this.playerHandler.jumpBackward()
|
|
},
|
|
mediaSessionSeekForward() {
|
|
console.log('Media session seek forward')
|
|
this.playerHandler.jumpForward()
|
|
},
|
|
mediaSessionSeekTo(e) {
|
|
console.log('Media session seek to', e)
|
|
if (e.seekTime !== null && !isNaN(e.seekTime)) {
|
|
this.playerHandler.seek(e.seekTime)
|
|
}
|
|
},
|
|
updateMediaSessionPlaybackState() {
|
|
if ('mediaSession' in navigator) {
|
|
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
|
}
|
|
},
|
|
setMediaSession() {
|
|
if (!this.streamLibraryItem) {
|
|
console.error('setMediaSession: No library item set')
|
|
return
|
|
}
|
|
|
|
if ('mediaSession' in navigator) {
|
|
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png')
|
|
const artwork = [
|
|
{
|
|
src: coverImageSrc
|
|
}
|
|
]
|
|
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title: this.title,
|
|
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
|
album: this.mediaMetadata.seriesName || '',
|
|
artwork
|
|
})
|
|
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
|
|
|
navigator.mediaSession.setActionHandler('play', this.mediaSessionPlay)
|
|
navigator.mediaSession.setActionHandler('pause', this.mediaSessionPause)
|
|
navigator.mediaSession.setActionHandler('stop', this.mediaSessionStop)
|
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
|
// navigator.mediaSession.setActionHandler('previoustrack')
|
|
// navigator.mediaSession.setActionHandler('nexttrack')
|
|
} else {
|
|
console.warn('Media session not available')
|
|
}
|
|
},
|
|
streamProgress(data) {
|
|
if (!data.numSegments) return
|
|
var chunks = data.chunks
|
|
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
|
} else {
|
|
console.error('No Audio Ref')
|
|
}
|
|
},
|
|
sessionOpen(session) {
|
|
// For opening session on init (temporarily unused)
|
|
this.$store.commit('setMediaPlaying', {
|
|
libraryItem: session.libraryItem,
|
|
episodeId: session.episodeId
|
|
})
|
|
this.playerHandler.prepareOpenSession(session, this.initialPlaybackRate)
|
|
},
|
|
streamOpen(session) {
|
|
console.log(`[StreamContainer] Stream session open`, session)
|
|
},
|
|
streamClosed(streamId) {
|
|
// Stream was closed from the server
|
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
|
console.warn('[StreamContainer] Closing stream due to request from server')
|
|
this.playerHandler.closePlayer()
|
|
}
|
|
},
|
|
streamReady() {
|
|
console.log(`[STREAM-CONTAINER] Stream Ready`)
|
|
if (this.$refs.audioPlayer) {
|
|
this.$refs.audioPlayer.setStreamReady()
|
|
} else {
|
|
console.error('No Audio Ref')
|
|
}
|
|
},
|
|
streamError(streamId) {
|
|
// Stream had critical error from the server
|
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
|
console.warn('[StreamContainer] Closing stream due to stream error from server')
|
|
this.playerHandler.closePlayer()
|
|
}
|
|
},
|
|
streamReset({ startTime, streamId }) {
|
|
this.playerHandler.resetStream(startTime, streamId)
|
|
},
|
|
castSessionActive(isActive) {
|
|
if (isActive && this.playerHandler.isPlayingLocalItem) {
|
|
// Cast session started switch to cast player
|
|
this.playerHandler.switchPlayer()
|
|
} else if (!isActive && this.playerHandler.isPlayingCastedItem) {
|
|
// Cast session ended switch to local player
|
|
this.playerHandler.switchPlayer()
|
|
}
|
|
},
|
|
async playLibraryItem(payload) {
|
|
var libraryItemId = payload.libraryItemId
|
|
var episodeId = payload.episodeId || null
|
|
|
|
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
|
this.seek(payload.startTime)
|
|
} else {
|
|
this.playerHandler.play()
|
|
}
|
|
return
|
|
}
|
|
|
|
var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
|
console.error('Failed to fetch full item', error)
|
|
return null
|
|
})
|
|
if (!libraryItem) return
|
|
this.$store.commit('setMediaPlaying', {
|
|
libraryItem,
|
|
episodeId,
|
|
queueItems: payload.queueItems || []
|
|
})
|
|
this.$nextTick(() => {
|
|
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
|
|
})
|
|
|
|
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate, payload.startTime)
|
|
},
|
|
pauseItem() {
|
|
this.playerHandler.pause()
|
|
},
|
|
showFailedProgressSyncs() {
|
|
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
|
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
|
}
|
|
},
|
|
mounted() {
|
|
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
|
this.$eventBus.$on('playback-seek', this.seek)
|
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
|
},
|
|
beforeDestroy() {
|
|
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
|
this.$eventBus.$off('playback-seek', this.seek)
|
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
#streamContainer {
|
|
box-shadow: 0px -6px 8px #1111113f;
|
|
}
|
|
</style> |