mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	This commit is contained in:
		
							parent
							
								
									9057afb5ee
								
							
						
					
					
						commit
						6fd3317454
					
				| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative"> | ||||
|     <!-- Cover size widget --> | ||||
|     <div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-30"> | ||||
|     <div v-show="!isSelectionMode && isGridMode" class="fixed bottom-2 right-4 z-30"> | ||||
|       <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> | ||||
|         <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> | ||||
|         <p class="px-2 font-mono">{{ bookCoverWidth }}</p> | ||||
| @ -25,17 +25,27 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-else id="bookshelf" class="w-full flex flex-col items-center"> | ||||
|       <template v-for="(shelf, index) in shelves"> | ||||
|         <div :key="index" class="w-full bookshelfRow relative"> | ||||
|           <div class="flex justify-center items-center"> | ||||
|             <template v-for="entity in shelf"> | ||||
|               <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-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" /> | ||||
|             </template> | ||||
|       <template v-if="viewMode === 'grid'"> | ||||
|         <template v-for="(shelf, index) in shelves"> | ||||
|           <div :key="index" class="w-full bookshelfRow relative"> | ||||
|             <div class="flex justify-center items-center"> | ||||
|               <template v-for="entity in shelf"> | ||||
|                 <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-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" /> | ||||
|               </template> | ||||
|             </div> | ||||
|             <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> | ||||
|           </div> | ||||
|           <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> | ||||
|         </div> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template v-else> | ||||
|         <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> | ||||
|       <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> | ||||
| @ -56,7 +66,8 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
|     searchQuery: String | ||||
|     searchQuery: String, | ||||
|     viewMode: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -95,6 +106,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     isGridMode() { | ||||
|       return this.viewMode === 'grid' | ||||
|     }, | ||||
|     keywordFilter() { | ||||
|       return this.$store.state.audiobooks.keywordFilter | ||||
|     }, | ||||
| @ -108,7 +122,9 @@ export default { | ||||
|       return this.bookCoverWidth / 120 | ||||
|     }, | ||||
|     bookCoverWidth() { | ||||
|       return this.availableSizes[this.selectedSizeIndex] | ||||
|       if (this.viewMode === 'list') return 60 | ||||
|       var coverWidth = this.availableSizes[this.selectedSizeIndex] | ||||
|       return coverWidth | ||||
|     }, | ||||
|     sizeMultiplier() { | ||||
|       return this.bookCoverWidth / 120 | ||||
|  | ||||
| @ -20,6 +20,14 @@ | ||||
|         <ui-text-input v-show="!selectedSeries" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40" /> | ||||
|         <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" /> | ||||
|         <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" /> | ||||
|         <div v-if="showExperimentalFeatures" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md"> | ||||
|           <div class="h-full px-2 text-white flex items-center rounded-l-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'grid')"> | ||||
|             <span class="material-icons" style="font-size: 1.4rem">view_module</span> | ||||
|           </div> | ||||
|           <div class="h-full px-2 text-white flex items-center rounded-r-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="!isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'list')"> | ||||
|             <span class="material-icons" style="font-size: 1.4rem">view_list</span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </template> | ||||
|       <template v-else-if="!isHome"> | ||||
|         <div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer"> | ||||
| @ -44,7 +52,8 @@ export default { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
|     searchQuery: String | ||||
|     searchQuery: String, | ||||
|     viewMode: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -53,6 +62,12 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     }, | ||||
|     isGridMode() { | ||||
|       return this.viewMode === 'grid' | ||||
|     }, | ||||
|     showSortFilters() { | ||||
|       return this.page === '' | ||||
|     }, | ||||
|  | ||||
							
								
								
									
										216
									
								
								client/components/app/BookshelfListRow.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								client/components/app/BookshelfListRow.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,216 @@ | ||||
