mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add podcast add modal
This commit is contained in:
		
							parent
							
								
									a9b9e23f46
								
							
						
					
					
						commit
						deadc63dbb
					
				| @ -8,7 +8,7 @@ | ||||
|     </div> | ||||
| 
 | ||||
|     <div v-if="loaded && !shelves.length && isRootUser && !search" class="w-full flex flex-col items-center justify-center py-12"> | ||||
|       <p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p> | ||||
|       <p class="text-center text-2xl font-book mb-4 py-4">Library is empty!</p> | ||||
|       <div class="flex"> | ||||
|         <ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn> | ||||
|         <ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn> | ||||
|  | ||||
| @ -7,10 +7,10 @@ | ||||
|     </template> | ||||
| 
 | ||||
|     <div v-if="initialized && !totalShelves && !hasFilter && isRootUser && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12"> | ||||
|       <p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p> | ||||
|       <p class="text-center text-2xl font-book mb-4 py-4">Library is empty!</p> | ||||
|       <div class="flex"> | ||||
|         <ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn> | ||||
|         <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> | ||||
|         <ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-else-if="!totalShelves && initialized" class="w-full py-16"> | ||||
|  | ||||
| @ -21,7 +21,7 @@ | ||||
|       <div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> | ||||
|       </svg> | ||||
| @ -31,7 +31,7 @@ | ||||
|       <div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <span class="material-icons-outlined">collections_bookmark</span> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.9rem">Collections</p> | ||||
| @ -39,7 +39,7 @@ | ||||
|       <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg class="w-6 h-6" viewBox="0 0 24 24"> | ||||
|         <path | ||||
|           fill="currentColor" | ||||
| @ -52,8 +52,8 @@ | ||||
|       <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link v-if="showExperimentalFeatures && isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <icons-podcasts-svg class="w-6 h-6" /> | ||||
|     <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <icons-podcast-svg class="w-6 h-6" /> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p> | ||||
| 
 | ||||
