mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27:01 -04:00 
			
		
		
		
	Update multi-series edit for match and make into separate component with inner modal
This commit is contained in:
		
							parent
							
								
									a5dacd7821
								
							
						
					
					
						commit
						f41d6d5c77
					
				
							
								
								
									
										120
									
								
								client/components/modals/EditSeriesInputInnerModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								client/components/modals/EditSeriesInputInnerModal.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 51" @click="clickClose"> | ||||
|     <div class="absolute top-5 right-5 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300"> | ||||
|       <span class="material-icons text-4xl">close</span> | ||||
|     </div> | ||||
|     <div ref="content" class="text-white"> | ||||
|       <form v-if="selectedSeries" @submit.prevent="submitSeriesForm"> | ||||
|         <div class="bg-bg rounded-lg p-8" @click.stop> | ||||
|           <div class="flex"> | ||||
|             <div class="flex-grow p-1 min-w-80"> | ||||
|               <ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" /> | ||||
|             </div> | ||||
|             <div class="w-40 p-1"> | ||||
|               <ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex justify-end mt-2 p-1"> | ||||
|             <ui-btn type="submit">Save</ui-btn> | ||||
|           </div> | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     selectedSeries: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
|     existingSeriesNames: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       el: null, | ||||
|       content: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     show(newVal) { | ||||
|       if (newVal) { | ||||
|         this.$nextTick(this.setShow) | ||||
|       } else { | ||||
|         this.setHide() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     show: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     submitSeriesForm() { | ||||
|       if (this.$refs.newSeriesSelect) { | ||||
|         this.$refs.newSeriesSelect.blur() | ||||
|       } | ||||
| 
 | ||||
|       this.$emit('submit') | ||||
|     }, | ||||
|     clickClose() { | ||||
|       this.show = false | ||||
|     }, | ||||
|     hotkey(action) { | ||||
|       if (action === this.$hotkeys.Modal.CLOSE) { | ||||
|         this.show = false | ||||
|       } | ||||
|     }, | ||||
|     setShow() { | ||||
|       if (!this.el || !this.content) { | ||||
|         this.init() | ||||
|       } | ||||
|       if (!this.el || !this.content) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       document.body.appendChild(this.el) | ||||
|       setTimeout(() => { | ||||
|         this.content.style.transform = 'scale(1)' | ||||
|       }, 10) | ||||
|       document.documentElement.classList.add('modal-open') | ||||
| 
 | ||||
|       this.$store.commit('setInnerModalOpen', true) | ||||
|       this.$eventBus.$on('modal-hotkey', this.hotkey) | ||||
|     }, | ||||
|     setHide() { | ||||
|       if (this.content) this.content.style.transform = 'scale(0)' | ||||
|       if (this.el) this.el.remove() | ||||
|       document.documentElement.classList.remove('modal-open') | ||||
| 
 | ||||
|       this.$store.commit('setInnerModalOpen', false) | ||||
|       this.$eventBus.$off('modal-hotkey', this.hotkey) | ||||
|     }, | ||||
|     init() { | ||||
|       this.el = this.$refs.wrapper | ||||
|       this.content = this.$refs.content | ||||
|       if (this.content && this.el) { | ||||
|         this.el.classList.remove('hidden') | ||||
|         this.el.classList.add('flex') | ||||
|         this.content.style.transform = 'scale(0)' | ||||
|         this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)' | ||||
|         this.el.style.opacity = 1 | ||||
|         this.el.remove() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -104,6 +104,7 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     hotkey(action) { | ||||
|       if (this.$store.state.innerModalOpen) return | ||||
|       if (action === this.$hotkeys.Modal.CLOSE) { | ||||
|         this.show = false | ||||
|       } | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full overflow-hidden px-4 py-6 relative"> | ||||
|   <div id="match-wrapper" class="w-full h-full overflow-hidden px-4 py-6 relative"> | ||||
|     <form @submit.prevent="submitSearch"> | ||||
|       <div class="flex items-center justify-start -mx-1 h-20"> | ||||
|         <div class="w-40 px-1"> | ||||
| @ -87,7 +87,7 @@ | ||||
|         <div v-if="selectedMatch.series" class="flex items-center py-2"> | ||||
|           <ui-checkbox v-model="selectedMatchUsage.series" /> | ||||
|           <div class="flex-grow ml-4"> | ||||
|             <ui-multi-select-query-input v-if="selectedMatch.series" ref="seriesSelect" v-model="seriesItems" text-key="displayName" label="Series" readonly /> | ||||
|             <widgets-series-input-widget v-model="selectedMatch.series" /> | ||||
|             <p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">Currently: {{ mediaMetadata.seriesName || '' }}</p> | ||||
|           </div> | ||||
|         </div> | ||||
| @ -198,9 +198,9 @@ export default { | ||||
|         publishedYear: true, | ||||
|         series: true, | ||||
|         volumeNumber: true, | ||||
|         genres: true,  | ||||
|         tags: true,  | ||||
|         language: true,  | ||||
|         genres: true, | ||||
|         tags: true, | ||||
|         language: true, | ||||
|         explicit: true, | ||||
|         asin: true, | ||||
|         isbn: true, | ||||
| @ -233,12 +233,15 @@ export default { | ||||
|       get() { | ||||
|         return this.selectedMatch.series.map((se) => { | ||||
|           return { | ||||
|             id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|             displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series, | ||||
|             ...se | ||||
|             name: se.series, | ||||
|             sequence: se.volumeNumber || '' | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       set(val) { | ||||
|         console.log('set series items', val) | ||||
|         this.selectedMatch.series = val | ||||
|       } | ||||
|     }, | ||||
| @ -332,9 +335,9 @@ export default { | ||||
|         publishedYear: true, | ||||
|         series: true, | ||||
|         volumeNumber: true, | ||||
|         genres: true,  | ||||
|         tags: true,  | ||||
|         language: true,  | ||||
|         genres: true, | ||||
|         tags: true, | ||||
|         language: true, | ||||
|         explicit: true, | ||||
|         asin: true, | ||||
|         isbn: true, | ||||
| @ -362,6 +365,17 @@ export default { | ||||
|       else this.provider = localStorage.getItem('book-provider') || 'google' | ||||
|     }, | ||||
|     selectMatch(match) { | ||||
|       if (match && match.series) { | ||||
|         match.series = match.series.map((se) => { | ||||
|           return { | ||||
|             id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|             displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series, | ||||
|             name: se.series, | ||||
|             sequence: se.volumeNumber || '' | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       this.selectedMatch = match | ||||
|     }, | ||||
|     buildMatchUpdatePayload() { | ||||
| @ -372,21 +386,33 @@ export default { | ||||
|       for (const key in this.selectedMatchUsage) { | ||||
|         if (this.selectedMatchUsage[key] && this.selectedMatch[key]) { | ||||
|           if (key === 'series') { | ||||
|             if(!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [{ series: this.selectedMatch[key], volumeNumber: volumeNumber }] | ||||
|             var seriesPayload = [] | ||||
|             this.selectedMatch[key].forEach(seriesItem => seriesPayload.push({ | ||||
|               id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|               name: seriesItem.series, | ||||
|               sequence: seriesItem.volumeNumber | ||||
|             })) | ||||
|             if (!Array.isArray(this.selectedMatch[key])) { | ||||
|               seriesPayload.push({ | ||||
|                 id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|                 name: this.selectedMatch[key], | ||||
|                 sequence: volumeNumber | ||||
|               }) | ||||
|             } else { | ||||
|               this.selectedMatch[key].forEach((seriesItem) => | ||||
|                 seriesPayload.push({ | ||||
|                   id: seriesItem.id, | ||||
|                   name: seriesItem.name, | ||||
|                   sequence: seriesItem.sequence | ||||
|                 }) | ||||
|               ) | ||||
|             } | ||||
| 
 | ||||
|             updatePayload.metadata.series = seriesPayload | ||||
|           } else if (key === 'author' && !this.isPodcast) { | ||||
|             if(!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]] | ||||
|             if (!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]] | ||||
|             var authorPayload = [] | ||||
|             this.selectedMatch[key].forEach(authorName => authorPayload.push({ | ||||
|               id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|               name: authorName | ||||
|             })) | ||||
|             this.selectedMatch[key].forEach((authorName) => | ||||
|               authorPayload.push({ | ||||
|                 id: `new-${Math.floor(Math.random() * 10000)}`, | ||||
|                 name: authorName | ||||
|               }) | ||||
|             ) | ||||
|             updatePayload.metadata.authors = authorPayload | ||||
|           } else if (key === 'narrator') { | ||||
|             updatePayload.metadata.narrators = [this.selectedMatch[key]] | ||||
| @ -401,7 +427,7 @@ export default { | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       console.log(updatePayload) | ||||
| 
 | ||||
|       return updatePayload | ||||
|     }, | ||||
|     async submitMatchUpdate() { | ||||
|  | ||||
| @ -22,7 +22,7 @@ | ||||
| 
 | ||||
|       <div class="flex mt-2 -mx-1"> | ||||
|         <div class="flex-grow px-1"> | ||||
|           <ui-multi-select-query-input ref="seriesSelect" v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" /> | ||||
|           <widgets-series-input-widget v-model="details.series" /> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
| @ -63,27 +63,6 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
| 
 | ||||
|     <div v-if="showSeriesForm" class="absolute top-0 left-0 z-20 w-full h-full bg-black bg-opacity-50 rounded-lg flex items-center justify-center" @click="cancelSeriesForm"> | ||||
|       <div class="absolute top-0 right-0 p-4"> | ||||
|         <span class="material-icons text-gray-200 hover:text-white text-4xl cursor-pointer">close</span> | ||||
|       </div> | ||||
|       <form @submit.prevent="submitSeriesForm"> | ||||
|         <div class="bg-bg rounded-lg p-8" @click.stop> | ||||
|           <div class="flex"> | ||||
|             <div class="flex-grow p-1 min-w-80"> | ||||
|               <ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" /> | ||||
|             </div> | ||||
|             <div class="w-40 p-1"> | ||||
|               <ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex justify-end mt-2 p-1"> | ||||
|             <ui-btn type="submit">Save</ui-btn> | ||||
|           </div> | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -97,8 +76,6 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selectedSeries: {}, | ||||
|       showSeriesForm: false, | ||||
|       details: { | ||||
|         title: null, | ||||
|         subtitle: null, | ||||
| @ -146,24 +123,6 @@ export default { | ||||
|     }, | ||||
|     filterData() { | ||||
|       return this.$store.state.libraries.filterData || {} | ||||
|     }, | ||||
|     existingSeriesNames() { | ||||
|       // Only show series names not already selected | ||||
|       var alreadySelectedSeriesIds = this.details.series.map((se) => se.id) | ||||
|       return this.series.filter((se) => !alreadySelectedSeriesIds.includes(se.id)).map((se) => se.name) | ||||
|     }, | ||||
|     seriesItems: { | ||||
|       get() { | ||||
|         return this.details.series.map((se) => { | ||||
|           return { | ||||
|             displayName: se.sequence ? `${se.name} #${se.sequence}` : se.name, | ||||
|             ...se | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.details.series = val | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -214,50 +173,6 @@ export default { | ||||
|         this.$refs.tagsSelect.forceBlur() | ||||
|       } | ||||
|     }, | ||||
|     cancelSeriesForm() { | ||||
|       this.showSeriesForm = false | ||||
|     }, | ||||
|     editSeriesItem(series) { | ||||
|       var _series = this.details.series.find((se) => se.id === series.id) | ||||
|       if (!_series) return | ||||
|       this.selectedSeries = { | ||||
|         ..._series | ||||
|       } | ||||
|       this.showSeriesForm = true | ||||
|     }, | ||||
|     addNewSeries() { | ||||
|       this.selectedSeries = { | ||||
|         id: `new-${Date.now()}`, | ||||
|         name: '', | ||||
|         sequence: '' | ||||
|       } | ||||
|       this.showSeriesForm = true | ||||
|     }, | ||||
|     submitSeriesForm() { | ||||
|       if (!this.selectedSeries.name) { | ||||
|         this.$toast.error('Must enter a series') | ||||
|         return | ||||
|       } | ||||
|       if (this.$refs.newSeriesSelect) { | ||||
|         this.$refs.newSeriesSelect.blur() | ||||
|       } | ||||
|       var existingSeriesIndex = this.details.series.findIndex((se) => se.id === this.selectedSeries.id) | ||||
| 
 | ||||
|       var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase()) | ||||
|       if (existingSeriesIndex < 0 && seriesSameName) { | ||||
|         this.selectedSeries.id = seriesSameName.id | ||||
|       } | ||||
| 
 | ||||
|       if (existingSeriesIndex >= 0) { | ||||
|         this.details.series.splice(existingSeriesIndex, 1, { ...this.selectedSeries }) | ||||
|       } else { | ||||
|         this.details.series.push({ | ||||
|           ...this.selectedSeries | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       this.showSeriesForm = false | ||||
|     }, | ||||
|     stringArrayEqual(array1, array2) { | ||||
|       // return false if different | ||||
|       if (array1.length !== array2.length) return false | ||||
|  | ||||
							
								
								
									
										111
									
								
								client/components/widgets/SeriesInputWidget.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								client/components/widgets/SeriesInputWidget.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,111 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <ui-multi-select-query-input v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" /> | ||||
| 
 | ||||
|     <modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selectedSeries: null, | ||||
|       showSeriesForm: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     seriesItems: { | ||||
|       get() { | ||||
|         return (this.value || []).map((se) => { | ||||
|           return { | ||||
|             displayName: se.sequence ? `${se.name} #${se.sequence}` : se.name, | ||||
|             ...se | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     series() { | ||||
|       return this.filterData.series || [] | ||||
|     }, | ||||
|     filterData() { | ||||
|       return this.$store.state.libraries.filterData || {} | ||||
|     }, | ||||
|     existingSeriesNames() { | ||||
|       // Only show series names not already selected | ||||
|       var alreadySelectedSeriesIds = (this.value || []).map((se) => se.id) | ||||
|       return this.series.filter((se) => !alreadySelectedSeriesIds.includes(se.id)).map((se) => se.name) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     cancelSeriesForm() { | ||||
|       this.showSeriesForm = false | ||||
|     }, | ||||
|     editSeriesItem(series) { | ||||
|       var _series = this.seriesItems.find((se) => se.id === series.id) | ||||
|       if (!_series) return | ||||
| 
 | ||||
|       this.selectedSeries = { | ||||
|         ..._series | ||||
|       } | ||||
| 
 | ||||
|       console.log('Selected series', this.selectedSeries) | ||||
|       this.showSeriesForm = true | ||||
|     }, | ||||
|     addNewSeries() { | ||||
|       this.selectedSeries = { | ||||
|         id: `new-${Date.now()}`, | ||||
|         name: '', | ||||
|         sequence: '' | ||||
|       } | ||||
| 
 | ||||
|       this.showSeriesForm = true | ||||
|     }, | ||||
|     submitSeriesForm() { | ||||
|       console.log('submit series form', this.value, this.selectedSeries) | ||||
| 
 | ||||
|       if (!this.selectedSeries.name) { | ||||
|         this.$toast.error('Must enter a series') | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       var existingSeriesIndex = this.seriesItems.findIndex((se) => se.id === this.selectedSeries.id) | ||||
| 
 | ||||
|       var existingSeriesSameName = this.seriesItems.findIndex((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase()) | ||||
|       if (existingSeriesSameName >= 0 && existingSeriesIndex < 0) { | ||||
|         console.error('Attempt to add duplicate series') | ||||
|         this.$toast.error('Cannot add two of the same series') | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       var seriesSameName = this.series.find((se) => se.name.toLowerCase() === this.selectedSeries.name.toLowerCase()) | ||||
|       if (existingSeriesIndex < 0 && seriesSameName) { | ||||
|         this.selectedSeries.id = seriesSameName.id | ||||
|       } | ||||
| 
 | ||||
|       var selectedSeriesCopy = { ...this.selectedSeries } | ||||
|       selectedSeriesCopy.displayName = selectedSeriesCopy.sequence ? `${selectedSeriesCopy.name} #${selectedSeriesCopy.sequence}` : selectedSeriesCopy.name | ||||
| 
 | ||||
|       var seriesCopy = this.seriesItems.map((v) => ({ ...v })) | ||||
|       if (existingSeriesIndex >= 0) { | ||||
|         seriesCopy.splice(existingSeriesIndex, 1, selectedSeriesCopy) | ||||
|         this.seriesItems = seriesCopy | ||||
|       } else { | ||||
|         seriesCopy.push(selectedSeriesCopy) | ||||
|         this.seriesItems = seriesCopy | ||||
|       } | ||||
| 
 | ||||
|       this.showSeriesForm = false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -20,6 +20,7 @@ export const state = () => ({ | ||||
|   backups: [], | ||||
|   bookshelfBookIds: [], | ||||
|   openModal: null, | ||||
|   innerModalOpen: false, | ||||
|   selectedBookshelfTexture: '/textures/wood_default.jpg', | ||||
|   lastBookshelfScrollData: {} | ||||
| }) | ||||
| @ -177,6 +178,9 @@ export const mutations = { | ||||
|   setOpenModal(state, val) { | ||||
|     state.openModal = val | ||||
|   }, | ||||
|   setInnerModalOpen(state, val) { | ||||
|     state.innerModalOpen = val | ||||
|   }, | ||||
|   setBookshelfTexture(state, val) { | ||||
|     state.selectedBookshelfTexture = val | ||||
|   } | ||||
|  | ||||
| @ -51,7 +51,6 @@ class LibraryItemController { | ||||
| 
 | ||||
|     var hasUpdates = libraryItem.update(req.body) | ||||
|     if (hasUpdates) { | ||||
| 
 | ||||
|       // Turn on podcast auto download cron if not already on
 | ||||
|       if (libraryItem.mediaType == 'podcast' && req.body.media.autoDownloadEpisodes && !this.podcastManager.episodeScheduleTask) { | ||||
|         this.podcastManager.schedulePodcastEpisodeCron() | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user