| <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> | ||||
| @ -8,7 +8,7 @@ | ||||
|       <div class="absolute -bottom-4 left-0 triangle-right" /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @click.stop> | ||||
|     <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @click.stop> | ||||
|       <nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer"> | ||||
|         <div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false"> | ||||
|           <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" /> | ||||
| @ -77,6 +77,10 @@ export default { | ||||
|       type: Number, | ||||
|       default: 120 | ||||
|     }, | ||||
|     paddingY: { | ||||
|       type: Number, | ||||
|       default: 16 | ||||
|     }, | ||||
|     showVolumeNumber: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|  | ||||
| @ -3,9 +3,9 @@ | ||||
|     <div class="rounded-sm h-full relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> | ||||
|       <nuxt-link :to="groupTo" class="cursor-pointer"> | ||||
|         <div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }"> | ||||
|           <cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" /> | ||||
|           <cards-group-cover ref="groupcover" :name="groupName" :group-to="groupTo" :book-items="bookItems" :width="height" :height="height" /> | ||||
| 
 | ||||
|           <div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> | ||||
|           <div v-if="hasValidCovers && !showExperimentalFeatures" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> | ||||
|             <p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p> | ||||
|           </div> | ||||
| 
 | ||||
| @ -100,6 +100,9 @@ export default { | ||||
|     hasValidCovers() { | ||||
|       var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover) | ||||
|       return !!validCovers.length | ||||
|     }, | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative"> | ||||
|   <div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative" @mouseover="mouseoverCover" @mouseleave="mouseleaveCover"> | ||||
|     <div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book" :style="{ padding: `${sizeMultiplier}rem` }"> | ||||
|       <p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p> | ||||
|     </div> | ||||
| @ -15,17 +15,21 @@ export default { | ||||
|       default: () => [] | ||||
|     }, | ||||
|     width: Number, | ||||
|     height: Number | ||||
|     height: Number, | ||||
|     groupTo: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       noValidCovers: false, | ||||
|       coverDiv: null, | ||||
|       isHovering: false, | ||||
|       coverWrapperEl: null, | ||||
|       coverImageEls: [], | ||||
|       coverWidth: 0, | ||||
|       offsetIncrement: 0, | ||||
|       isFannedOut: false | ||||
|       isFannedOut: false, | ||||
|       isDetached: false, | ||||
|       isAttaching: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -42,14 +46,66 @@ export default { | ||||
|   computed: { | ||||
|     sizeMultiplier() { | ||||
|       return this.width / 192 | ||||
|     }, | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     mouseoverCover() { | ||||
|       if (this.showExperimentalFeatures) this.setHover(true) | ||||
|     }, | ||||
|     mouseleaveCover() { | ||||
|       if (this.showExperimentalFeatures) this.setHover(false) | ||||
|     }, | ||||
|     detchCoverWrapper() { | ||||
|       if (!this.coverWrapperEl || !this.$refs.wrapper || this.isDetached) return | ||||
| 
 | ||||
|       this.coverWrapperEl.remove() | ||||
| 
 | ||||
|       this.isDetached = true | ||||
|       document.body.appendChild(this.coverWrapperEl) | ||||
|       this.coverWrapperEl.addEventListener('mouseleave', this.mouseleaveCover) | ||||
| 
 | ||||
|       this.coverWrapperEl.style.position = 'absolute' | ||||
|       this.coverWrapperEl.style.zIndex = 40 | ||||
| 
 | ||||
|       this.updatePosition() | ||||
|     }, | ||||
|     attachCoverWrapper() { | ||||
|       if (!this.coverWrapperEl || !this.$refs.wrapper || !this.isDetached) return | ||||
| 
 | ||||
|       this.coverWrapperEl.remove() | ||||
|       this.coverWrapperEl.style.position = 'relative' | ||||
|       this.coverWrapperEl.style.left = 'unset' | ||||
|       this.coverWrapperEl.style.top = 'unset' | ||||
|       this.coverWrapperEl.style.width = this.$refs.wrapper.clientWidth + 'px' | ||||
| 
 | ||||
|       this.$refs.wrapper.appendChild(this.coverWrapperEl) | ||||
|       console.log('Appended to wrapper', this.$refs.wrapper.children) | ||||
|       this.isDetached = false | ||||
|     }, | ||||
|     updatePosition() { | ||||
|       var rect = this.$refs.wrapper.getBoundingClientRect() | ||||
|       this.coverWrapperEl.style.top = rect.top + window.scrollY + 'px' | ||||
| 
 | ||||
|       this.coverWrapperEl.style.left = rect.left + window.scrollX + 4 + 'px' | ||||
| 
 | ||||
|       this.coverWrapperEl.style.height = rect.height + 'px' | ||||
|       this.coverWrapperEl.style.width = rect.width + 'px' | ||||
|     }, | ||||
|     setHover(val) { | ||||
|       if (this.isAttaching) return | ||||
|       if (val && !this.isHovering) { | ||||
|         this.detchCoverWrapper() | ||||
|         this.fanOutCovers() | ||||
|       } else if (!val && this.isHovering) { | ||||
|         this.isAttaching = true | ||||
|         this.reverseFan() | ||||
|         setTimeout(() => { | ||||
|           this.attachCoverWrapper() | ||||
|           this.isAttaching = false | ||||
|         }, 100) | ||||
|       } | ||||
|       this.isHovering = val | ||||
|     }, | ||||
| @ -57,15 +113,44 @@ export default { | ||||
|       if (this.coverImageEls.length < 2 || this.isFannedOut) return | ||||
|       this.isFannedOut = true | ||||
|       var fanCoverWidth = this.coverWidth * 0.75 | ||||
|       var maximumWidth = window.innerWidth - 80 | ||||
| 
 | ||||
|       var totalFanWidth = (this.coverImageEls.length + 1) * fanCoverWidth | ||||
| 
 | ||||
|       // If Fan width is too large, set new fan cover width | ||||
|       if (totalFanWidth > maximumWidth) { | ||||
|         fanCoverWidth = maximumWidth / (this.coverImageEls.length + 1) | ||||
|       } | ||||
| 
 | ||||
|       var fanWidth = (this.coverImageEls.length - 1) * fanCoverWidth | ||||
|       var offsetLeft = (-1 * fanWidth) / 2 | ||||
| 
 | ||||
|       var rect = this.$refs.wrapper.getBoundingClientRect() | ||||
| 
 | ||||
|       // If fan is going off page left or right, make adjustment | ||||
|       var leftEdge = rect.left + offsetLeft | ||||
|       var rightEdge = rect.left + rect.width - offsetLeft | ||||
|       if (leftEdge < 0) { | ||||
|         offsetLeft += leftEdge * -1 | ||||
|       } | ||||
|       if (rightEdge + 80 > window.innerWidth) { | ||||
|         var difference = rightEdge + 80 - window.innerWidth | ||||
|         offsetLeft -= difference / 2 | ||||
|       } | ||||
| 
 | ||||
|       for (let i = 0; i < this.coverImageEls.length; i++) { | ||||
|         var coverEl = this.coverImageEls[i] | ||||
| 
 | ||||
|         // Series name card pop out further | ||||
|         if (i === this.coverImageEls.length - 1) { | ||||
|           offsetLeft += fanCoverWidth * 0.25 | ||||
|         } | ||||
| 
 | ||||
|         coverEl.style.transform = `translateX(${offsetLeft}px)` | ||||
|         offsetLeft += fanCoverWidth | ||||
| 
 | ||||
|         var coverOverlay = document.createElement('div') | ||||
|         coverOverlay.className = 'absolute top-0 left-0 w-full h-full hover:bg-black hover:bg-opacity-40 text-white text-opacity-0 hover:text-opacity-100 flex items-center justify-center' | ||||
|         coverOverlay.className = 'absolute top-0 left-0 w-full h-full hover:bg-black hover:bg-opacity-40 text-white text-opacity-0 hover:text-opacity-100 flex items-center justify-center cursor-pointer' | ||||
| 
 | ||||
|         if (coverEl.dataset.volumeNumber) { | ||||
|           var pEl = document.createElement('p') | ||||
| @ -73,13 +158,22 @@ export default { | ||||
|           pEl.textContent = `#${coverEl.dataset.volumeNumber}` | ||||
|           coverOverlay.appendChild(pEl) | ||||
|         } | ||||
|         if (coverEl.dataset.audiobookId) { | ||||
|           let audiobookId = coverEl.dataset.audiobookId | ||||
|           coverOverlay.addEventListener('click', (e) => { | ||||
|             this.$router.push(`/audiobook/${audiobookId}`) | ||||
|             e.stopPropagation() | ||||
|             e.preventDefault() | ||||
|           }) | ||||
|         } else { | ||||
|           // Is Series | ||||
|           coverOverlay.addEventListener('click', (e) => { | ||||
|             this.$router.push(this.groupTo) | ||||
|             e.stopPropagation() | ||||
|             e.preventDefault() | ||||
|           }) | ||||
|         } | ||||
| 
 | ||||
|         let audiobookId = coverEl.dataset.audiobookId | ||||
|         coverOverlay.addEventListener('click', (e) => { | ||||
|           this.$router.push(`/audiobook/${audiobookId}`) | ||||
|           e.stopPropagation() | ||||
|           e.preventDefault() | ||||
|         }) | ||||
|         coverEl.appendChild(coverOverlay) | ||||
|       } | ||||
|     }, | ||||
| @ -89,7 +183,7 @@ export default { | ||||
|       for (let i = 0; i < this.coverImageEls.length; i++) { | ||||
|         var coverEl = this.coverImageEls[i] | ||||
|         coverEl.style.transform = 'translateX(0px)' | ||||
|         if (coverEl.lastChild) coverEl.lastChild.remove() | ||||
|         if (coverEl.lastChild) coverEl.lastChild.remove() // Remove cover overlay | ||||
|       } | ||||
|     }, | ||||
|     getCoverUrl(book) { | ||||
| @ -157,6 +251,22 @@ export default { | ||||
|       imgdiv.appendChild(img) | ||||
|       return imgdiv | ||||
|     }, | ||||
|     createSeriesNameCover(offsetLeft) { | ||||
|       var imgdiv = document.createElement('div') | ||||
|       imgdiv.style.height = this.height + 'px' | ||||
|       imgdiv.style.width = this.height / 1.6 + 'px' | ||||
|       imgdiv.style.left = offsetLeft + 'px' | ||||
|       imgdiv.className = 'absolute top-0 box-shadow-book transition-transform flex items-center justify-center' | ||||
|       imgdiv.style.boxShadow = '4px 0px 4px #11111166' | ||||
|       imgdiv.style.backgroundColor = '#111' | ||||
| 
 | ||||
|       var innerP = document.createElement('p') | ||||
|       innerP.textContent = this.name | ||||
|       innerP.className = 'text-sm font-book text-white' | ||||
|       imgdiv.appendChild(innerP) | ||||
| 
 | ||||
|       return imgdiv | ||||
|     }, | ||||
|     async init() { | ||||
|       if (this.coverDiv) { | ||||
|         this.coverDiv.remove() | ||||
| @ -187,16 +297,25 @@ export default { | ||||
|       this.offsetIncrement = widthPer | ||||
| 
 | ||||
|       var outerdiv = document.createElement('div') | ||||
|       this.coverWrapperEl = outerdiv | ||||
|       outerdiv.className = 'w-full h-full relative' | ||||
| 
 | ||||
|       var coverImageEls = [] | ||||
|       var offsetLeft = 0 | ||||
|       for (let i = 0; i < validCovers.length; i++) { | ||||
|         var offsetLeft = widthPer * i | ||||
|         offsetLeft = widthPer * i | ||||
|         var zIndex = validCovers.length - i | ||||
|         var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, zIndex, validCovers.length === 1) | ||||
|         outerdiv.appendChild(img) | ||||
|         coverImageEls.push(img) | ||||
|       } | ||||
| 
 | ||||
|       if (this.showExperimentalFeatures) { | ||||
|         var seriesNameCover = this.createSeriesNameCover(offsetLeft) | ||||
|         outerdiv.appendChild(seriesNameCover) | ||||
|         coverImageEls.push(seriesNameCover) | ||||
|       } | ||||
| 
 | ||||
|       this.coverImageEls = coverImageEls | ||||
| 
 | ||||
|       if (this.$refs.wrapper) { | ||||
| @ -205,6 +324,9 @@ export default { | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|   mounted() {}, | ||||
|   beforeDestroy() { | ||||
|     if (this.coverWrapperEl) this.coverWrapperEl.remove() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :disabled="disabled" :class="className" @click="clickBtn"> | ||||
|     <span class="material-icons icon-text">{{ icon }}</span> | ||||
|     <span class="material-icons" :style="{ fontSize }">{{ icon }}</span> | ||||
|   </button> | ||||
| </template> | ||||
| 
 | ||||
| @ -22,6 +22,10 @@ export default { | ||||
|       var classes = [] | ||||
|       classes.push(`bg-${this.bgColor}`) | ||||
|       return classes.join(' ') | ||||
|     }, | ||||
|     fontSize() { | ||||
|       if (this.icon === 'edit') return '1.25rem' | ||||
|       return '1.4rem' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.5.7", | ||||
|   "version": "1.5.8", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -316,9 +316,6 @@ export default { | ||||
|     numEbooks() { | ||||
|       return this.audiobook.numEbooks | ||||
|     }, | ||||
|     userToken() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
|     }, | ||||
|     description() { | ||||
|       return this.book.description || '' | ||||
|     }, | ||||
|  | ||||
| @ -1,13 +1,5 @@ | ||||
| <template> | ||||
|   <div class="page" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <!-- <app-book-shelf-toolbar /> --> | ||||
|     <!-- <div class="flex h-full"> | ||||
|       <app-side-rail /> | ||||
|       <div class="flex-grow"> --> | ||||
|     <!-- <app-book-shelf /> --> | ||||
|     <!-- </div> --> | ||||
|     <!-- </div> --> | ||||
| 
 | ||||
|     <div class="flex h-full"> | ||||
|       <app-side-rail /> | ||||
|       <div class="flex-grow"> | ||||
|  | ||||
| @ -3,8 +3,8 @@ | ||||
|     <div class="flex h-full"> | ||||
|       <app-side-rail /> | ||||
|       <div class="flex-grow"> | ||||
|         <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" /> | ||||
|         <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" /> | ||||
|         <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode.sync="viewMode" /> | ||||
|         <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode="viewMode" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -56,7 +56,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return {} | ||||
|     return { | ||||
|       viewMode: 'grid' | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     '$route.query'(newVal) { | ||||
|  | ||||
| @ -150,6 +150,15 @@ export const mutations = { | ||||
|       Vue.set(state, 'selectedAudiobooks', newSel) | ||||
|     } | ||||
|   }, | ||||
|   setAudiobookSelected(state, { audiobookId, selected }) { | ||||
|     var isThere = state.selectedAudiobooks.includes(audiobookId) | ||||
|     if (isThere && !selected) { | ||||
|       state.selectedAudiobooks = state.selectedAudiobooks.filter(a => a !== audiobookId) | ||||
|     } else if (selected && !isThere) { | ||||
|       var newSel = state.selectedAudiobooks.concat([audiobookId]) | ||||
|       Vue.set(state, 'selectedAudiobooks', newSel) | ||||
|     } | ||||
|   }, | ||||
|   setProcessingBatch(state, val) { | ||||
|     state.processingBatch = val | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.5.7", | ||||
|   "version": "1.5.8", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user