diff --git a/client/components/modals/podcast/EditEpisode.vue b/client/components/modals/podcast/EditEpisode.vue index 236a0d87..ed9c8f28 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,118 @@ 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' + 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.episodeItem = prevEpisode + this.$store.commit('globals/setSelectedEpisode', prevEpisode) + } 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.episodeItem = nextEpisode + this.$store.commit('globals/setSelectedEpisode', nextEpisode) + } 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 + } + }, + 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) + }, + unregisterListeners() { + this.$eventBus.$off('modal-hotkey', this.hotkey) } }, - mounted() {} + mounted() {}, + beforeDestroy() { + this.unregisterListeners() + } } @@ -77,4 +198,4 @@ export default { .tab.tab-selected { height: 41px; } - \ No newline at end of file + diff --git a/client/components/modals/podcast/tabs/EpisodeDetails.vue b/client/components/modals/podcast/tabs/EpisodeDetails.vue index cdc08f2f..debf9155 100644 --- a/client/components/modals/podcast/tabs/EpisodeDetails.vue +++ b/client/components/modals/podcast/tabs/EpisodeDetails.vue @@ -24,7 +24,12 @@
- {{ $strings.ButtonSubmit }} + + + + + + {{ $strings.ButtonSave }}

Episode URL from RSS feed

@@ -125,26 +130,41 @@ export default { } return updatePayload }, - submit() { - const payload = this.getUpdatePayload() - if (!Object.keys(payload).length) { - return this.$toast.info('No updates were made') + async saveAndClose() { + const wasUpdated = await this.submit() + if (wasUpdated !== null) this.$emit('close') + }, + async submit() { + if (this.isProcessing) { + return null } + const updatedDetails = this.getUpdatePayload() + if (!Object.keys(updatedDetails).length) { + this.$toast.info('No changes were made') + return false + } + return this.updateDetails(updatedDetails) + }, + async updateDetails(updatedDetails) { this.isProcessing = true - this.$axios - .$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload) - .then(() => { - this.isProcessing = false + const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => { + console.error('Failed update episode', error) + this.isProcessing = false + this.$toast.error(error?.response?.data || 'Failed to update episode') + return false + }) + + this.isProcessing = false + if (updateResult) { + if (updateResult) { this.$toast.success('Podcast episode updated') - this.$emit('close') - }) - .catch((error) => { - var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode' - console.error('Failed update episode', error) - this.isProcessing = false - this.$toast.error(errorMsg) - }) + return true + } else { + this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) + } + } + return false } }, mounted() {} diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue index 1f0baf35..d92f3181 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/EpisodesTable.vue @@ -287,6 +287,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) diff --git a/client/components/ui/RichTextEditor.vue b/client/components/ui/RichTextEditor.vue index 068bc95f..582f5e8f 100644 --- a/client/components/ui/RichTextEditor.vue +++ b/client/components/ui/RichTextEditor.vue @@ -68,8 +68,6 @@ export default { } }, mounted() {}, - beforeDestroy() { - console.log('Before destroy') - } + beforeDestroy() {} } \ 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..f19bba9f 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 454eb1a3..4c8569cf 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -236,6 +236,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))