mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #3487 from mikiher/lazy-bookshelf-authors
Move authors to LazyBookshelf
This commit is contained in:
		
						commit
						a7ac82b023
					
				| @ -24,7 +24,7 @@ | ||||
|         </div> | ||||
|         <div v-if="shelf.type === 'authors'" class="flex items-center"> | ||||
|           <template v-for="entity in shelf.entities"> | ||||
|             <cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" /> | ||||
|             <cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" /> | ||||
|           </template> | ||||
|         </div> | ||||
|         <div v-if="shelf.type === 'narrators'" class="flex items-center"> | ||||
|  | ||||
| @ -30,7 +30,7 @@ | ||||
|         <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> | ||||
|         <span v-else class="material-symbols text-lg"></span> | ||||
|       </nuxt-link> | ||||
|       <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> | ||||
|       <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> | ||||
|         <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p> | ||||
|         <svg v-else class="w-5 h-5" viewBox="0 0 24 24"> | ||||
|           <path | ||||
| @ -62,7 +62,7 @@ | ||||
|         <ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" /> | ||||
|       </template> | ||||
|       <!-- library & collections page --> | ||||
|       <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome"> | ||||
|       <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage"> | ||||
|         <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p> | ||||
| 
 | ||||
|         <div class="flex-grow hidden sm:inline-block" /> | ||||
| @ -92,12 +92,14 @@ | ||||
|         <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" /> | ||||
|       </template> | ||||
|       <!-- authors page --> | ||||
|       <template v-else-if="page === 'authors'"> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn> | ||||
|       <template v-else-if="isAuthorsPage"> | ||||
|         <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p> | ||||
| 
 | ||||
|         <div class="flex-grow hidden sm:inline-block" /> | ||||
|         <ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn> | ||||
| 
 | ||||
|         <!-- author sort select --> | ||||
|         <controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" /> | ||||
|         <controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" /> | ||||
|       </template> | ||||
|       <!-- home page --> | ||||
|       <template v-else-if="isHome"> | ||||
| @ -117,11 +119,7 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     }, | ||||
|     searchQuery: String, | ||||
|     authors: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     } | ||||
|     searchQuery: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -268,7 +266,7 @@ export default { | ||||
|       return this.$route.name === 'library-library-podcast-latest' | ||||
|     }, | ||||
|     isAuthorsPage() { | ||||
|       return this.$route.name === 'library-library-authors' | ||||
|       return this.page === 'authors' | ||||
|     }, | ||||
|     isAlbumsPage() { | ||||
|       return this.page === 'albums' | ||||
| @ -284,6 +282,7 @@ export default { | ||||
|       if (this.isSeriesPage) return this.$strings.LabelSeries | ||||
|       if (this.isCollectionsPage) return this.$strings.LabelCollections | ||||
|       if (this.isPlaylistsPage) return this.$strings.LabelPlaylists | ||||
|       if (this.isAuthorsPage) return this.$strings.LabelAuthors | ||||
|       return '' | ||||
|     }, | ||||
|     seriesId() { | ||||
| @ -479,36 +478,48 @@ export default { | ||||
|           this.processingSeries = false | ||||
|         }) | ||||
|     }, | ||||
|     async fetchAllAuthors() { | ||||
|       // fetch all authors from the server, in the order that they are currently displayed | ||||
|       const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`) | ||||
|       return response.authors | ||||
|     }, | ||||
|     async matchAllAuthors() { | ||||
|       this.processingAuthors = true | ||||
| 
 | ||||
|       for (const author of this.authors) { | ||||
|         const payload = {} | ||||
|         if (author.asin) payload.asin = author.asin | ||||
|         else payload.q = author.name | ||||
|       try { | ||||
|         const authors = await this.fetchAllAuthors() | ||||
| 
 | ||||
|         payload.region = 'us' | ||||
|         if (this.libraryProvider.startsWith('audible.')) { | ||||
|           payload.region = this.libraryProvider.split('.').pop() || 'us' | ||||
|         for (const author of authors) { | ||||
|           const payload = {} | ||||
|           if (author.asin) payload.asin = author.asin | ||||
|           else payload.q = author.name | ||||
| 
 | ||||
|           payload.region = 'us' | ||||
|           if (this.libraryProvider.startsWith('audible.')) { | ||||
|             payload.region = this.libraryProvider.split('.').pop() || 'us' | ||||
|           } | ||||
| 
 | ||||
|           this.$eventBus.$emit(`searching-author-${author.id}`, true) | ||||
| 
 | ||||
|           var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => { | ||||
|             console.error('Failed', error) | ||||
|             return null | ||||
|           }) | ||||
|           if (!response) { | ||||
|             console.error(`Author ${author.name} not found`) | ||||
|             this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name])) | ||||
|           } else if (response.updated) { | ||||
|             if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`) | ||||
|             else console.log(`Author ${response.author.name} was updated (no image found)`) | ||||
|           } else { | ||||
|             console.log(`No updates were made for Author ${response.author.name}`) | ||||
|           } | ||||
| 
 | ||||
