diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 1d41ca95..32c41224 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -477,6 +477,21 @@ export default { text: this.$strings.ButtonRemoveFromContinueListening }) } + if (!this.isPodcast) { + if (this.libraryItemIdStreaming && !this.isStreamingFromDifferentLibrary) { + if (!this.isQueued) { + items.push({ + func: 'addToQueue', + text: this.$strings.ButtonQueueAddItem + }) + } else if (!this.isStreaming) { + items.push({ + func: 'removeFromQueue', + text: this.$strings.ButtonQueueRemoveItem + }) + } + } + } return items }, _socket() { @@ -690,8 +705,9 @@ export default { }) }, addToQueue() { + var queueItem = {} if (this.recentEpisode) { - const queueItem = { + queueItem = { libraryItemId: this.libraryItemId, libraryId: this.libraryId, episodeId: this.recentEpisode.id, @@ -701,8 +717,19 @@ export default { duration: this.recentEpisode.audioFile.duration || null, coverPath: this.media.coverPath || null } - this.store.commit('addItemToQueue', queueItem) + } else { + queueItem = { + libraryItemId: this.libraryItemId, + libraryId: this.libraryId, + episodeId: null, + title: this.title, + subtitle: this.author, + caption: '', + duration: this.media.duration || null, + coverPath: this.media.coverPath || null + } } + this.store.commit('addItemToQueue', queueItem) }, removeFromQueue() { const episodeId = this.recentEpisode ? this.recentEpisode.id : null @@ -815,6 +842,18 @@ export default { } } } + } else { + const queueItem = { + libraryItemId: this.libraryItemId, + libraryId: this.libraryId, + episodeId: null, + title: this.title, + subtitle: this.author, + caption: '', + duration: this.media.duration || null, + coverPath: this.media.coverPath || null + } + queueItems.push(queueItem) } eventBus.$emit('play-item', { diff --git a/client/components/covers/PreviewCover.vue b/client/components/covers/PreviewCover.vue index e937dc3c..2707132a 100644 --- a/client/components/covers/PreviewCover.vue +++ b/client/components/covers/PreviewCover.vue @@ -4,7 +4,7 @@
- + open_in_new @@ -63,6 +63,9 @@ export default { }, resolution() { return `${this.naturalWidth}x${this.naturalHeight}px` + }, + placeholderUrl() { + return `${this.$config.routerBasePath}/book_placeholder.jpg` } }, methods: { @@ -72,7 +75,7 @@ export default { } }, imageLoaded() { - if (this.$refs.cover) { + if (this.$refs.cover && this.src !== this.placeholderUrl) { var { naturalWidth, naturalHeight } = this.$refs.cover this.naturalHeight = naturalHeight this.naturalWidth = naturalWidth diff --git a/client/components/modals/player/QueueItemRow.vue b/client/components/modals/player/QueueItemRow.vue index fcb2c788..9e92cc9f 100644 --- a/client/components/modals/player/QueueItemRow.vue +++ b/client/components/modals/player/QueueItemRow.vue @@ -7,7 +7,7 @@

{{ caption }}

-

Streaming

+

{{ $strings.ButtonPlaying }}

+
timelapse
- -
diff --git a/client/components/tables/ChaptersTable.vue b/client/components/tables/ChaptersTable.vue index e9c7e39d..d7e616ed 100644 --- a/client/components/tables/ChaptersTable.vue +++ b/client/components/tables/ChaptersTable.vue @@ -72,11 +72,23 @@ export default { this.expanded = !this.expanded }, goToTimestamp(time) { + const queueItem = { + libraryItemId: this.libraryItemId, + libraryId: this.libraryItem.libraryId, + episodeId: null, + title: this.metadata.title, + subtitle: this.metadata.authors.map((au) => au.name).join(', '), + caption: '', + duration: this.media.duration || null, + coverPath: this.media.coverPath || null + } + if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) { this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: null, - startTime: time + startTime: time, + queueItems: [queueItem] }) } else { const payload = { @@ -86,7 +98,8 @@ export default { this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: null, - startTime: time + startTime: time, + queueItems: [queueItem] }) } }, diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue index 0177b331..b56a7e42 100644 --- a/client/components/tables/collection/BookTableRow.vue +++ b/client/components/tables/collection/BookTableRow.vue @@ -137,8 +137,22 @@ export default { this.isHovering = false }, playClick() { + const queueItems = [ + { + libraryItemId: this.book.id, + libraryId: this.book.libraryId, + episodeId: null, + title: this.bookTitle, + subtitle: this.bookAuthors.map((au) => au.name).join(', '), + caption: '', + duration: this.media.duration || null, + coverPath: this.media.coverPath || null + } + ] + this.$eventBus.$emit('play-item', { - libraryItemId: this.book.id + libraryItemId: this.book.id, + queueItems }) }, clickEdit() { diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue index 976583d5..e402cf50 100644 --- a/client/pages/collection/_id.vue +++ b/client/pages/collection/_id.vue @@ -122,13 +122,42 @@ export default { } }, clickPlay() { - var nextBookNotRead = this.playableBooks.find((pb) => { - var prog = this.$store.getters['user/getUserMediaProgress'](pb.id) - return !prog || !prog.isFinished + const queueItems = [] + + // Collection queue will start at the first unfinished book + // if all books are finished then entire collection is queued + const itemsWithProgress = this.playableBooks.map((item) => { + return { + ...item, + progress: this.$store.getters['user/getUserMediaProgress'](item.id) + } }) - if (nextBookNotRead) { + + const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished) + if (!hasUnfinishedItems) { + console.warn('All items in collection are finished - starting at first item') + } + + for (let i = 0; i < itemsWithProgress.length; i++) { + const libraryItem = itemsWithProgress[i] + if (!hasUnfinishedItems || !libraryItem.progress || !libraryItem.progress.isFinished) { + queueItems.push({ + libraryItemId: libraryItem.id, + libraryId: libraryItem.libraryId, + episodeId: null, + title: libraryItem.media.metadata.title, + subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '), + caption: '', + duration: libraryItem.media.duration || null, + coverPath: libraryItem.media.coverPath || null + }) + } + } + + if (queueItems.length >= 0) { this.$eventBus.$emit('play-item', { - libraryItemId: nextBookNotRead.id + libraryItemId: queueItems[0].libraryItemId, + queueItems }) } } diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 3799714b..0ca72da7 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -127,12 +127,38 @@ export default { this.processingGoToTimestamp = false return } - if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) { + if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { this.$toast.error('Failed to get podcast episode') this.processingGoToTimestamp = false return } + var queueItem = {} + if (session.episodeId) { + var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId) + queueItem = { + libraryItemId: libraryItem.id, + libraryId: libraryItem.libraryId, + episodeId: episode.id, + title: episode.title, + subtitle: libraryItem.media.metadata.title, + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + duration: episode.audioFile.duration || null, + coverPath: libraryItem.media.coverPath || null + } + } else { + queueItem = { + libraryItemId: libraryItem.id, + libraryId: libraryItem.libraryId, + episodeId: null, + title: libraryItem.media.metadata.title, + subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '), + caption: '', + duration: libraryItem.media.duration || null, + coverPath: libraryItem.media.coverPath || null + } + } + const payload = { message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]), callback: (confirmed) => { @@ -140,7 +166,8 @@ export default { this.$eventBus.$emit('play-item', { libraryItemId: libraryItem.id, episodeId: session.episodeId || null, - startTime: session.currentTime + startTime: session.currentTime, + queueItems: [queueItem] }) } this.processingGoToTimestamp = false diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 63544d54..2d2dd4e9 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -114,12 +114,38 @@ export default { this.processingGoToTimestamp = false return } - if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) { + if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { this.$toast.error('Failed to get podcast episode') this.processingGoToTimestamp = false return } + var queueItem = {} + if (session.episodeId) { + var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId) + queueItem = { + libraryItemId: libraryItem.id, + libraryId: libraryItem.libraryId, + episodeId: episode.id, + title: episode.title, + subtitle: libraryItem.media.metadata.title, + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + duration: episode.audioFile.duration || null, + coverPath: libraryItem.media.coverPath || null + } + } else { + queueItem = { + libraryItemId: libraryItem.id, + libraryId: libraryItem.libraryId, + episodeId: null, + title: libraryItem.media.metadata.title, + subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '), + caption: '', + duration: libraryItem.media.duration || null, + coverPath: libraryItem.media.coverPath || null + } + } + const payload = { message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]), callback: (confirmed) => { @@ -127,7 +153,8 @@ export default { this.$eventBus.$emit('play-item', { libraryItemId: libraryItem.id, episodeId: session.episodeId || null, - startTime: session.currentTime + startTime: session.currentTime, + queueItems: [queueItem] }) } this.processingGoToTimestamp = false diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index dbd0856a..f0070744 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -137,12 +137,16 @@ {{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }} + + + + auto_stories {{ $strings.ButtonRead }} - + @@ -398,6 +402,9 @@ export default { isStreaming() { return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId }, + isQueued() { + return this.$store.getters['getIsMediaQueued'](this.libraryItemId) + }, userCanUpdate() { return this.$store.getters['user/getUserCanUpdate'] }, @@ -412,6 +419,10 @@ export default { // If rss feed is open then show feed url to users otherwise just show to admins return this.userIsAdminOrUp || this.rssFeedUrl + }, + showQueueBtn() { + if (this.isPodcast || this.isVideo) return false + return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem } }, methods: { @@ -536,6 +547,7 @@ export default { if (!podcastProgress || !podcastProgress.isFinished) { queueItems.push({ libraryItemId: this.libraryItemId, + libraryId: this.libraryId, episodeId: episode.id, title: episode.title, subtitle: this.title, @@ -545,6 +557,18 @@ export default { }) } } + } else { + const queueItem = { + libraryItemId: this.libraryItemId, + libraryId: this.libraryId, + episodeId: null, + title: this.title, + subtitle: this.authors.map((au) => au.name).join(', '), + caption: '', + duration: this.duration || null, + coverPath: this.media.coverPath || null + } + queueItems.push(queueItem) } this.$eventBus.$emit('play-item', { @@ -615,6 +639,26 @@ export default { console.log('RSS Feed Closed', data) this.rssFeedUrl = null } + }, + queueBtnClick() { + if (this.isQueued) { + // Remove from queue + this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId }) + } else { + // Add to queue + + const queueItem = { + libraryItemId: this.libraryItemId, + libraryId: this.libraryId, + episodeId: null, + title: this.title, + subtitle: this.authors.map((au) => au.name).join(', '), + caption: '', + duration: this.duration || null, + coverPath: this.media.coverPath || null + } + this.$store.commit('addItemToQueue', queueItem) + } } }, mounted() { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 9753c340..07ac9160 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -107,6 +107,7 @@ "HeaderOtherFiles": "Other Files", "HeaderOpenRSSFeed": "Open RSS Feed", "HeaderPermissions": "Permissions", + "HeaderPlayerQueue": "Player Queue", "HeaderPodcastsToAdd": "Podcasts to Add", "HeaderPreviewCover": "Preview Cover", "HeaderRemoveEpisode": "Remove Episode",