mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-26 08:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			187 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div class="relative w-full py-4 px-6 border border-white border-opacity-10 shadow-lg rounded-md my-6">
 | |
|     <div class="absolute -top-3 -left-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full">
 | |
|       <p class="text-base text-white text-opacity-80 font-mono">#{{ item.index }}</p>
 | |
|     </div>
 | |
| 
 | |
|     <div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
 | |
|       <span class="text-base text-white text-opacity-80 font-mono material-symbols">close</span>
 | |
|     </div>
 | |
| 
 | |
|     <template v-if="!uploadSuccess && !uploadFailed">
 | |
|       <widgets-alert v-if="error" type="error">
 | |
|         <p class="text-base">{{ error }}</p>
 | |
|       </widgets-alert>
 | |
| 
 | |
|       <div class="flex my-2 -mx-2">
 | |
|         <div class="w-1/2 px-2">
 | |
|           <ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
 | |
|         </div>
 | |
|         <div class="w-1/2 px-2">
 | |
|           <div v-if="!isPodcast" class="flex items-end">
 | |
|             <ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
 | |
|             <ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
 | |
|               <div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
 | |
|                 <span class="text-base text-white text-opacity-80 font-mono material-symbols">sync</span>
 | |
|               </div>
 | |
|             </ui-tooltip>
 | |
|           </div>
 | |
|           <div v-else class="w-full">
 | |
|             <p class="px-1 text-sm font-semibold">
 | |
|               {{ $strings.LabelDirectory }}
 | |
|               <em class="font-normal text-xs pl-2">(auto)</em>
 | |
|             </p>
 | |
|             <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|       <div v-if="!isPodcast" class="flex my-2 -mx-2">
 | |
|         <div class="w-1/2 px-2">
 | |
|           <ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
 | |
|         </div>
 | |
|         <div class="w-1/2 px-2">
 | |
|           <div class="w-full">
 | |
|             <label class="px-1 text-sm font-semibold">
 | |
|               {{ $strings.LabelDirectory }}
 | |
|               <em class="font-normal text-xs pl-2">(auto)</em>
 | |
|             </label>
 | |
|             <ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       <tables-uploaded-files-table :files="item.itemFiles" :title="$strings.HeaderItemFiles" class="mt-8" />
 | |
|       <tables-uploaded-files-table v-if="item.otherFiles.length" :title="$strings.HeaderOtherFiles" :files="item.otherFiles" />
 | |
|       <tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
 | |
|     </template>
 | |
|     <widgets-alert v-if="uploadSuccess" type="success">
 | |
|       <p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemSuccess }}</p>
 | |
|     </widgets-alert>
 | |
|     <widgets-alert v-if="uploadFailed" type="error">
 | |
|       <p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
 | |
|     </widgets-alert>
 | |
| 
 | |
|     <div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
 | |
|       <ui-loading-indicator :text="nonInteractionLabel" />
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| import Path from 'path'
 | |
| 
 | |
| export default {
 | |
|   props: {
 | |
|     item: {
 | |
|       type: Object,
 | |
|       default: () => {}
 | |
|     },
 | |
|     mediaType: String,
 | |
|     processing: Boolean,
 | |
|     provider: String
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       itemData: {
 | |
|         title: '',
 | |
|         author: '',
 | |
|         series: ''
 | |
|       },
 | |
|       error: '',
 | |
|       isUploading: false,
 | |
|       uploadFailed: false,
 | |
|       uploadSuccess: false,
 | |
|       isFetchingMetadata: false
 | |
|     }
 | |
|   },
 | |
|   computed: {
 | |
|     isPodcast() {
 | |
|       return this.mediaType === 'podcast'
 | |
|     },
 | |
|     directory() {
 | |
|       if (!this.itemData.title) return ''
 | |
|       if (this.isPodcast) return this.itemData.title
 | |
| 
 | |
|       const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
 | |
|       const cleanedOutputPathParts = outputPathParts.filter(Boolean).map((part) => this.$sanitizeFilename(part))
 | |
| 
 | |
|       return Path.join(...cleanedOutputPathParts)
 | |
|     },
 | |
|     isNonInteractable() {
 | |
|       return this.isUploading || this.isFetchingMetadata
 | |
|     },
 | |
|     nonInteractionLabel() {
 | |
|       if (this.isUploading) {
 | |
|         return this.$strings.MessageUploading
 | |
|       } else if (this.isFetchingMetadata) {
 | |
|         return this.$strings.LabelFetchingMetadata
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   methods: {
 | |
|     setUploadStatus(status) {
 | |
|       this.isUploading = status === 'uploading'
 | |
|       this.uploadFailed = status === 'failed'
 | |
|       this.uploadSuccess = status === 'success'
 | |
|     },
 | |
|     titleUpdated() {
 | |
|       this.error = ''
 | |
|     },
 | |
|     async fetchMetadata() {
 | |
|       if (!this.itemData.title.trim().length) {
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       this.isFetchingMetadata = true
 | |
|       this.error = ''
 | |
| 
 | |
|       try {
 | |
|         const searchQueryString = new URLSearchParams({
 | |
|           title: this.itemData.title,
 | |
|           author: this.itemData.author,
 | |
|           provider: this.provider
 | |
|         })
 | |
|         const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
 | |
| 
 | |
|         if (bestCandidate) {
 | |
|           this.itemData = {
 | |
|             ...this.itemData,
 | |
|             title: bestCandidate.title,
 | |
|             author: bestCandidate.author,
 | |
|             series: (bestCandidate.series || [])[0]?.series
 | |
|           }
 | |
|         } else {
 | |
|           this.error = this.$strings.ErrorUploadFetchMetadataNoResults
 | |
|         }
 | |
|       } catch (e) {
 | |
|         console.error('Failed', e)
 | |
|         this.error = this.$strings.ErrorUploadFetchMetadataAPI
 | |
|       } finally {
 | |
|         this.isFetchingMetadata = false
 | |
|       }
 | |
|     },
 | |
|     getData() {
 | |
|       if (!this.itemData.title) {
 | |
|         this.error = this.$strings.ErrorUploadLacksTitle
 | |
|         return null
 | |
|       }
 | |
|       this.error = ''
 | |
|       var files = this.item.itemFiles.concat(this.item.otherFiles)
 | |
|       return {
 | |
|         index: this.item.index,
 | |
|         directory: this.directory,
 | |
|         ...this.itemData,
 | |
|         files
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   mounted() {
 | |
|     if (this.item) {
 | |
|       this.itemData.title = this.item.title
 | |
|       this.itemData.author = this.item.author
 | |
|       this.itemData.series = this.item.series
 | |
|     }
 | |
|   }
 | |
| }
 | |
| </script>
 |