|           this.$eventBus.$emit(`searching-author-${author.id}`, false) | ||||
|         } | ||||
| 
 | ||||
|         this.$eventBus.$emit(`searching-author-${author.id}`, true) | ||||
| 
 | ||||
|         var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|           return null | ||||
|         }) | ||||
|         if (!response) { | ||||
|           console.error(`Author ${author.name} not found`) | ||||
|           this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name])) | ||||
|         } else if (response.updated) { | ||||
|           if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`) | ||||
|           else console.log(`Author ${response.author.name} was updated (no image found)`) | ||||
|         } else { | ||||
|           console.log(`No updates were made for Author ${response.author.name}`) | ||||
|         } | ||||
| 
 | ||||
|         this.$eventBus.$emit(`searching-author-${author.id}`, false) | ||||
|       } catch (error) { | ||||
|         console.error('Failed to match all authors', error) | ||||
|         this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed) | ||||
|       } | ||||
|       this.processingAuthors = false | ||||
|     }, | ||||
|  | ||||
| @ -91,6 +91,7 @@ export default { | ||||
|       if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries | ||||
|       if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections | ||||
|       if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists | ||||
|       if (this.page === 'authors') return this.$strings.MessageNoAuthors | ||||
|       if (this.hasFilter) { | ||||
|         if (this.filterName === 'Issues') return this.$strings.MessageNoIssues | ||||
|         else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds | ||||
| @ -111,6 +112,12 @@ export default { | ||||
|     seriesFilterBy() { | ||||
|       return this.$store.getters['user/getUserSetting']('seriesFilterBy') | ||||
|     }, | ||||
|     authorSortBy() { | ||||
|       return this.$store.getters['user/getUserSetting']('authorSortBy') | ||||
|     }, | ||||
|     authorSortDesc() { | ||||
|       return !!this.$store.getters['user/getUserSetting']('authorSortDesc') | ||||
|     }, | ||||
|     orderBy() { | ||||
|       return this.$store.getters['user/getUserSetting']('orderBy') | ||||
|     }, | ||||
| @ -217,6 +224,8 @@ export default { | ||||
|         this.$store.commit('globals/setEditCollection', entity) | ||||
|       } else if (this.entityName === 'playlists') { | ||||
|         this.$store.commit('globals/setEditPlaylist', entity) | ||||
|       } else if (this.entityName === 'authors') { | ||||
|         this.$store.commit('globals/showEditAuthorModal', entity) | ||||
|       } | ||||
|     }, | ||||
|     clearSelectedEntities() { | ||||
| @ -457,6 +466,9 @@ export default { | ||||
|         if (this.collapseBookSeries) { | ||||
|           searchParams.set('collapseseries', 1) | ||||
|         } | ||||
|       } else if (this.page === 'authors') { | ||||
|         searchParams.set('sort', this.authorSortBy) | ||||
|         searchParams.set('desc', this.authorSortDesc ? 1 : 0) | ||||
|       } else { | ||||
|         if (this.filterBy && this.filterBy !== 'all') { | ||||
|           searchParams.set('filter', this.filterBy) | ||||
| @ -601,6 +613,34 @@ export default { | ||||
|         this.executeRebuild() | ||||
|       } | ||||
|     }, | ||||
|     authorAdded(author) { | ||||
|       if (this.entityName !== 'authors') return | ||||
|       console.log(`[LazyBookshelf] authorAdded ${author.id}`, author) | ||||
|       this.resetEntities() | ||||
|     }, | ||||
|     authorUpdated(author) { | ||||
|       if (this.entityName !== 'authors') return | ||||
|       console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author) | ||||
|       const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id) | ||||
|       if (indexOf >= 0) { | ||||
|         this.entities[indexOf] = author | ||||
|         if (this.entityComponentRefs[indexOf]) { | ||||
|           this.entityComponentRefs[indexOf].setEntity(author) | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     authorRemoved(author) { | ||||
|       if (this.entityName !== 'authors') return | ||||
|       console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author) | ||||
|       const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id) | ||||
|       if (indexOf >= 0) { | ||||
|         this.entities = this.entities.filter((ent) => ent.id !== author.id) | ||||
|         this.totalEntities-- | ||||
|         this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities) | ||||
|         this.executeRebuild() | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     shareOpen(mediaItemShare) { | ||||
|       if (this.entityName === 'items' || this.entityName === 'series-books') { | ||||
|         var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId) | ||||
| @ -727,6 +767,9 @@ export default { | ||||
|         this.$root.socket.on('playlist_added', this.playlistAdded) | ||||
|         this.$root.socket.on('playlist_updated', this.playlistUpdated) | ||||
|         this.$root.socket.on('playlist_removed', this.playlistRemoved) | ||||
|         this.$root.socket.on('author_added', this.authorAdded) | ||||
|         this.$root.socket.on('author_updated', this.authorUpdated) | ||||
|         this.$root.socket.on('author_removed', this.authorRemoved) | ||||
|         this.$root.socket.on('share_open', this.shareOpen) | ||||
|         this.$root.socket.on('share_closed', this.shareClosed) | ||||
|       } else { | ||||
| @ -756,6 +799,9 @@ export default { | ||||
|         this.$root.socket.off('playlist_added', this.playlistAdded) | ||||
|         this.$root.socket.off('playlist_updated', this.playlistUpdated) | ||||
|         this.$root.socket.off('playlist_removed', this.playlistRemoved) | ||||
|         this.$root.socket.off('author_added', this.authorAdded) | ||||
|         this.$root.socket.off('author_updated', this.authorUpdated) | ||||
|         this.$root.socket.off('author_removed', this.authorRemoved) | ||||
|         this.$root.socket.off('share_open', this.shareOpen) | ||||
|         this.$root.socket.off('share_closed', this.shareClosed) | ||||
|       } else { | ||||
|  | ||||
| @ -58,7 +58,7 @@ | ||||
|         <div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|       </nuxt-link> | ||||
| 
 | ||||
|       <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|         <svg class="w-6 h-6" viewBox="0 0 24 24"> | ||||
|           <path | ||||
|             fill="currentColor" | ||||
| @ -180,7 +180,7 @@ export default { | ||||
|       return this.$route.name === 'library-library-series-id' || this.paramId === 'series' | ||||
|     }, | ||||
|     isAuthorsPage() { | ||||
|       return this.$route.name === 'library-library-authors' | ||||
|       return this.libraryBookshelfPage && this.paramId === 'authors' | ||||
|     }, | ||||
|     isNarratorsPage() { | ||||
|       return this.$route.name === 'library-library-narrators' | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }"> | ||||
|     <nuxt-link :to="`/author/${author.id}`"> | ||||
|   <div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }"> | ||||
|     <nuxt-link :to="`/author/${author?.id}`"> | ||||
|       <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||
|         <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> | ||||
|           <!-- Image or placeholder --> | ||||
| @ -40,7 +40,7 @@ | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     author: { | ||||
|     authorMount: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
| @ -57,7 +57,8 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       searching: false, | ||||
|       isHovering: false | ||||
|       isHovering: false, | ||||
|       author: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -68,34 +69,37 @@ export default { | ||||
|       return this.height * this.sizeMultiplier | ||||
|     }, | ||||
|     userToken() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
|       return this.store.getters['user/getToken'] | ||||
|     }, | ||||
|     _author() { | ||||
|       return this.author || {} | ||||
|     }, | ||||
|     authorId() { | ||||
|       return this._author.id | ||||
|       return this._author?.id || '' | ||||
|     }, | ||||
|     name() { | ||||
|       return this._author.name || '' | ||||
|       return this._author?.name || '' | ||||
|     }, | ||||
|     asin() { | ||||
|       return this._author.asin || '' | ||||
|       return this._author?.asin || '' | ||||
|     }, | ||||
|     numBooks() { | ||||
|       return this._author.numBooks || 0 | ||||
|       return this._author?.numBooks || 0 | ||||
|     }, | ||||
|     store() { | ||||
|       return this.$store || this.$nuxt.$store | ||||
|     }, | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|       return this.store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     currentLibraryId() { | ||||
|       return this.$store.state.libraries.currentLibraryId | ||||
|       return this.store.state.libraries.currentLibraryId | ||||
|     }, | ||||
|     libraryProvider() { | ||||
|       return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' | ||||
|       return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' | ||||
|     }, | ||||
|     sizeMultiplier() { | ||||
|       return this.$store.getters['user/getSizeMultiplier'] | ||||
|       return this.store.getters['user/getSizeMultiplier'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -132,13 +136,40 @@ export default { | ||||
|     }, | ||||
|     setSearching(isSearching) { | ||||
|       this.searching = isSearching | ||||
|     } | ||||
|     }, | ||||
|     setEntity(author) { | ||||
|       this.removeListeners() | ||||
|       this.author = author | ||||
|       this.addListeners() | ||||
|     }, | ||||
|     addListeners() { | ||||
|       if (this.author) { | ||||
|         this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching) | ||||
|       } | ||||
|     }, | ||||
|     removeListeners() { | ||||
|       if (this.author) { | ||||
|         this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching) | ||||
|       } | ||||
|     }, | ||||
|     destroy() { | ||||
|       // destroy the vue listeners, etc | ||||
|       this.$destroy() | ||||
| 
 | ||||
|       // remove the element from the DOM | ||||
|       if (this.$el && this.$el.parentNode) { | ||||
|         this.$el.parentNode.removeChild(this.$el) | ||||
|       } else if (this.$el && this.$el.remove) { | ||||
|         this.$el.remove() | ||||
|       } | ||||
|     }, | ||||
|     setSelectionMode(val) {} | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching) | ||||
|     if (this.authorMount) this.setEntity(this.authorMount) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching) | ||||
|     this.removeListeners() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -65,7 +65,7 @@ export default { | ||||
|         }, | ||||
|         authors: { | ||||
|           component: 'cards-author-card', | ||||
|           itemPropName: 'author', | ||||
|           itemPropName: 'author-mount', | ||||
|           itemIdFunc: (item) => item.id | ||||
|         }, | ||||
|         narrators: { | ||||
|  | ||||
| @ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue' | ||||
| import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue' | ||||
| 
 | ||||
| describe('AuthorCard', () => { | ||||
|   const author = { | ||||
|   const authorMount = { | ||||
|     id: 1, | ||||
|     name: 'John Doe', | ||||
|     numBooks: 5 | ||||
|   } | ||||
| 
 | ||||
|   const propsData = { | ||||
|     author, | ||||
|     authorMount, | ||||
|     nameBelow: false | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard' | ||||
| import LazyCollectionCard from '@/components/cards/LazyCollectionCard' | ||||
| import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard' | ||||
| import LazyAlbumCard from '@/components/cards/LazyAlbumCard' | ||||
| import AuthorCard from '@/components/cards/AuthorCard' | ||||
| 
 | ||||
| export default { | ||||
|   data() { | ||||
| @ -20,6 +21,7 @@ export default { | ||||
|       if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard) | ||||
|       if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard) | ||||
|       if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard) | ||||
|       if (this.entityName === 'authors') return Vue.extend(AuthorCard) | ||||
|       return Vue.extend(LazyBookCard) | ||||
|     }, | ||||
|     getComponentName() { | ||||
| @ -27,6 +29,7 @@ export default { | ||||
|       if (this.entityName === 'collections') return 'cards-lazy-collection-card' | ||||
|       if (this.entityName === 'playlists') return 'cards-lazy-playlist-card' | ||||
|       if (this.entityName === 'albums') return 'cards-lazy-album-card' | ||||
|       if (this.entityName === 'authors') return 'cards-author-card' | ||||
|       return 'cards-lazy-book-card' | ||||
|     }, | ||||
|     async setCardSize() { | ||||
| @ -46,13 +49,14 @@ export default { | ||||
|         props.orderBy = this.seriesSortBy | ||||
|       } | ||||
|       const instance = new ComponentClass({ | ||||
|         propsData: props | ||||
|         propsData: props, | ||||
|         parent: this | ||||
|       }) | ||||
|       instance.$mount() | ||||
|       this.resizeObserver = new ResizeObserver((entries) => { | ||||
|         for (let entry of entries) { | ||||
|           this.cardWidth = entry.contentRect.width | ||||
|           this.cardHeight = entry.contentRect.height | ||||
|           this.cardWidth = entry.borderBoxSize[0].inlineSize | ||||
|           this.cardHeight = entry.borderBoxSize[0].blockSize | ||||
|           this.resizeObserver.disconnect() | ||||
|           this.$refs.bookshelf.removeChild(instance.$el) | ||||
|         } | ||||
| @ -72,7 +76,7 @@ export default { | ||||
|       }) | ||||
|       const timeAfter = performance.now() | ||||
|     }, | ||||
|     async mountEntityCard(index) { | ||||
|     mountEntityCard(index) { | ||||
|       var shelf = Math.floor(index / this.entitiesPerShelf) | ||||
|       var shelfEl = document.getElementById(`shelf-${shelf}`) | ||||
|       if (!shelfEl) { | ||||
| @ -114,6 +118,7 @@ export default { | ||||
|       const _this = this | ||||
|       const instance = new ComponentClass({ | ||||
|         propsData: props, | ||||
|         parent: this, | ||||
|         created() { | ||||
|           this.$on('edit', (entity) => { | ||||
|             if (_this.editEntity) _this.editEntity(entity) | ||||
|  | ||||
| @ -53,7 +53,7 @@ export default { | ||||
|     }) | ||||
| 
 | ||||
|     if (!author) { | ||||
|       return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`) | ||||
|       return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`) | ||||
|     } | ||||
| 
 | ||||
|     if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) { | ||||
| @ -109,7 +109,7 @@ export default { | ||||
|     authorRemoved(author) { | ||||
|       if (author.id === this.author.id) { | ||||
|         console.warn('Author was removed') | ||||
|         this.$router.replace(`/library/${this.currentLibraryId}/authors`) | ||||
|         this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -1,115 +0,0 @@ | ||||
| <template> | ||||
|   <div class="page" :class="streamLibraryItem ? 'streaming' : ''"> | ||||
|     <app-book-shelf-toolbar page="authors" is-home :authors="authors" /> | ||||
|     <div id="bookshelf" class="w-full h-full p-8e overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }"> | ||||
|       <!-- Cover size widget --> | ||||
|       <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" /> | ||||
|       <div class="flex flex-wrap justify-center"> | ||||
|         <template v-for="author in authorsSorted"> | ||||
|           <cards-author-card :key="author.id" :author="author" class="p-3e" @edit="editAuthor" /> | ||||
|         </template> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   async asyncData({ store, params, redirect, query, app }) { | ||||
|     var libraryId = params.library | ||||
|     var libraryData = await store.dispatch('libraries/fetch', libraryId) | ||||
|     if (!libraryData) { | ||||
|       return redirect('/oops?message=Library not found') | ||||
|     } | ||||
| 
 | ||||
|     const library = libraryData.library | ||||
|     if (library.mediaType === 'podcast') { | ||||
|       return redirect(`/library/${libraryId}`) | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       libraryId | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       loading: true, | ||||
|       authors: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     sizeMultiplier() { | ||||
|       return this.$store.getters['user/getSizeMultiplier'] | ||||
|     }, | ||||
|     streamLibraryItem() { | ||||
|       return this.$store.state.streamLibraryItem | ||||
|     }, | ||||
|     currentLibraryId() { | ||||
|       return this.$store.state.libraries.currentLibraryId | ||||
|     }, | ||||
|     selectedAuthor() { | ||||
|       return this.$store.state.globals.selectedAuthor | ||||
|     }, | ||||
|     authorSortBy() { | ||||
|       return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name' | ||||
|     }, | ||||
|     authorSortDesc() { | ||||
|       return !!this.$store.getters['user/getUserSetting']('authorSortDesc') | ||||
|     }, | ||||
|     authorsSorted() { | ||||
|       const sortProp = this.authorSortBy | ||||
|       const bDesc = this.authorSortDesc ? -1 : 1 | ||||
|       return this.authors.sort((a, b) => { | ||||
|         if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { | ||||
|           // Fallback to name sort if equal | ||||
|           if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc | ||||
|           return a[sortProp] > b[sortProp] ? bDesc : -bDesc | ||||
|         } | ||||
|         return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     async init() { | ||||
|       this.authors = await this.$axios | ||||
|         .$get(`/api/libraries/${this.currentLibraryId}/authors`) | ||||
|         .then((response) => response.authors) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to load authors', error) | ||||
|           return [] | ||||
|         }) | ||||
|       this.loading = false | ||||
|     }, | ||||
|     authorAdded(author) { | ||||
|       if (!this.authors.some((au) => au.id === author.id)) { | ||||
|         this.authors.push(author) | ||||
|       } | ||||
|     }, | ||||
|     authorUpdated(author) { | ||||
|       this.authors = this.authors.map((au) => { | ||||
|         if (au.id === author.id) { | ||||
|           return author | ||||
|         } | ||||
|         return au | ||||
|       }) | ||||
|     }, | ||||
|     authorRemoved(author) { | ||||
|       this.authors = this.authors.filter((au) => au.id !== author.id) | ||||
|     }, | ||||
|     editAuthor(author) { | ||||
|       this.$store.commit('globals/showEditAuthorModal', author) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
|     this.$root.socket.on('author_added', this.authorAdded) | ||||
|     this.$root.socket.on('author_updated', this.authorUpdated) | ||||
|     this.$root.socket.on('author_removed', this.authorRemoved) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     this.$root.socket.off('author_added', this.authorAdded) | ||||
|     this.$root.socket.off('author_updated', this.authorUpdated) | ||||
|     this.$root.socket.off('author_removed', this.authorRemoved) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -27,7 +27,7 @@ export default { | ||||
| 
 | ||||
|     // Redirect podcast libraries | ||||
|     const library = libraryData.library | ||||
|     if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) { | ||||
|     if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) { | ||||
|       return redirect(`/library/${libraryId}`) | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -920,6 +920,7 @@ | ||||
|   "ToastLibraryScanFailedToStart": "Failed to start scan", | ||||
|   "ToastLibraryScanStarted": "Library scan started", | ||||
|   "ToastLibraryUpdateSuccess": "Library \"{0}\" updated", | ||||
|   "ToastMatchAllAuthorsFailed": "Failed to match all authors", | ||||
|   "ToastNameEmailRequired": "Name and email are required", | ||||
|   "ToastNameRequired": "Name is required", | ||||
|   "ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"", | ||||
|  | ||||
| @ -493,8 +493,8 @@ class LibraryController { | ||||
|     const payload = { | ||||
|       results: [], | ||||
|       total: undefined, | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, | ||||
|       limit: req.query.limit, | ||||
|       page: req.query.page, | ||||
|       sortBy: req.query.sort, | ||||
|       sortDesc: req.query.desc === '1', | ||||
|       filterBy: req.query.filter, | ||||
| @ -504,13 +504,6 @@ class LibraryController { | ||||
|       include: include.join(',') | ||||
|     } | ||||
| 
 | ||||
|     if (!Number.isInteger(payload.limit) || payload.limit < 0) { | ||||
|       return res.status(400).send('Invalid request. Limit must be a positive integer') | ||||
|     } | ||||
|     if (!Number.isInteger(payload.page) || payload.page < 0) { | ||||
|       return res.status(400).send('Invalid request. Page must be a positive integer') | ||||
|     } | ||||
| 
 | ||||
|     payload.offset = payload.page * payload.limit | ||||
| 
 | ||||
|     // TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
 | ||||
| @ -602,8 +595,8 @@ class LibraryController { | ||||
|     const payload = { | ||||
|       results: [], | ||||
|       total: 0, | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, | ||||
|       limit: req.query.limit, | ||||
|       page: req.query.page, | ||||
|       sortBy: req.query.sort, | ||||
|       sortDesc: req.query.desc === '1', | ||||
|       filterBy: req.query.filter, | ||||
| @ -674,8 +667,8 @@ class LibraryController { | ||||
|     const payload = { | ||||
|       results: [], | ||||
|       total: 0, | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, | ||||
|       limit: req.query.limit, | ||||
|       page: req.query.page, | ||||
|       sortBy: req.query.sort, | ||||
|       sortDesc: req.query.desc === '1', | ||||
|       filterBy: req.query.filter, | ||||
| @ -710,8 +703,8 @@ class LibraryController { | ||||
|     const payload = { | ||||
|       results: [], | ||||
|       total: playlistsForUser.length, | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0 | ||||
|       limit: req.query.limit, | ||||
|       page: req.query.page | ||||
|     } | ||||
| 
 | ||||
|     if (payload.limit) { | ||||
| @ -742,7 +735,7 @@ class LibraryController { | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async getUserPersonalizedShelves(req, res) { | ||||
|     const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 | ||||
|     const limitPerShelf = req.query.limit || 10 | ||||
|     const include = (req.query.include || '') | ||||
|       .split(',') | ||||
|       .map((v) => v.trim().toLowerCase()) | ||||
| @ -815,7 +808,7 @@ class LibraryController { | ||||
|       return res.status(400).send('Invalid request. Query param "q" must be a string') | ||||
|     } | ||||
| 
 | ||||
|     const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 | ||||
|     const limit = req.query.limit || 12 | ||||
|     const query = asciiOnlyToLowerCase(req.query.q.trim()) | ||||
| 
 | ||||
|     const matches = await libraryItemFilters.search(req.user, req.library, query, limit) | ||||
| @ -873,8 +866,40 @@ class LibraryController { | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async getAuthors(req, res) { | ||||
|     const isPaginated = req.query.limit && !isNaN(req.query.limit) && !isNaN(req.query.page) | ||||
| 
 | ||||
|     const payload = { | ||||
|       results: [], | ||||
|       total: 0, | ||||
|       limit: isPaginated ? Number(req.query.limit) : 0, | ||||
|       page: isPaginated ? Number(req.query.page) : 0, | ||||
|       sortBy: req.query.sort, | ||||
|       sortDesc: req.query.desc === '1', | ||||
|       filterBy: req.query.filter, | ||||
|       minified: req.query.minified === '1', | ||||
|       include: req.query.include | ||||
|     } | ||||
| 
 | ||||
|     // create order, limit and offset for pagination
 | ||||
|     let offset = isPaginated ? payload.page * payload.limit : undefined | ||||
|     let limit = isPaginated ? payload.limit : undefined | ||||
|     let order = undefined | ||||
|     const direction = payload.sortDesc ? 'DESC' : 'ASC' | ||||
|     if (payload.sortBy === 'name') { | ||||
|       order = [[Sequelize.literal('name COLLATE NOCASE'), direction]] | ||||
|     } else if (payload.sortBy === 'lastFirst') { | ||||
|       order = [[Sequelize.literal('lastFirst COLLATE NOCASE'), direction]] | ||||
|     } else if (payload.sortBy === 'addedAt') { | ||||
|       order = [['createdAt', direction]] | ||||
|     } else if (payload.sortBy === 'updatedAt') { | ||||
|       order = [['updatedAt', direction]] | ||||
|     } else if (payload.sortBy === 'numBooks') { | ||||
|       offset = undefined | ||||
|       limit = undefined | ||||
|     } | ||||
| 
 | ||||
|     const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user) | ||||
|     const authors = await Database.authorModel.findAll({ | ||||
|     const { rows: authors, count } = await Database.authorModel.findAndCountAll({ | ||||
|       where: { | ||||
|         libraryId: req.library.id | ||||
|       }, | ||||
| @ -888,10 +913,13 @@ class LibraryController { | ||||
|           attributes: [] | ||||
|         } | ||||
|       }, | ||||
|       order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']] | ||||
|       order: order, | ||||
|       limit: limit, | ||||
|       offset: offset, | ||||
|       distinct: true | ||||
|     }) | ||||
| 
 | ||||
|     const oldAuthors = [] | ||||
|     let oldAuthors = [] | ||||
| 
 | ||||
|     for (const author of authors) { | ||||
|       const oldAuthor = author.toOldJSONExpanded(author.books.length) | ||||
| @ -899,9 +927,25 @@ class LibraryController { | ||||
|       oldAuthors.push(oldAuthor) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       authors: oldAuthors | ||||
|     }) | ||||
|     // numBooks sort is handled post-query
 | ||||
|     if (payload.sortBy === 'numBooks') { | ||||
|       oldAuthors.sort((a, b) => (payload.sortDesc ? b.numBooks - a.numBooks : a.numBooks - b.numBooks)) | ||||
|       if (isPaginated) { | ||||
|         const startIndex = payload.page * payload.limit | ||||
|         const endIndex = startIndex + payload.limit | ||||
|         oldAuthors = oldAuthors.slice(startIndex, endIndex) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     payload.results = oldAuthors | ||||
|     if (isPaginated) { | ||||
|       payload.total = count | ||||
|       res.json(payload) | ||||
|     } else { | ||||
|       res.json({ | ||||
|         authors: payload.results | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -1096,8 +1140,8 @@ class LibraryController { | ||||
| 
 | ||||
|     const payload = { | ||||
|       episodes: [], | ||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0 | ||||
|       limit: req.query.limit, | ||||
|       page: req.query.page | ||||
|     } | ||||
| 
 | ||||
|     const offset = payload.page * payload.limit | ||||
| @ -1200,6 +1244,17 @@ class LibraryController { | ||||
|       return res.status(404).send('Library not found') | ||||
|     } | ||||
|     req.library = library | ||||
| 
 | ||||
|     // Ensure pagination query params are positive integers
 | ||||
|     for (const queryKey of ['limit', 'page']) { | ||||
|       if (req.query[queryKey] !== undefined) { | ||||
|         req.query[queryKey] = !isNaN(req.query[queryKey]) ? Number(req.query[queryKey]) : 0 | ||||
|         if (!Number.isInteger(req.query[queryKey]) || req.query[queryKey] < 0) { | ||||
|           return res.status(400).send(`Invalid request. ${queryKey} must be a positive integer`) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     next() | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user