mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add: Experimental list view #149
This commit is contained in:
		
							parent
							
								
									6fd3317454
								
							
						
					
					
						commit
						729654f5b2
					
				| @ -19,6 +19,9 @@ | |||||||
| ::-webkit-scrollbar { | ::-webkit-scrollbar { | ||||||
|   width: 8px; |   width: 8px; | ||||||
| } | } | ||||||
|  | ::-webkit-scrollbar:horizontal { | ||||||
|  |   height: 8px; | ||||||
|  | } | ||||||
| /* ::-webkit-scrollbar:horizontal { */ | /* ::-webkit-scrollbar:horizontal { */ | ||||||
|   /* height: 16px; */ |   /* height: 16px; */ | ||||||
|   /* height: 24px; |   /* height: 24px; | ||||||
|  | |||||||
							
								
								
									
										127
									
								
								client/components/app/BookList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								client/components/app/BookList.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="outer-container"> | ||||||
|  |     <!-- absolute positioned container --> | ||||||
|  |     <div class="inner-container"> | ||||||
|  |       <div class="relative h-10"> | ||||||
|  |         <div class="table-header" id="headerdiv"> | ||||||
|  |           <table id="headertable" width="100%" cellpadding="0" cellspacing="0"> | ||||||
|  |             <thead> | ||||||
|  |               <tr> | ||||||
|  |                 <th class="header-cell min-w-12 max-w-12"></th> | ||||||
|  |                 <th class="header-cell min-w-6 max-w-6"></th> | ||||||
|  |                 <th class="header-cell min-w-64 max-w-64 px-2">Title</th> | ||||||
|  |                 <th class="header-cell min-w-48 max-w-48 px-2">Author</th> | ||||||
|  |                 <th class="header-cell min-w-48 max-w-48 px-2">Series</th> | ||||||
|  |                 <th class="header-cell min-w-24 max-w-24 px-2">Year</th> | ||||||
|  |                 <th class="header-cell min-w-80 max-w-80 px-2">Description</th> | ||||||
|  |                 <th class="header-cell min-w-48 max-w-48 px-2">Narrator</th> | ||||||
|  |                 <th class="header-cell min-w-48 max-w-48 px-2">Genres</th> | ||||||
|  |                 <th class="header-cell min-w-48 max-w-48 px-2">Tags</th> | ||||||
|  |                 <th class="header-cell min-w-24 max-w-24 px-2"></th> | ||||||
|  |               </tr> | ||||||
|  |             </thead> | ||||||
|  |           </table> | ||||||
|  |         </div> | ||||||
|  |         <div class="absolute top-0 left-0 w-full h-full pointer-events-none" :class="isScrollable ? 'header-shadow' : ''" /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div ref="tableBody" class="table-body" onscroll="document.getElementById('headerdiv').scrollLeft = this.scrollLeft;" @scroll="tableScrolled"> | ||||||
|  |         <table id="bodytable" width="100%" cellpadding="0" cellspacing="0"> | ||||||
|  |           <tbody> | ||||||
|  |             <template v-for="book in books"> | ||||||
|  |               <app-book-list-row :key="book.id" :book="book" @edit="editBook" /> | ||||||
|  |             </template> | ||||||
|  |           </tbody> | ||||||
|  |         </table> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     books: { | ||||||
|  |       type: Array, | ||||||
|  |       default: () => [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       isScrollable: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: {}, | ||||||
|  |   methods: { | ||||||
|  |     checkIsScrolled() { | ||||||
|  |       if (!this.$refs.tableBody) return | ||||||
|  |       this.isScrollable = this.$refs.tableBody.scrollTop > 0 | ||||||
|  |     }, | ||||||
|  |     tableScrolled() { | ||||||
|  |       this.checkIsScrolled() | ||||||
|  |     }, | ||||||
|  |     editBook(book) { | ||||||
|  |       var bookIds = this.books.map((e) => e.id) | ||||||
|  |       this.$store.commit('setBookshelfBookIds', bookIds) | ||||||
|  |       this.$store.commit('showEditModal', book) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.checkIsScrolled() | ||||||
|  |   }, | ||||||
|  |   beforeDestroy() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .outer-container { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0; | ||||||
|  |   left: 0; | ||||||
|  |   overflow: visible; | ||||||
|  |   height: calc(100% - 50px); | ||||||
|  |   width: calc(100% - 10px); | ||||||
|  |   margin: 10px; | ||||||
|  | } | ||||||
|  | .inner-container { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | .table-header { | ||||||
|  |   float: left; | ||||||
|  |   overflow: hidden; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  | .header-shadow { | ||||||
|  |   box-shadow: 3px 8px 3px #11111155; | ||||||
|  | } | ||||||
|  | .table-body { | ||||||
|  |   float: left; | ||||||
|  |   height: 100%; | ||||||
|  |   width: inherit; | ||||||
|  |   overflow-y: scroll; | ||||||
|  |   padding-right: 0px; | ||||||
|  | } | ||||||
|  | .header-cell { | ||||||
|  |   background-color: #22222288; | ||||||
|  |   padding: 0px 4px; | ||||||
|  |   text-align: left; | ||||||
|  |   height: 40px; | ||||||
|  |   font-size: 0.9rem; | ||||||
|  |   font-weight: semi-bold; | ||||||
|  | } | ||||||
|  | .body-cell { | ||||||
|  |   text-align: left; | ||||||
|  |   font-size: 0.9rem; | ||||||
|  | } | ||||||
|  | .book-row { | ||||||
|  |   background-color: #22222288; | ||||||
|  | } | ||||||
|  | .book-row:nth-child(odd) { | ||||||
|  |   background-color: #333; | ||||||
|  | } | ||||||
|  | .book-row.selected { | ||||||
|  |   background-color: rgba(0, 255, 0, 0.05); | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										184
									
								
								client/components/app/BookListRow.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								client/components/app/BookListRow.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,184 @@ | |||||||
|  | <template> | ||||||
|  |   <tr class="book-row" :class="selected ? 'selected' : ''"> | ||||||
|  |     <td class="body-cell min-w-12 max-w-12"> | ||||||
|  |       <div class="flex justify-center"> | ||||||
|  |         <div class="bg-white border-2 rounded border-gray-400 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500 w-4 h-4" @click="selectBtnClick"> | ||||||
|  |           <svg v-if="selected" class="fill-current text-green-500 pointer-events-none w-2.5 h-2.5" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-6 max-w-6"> | ||||||
|  |       <cards-book-cover :width="24" :audiobook="book" /> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-64 max-w-64 px-2"> | ||||||
|  |       <p class="truncate"> | ||||||
|  |         {{ book.book.title }}<span v-if="book.book.subtitle">: {{ book.book.subtitle }}</span> | ||||||
|  |       </p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-48 max-w-48 px-2"> | ||||||
|  |       <p class="truncate">{{ book.book.authorFL }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-48 max-w-48 px-2"> | ||||||
|  |       <p class="truncate">{{ seriesText }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-24 max-w-24 px-2"> | ||||||
|  |       <p class="truncate">{{ book.book.publishYear }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-80 max-w-80 px-2"> | ||||||
|  |       <p class="truncate">{{ book.book.description }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-48 max-w-48 px-2"> | ||||||
|  |       <p class="truncate">{{ book.book.narrator }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-48 max-w-48 px-2"> | ||||||
|  |       <p class="truncate">{{ genresText }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-48 max-w-48 px-2"> | ||||||
|  |       <p class="truncate">{{ tagsText }}</p> | ||||||
|  |     </td> | ||||||
|  |     <td class="body-cell min-w-24 max-w-24 px-2"> | ||||||
|  |       <div class="flex"> | ||||||
|  |         <span v-if="userCanUpdate" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-xl" @click="editClick">edit</span> | ||||||
|  |         <span v-if="showPlayButton" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-2xl mx-1" @click="startStream">play_arrow</span> | ||||||
|  |         <span v-if="showReadButton" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-xl" @click="openEbook">auto_stories</span> | ||||||
|  |       </div> | ||||||
|  |     </td> | ||||||
|  |   </tr> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     book: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => {} | ||||||
|  |     }, | ||||||
|  |     userAudiobook: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => {} | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       isProcessingReadUpdate: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     showExperimentalFeatures() { | ||||||
|  |       return this.$store.state.showExperimentalFeatures | ||||||
|  |     }, | ||||||
|  |     audiobookId() { | ||||||
|  |       return this.book.id | ||||||
|  |     }, | ||||||
|  |     isSelectionMode() { | ||||||
|  |       return !!this.selectedAudiobooks.length | ||||||
|  |     }, | ||||||
|  |     selectedAudiobooks() { | ||||||
|  |       return this.$store.state.selectedAudiobooks | ||||||
|  |     }, | ||||||
|  |     selected: { | ||||||
|  |       get() { | ||||||
|  |         return this.$store.getters['getIsAudiobookSelected'](this.audiobookId) | ||||||
|  |       }, | ||||||
|  |       set(val) { | ||||||
|  |         if (this.processingBatch) return | ||||||
|  |         this.$store.commit('setAudiobookSelected', { audiobookId: this.audiobookId, selected: val }) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     processingBatch() { | ||||||
|  |       return this.$store.state.processingBatch | ||||||
|  |     }, | ||||||
|  |     bookObj() { | ||||||
|  |       return this.book.book || {} | ||||||
|  |     }, | ||||||
|  |     series() { | ||||||
|  |       return this.bookObj.series || null | ||||||
|  |     }, | ||||||
|  |     volumeNumber() { | ||||||
|  |       return this.bookObj.volumeNumber || null | ||||||
|  |     }, | ||||||
|  |     seriesText() { | ||||||
|  |       if (!this.series) return '' | ||||||
|  |       if (!this.volumeNumber) return this.series | ||||||
|  |       return `${this.series} #${this.volumeNumber}` | ||||||
|  |     }, | ||||||
|  |     genresText() { | ||||||
|  |       if (!this.bookObj.genres) return '' | ||||||
|  |       return this.bookObj.genres.join(', ') | ||||||
|  |     }, | ||||||
|  |     tagsText() { | ||||||
|  |       return (this.book.tags || []).join(', ') | ||||||
|  |     }, | ||||||
|  |     isMissing() { | ||||||
|  |       return this.book.isMissing | ||||||
|  |     }, | ||||||
|  |     isIncomplete() { | ||||||
|  |       return this.book.isIncomplete | ||||||
|  |     }, | ||||||
|  |     numEbooks() { | ||||||
|  |       return this.book.numEbooks | ||||||
|  |     }, | ||||||
|  |     numTracks() { | ||||||
|  |       return this.book.numTracks | ||||||
|  |     }, | ||||||
|  |     isStreaming() { | ||||||
|  |       return this.$store.getters['getAudiobookIdStreaming'] === this.audiobookId | ||||||
|  |     }, | ||||||
|  |     showReadButton() { | ||||||
|  |       return this.showExperimentalFeatures && this.numEbooks | ||||||
|  |     }, | ||||||
|  |     showPlayButton() { | ||||||
|  |       return !this.isMissing && !this.isIncomplete && this.numTracks && !this.isStreaming | ||||||
|  |     }, | ||||||
|  |     userIsRead() { | ||||||
|  |       return this.userAudiobook ? !!this.userAudiobook.isRead : false | ||||||
|  |     }, | ||||||
|  |     userCanUpdate() { | ||||||
|  |       return this.$store.getters['user/getUserCanUpdate'] | ||||||
|  |     }, | ||||||
|  |     userCanDelete() { | ||||||
|  |       return this.$store.getters['user/getUserCanDelete'] | ||||||
|  |     }, | ||||||
|  |     userCanDownload() { | ||||||
|  |       return this.$store.getters['user/getUserCanDownload'] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     selectBtnClick() { | ||||||
|  |       if (this.processingBatch) return | ||||||
|  |       this.$store.commit('toggleAudiobookSelected', this.audiobookId) | ||||||
|  |     }, | ||||||
|  |     openEbook() { | ||||||
|  |       this.$store.commit('showEReader', this.book) | ||||||
|  |     }, | ||||||
|  |     downloadClick() { | ||||||
|  |       this.$store.commit('showEditModalOnTab', { audiobook: this.book, tab: 'download' }) | ||||||
|  |     }, | ||||||
|  |     toggleRead() { | ||||||
|  |       var updatePayload = { | ||||||
|  |         isRead: !this.userIsRead | ||||||
|  |       } | ||||||
|  |       this.isProcessingReadUpdate = true | ||||||
|  |       this.$axios | ||||||
|  |         .$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload) | ||||||
|  |         .then(() => { | ||||||
|  |           this.isProcessingReadUpdate = false | ||||||
|  |           this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) | ||||||
|  |         }) | ||||||
|  |         .catch((error) => { | ||||||
|  |           console.error('Failed', error) | ||||||
|  |           this.isProcessingReadUpdate = false | ||||||
|  |           this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) | ||||||
|  |         }) | ||||||
|  |     }, | ||||||
|  |     startStream() { | ||||||
|  |       this.$store.commit('setStreamAudiobook', this.book) | ||||||
|  |       this.$root.socket.emit('open_stream', this.book.id) | ||||||
|  |     }, | ||||||
|  |     editClick() { | ||||||
|  |       this.$emit('edit', this.book) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @ -1,57 +1,56 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative"> |   <div class="bookshelf overflow-hidden relative block max-h-full"> | ||||||
|     <!-- Cover size widget --> |     <div ref="wrapper" class="h-full w-full relative" :class="isGridMode ? 'overflow-y-scroll' : 'overflow-hidden'"> | ||||||
|     <div v-show="!isSelectionMode && isGridMode" class="fixed bottom-2 right-4 z-30"> |       <!-- Cover size widget --> | ||||||
|       <div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> |       <div v-show="!isSelectionMode && isGridMode" class="fixed bottom-2 right-4 z-30"> | ||||||
|         <span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span> |         <div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> | ||||||
|         <p class="px-2 font-mono">{{ bookCoverWidth }}</p> |           <span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span> | ||||||
|         <span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize">add</span> |           <p class="px-2 font-mono">{{ bookCoverWidth }}</p> | ||||||
|  |           <span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize">add</span> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |  | ||||||
| 
 | 
 | ||||||
|     <div v-if="!audiobooks.length" class="w-full flex flex-col items-center justify-center py-12"> |       <div v-if="!audiobooks.length" class="w-full flex flex-col items-center justify-center py-12"> | ||||||
|       <p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p> |         <p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p> | ||||||
|       <div class="flex"> |         <div class="flex"> | ||||||
|         <ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn> |           <ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn> | ||||||
|         <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> |           <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |       <div v-else-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center"> | ||||||
|     <div v-else-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center"> |         <template v-for="(shelf, index) in categorizedShelves"> | ||||||
|       <template v-for="(shelf, index) in categorizedShelves"> |           <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" /> | ||||||
|         <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" /> |         </template> | ||||||
|       </template> |         <div v-show="!categorizedShelves.length" class="w-full py-16 text-center text-xl"> | ||||||
|       <div v-show="!categorizedShelves.length" class="w-full py-16 text-center text-xl"> |           <div class="py-4 mb-6"><p class="text-2xl">No Results</p></div> | ||||||
|         <div class="py-4 mb-6"><p class="text-2xl">No Results</p></div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |       <div v-else class="w-full"> | ||||||
|     <div v-else id="bookshelf" class="w-full flex flex-col items-center"> |         <template v-if="viewMode === 'grid'"> | ||||||
|       <template v-if="viewMode === 'grid'"> |           <div class="w-full flex flex-col items-center"> | ||||||
|         <template v-for="(shelf, index) in shelves"> |             <template v-for="(shelf, index) in shelves"> | ||||||
|           <div :key="index" class="w-full bookshelfRow relative"> |               <div :key="index" class="w-full bookshelfRow relative"> | ||||||
|             <div class="flex justify-center items-center"> |                 <div class="flex justify-center items-center"> | ||||||
|               <template v-for="entity in shelf"> |                   <template v-for="entity in shelf"> | ||||||
|                 <cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" /> |                     <cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" /> | ||||||
|                 <!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> --> |                     <!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> --> | ||||||
|                 <cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" /> |                     <cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" /> | ||||||
|               </template> |                   </template> | ||||||
|             </div> |                 </div> | ||||||
|             <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> |                 <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> | ||||||
|  |               </div> | ||||||
|  |             </template> | ||||||
|           </div> |           </div> | ||||||
|         </template> |         </template> | ||||||
|       </template> |         <template v-else> | ||||||
|       <template v-else> |           <app-book-list :books="entities" /> | ||||||
|         <template v-for="(entity, index) in entities"> |  | ||||||
|           <div :key="index" class="w-full bookshelfRow relative"> |  | ||||||
|             <app-bookshelf-list-row :book-item="entity" :book-cover-width="bookCoverWidth" :user-audiobook="userAudiobooks[entity.id]" /> |  | ||||||
|             <div class="bookshelfDivider h-3 w-full absolute bottom-0 left-0 right-0 z-10" /> |  | ||||||
|           </div> |  | ||||||
|         </template> |         </template> | ||||||
|       </template> |         <div v-show="!shelves.length" class="w-full py-16 text-center text-xl"> | ||||||
|       <div v-show="!shelves.length" class="w-full py-16 text-center text-xl"> |           <div v-if="page === 'search'" class="py-4 mb-6"><p class="text-2xl">No Results</p></div> | ||||||
|         <div v-if="page === 'search'" class="py-4 mb-6"><p class="text-2xl">No Results</p></div> |           <div v-else class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div> | ||||||
|         <div v-else class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div> |           <ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn> | ||||||
|         <ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn> |           <ui-btn v-else-if="page === 'search'" to="/library">Back to Library</ui-btn> | ||||||
|         <ui-btn v-else-if="page === 'search'" to="/library">Back to Library</ui-btn> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @ -122,7 +121,6 @@ export default { | |||||||
|       return this.bookCoverWidth / 120 |       return this.bookCoverWidth / 120 | ||||||
|     }, |     }, | ||||||
|     bookCoverWidth() { |     bookCoverWidth() { | ||||||
|       if (this.viewMode === 'list') return 60 |  | ||||||
|       var coverWidth = this.availableSizes[this.selectedSizeIndex] |       var coverWidth = this.availableSizes[this.selectedSizeIndex] | ||||||
|       return coverWidth |       return coverWidth | ||||||
|     }, |     }, | ||||||
| @ -363,8 +361,9 @@ export default { | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
| #bookshelf { | .bookshelf { | ||||||
|   height: calc(100% - 40px); |   height: calc(100% - 40px); | ||||||
|  |   width: calc(100vw - 80px); | ||||||
| } | } | ||||||
| .bookshelfRow { | .bookshelfRow { | ||||||
|   background-image: url(/wood_panels.jpg); |   background-image: url(/wood_panels.jpg); | ||||||
|  | |||||||
| @ -1,216 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div :class="selected ? 'bg-success bg-opacity-10' : ''"> |  | ||||||
|     <div class="flex px-12 mx-auto" style="max-width: 1400px"> |  | ||||||
|       <div class="w-12 h-full flex items-center justify-center self-center"> |  | ||||||
|         <ui-checkbox v-model="selected" /> |  | ||||||
|       </div> |  | ||||||
|       <div class="p-3"> |  | ||||||
|         <cards-book-cover :width="bookCoverWidth" :audiobook="bookItem" /> |  | ||||||
|       </div> |  | ||||||
|       <div class="flex-grow p-3"> |  | ||||||
|         <div class="flex h-full"> |  | ||||||
|           <div class="w-full max-w-xl"> |  | ||||||
|             <nuxt-link :to="`/audiobook/${audiobookId}`" class="flex items-center hover:underline"> |  | ||||||
|               <p class="text-base font-book">{{ title }}<span v-if="subtitle">:</span></p> |  | ||||||
|               <p class="text-base font-book pl-2 text-gray-200">{{ subtitle }}</p> |  | ||||||
|             </nuxt-link> |  | ||||||
|             <p class="text-gray-200 text-sm" v-if="seriesText">{{ seriesText }}</p> |  | ||||||
|             <p class="text-sm text-gray-300">{{ author }}</p> |  | ||||||
|             <div class="flex pt-2"> |  | ||||||
|               <div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200"> |  | ||||||
|                 <p>{{ numTracks }} Tracks</p> |  | ||||||
|               </div> |  | ||||||
|               <div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200 mx-2"> |  | ||||||
|                 <p>{{ durationPretty }}</p> |  | ||||||
|               </div> |  | ||||||
|               <div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200"> |  | ||||||
|                 <p>{{ sizePretty }}</p> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|           <div class="w-full max-w-xl pr-6 pl-12 items-center h-full pb-3 hidden xl:flex"> |  | ||||||
|             <p class="text-sm text-gray-200 max-3-lines">{{ description }}</p> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div class="w-32 h-full self-center"> |  | ||||||
|         <div class="flex justify-center mb-2"> |  | ||||||
|           <ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9" @click="startStream"> |  | ||||||
|             <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> |  | ||||||
|             {{ streaming ? 'Streaming' : 'Play' }} |  | ||||||
|           </ui-btn> |  | ||||||
|         </div> |  | ||||||
|         <div class="flex"> |  | ||||||
|           <ui-tooltip v-if="userCanUpdate" text="Edit" direction="top"> |  | ||||||
|             <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" /> |  | ||||||
|           </ui-tooltip> |  | ||||||
| 
 |  | ||||||
|           <ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top"> |  | ||||||
|             <ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" /> |  | ||||||
|           </ui-tooltip> |  | ||||||
| 
 |  | ||||||
|           <ui-tooltip :text="userIsRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top"> |  | ||||||
|             <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsRead" class="mx-0.5" @click="toggleRead" /> |  | ||||||
|           </ui-tooltip> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     bookItem: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => {} |  | ||||||
|     }, |  | ||||||
|     userAudiobook: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => {} |  | ||||||
|     }, |  | ||||||
|     bookCoverWidth: Number |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       isProcessingReadUpdate: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     audiobookId() { |  | ||||||
|       return this.bookItem.id |  | ||||||
|     }, |  | ||||||
|     isSelectionMode() { |  | ||||||
|       return !!this.selectedAudiobooks.length |  | ||||||
|     }, |  | ||||||
|     selectedAudiobooks() { |  | ||||||
|       return this.$store.state.selectedAudiobooks |  | ||||||
|     }, |  | ||||||
|     selected: { |  | ||||||
|       get() { |  | ||||||
|         return this.$store.getters['getIsAudiobookSelected'](this.audiobookId) |  | ||||||
|       }, |  | ||||||
|       set(val) { |  | ||||||
|         if (this.processingBatch) return |  | ||||||
|         this.$store.commit('setAudiobookSelected', { audiobookId: this.audiobookId, selected: val }) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     processingBatch() { |  | ||||||
|       return this.$store.state.processingBatch |  | ||||||
|     }, |  | ||||||
|     isMissing() { |  | ||||||
|       return this.bookItem.isMissing |  | ||||||
|     }, |  | ||||||
|     isIncomplete() { |  | ||||||
|       return this.bookItem.isIncomplete |  | ||||||
|     }, |  | ||||||
|     numTracks() { |  | ||||||
|       return this.bookItem.numTracks |  | ||||||
|     }, |  | ||||||
|     durationPretty() { |  | ||||||
|       return this.$elapsedPretty(this.bookItem.duration) |  | ||||||
|     }, |  | ||||||
|     sizePretty() { |  | ||||||
|       return this.$bytesPretty(this.bookItem.size) |  | ||||||
|     }, |  | ||||||
|     streamAudiobook() { |  | ||||||
|       return this.$store.state.streamAudiobook |  | ||||||
|     }, |  | ||||||
|     streaming() { |  | ||||||
|       return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId |  | ||||||
|     }, |  | ||||||
|     book() { |  | ||||||
|       return this.bookItem.book || {} |  | ||||||
|     }, |  | ||||||
|     title() { |  | ||||||
|       return this.book.title |  | ||||||
|     }, |  | ||||||
|     subtitle() { |  | ||||||
|       return this.book.subtitle |  | ||||||
|     }, |  | ||||||
|     series() { |  | ||||||
|       return this.book.series || null |  | ||||||
|     }, |  | ||||||
|     volumeNumber() { |  | ||||||
|       return this.book.volumeNumber || null |  | ||||||
|     }, |  | ||||||
|     seriesText() { |  | ||||||
|       if (!this.series) return '' |  | ||||||
|       if (!this.volumeNumber) return this.series |  | ||||||
|       return `${this.series} #${this.volumeNumber}` |  | ||||||
|     }, |  | ||||||
|     description() { |  | ||||||
|       return this.book.description |  | ||||||
|     }, |  | ||||||
|     author() { |  | ||||||
|       return this.book.authorFL |  | ||||||
|     }, |  | ||||||
|     showPlayButton() { |  | ||||||
|       return !this.isMissing && !this.isIncomplete && this.numTracks |  | ||||||
|     }, |  | ||||||
|     userCurrentTime() { |  | ||||||
|       return this.userAudiobook ? this.userAudiobook.currentTime : 0 |  | ||||||
|     }, |  | ||||||
|     userIsRead() { |  | ||||||
|       return this.userAudiobook ? !!this.userAudiobook.isRead : false |  | ||||||
|     }, |  | ||||||
|     userCanUpdate() { |  | ||||||
|       return this.$store.getters['user/getUserCanUpdate'] |  | ||||||
|     }, |  | ||||||
|     userCanDelete() { |  | ||||||
|       return this.$store.getters['user/getUserCanDelete'] |  | ||||||
|     }, |  | ||||||
|     userCanDownload() { |  | ||||||
|       return this.$store.getters['user/getUserCanDownload'] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     selectBtnClick() { |  | ||||||
|       if (this.processingBatch) return |  | ||||||
|       this.$store.commit('toggleAudiobookSelected', this.audiobookId) |  | ||||||
|     }, |  | ||||||
|     openEbook() { |  | ||||||
|       this.$store.commit('showEReader', this.bookItem) |  | ||||||
|     }, |  | ||||||
|     downloadClick() { |  | ||||||
|       this.$store.commit('showEditModalOnTab', { audiobook: this.bookItem, tab: 'download' }) |  | ||||||
|     }, |  | ||||||
|     toggleRead() { |  | ||||||
|       var updatePayload = { |  | ||||||
|         isRead: !this.userIsRead |  | ||||||
|       } |  | ||||||
|       this.isProcessingReadUpdate = true |  | ||||||
|       this.$axios |  | ||||||
|         .$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload) |  | ||||||
|         .then(() => { |  | ||||||
|           this.isProcessingReadUpdate = false |  | ||||||
|           this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) |  | ||||||
|         }) |  | ||||||
|         .catch((error) => { |  | ||||||
|           console.error('Failed', error) |  | ||||||
|           this.isProcessingReadUpdate = false |  | ||||||
|           this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) |  | ||||||
|         }) |  | ||||||
|     }, |  | ||||||
|     startStream() { |  | ||||||
|       this.$store.commit('setStreamAudiobook', this.bookItem) |  | ||||||
|       this.$root.socket.emit('open_stream', this.bookItem.id) |  | ||||||
|     }, |  | ||||||
|     editClick() { |  | ||||||
|       this.$store.commit('setBookshelfBookIds', []) |  | ||||||
|       this.$store.commit('showEditModal', this.bookItem) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() {} |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
| .max-3-lines { |  | ||||||
|   overflow: hidden; |  | ||||||
|   text-overflow: ellipsis; |  | ||||||
|   display: -webkit-box; |  | ||||||
|   -webkit-line-clamp: 3; /* number of lines to show */ |  | ||||||
|   -webkit-box-orient: vertical; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @ -1,8 +1,8 @@ | |||||||
| <template> | <template> | ||||||
|   <label class="flex justify-start items-start"> |   <label class="flex justify-start items-start"> | ||||||
|     <div class="bg-white border-2 rounded border-gray-400 w-6 h-6 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500"> |     <div class="bg-white border-2 rounded border-gray-400 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500" :class="wrapperClass"> | ||||||
|       <input v-model="selected" type="checkbox" class="opacity-0 absolute" /> |       <input v-model="selected" type="checkbox" class="opacity-0 absolute" /> | ||||||
|       <svg v-if="selected" class="fill-current w-4 h-4 text-green-500 pointer-events-none" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> |       <svg v-if="selected" class="fill-current text-green-500 pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> | ||||||
|     </div> |     </div> | ||||||
|     <div v-if="label" class="select-none">{{ label }}</div> |     <div v-if="label" class="select-none">{{ label }}</div> | ||||||
|   </label> |   </label> | ||||||
| @ -12,7 +12,8 @@ | |||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     value: Boolean, |     value: Boolean, | ||||||
|     label: Boolean |     label: Boolean, | ||||||
|  |     small: Boolean | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return {} |     return {} | ||||||
| @ -25,6 +26,14 @@ export default { | |||||||
|       set(val) { |       set(val) { | ||||||
|         this.$emit('input', !!val) |         this.$emit('input', !!val) | ||||||
|       } |       } | ||||||
|  |     }, | ||||||
|  |     wrapperClass() { | ||||||
|  |       if (this.small) return 'w-4 h-4' | ||||||
|  |       return 'w-6 h-6' | ||||||
|  |     }, | ||||||
|  |     svgClass() { | ||||||
|  |       if (this.small) return 'w-3 h-3' | ||||||
|  |       return 'w-4 h-4' | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: {}, |   methods: {}, | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf-client", |   "name": "audiobookshelf-client", | ||||||
|   "version": "1.5.8", |   "version": "1.5.9", | ||||||
|   "description": "Audiobook manager and player", |   "description": "Audiobook manager and player", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | |||||||
| @ -17,6 +17,24 @@ module.exports = { | |||||||
|       height: { |       height: { | ||||||
|         '7.5': '1.75rem' |         '7.5': '1.75rem' | ||||||
|       }, |       }, | ||||||
|  |       maxWidth: { | ||||||
|  |         '6': '1.5rem', | ||||||
|  |         '12': '3rem', | ||||||
|  |         '24': '6rem', | ||||||
|  |         '32': '8rem', | ||||||
|  |         '48': '12rem', | ||||||
|  |         '64': '16rem', | ||||||
|  |         '80': '20rem' | ||||||
|  |       }, | ||||||
|  |       minWidth: { | ||||||
|  |         '6': '1.5rem', | ||||||
|  |         '12': '3rem', | ||||||
|  |         '24': '6rem', | ||||||
|  |         '32': '8rem', | ||||||
|  |         '48': '12rem', | ||||||
|  |         '64': '16rem', | ||||||
|  |         '80': '20rem' | ||||||
|  |       }, | ||||||
|       spacing: { |       spacing: { | ||||||
|         '-54': '-13.5rem' |         '-54': '-13.5rem' | ||||||
|       }, |       }, | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf", |   "name": "audiobookshelf", | ||||||
|   "version": "1.5.8", |   "version": "1.5.9", | ||||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", |   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user