diff --git a/client/components/modals/podcast/EditEpisode.vue b/client/components/modals/podcast/EditEpisode.vue index 236a0d87..e2126be2 100644 --- a/client/components/modals/podcast/EditEpisode.vue +++ b/client/components/modals/podcast/EditEpisode.vue @@ -11,8 +11,15 @@ +
+
arrow_back_ios
+
+
+
arrow_forward_ios
+
+
- +
@@ -21,8 +28,8 @@ export default { data() { return { + episodeItem: null, processing: false, - selectedTab: 'details', tabs: [ { id: 'details', @@ -37,6 +44,29 @@ export default { ] } }, + watch: { + show: { + handler(newVal) { + if (newVal) { + const availableTabIds = this.tabs.map((tab) => tab.id); + if (!availableTabIds.length) { + this.show = false + return + } + + if (!availableTabIds.includes(this.selectedTab)) { + this.selectedTab = availableTabIds[0] + } + + this.episodeItem = null + this.init() + this.registerListeners() + } else { + this.unregisterListeners() + } + } + } + }, computed: { show: { get() { @@ -46,27 +76,128 @@ export default { this.$store.commit('globals/setShowEditPodcastEpisodeModal', val) } }, + selectedTab: { + get() { + return this.$store.state.editPodcastModalTab + }, + set(val) { + this.$store.commit('setEditPodcastModalTab', val) + } + }, libraryItem() { return this.$store.state.selectedLibraryItem }, episode() { return this.$store.state.globals.selectedEpisode }, + selectedEpisodeId() { + return this.episode.id + }, title() { if (!this.libraryItem) return '' return this.libraryItem.media.metadata.title || 'Unknown' }, tabComponentName() { - var _tab = this.tabs.find((t) => t.id === this.selectedTab) + const _tab = this.tabs.find((t) => t.id === this.selectedTab); return _tab ? _tab.component : '' + }, + episodeTableEpisodeIds() { + return this.$store.state.episodeTableEpisodeIds || [] + }, + currentEpisodeIndex() { + if (!this.episodeTableEpisodeIds.length) return 0 + return this.episodeTableEpisodeIds.findIndex((bid) => bid === this.selectedEpisodeId) + }, + canGoPrev() { + return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex > 0 + }, + canGoNext() { + return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex < this.episodeTableEpisodeIds.length - 1 } }, methods: { + async goPrevEpisode() { + if (this.currentEpisodeIndex - 1 < 0) return + const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]; + this.processing = true + const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => { + const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'; + this.$toast.error(errorMsg) + return null + }); + this.processing = false + if (prevEpisode) { + this.unregisterListeners() + this.episodeItem = prevEpisode + this.$store.commit('globals/setSelectedEpisode', prevEpisode) + this.$nextTick(this.registerListeners) + } else { + console.error('Episode not found', prevEpisodeId) + } + }, + async goNextEpisode() { + if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return + this.processing = true + const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]; + const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => { + const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'; + this.$toast.error(errorMsg) + return null + }); + this.processing = false + if (nextEpisode) { + this.unregisterListeners() + this.episodeItem = nextEpisode + this.$store.commit('globals/setSelectedEpisode', nextEpisode) + this.$nextTick(this.registerListeners) + } else { + console.error('Episode not found', nextEpisodeId) + } + }, selectTab(tab) { - this.selectedTab = tab + if (this.selectedTab === tab) return + if (this.tabs.find((t) => t.id === tab)) { + this.selectedTab = tab + this.processing = false + } + }, + libraryItemUpdated(expandedLibraryItem) { + this.libraryItem = expandedLibraryItem + }, + init() { + this.fetchFull() + }, + async fetchFull() { + try { + this.processing = true + this.episodeItem = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${this.selectedEpisodeId}`) + this.processing = false + } catch (error) { + console.error('Failed to fetch episode', this.selectedEpisodeId, error) + this.processing = false + this.show = false + } + }, + hotkey(action) { + if (action === this.$hotkeys.Modal.NEXT_PAGE) { + this.goNextEpisode() + } else if (action === this.$hotkeys.Modal.PREV_PAGE) { + this.goPrevEpisode() + } + }, + registerListeners() { + this.$eventBus.$on('modal-hotkey', this.hotkey) + this.$eventBus.$on(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated) + }, + unregisterListeners() { + this.$eventBus.$off('modal-hotkey', this.hotkey) + this.$eventBus.$off(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated) } }, - mounted() {} + mounted() {}, + beforeDestroy() { + this.unregisterListeners() + } } @@ -77,4 +208,4 @@ export default { .tab.tab-selected { height: 41px; } - \ No newline at end of file + diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue index 39228a39..03883bc1 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/EpisodesTable.vue @@ -281,6 +281,8 @@ export default { this.showPodcastRemoveModal = true }, editEpisode(episode) { + const episodeIds = this.episodesSorted.map((e) => e.id) + this.$store.commit('setEpisodeTableEpisodeIds', episodeIds) this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) @@ -314,4 +316,4 @@ export default { .episode-leave-active { position: absolute; } - \ No newline at end of file + diff --git a/client/store/index.js b/client/store/index.js index 1529d864..57710da2 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -13,6 +13,7 @@ export const state = () => ({ playerQueueAutoPlay: true, playerIsFullscreen: false, editModalTab: 'details', + editPodcastModalTab: 'details', showEditModal: false, showEReader: false, selectedLibraryItem: null, @@ -21,6 +22,7 @@ export const state = () => ({ previousPath: '/', showExperimentalFeatures: false, bookshelfBookIds: [], + episodeTableEpisodeIds: [], openModal: null, innerModalOpen: false, lastBookshelfScrollData: {}, @@ -135,6 +137,9 @@ export const mutations = { setBookshelfBookIds(state, val) { state.bookshelfBookIds = val || [] }, + setEpisodeTableEpisodeIds(state, val) { + state.episodeTableEpisodeIds = val || [] + }, setPreviousPath(state, val) { state.previousPath = val }, @@ -198,6 +203,9 @@ export const mutations = { setShowEditModal(state, val) { state.showEditModal = val }, + setEditPodcastModalTab(state, tab) { + state.editPodcastModalTab = tab + }, showEReader(state, libraryItem) { state.selectedLibraryItem = libraryItem @@ -225,4 +233,4 @@ export const mutations = { setInnerModalOpen(state, val) { state.innerModalOpen = val } -} \ No newline at end of file +} diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 23cec882..60f1e35d 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -225,6 +225,20 @@ class PodcastController { res.json(libraryItem.toJSONExpanded()) } + // GET: api/podcasts/:id/episode/:episodeId + async getEpisode(req, res) { + const episodeId = req.params.episodeId; + const libraryItem = req.libraryItem; + + const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId); + if (!episode) { + Logger.error(`[PodcastController] getEpisode episode ${episodeId} not found for item ${libraryItem.id}`) + return res.sendStatus(404) + } + + res.json(episode) + } + // DELETE: api/podcasts/:id/episode/:episodeId async removeEpisode(req, res) { var episodeId = req.params.episodeId @@ -283,4 +297,4 @@ class PodcastController { next() } } -module.exports = new PodcastController() \ No newline at end of file +module.exports = new PodcastController() diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 41c26769..53721706 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -235,6 +235,7 @@ class ApiRouter { this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this)) this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this)) this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.bind(this)) + this.router.get('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.getEpisode.bind(this)) this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this)) this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this)) @@ -553,4 +554,4 @@ class ApiRouter { } } } -module.exports = ApiRouter \ No newline at end of file +module.exports = ApiRouter