diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 2b64756c..e8fabfc0 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -44,11 +44,14 @@ @close="closePlayer" @showBookmarks="showBookmarks" @showSleepTimer="showSleepTimerModal = true" + @showPlayerQueueItems="showPlayerQueueItemsModal = true" /> + + @@ -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() diff --git a/client/components/covers/PreviewCover.vue b/client/components/covers/PreviewCover.vue index 4803d5b8..e937dc3c 100644 --- a/client/components/covers/PreviewCover.vue +++ b/client/components/covers/PreviewCover.vue @@ -18,7 +18,7 @@ -

{{ resolution }}

+

{{ resolution }}

@@ -31,7 +31,11 @@ export default { default: 120 }, showOpenNewTab: Boolean, - bookCoverAspectRatio: Number + bookCoverAspectRatio: Number, + showResolution: { + type: Boolean, + default: true + } }, data() { return { diff --git a/client/components/modals/player/QueueItemRow.vue b/client/components/modals/player/QueueItemRow.vue new file mode 100644 index 00000000..aa710df7 --- /dev/null +++ b/client/components/modals/player/QueueItemRow.vue @@ -0,0 +1,98 @@ + + + + + \ No newline at end of file diff --git a/client/components/modals/player/QueueItemsModal.vue b/client/components/modals/player/QueueItemsModal.vue new file mode 100644 index 00000000..b4ee5aef --- /dev/null +++ b/client/components/modals/player/QueueItemsModal.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index c23c9f9f..83189fa4 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -27,6 +27,10 @@ timelapse + + @@ -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: { diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/EpisodeTableRow.vue index 31509954..74f6c847 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/EpisodeTableRow.vue @@ -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) { diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue index 8e402ae0..26452313 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/EpisodesTable.vue @@ -11,7 +11,7 @@

No Episodes

@@ -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 diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 5b711ad8..fe4e6f01 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -12,7 +12,7 @@
-
+
play_circle_filled
@@ -128,7 +128,7 @@
- + play_arrow {{ isStreaming ? 'Playing' : 'Play' }} @@ -429,14 +429,14 @@ export default { message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`, callback: (confirmed) => { if (confirmed) { - this.startStream(bookmark.time) + this.playItem(bookmark.time) } }, type: 'yesNo' } this.$store.commit('globals/setConfirmPrompt', payload) } else { - this.startStream(bookmark.time) + this.playItem(bookmark.time) } this.showBookmarksModal = false }, @@ -515,21 +515,37 @@ export default { this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) }) }, - startStream(startTime = null) { + playItem(startTime = null) { var episodeId = null + const queueItems = [] if (this.isPodcast) { - var episode = this.podcastEpisodes.find((ep) => { + var episodeIndex = this.podcastEpisodes.findIndex((ep) => { var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id) return !podcastProgress || !podcastProgress.isFinished }) - if (!episode) episode = this.podcastEpisodes[0] - episodeId = episode.id + if (episodeIndex < 0) episodeIndex = 0 + + episodeId = this.podcastEpisodes[episodeIndex].id + + for (let i = episodeIndex; i < this.podcastEpisodes.length; i++) { + const episode = this.podcastEpisodes[i] + const audioFile = episode.audioFile + queueItems.push({ + libraryItemId: this.libraryItemId, + episodeId: episode.id, + title: episode.title, + subtitle: this.title, + duration: audioFile.duration || null, + coverPath: this.libraryItem.media.coverPath || null + }) + } } this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItem.id, episodeId, - startTime + startTime, + queueItems }) }, editClick() { diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js index f50c3aaf..43ce84fa 100644 --- a/client/players/PlayerHandler.js +++ b/client/players/PlayerHandler.js @@ -133,6 +133,8 @@ export default class PlayerHandler { // TODO: Add listening time between last sync and now? this.sendProgressSync(currentTime) + + this.ctx.mediaFinished(this.libraryItemId, this.episodeId) } playerStateChange(state) { diff --git a/client/store/globals.js b/client/store/globals.js index 8a5edc36..21a31d5a 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -46,6 +46,14 @@ export const getters = { return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` } return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` + }, + getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = '/book_placeholder.jpg') => { + if (!libraryItemId) return placeholder + var userToken = rootGetters['user/getToken'] + if (process.env.NODE_ENV !== 'production') { // Testing + return `http://localhost:3333/api/items/${libraryItemId}/cover?token=${userToken}` + } + return `/api/items/${libraryItemId}/cover?token=${userToken}` } } diff --git a/client/store/index.js b/client/store/index.js index df313576..1cff645d 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -9,6 +9,7 @@ export const state = () => ({ streamLibraryItem: null, streamEpisodeId: null, streamIsPlaying: false, + playerQueueItems: [], playerIsFullscreen: false, editModalTab: 'details', showEditModal: false, @@ -144,14 +145,19 @@ export const mutations = { state.streamLibraryItem = null state.streamEpisodeId = null state.streamIsPlaying = false + state.playerQueueItems = [] } else { state.streamLibraryItem = payload.libraryItem state.streamEpisodeId = payload.episodeId || null + state.playerQueueItems = payload.queueItems || [] } }, setIsPlaying(state, isPlaying) { state.streamIsPlaying = isPlaying }, + setPlayerQueueItems(state, items) { + state.playerQueueItems = items || [] + }, showEditModal(state, libraryItem) { state.editModalTab = 'details' state.selectedLibraryItem = libraryItem diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 6a1419d9..39092556 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -11,6 +11,7 @@ module.exports = { safelist: [ 'bg-success', 'bg-red-600', + 'bg-yellow-400', 'text-green-500', 'py-1.5', 'bg-info',