<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>