mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-30 18:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			308 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div class="w-full h-full relative">
 | |
|     <form class="w-full h-full" @submit.prevent="submitForm">
 | |
|       <div ref="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
 | |
|         <div class="flex -mx-1">
 | |
|           <div class="w-1/2 px-1">
 | |
|             <ui-text-input-with-label v-model="details.title" label="Title" />
 | |
|           </div>
 | |
|           <div class="flex-grow px-1">
 | |
|             <ui-text-input-with-label v-model="details.subtitle" label="Subtitle" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div class="flex mt-2 -mx-1">
 | |
|           <div class="w-3/4 px-1">
 | |
|             <ui-text-input-with-label v-model="details.author" label="Author" />
 | |
|           </div>
 | |
|           <div class="flex-grow px-1">
 | |
|             <ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div class="flex mt-2 -mx-1">
 | |
|           <div class="w-3/4 px-1">
 | |
|             <ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" />
 | |
|           </div>
 | |
|           <div class="flex-grow px-1">
 | |
|             <ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
 | |
| 
 | |
|         <div class="flex mt-2 -mx-1">
 | |
|           <div class="w-1/2 px-1">
 | |
|             <ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
 | |
|           </div>
 | |
|           <div class="flex-grow px-1">
 | |
|             <ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div class="flex mt-2 -mx-1">
 | |
|           <div class="w-1/3 px-1">
 | |
|             <ui-text-input-with-label v-model="details.narrator" label="Narrator" />
 | |
|           </div>
 | |
|           <div class="w-1/3 px-1">
 | |
|             <ui-text-input-with-label v-model="details.publisher" label="Publisher" />
 | |
|           </div>
 | |
|           <div class="flex-grow px-1">
 | |
|             <ui-text-input-with-label v-model="details.language" label="Language" />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div class="flex mt-2 -mx-1">
 | |
|           <div class="w-1/3 px-1">
 | |
|             <ui-text-input-with-label v-model="details.isbn" label="ISBN" />
 | |
|           </div>
 | |
|           <div class="w-1/3 px-1">
 | |
|             <ui-text-input-with-label v-model="details.asin" label="ASIN" />
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       <div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
 | |
|         <div class="flex items-center px-4">
 | |
|           <ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
 | |
| 
 | |
|           <div class="flex-grow" />
 | |
| 
 | |
|           <ui-tooltip v-if="!isMissing" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4 hidden sm:block">
 | |
|             <ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
 | |
|           </ui-tooltip>
 | |
| 
 | |
|           <ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
 | |
|             <ui-btn v-if="isRootUser" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
 | |
|           </ui-tooltip>
 | |
| 
 | |
|           <ui-btn type="submit">Submit</ui-btn>
 | |
|         </div>
 | |
|       </div>
 | |
|     </form>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| export default {
 | |
|   props: {
 | |
|     processing: Boolean,
 | |
|     audiobook: {
 | |
|       type: Object,
 | |
|       default: () => {}
 | |
|     }
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       details: {
 | |
|         title: null,
 | |
|         subtitle: null,
 | |
|         description: null,
 | |
|         author: null,
 | |
|         narrator: null,
 | |
|         series: null,
 | |
|         volumeNumber: null,
 | |
|         publishYear: null,
 | |
|         publisher: null,
 | |
|         language: null,
 | |
|         isbn: null,
 | |
|         asin: null,
 | |
|         genres: []
 | |
|       },
 | |
|       newTags: [],
 | |
|       resettingProgress: false,
 | |
|       isScrollable: false,
 | |
|       savingMetadata: false,
 | |
|       rescanning: false
 | |
|     }
 | |
|   },
 | |
