<template> <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative"> <!-- Cover size widget --> <widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" /> <!-- Experimental Bookshelf Texture --> <div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40"> <div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div> </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">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> </div> </div> <div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center"> <p class="text-center text-xl font-book py-4">No results for query</p> </div> <div v-else class="w-full flex flex-col items-center"> <template v-for="(shelf, index) in shelves"> <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> </template> </div> </div> </template> <script> export default { props: { search: Boolean, results: { type: Object, default: () => {} } }, data() { return { loaded: false, keywordFilterTimeout: null, scannerParseSubtitle: false, wrapperClientWidth: 0, shelves: [] } }, computed: { isRootUser() { return this.$store.getters['user/getIsRoot'] }, showExperimentalFeatures() { return this.$store.state.showExperimentalFeatures }, currentLibraryId() { return this.$store.state.libraries.currentLibraryId }, bookCoverWidth() { var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') if (this.isCoverSquareAspectRatio) return coverSize * 1.6 return coverSize }, coverAspectRatio() { return this.$store.getters['getServerSetting']('coverAspectRatio') }, isCoverSquareAspectRatio() { return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE }, bookCoverAspectRatio() { return this.isCoverSquareAspectRatio ? 1 : 1.6 }, sizeMultiplier() { var baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.bookCoverWidth / baseSize } }, methods: { showBookshelfTextureModal() { this.$store.commit('globals/setShowBookshelfTextureModal', true) }, async init() { this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0 if (this.search) { this.setShelvesFromSearch() } else { await this.fetchCategories() } this.loaded = true }, async fetchCategories() { var categories = await this.$axios .$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`) .then((data) => { return data }) .catch((error) => { console.error('Failed to fetch categories', error) return [] }) this.shelves = categories }, async setShelvesFromSearch() { var shelves = [] if (this.results.books) { shelves.push({ id: 'books', label: 'Books', type: 'book', entities: this.results.books.map((res) => res.libraryItem) }) } if (this.results.series) { shelves.push({ id: 'series', label: 'Series', type: 'series', entities: this.results.series.map((seriesObj) => { return { name: seriesObj.series.name, series: seriesObj.series, books: seriesObj.books, type: 'series' } }) }) } if (this.results.tags) { shelves.push({ id: 'tags', label: 'Tags', type: 'tags', entities: this.results.tags.map((tagObj) => { return { name: tagObj.name, books: tagObj.books || [], type: 'tags' } }) }) } if (this.results.authors) { shelves.push({ id: 'authors', label: 'Authors', type: 'authors', entities: this.results.authors.map((a) => { return { ...a, type: 'author' } }) }) } this.shelves = shelves }, settingsUpdated(settings) {}, scan() { this.$store.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId }) }, libraryItemAdded(libraryItem) { console.log('libraryItem added', libraryItem) // TODO: Check if libraryItem would be on this shelf if (!this.search) { this.fetchCategories() } }, libraryItemUpdated(libraryItem) { console.log('libraryItem updated', libraryItem) this.shelves.forEach((shelf) => { if (shelf.type === 'books') { shelf.entities = shelf.entities.map((ent) => { if (ent.id === libraryItem.id) { return libraryItem } return ent }) } else if (shelf.type === 'series') { shelf.entities.forEach((ent) => { ent.books = ent.books.map((book) => { if (book.id === libraryItem.id) return libraryItem return book }) }) } }) }, removeBookFromShelf(libraryItem) { this.shelves.forEach((shelf) => { if (shelf.type === 'books') { shelf.entities = shelf.entities.filter((ent) => { return ent.id !== libraryItem.id }) } else if (shelf.type === 'series') { shelf.entities.forEach((ent) => { ent.books = ent.books.filter((book) => { return book.id !== libraryItem.id }) }) } }) }, libraryItemRemoved(libraryItem) { this.removeBookFromShelf(libraryItem) }, libraryItemsAdded(libraryItems) { console.log('libraryItems added', libraryItems) // TODO: Check if audiobook would be on this shelf if (!this.search) { this.fetchCategories() } }, libraryItemsUpdated(items) { items.forEach((li) => { this.libraryItemUpdated(li) }) }, initListeners() { this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) if (this.$root.socket) { this.$root.socket.on('item_updated', this.libraryItemUpdated) this.$root.socket.on('item_added', this.libraryItemAdded) this.$root.socket.on('item_removed', this.libraryItemRemoved) this.$root.socket.on('items_updated', this.libraryItemsUpdated) this.$root.socket.on('items_added', this.libraryItemsAdded) } else { console.error('Error socket not initialized') } }, removeListeners() { this.$store.commit('user/removeSettingsListener', 'bookshelf') if (this.$root.socket) { this.$root.socket.off('item_updated', this.libraryItemUpdated) this.$root.socket.off('item_added', this.libraryItemAdded) this.$root.socket.off('item_removed', this.libraryItemRemoved) this.$root.socket.off('items_updated', this.libraryItemsUpdated) this.$root.socket.off('items_added', this.libraryItemsAdded) } else { console.error('Error socket not initialized') } } }, mounted() { this.initListeners() this.init() }, beforeDestroy() { this.removeListeners() } } </script>