| @ -92,7 +92,7 @@ export default { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] | ||||
|     }, | ||||
|     isPodcastLibrary() { | ||||
|       return this.currentLibraryMediaType === 'podcasts' | ||||
|       return this.currentLibraryMediaType === 'podcast' | ||||
|     }, | ||||
|     isPodcastSearchPage() { | ||||
|       return this.$route.name === 'library-library-podcast-search' | ||||
|  | ||||
							
								
								
									
										177
									
								
								client/components/modals/podcast/NewModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								client/components/modals/podcast/NewModal.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,177 @@ | ||||
| <template> | ||||
|   <modals-modal v-model="show" name="new-podcast-modal" :width="1200" :height="'unset'" :processing="processing"> | ||||
|     <template #outer> | ||||
|       <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> | ||||
|         <p class="font-book text-3xl text-white truncate">{{ title }}</p> | ||||
|       </div> | ||||
|     </template> | ||||
|     <div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden"> | ||||
|       <div class="flex flex-wrap"> | ||||
|         <div class="w-full md:w-1/2 p-4"> | ||||
|           <p class="text-lg font-semibold mb-2">Details</p> | ||||
|           <div class="flex flex-wrap"> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.title" label="Title" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.author" label="Author" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-multi-select v-model="podcast.genres" :items="podcast.genres" label="Genres" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.releaseDate" label="Release Date" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.itunesPageUrl" label="Page URL" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-text-input-with-label v-model="podcast.feedImageUrl" label="Feed Image URL" /> | ||||
|             </div> | ||||
|             <div class="p-2 w-full"> | ||||
|               <ui-checkbox v-model="podcast.autoDownloadEpisodes" label="Auto Download Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="w-full md:w-1/2 p-4"> | ||||
|           <p class="text-lg font-semibold mb-2">Episodes</p> | ||||
|           <div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto"> | ||||
|             <div v-for="(episode, index) in episodes" :key="index" class="relative cursor-pointer" :class="index % 2 == 0 ? 'bg-primary bg-opacity-25 hover:bg-opacity-40' : 'bg-primary bg-opacity-5 hover:bg-opacity-25'" @click="toggleSelectEpisode(index)"> | ||||
|               <div class="absolute top-0 left-0 h-full flex items-center p-2"> | ||||
|                 <ui-checkbox v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" /> | ||||
|               </div> | ||||
|               <div class="px-8 py-2"> | ||||
|                 <p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p> | ||||
|                 <p class="break-words">{{ episode.title }}</p> | ||||
|                 <p class="text-xs text-gray-300">Published {{ episode.pubDate || 'Unknown' }}</p> | ||||
|                 <!-- <span class="material-icons cursor-pointer text-lg hover:text-success" @click="saveEpisode(episode)">save</span> --> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex items-center py-4"> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn color="success" :disabled="disableSubmit" @click="submit">{{ buttonText }}</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|   </modals-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     podcastData: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     }, | ||||
|     podcastFeedData: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       processing: false, | ||||
|       podcast: { | ||||
|         title: '', | ||||
|         author: '', | ||||
|         description: '', | ||||
|         releaseDate: '', | ||||
|         genres: [], | ||||
|         feedUrl: '', | ||||
|         feedImageUrl: '', | ||||
|         itunesPageUrl: '', | ||||
|         itunesId: '', | ||||
|         itunesArtistId: '', | ||||
|         autoDownloadEpisodes: false | ||||
|       }, | ||||
|       selectedEpisodes: {} | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     show: { | ||||
|       immediate: true, | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.init() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     show: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     title() { | ||||
|       return this._podcastData.title | ||||
|     }, | ||||
|     _podcastData() { | ||||
|       return this.podcastData || {} | ||||
|     }, | ||||
|     feedMetadata() { | ||||
|       return this._podcastData.metadata || {} | ||||
|     }, | ||||
|     episodes() { | ||||
|       if (!this.podcastFeedData) return [] | ||||
|       return this.podcastFeedData.episodes || [] | ||||
|     }, | ||||
|     episodesSelected() { | ||||
|       return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) | ||||
|     }, | ||||
|     disableSubmit() { | ||||
|       return !this.episodesSelected.length && !this.podcast.autoDownloadEpisodes | ||||
|     }, | ||||
|     buttonText() { | ||||
|       if (!this.episodesSelected.length) return 'Add Podcast' | ||||
|       if (this.episodesSelected.length == 1) return 'Add Podcast & Download 1 Episode' | ||||
|       return `Add Podcast & Download ${this.episodesSelected.length} Episodes` | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleSelectEpisode(index) { | ||||
|       this.selectedEpisodes[String(index)] = !this.selectedEpisodes[String(index)] | ||||
|     }, | ||||
|     submit() {}, | ||||
|     saveEpisode(episode) { | ||||
|       console.log('Save episode', episode) | ||||
|     }, | ||||
|     init() { | ||||
|       this.podcast.title = this._podcastData.title | ||||
|       this.podcast.author = this._podcastData.artistName || '' | ||||
|       this.podcast.description = this._podcastData.description || this.feedMetadata.description || '' | ||||
|       this.podcast.releaseDate = this._podcastData.releaseDate || '' | ||||
|       this.podcast.genres = this._podcastData.genres || [] | ||||
|       this.podcast.feedUrl = this._podcastData.feedUrl | ||||
|       this.podcast.feedImageUrl = this._podcastData.cover || '' | ||||
|       this.podcast.itunesPageUrl = this._podcastData.pageUrl || '' | ||||
|       this.podcast.itunesId = this._podcastData.id || '' | ||||
|       this.podcast.itunesArtistId = this._podcastData.artistId || '' | ||||
|       this.podcast.autoDownloadEpisodes = false | ||||
| 
 | ||||
|       for (let i = 0; i < this.episodes.length; i++) { | ||||
|         this.$set(this.selectedEpisodes, String(i), false) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| #podcast-wrapper { | ||||
|   min-height: 400px; | ||||
|   max-height: 80vh; | ||||
| } | ||||
| #episodes-scroll { | ||||
|   max-height: calc(80vh - 200px); | ||||
| } | ||||
| </style> | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :class="classList"> | ||||
|   <nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> | ||||
|     <slot /> | ||||
|     <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> | ||||
|       <!-- <span class="material-icons animate-spin">refresh</span> --> | ||||
|  | ||||
| @ -35,6 +35,8 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <modals-podcast-new-modal v-model="showNewPodcastModal" :podcast-data="selectedPodcast" :podcast-feed-data="selectedPodcastFeed" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -45,7 +47,11 @@ export default { | ||||
|       searchTerm: '', | ||||
|       results: [], | ||||
|       termSearched: '', | ||||
|       processing: false | ||||
|       processing: false, | ||||
| 
 | ||||
|       showNewPodcastModal: false, | ||||
|       selectedPodcast: null, | ||||
|       selectedPodcastFeed: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -83,6 +89,9 @@ export default { | ||||
|       }) | ||||
|       this.processing = false | ||||
|       if (!podcastfeed) return | ||||
|       this.selectedPodcastFeed = podcastfeed | ||||
|       this.selectedPodcast = podcast | ||||
|       this.showNewPodcastModal = true | ||||
|       console.log('Got podcast feed', podcastfeed) | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -11,6 +11,8 @@ class Podcast { | ||||
|     this.tags = [] | ||||
|     this.episodes = [] | ||||
| 
 | ||||
|     this.autoDownloadEpisodes = false | ||||
| 
 | ||||
|     this.lastCoverSearch = null | ||||
|     this.lastCoverSearchQuery = null | ||||
| 
 | ||||
| @ -25,6 +27,7 @@ class Podcast { | ||||
|     this.coverPath = podcast.coverPath | ||||
|     this.tags = [...podcast.tags] | ||||
|     this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e)) | ||||
|     this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
| @ -34,6 +37,7 @@ class Podcast { | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       episodes: this.episodes.map(e => e.toJSON()), | ||||
|       autoDownloadEpisodes: this.autoDownloadEpisodes | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -43,7 +47,8 @@ class Podcast { | ||||
|       metadata: this.metadata.toJSON(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       episodes: this.episodes.map(e => e.toJSON()) | ||||
|       episodes: this.episodes.map(e => e.toJSON()), | ||||
|       autoDownloadEpisodes: this.autoDownloadEpisodes | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -53,7 +58,8 @@ class Podcast { | ||||
|       metadata: this.metadata.toJSONExpanded(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       episodes: this.episodes.map(e => e.toJSON()) | ||||
|       episodes: this.episodes.map(e => e.toJSON()), | ||||
|       autoDownloadEpisodes: this.autoDownloadEpisodes | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| class PodcastMetadata { | ||||
|   constructor(metadata) { | ||||
|     this.title = null | ||||
|     this.artist = null | ||||
|     this.author = null | ||||
|     this.description = null | ||||
|     this.releaseDate = null | ||||
|     this.genres = [] | ||||
|     this.feedUrl = null | ||||
|     this.feedImageUrl = null | ||||
|     this.itunesPageUrl = null | ||||
|     this.itunesId = null | ||||
|     this.itunesArtistId = null | ||||
| @ -18,11 +19,12 @@ class PodcastMetadata { | ||||
| 
 | ||||
|   construct(metadata) { | ||||
|     this.title = metadata.title | ||||
|     this.artist = metadata.artist | ||||
|     this.author = metadata.author | ||||
|     this.description = metadata.description | ||||
|     this.releaseDate = metadata.releaseDate | ||||
|     this.genres = [...metadata.genres] | ||||
|     this.feedUrl = metadata.feedUrl | ||||
|     this.feedImageUrl = metadata.feedImageUrl | ||||
|     this.itunesPageUrl = metadata.itunesPageUrl | ||||
|     this.itunesId = metadata.itunesId | ||||
|     this.itunesArtistId = metadata.itunesArtistId | ||||
| @ -32,11 +34,12 @@ class PodcastMetadata { | ||||
|   toJSON() { | ||||
|     return { | ||||
|       title: this.title, | ||||
|       artist: this.artist, | ||||
|       author: this.author, | ||||
|       description: this.description, | ||||
|       releaseDate: this.releaseDate, | ||||
|       genres: [...this.genres], | ||||
|       feedUrl: this.feedUrl, | ||||
|       feedImageUrl: this.feedImageUrl, | ||||
|       itunesPageUrl: this.itunesPageUrl, | ||||
|       itunesId: this.itunesId, | ||||
|       itunesArtistId: this.itunesArtistId, | ||||
| @ -53,7 +56,7 @@ class PodcastMetadata { | ||||
|   } | ||||
| 
 | ||||
|   searchQuery(query) { // Returns key if match is found
 | ||||
|     var keysToCheck = ['title', 'artist', 'itunesId', 'itunesArtistId'] | ||||
|     var keysToCheck = ['title', 'author', 'itunesId', 'itunesArtistId'] | ||||
|     for (var key of keysToCheck) { | ||||
|       if (this[key] && String(this[key]).toLowerCase().includes(query)) { | ||||
|         return { | ||||
|  | ||||
| @ -91,7 +91,6 @@ function cleanPodcastJson(rssJson) { | ||||
|     Logger.error(`[podcastUtil] Invalid podcast no episodes`) | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
|   var podcast = { | ||||
|     metadata: extractPodcastMetadata(channel), | ||||
|     episodes: extractPodcastEpisodes(channel.item) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user