Add:Player queue for podcast episodes & autoplay next episode #603

This commit is contained in:
advplyr 2022-08-28 13:12:38 -05:00
parent 91e116969a
commit c0dd58a94e
12 changed files with 267 additions and 18 deletions

View file

@ -44,11 +44,14 @@
@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>
@ -66,6 +69,7 @@ export default {
isPlaying: false,
currentTime: 0,
showSleepTimerModal: false,
showPlayerQueueItemsModal: false,
sleepTimerSet: false,
sleepTimerTime: 0,
sleepTimerRemaining: 0,
@ -138,9 +142,35 @@ export default {
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) 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', this.playerQueueItems)
return
}
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)
@ -312,7 +342,8 @@ export default {
console.error('No Audio Ref')
}
},
sessionOpen(session) { // For opening session on init (temporarily unused)
sessionOpen(session) {
// For opening session on init (temporarily unused)
this.$store.commit('setMediaPlaying', {
libraryItem: session.libraryItem,
episodeId: session.episodeId
@ -376,7 +407,8 @@ export default {
if (!libraryItem) return
this.$store.commit('setMediaPlaying', {
libraryItem,
episodeId
episodeId,
queueItems: payload.queueItems || []
})
this.$nextTick(() => {
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()

View file

@ -18,7 +18,7 @@
</div>
</div>
<p v-if="!imageFailed" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
<p v-if="!imageFailed && showResolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
</div>
</template>
@ -31,7 +31,11 @@ export default {
default: 120
},
showOpenNewTab: Boolean,
bookCoverAspectRatio: Number
bookCoverAspectRatio: Number,
showResolution: {
type: Boolean,
default: true
}
},
data() {
return {

View file

@ -0,0 +1,98 @@
<template>
<div v-if="item" class="w-full flex items-center px-4 py-2" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
<covers-preview-cover :src="coverUrl" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
<div class="flex-grow px-2 py-1 queue-item-row-content truncate">
<p class="text-gray-200 text-sm truncate">{{ title }}</p>
<p class="text-gray-400 text-sm">{{ subtitle }}</p>
</div>
<div class="w-28">
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">Streaming</p>
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
<span class="material-icons text-success">play_arrow</span>
</button>
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
<span class="material-icons text-error">close</span>
</button>
</div>
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => {}
},
index: Number
},
data() {
return {
isHovering: false
}
},
computed: {
title() {
return this.item.title || ''
},
subtitle() {
return this.item.subtitle || ''
},
libraryItemId() {
return this.item.libraryItemId
},
episodeId() {
return this.item.episodeId
},
coverPath() {
return this.item.coverPath
},
coverUrl() {
if (!this.coverPath) return '/book_placeholder.jpg'
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId)
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
duration() {
return this.item.duration
},
durationPretty() {
if (!this.duration) return 'N/A'
return this.$elapsedPretty(this.duration)
},
isOpenInPlayer() {
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
},
wrapperClass() {
if (this.isOpenInPlayer) return 'bg-yellow-400 bg-opacity-10'
if (this.index % 2 === 0) return 'bg-gray-300 bg-opacity-5 hover:bg-opacity-10'
return 'bg-bg hover:bg-gray-300 hover:bg-opacity-10'
}
},
methods: {
mouseover() {
this.isHovering = true
},
mouseleave() {
this.isHovering = false
},
playClick() {
this.$emit('play', this.item)
},
removeClick() {
this.$emit('remove', this.item)
}
},
mounted() {}
}
</script>
<style scoped>
.queue-item-row-content {
max-width: calc(100% - 48px - 128px);
}
</style>

View file

@ -0,0 +1,56 @@
<template>
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="font-book text-3xl text-white truncate">Player Queue</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
<div v-if="show" class="w-full h-full">
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" />
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
libraryItemId: String
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
playerQueueItems() {
return this.$store.state.playerQueueItems || []
}
},
methods: {
playItem(item) {
this.$eventBus.$emit('play-item', {
libraryItemId: item.libraryItemId,
episodeId: item.episodeId || null,
queueItems: this.playerQueueItems
})
this.show = false
},
removeItem(item) {
const updatedQueue = this.playerQueueItems.filter((i) => {
if (!i.episodeId) return i.libraryItemId !== item.libraryItemId
return i.libraryItemId !== item.libraryItemId || i.episodeId !== item.episodeId
})
this.$store.commit('setPlayerQueueItems', updatedQueue)
}
}
}
</script>

View file

@ -27,6 +27,10 @@
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
</div>
</ui-tooltip>
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2xl sm:text-3xl">playlist_play</span>
</button>
</div>
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
@ -138,6 +142,9 @@ export default {
hasNextChapter() {
if (!this.chapters.length) return false
return this.currentChapterIndex < this.chapters.length - 1
},
playerQueueItems() {
return this.$store.state.playerQueueItems || []
}
},
methods: {

View file

@ -133,10 +133,7 @@ export default {
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: this.episode.id
})
this.$emit('play', this.episode)
}
},
toggleFinished(confirmed = false) {

View file

@ -11,7 +11,7 @@
</div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
<template v-for="episode in episodesSorted">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" />
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" />
</template>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
@ -91,6 +91,28 @@ export default {
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
}
},
playEpisode(episode) {
const queueItems = []
const episodeIndex = this.episodes.findIndex((e) => e.id === episode.id)
for (let i = episodeIndex; i < this.episodes.length; i++) {
const episode = this.episodes[i]
const audioFile = episode.audioFile
queueItems.push({
libraryItemId: this.libraryItem.id,
episodeId: episode.id,
title: episode.title,
subtitle: this.mediaMetadata.title,
duration: audioFile.duration || null,
coverPath: this.media.coverPath || null
})
}
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id,
episodeId: episode.id,
queueItems
})
},
removeEpisode(episode) {
this.episodesToRemove = [episode]
this.showPodcastRemoveModal = true