Merge pull request #1581 from mfcar/improvePodcastEditing

Improve podcast editing
This commit is contained in:
advplyr 2023-03-05 12:28:12 -06:00 committed by GitHub
commit aef2c52630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 194 additions and 30 deletions

View File

@ -11,8 +11,15 @@
</template> </template>
</div> </div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
</div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
</div>
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh"> <div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" /> <component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episodeItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@ -21,8 +28,8 @@
export default { export default {
data() { data() {
return { return {
episodeItem: null,
processing: false, processing: false,
selectedTab: 'details',
tabs: [ tabs: [
{ {
id: 'details', 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: { computed: {
show: { show: {
get() { get() {
@ -46,27 +76,118 @@ export default {
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val) this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
} }
}, },
selectedTab: {
get() {
return this.$store.state.editPodcastModalTab
},
set(val) {
this.$store.commit('setEditPodcastModalTab', val)
}
},
libraryItem() { libraryItem() {
return this.$store.state.selectedLibraryItem return this.$store.state.selectedLibraryItem
}, },
episode() { episode() {
return this.$store.state.globals.selectedEpisode return this.$store.state.globals.selectedEpisode
}, },
selectedEpisodeId() {
return this.episode.id
},
title() { title() {
if (!this.libraryItem) return '' return this.libraryItem?.media.metadata.title || 'Unknown'
return this.libraryItem.media.metadata.title || 'Unknown'
}, },
tabComponentName() { 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 : '' 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: { 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) { 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()
}
} }
</script> </script>

View File

@ -24,7 +24,12 @@
</div> </div>
</div> </div>
<div class="flex items-center justify-end pt-4"> <div class="flex items-center justify-end pt-4">
<ui-btn @click="submit">{{ $strings.ButtonSubmit }}</ui-btn> <!-- desktop -->
<ui-btn @click="submit" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
<ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
<!-- mobile -->
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
<div v-if="enclosureUrl" class="py-4"> <div v-if="enclosureUrl" class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p> <p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
@ -125,26 +130,41 @@ export default {
} }
return updatePayload return updatePayload
}, },
submit() { async saveAndClose() {
const payload = this.getUpdatePayload() const wasUpdated = await this.submit()
if (!Object.keys(payload).length) { if (wasUpdated !== null) this.$emit('close')
return this.$toast.info('No updates were made') },
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.isProcessing = true
this.$axios const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload) console.error('Failed update episode', error)
.then(() => { this.isProcessing = false
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.$toast.success('Podcast episode updated')
this.$emit('close') return true
}) } else {
.catch((error) => { this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode' }
console.error('Failed update episode', error) }
this.isProcessing = false return false
this.$toast.error(errorMsg)
})
} }
}, },
mounted() {} mounted() {}

View File

@ -287,6 +287,8 @@ export default {
this.showPodcastRemoveModal = true this.showPodcastRemoveModal = true
}, },
editEpisode(episode) { editEpisode(episode) {
const episodeIds = this.episodesSorted.map((e) => e.id)
this.$store.commit('setEpisodeTableEpisodeIds', episodeIds)
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)

View File

@ -68,8 +68,6 @@ export default {
} }
}, },
mounted() {}, mounted() {},
beforeDestroy() { beforeDestroy() {}
console.log('Before destroy')
}
} }
</script> </script>

View File

@ -13,6 +13,7 @@ export const state = () => ({
playerQueueAutoPlay: true, playerQueueAutoPlay: true,
playerIsFullscreen: false, playerIsFullscreen: false,
editModalTab: 'details', editModalTab: 'details',
editPodcastModalTab: 'details',
showEditModal: false, showEditModal: false,
showEReader: false, showEReader: false,
selectedLibraryItem: null, selectedLibraryItem: null,
@ -21,6 +22,7 @@ export const state = () => ({
previousPath: '/', previousPath: '/',
showExperimentalFeatures: false, showExperimentalFeatures: false,
bookshelfBookIds: [], bookshelfBookIds: [],
episodeTableEpisodeIds: [],
openModal: null, openModal: null,
innerModalOpen: false, innerModalOpen: false,
lastBookshelfScrollData: {}, lastBookshelfScrollData: {},
@ -135,6 +137,9 @@ export const mutations = {
setBookshelfBookIds(state, val) { setBookshelfBookIds(state, val) {
state.bookshelfBookIds = val || [] state.bookshelfBookIds = val || []
}, },
setEpisodeTableEpisodeIds(state, val) {
state.episodeTableEpisodeIds = val || []
},
setPreviousPath(state, val) { setPreviousPath(state, val) {
state.previousPath = val state.previousPath = val
}, },
@ -198,6 +203,9 @@ export const mutations = {
setShowEditModal(state, val) { setShowEditModal(state, val) {
state.showEditModal = val state.showEditModal = val
}, },
setEditPodcastModalTab(state, tab) {
state.editPodcastModalTab = tab
},
showEReader(state, libraryItem) { showEReader(state, libraryItem) {
state.selectedLibraryItem = libraryItem state.selectedLibraryItem = libraryItem

View File

@ -225,6 +225,20 @@ class PodcastController {
res.json(libraryItem.toJSONExpanded()) 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 // DELETE: api/podcasts/:id/episode/:episodeId
async removeEpisode(req, res) { async removeEpisode(req, res) {
var episodeId = req.params.episodeId var episodeId = req.params.episodeId

View File

@ -236,6 +236,7 @@ class ApiRouter {
this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this)) 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/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.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.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)) this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))