|   watch: {
 | |
|     audiobook: {
 | |
|       immediate: true,
 | |
|       handler(newVal) {
 | |
|         if (newVal) this.init()
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   computed: {
 | |
|     isProcessing: {
 | |
|       get() {
 | |
|         return this.processing
 | |
|       },
 | |
|       set(val) {
 | |
|         this.$emit('update:processing', val)
 | |
|       }
 | |
|     },
 | |
|     isRootUser() {
 | |
|       return this.$store.getters['user/getIsRoot']
 | |
|     },
 | |
|     isMissing() {
 | |
|       return !!this.audiobook && !!this.audiobook.isMissing
 | |
|     },
 | |
|     audiobookId() {
 | |
|       return this.audiobook ? this.audiobook.id : null
 | |
|     },
 | |
|     book() {
 | |
|       return this.audiobook ? this.audiobook.book || {} : {}
 | |
|     },
 | |
|     userCanDelete() {
 | |
|       return this.$store.getters['user/getUserCanDelete']
 | |
|     },
 | |
|     genres() {
 | |
|       return this.filterData.genres || []
 | |
|     },
 | |
|     tags() {
 | |
|       return this.filterData.tags || []
 | |
|     },
 | |
|     series() {
 | |
|       return this.filterData.series || []
 | |
|     },
 | |
|     filterData() {
 | |
|       return this.$store.state.libraries.filterData || {}
 | |
|     },
 | |
|     libraryId() {
 | |
|       return this.audiobook ? this.audiobook.libraryId : null
 | |
|     },
 | |
|     libraryScan() {
 | |
|       if (!this.libraryId) return null
 | |
|       return this.$store.getters['scanners/getLibraryScan'](this.libraryId)
 | |
|     }
 | |
|   },
 | |
|   methods: {
 | |
|     audiobookScanComplete(result) {
 | |
|       this.rescanning = false
 | |
|       if (!result) {
 | |
|         this.$toast.error(`Re-Scan Failed for "${this.title}"`)
 | |
|       } else if (result === 'UPDATED') {
 | |
|         this.$toast.success(`Re-Scan complete audiobook was updated`)
 | |
|       } else if (result === 'UPTODATE') {
 | |
|         this.$toast.success(`Re-Scan complete audiobook was up to date`)
 | |
|       } else if (result === 'REMOVED') {
 | |
|         this.$toast.error(`Re-Scan complete audiobook was removed`)
 | |
|       }
 | |
|     },
 | |
|     rescan() {
 | |
|       this.rescanning = true
 | |
|       this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete)
 | |
|       this.$root.socket.emit('scan_audiobook', this.audiobookId)
 | |
|     },
 | |
|     saveMetadataComplete(result) {
 | |
|       this.savingMetadata = false
 | |
|       if (result.error) {
 | |
|         this.$toast.error(result.error)
 | |
|       } else if (result.audiobookId) {
 | |
|         var { savedPath } = result
 | |
|         if (!savedPath) {
 | |
|           this.$toast.error(`Failed to save metadata file (${result.audiobookId})`)
 | |
|         } else {
 | |
|           this.$toast.success(`Metadata file saved "${result.audiobookTitle}"`)
 | |
|         }
 | |
|       }
 | |
|     },
 | |
|     saveMetadata() {
 | |
|       this.savingMetadata = true
 | |
|       this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
 | |
|       this.$root.socket.emit('save_metadata', this.audiobookId)
 | |
|     },
 | |
|     submitForm() {
 | |
|       if (this.isProcessing) {
 | |
|         return
 | |
|       }
 | |
|       this.isProcessing = true
 | |
|       if (this.$refs.seriesDropdown && this.$refs.seriesDropdown.isFocused) {
 | |
|         this.$refs.seriesDropdown.blur()
 | |
|       }
 | |
|       if (this.$refs.genresSelect && this.$refs.genresSelect.isFocused) {
 | |
|         this.$refs.genresSelect.forceBlur()
 | |
|       }
 | |
|       if (this.$refs.tagsSelect && this.$refs.tagsSelect.isFocused) {
 | |
|         this.$refs.tagsSelect.forceBlur()
 | |
|       }
 | |
|       this.$nextTick(this.handleForm)
 | |
|     },
 | |
|     async handleForm() {
 | |
|       const updatePayload = {
 | |
|         book: this.details,
 | |
|         tags: this.newTags
 | |
|       }
 | |
| 
 | |
|       var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
 | |
|         console.error('Failed to update', error)
 | |
|         return false
 | |
|       })
 | |
|       this.isProcessing = false
 | |
|       if (updatedAudiobook) {
 | |
|         this.$toast.success('Update Successful')
 | |
|         this.$emit('close')
 | |
|       }
 | |
|     },
 | |
|     init() {
 | |
|       this.details.title = this.book.title
 | |
|       this.details.subtitle = this.book.subtitle
 | |
|       this.details.description = this.book.description
 | |
|       this.details.author = this.book.author
 | |
|       this.details.narrator = this.book.narrator
 | |
|       this.details.genres = this.book.genres || []
 | |
|       this.details.series = this.book.series
 | |
|       this.details.volumeNumber = this.book.volumeNumber
 | |
|       this.details.publishYear = this.book.publishYear
 | |
|       this.details.publisher = this.book.publisher || null
 | |
|       this.details.language = this.book.language || null
 | |
|       this.details.isbn = this.book.isbn || null
 | |
|       this.details.asin = this.book.asin || null
 | |
| 
 | |
|       this.newTags = this.audiobook.tags || []
 | |
|     },
 | |
|     deleteAudiobook() {
 | |
|       if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) {
 | |
|         this.isProcessing = true
 | |
|         this.$axios
 | |
|           .$delete(`/api/books/${this.audiobookId}`)
 | |
|           .then(() => {
 | |
|             console.log('Audiobook removed')
 | |
|             this.$toast.success('Audiobook Removed')
 | |
|             this.$emit('close')
 | |
|             this.isProcessing = false
 | |
|           })
 | |
|           .catch((error) => {
 | |
|             console.error('Remove Audiobook failed', error)
 | |
|             this.isProcessing = false
 | |
|           })
 | |
|       }
 | |
|     },
 | |
|     checkIsScrollable() {
 | |
|       this.$nextTick(() => {
 | |
|         if (this.$refs.formWrapper) {
 | |
|           if (this.$refs.formWrapper.scrollHeight > this.$refs.formWrapper.clientHeight) {
 | |
|             this.isScrollable = true
 | |
|           } else {
 | |
|             this.isScrollable = false
 | |
|           }
 | |
|         }
 | |
|       })
 | |
|     },
 | |
|     setResizeObserver() {
 | |
|       try {
 | |
|         this.$nextTick(() => {
 | |
|           const resizeObserver = new ResizeObserver(() => {
 | |
|             this.checkIsScrollable()
 | |
|           })
 | |
|           resizeObserver.observe(this.$refs.formWrapper)
 | |
|         })
 | |
|       } catch (error) {
 | |
|         console.error('Failed to set resize observer')
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   mounted() {
 | |
|     this.setResizeObserver()
 | |
|   }
 | |
| }
 | |
| </script>
 | |
| 
 | |
| <style scoped>
 | |
| .details-form-wrapper {
 | |
|   height: calc(100% - 70px);
 | |
|   max-height: calc(100% - 70px);
 | |
| }
 | |
| </style> |