mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:Native audio player set time and play on load #227, Change:Mobile UI updates & cleanup old bookshelf
This commit is contained in:
		
							parent
							
								
									4592e1f494
								
							
						
					
					
						commit
						6d5f6bc46e
					
				| @ -22,7 +22,7 @@ | |||||||
|         <controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" /> |         <controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" /> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div class="flex pb-8 sm:pb-4 md:pb-2"> |       <div class="flex pb-4 md:pb-2"> | ||||||
|         <div class="flex-grow" /> |         <div class="flex-grow" /> | ||||||
|         <template v-if="!loading"> |         <template v-if="!loading"> | ||||||
|           <div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart"> |           <div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart"> | ||||||
| @ -82,7 +82,7 @@ | |||||||
|       <p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> |       <p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <audio ref="audio" @progress="progress" @timeupdate="timeupdate" @loadedmetadata="audioLoadedMetadata" @loadeddata="audioLoadedData" @play="audioPlayed" @pause="audioPaused" @error="audioError" @ended="audioEnded" @stalled="audioStalled" @suspend="audioSuspended" /> |     <audio ref="audio" @progress="progress" @timeupdate="timeupdate" @loadedmetadata="audioLoadedMetadata" @play="audioPlayed" @pause="audioPaused" @error="audioError" @ended="audioEnded" @stalled="audioStalled" @suspend="audioSuspended" /> | ||||||
| 
 | 
 | ||||||
|     <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" /> |     <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" /> | ||||||
|   </div> |   </div> | ||||||
| @ -109,6 +109,9 @@ export default { | |||||||
|     return { |     return { | ||||||
|       hlsInstance: null, |       hlsInstance: null, | ||||||
|       staleHlsInstance: null, |       staleHlsInstance: null, | ||||||
|  |       usingNativeAudioPlayer: false, | ||||||
|  |       playOnLoad: false, | ||||||
|  |       startTime: 0, | ||||||
|       volume: 1, |       volume: 1, | ||||||
|       playbackRate: 1, |       playbackRate: 1, | ||||||
|       trackWidth: 0, |       trackWidth: 0, | ||||||
| @ -194,7 +197,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     audioStalled() { |     audioStalled() { | ||||||
|       if (!this.$refs.audio) return |       if (!this.$refs.audio) return | ||||||
|       console.warn('Audio Ended', this.$refs.audio.paused, this.$refs.audio.currentTime) |       console.warn('Audio Stalled', this.$refs.audio.paused, this.$refs.audio.currentTime) | ||||||
|     }, |     }, | ||||||
|     audioSuspended() { |     audioSuspended() { | ||||||
|       if (!this.$refs.audio) return |       if (!this.$refs.audio) return | ||||||
| @ -555,11 +558,13 @@ export default { | |||||||
|       this.playedTrackWidth = ptWidth |       this.playedTrackWidth = ptWidth | ||||||
|     }, |     }, | ||||||
|     audioLoadedMetadata() { |     audioLoadedMetadata() { | ||||||
|       console.log('Audio METADATA Loaded, total duration', this.audioEl.duration) |  | ||||||
|       this.totalDuration = this.audioEl.duration |       this.totalDuration = this.audioEl.duration | ||||||
|       this.$emit('loaded', this.totalDuration) |       this.$emit('loaded', this.totalDuration) | ||||||
|  |       if (this.usingNativeAudioPlayer) { | ||||||
|  |         this.audioEl.currentTime = this.startTime | ||||||
|  |         this.play() | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     audioLoadedData() {}, |  | ||||||
|     set(url, currentTime, playOnLoad = false) { |     set(url, currentTime, playOnLoad = false) { | ||||||
|       if (this.hlsInstance) { |       if (this.hlsInstance) { | ||||||
|         this.terminateStream() |         this.terminateStream() | ||||||
| @ -570,6 +575,8 @@ export default { | |||||||
|       } |       } | ||||||
|       this.listeningTimeSinceLastUpdate = 0 |       this.listeningTimeSinceLastUpdate = 0 | ||||||
| 
 | 
 | ||||||
|  |       this.playOnLoad = playOnLoad | ||||||
|  |       this.startTime = currentTime | ||||||
|       this.url = url |       this.url = url | ||||||
|       if (process.env.NODE_ENV === 'development') { |       if (process.env.NODE_ENV === 'development') { | ||||||
|         url = `${process.env.serverUrl}${url}` |         url = `${process.env.serverUrl}${url}` | ||||||
| @ -584,6 +591,7 @@ export default { | |||||||
|       // iOS does not support Media Elements but allows for HLS in the native audio player |       // iOS does not support Media Elements but allows for HLS in the native audio player | ||||||
|       if (!Hls.isSupported()) { |       if (!Hls.isSupported()) { | ||||||
|         console.warn('HLS is not supported - fallback to using audio element') |         console.warn('HLS is not supported - fallback to using audio element') | ||||||
|  |         this.usingNativeAudioPlayer = true | ||||||
|         audio.src = this.src + '?token=' + this.token |         audio.src = this.src + '?token=' + this.token | ||||||
|         audio.currentTime = currentTime |         audio.currentTime = currentTime | ||||||
|         return |         return | ||||||
| @ -604,10 +612,10 @@ export default { | |||||||
|         // console.log('[HLS] MEDIA ATTACHED') |         // console.log('[HLS] MEDIA ATTACHED') | ||||||
|         this.hlsInstance.loadSource(url) |         this.hlsInstance.loadSource(url) | ||||||
| 
 | 
 | ||||||
|         this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, function () { |         this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { | ||||||
|           console.log('[HLS] Manifest Parsed') |           console.log('[HLS] Manifest Parsed') | ||||||
|           if (playOnLoad) { |           if (playOnLoad) { | ||||||
|             audio.play() |             this.play() | ||||||
|           } |           } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,449 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div id="bookshelf" class="overflow-hidden relative block max-h-full"> |  | ||||||
|     <div ref="wrapper" class="h-full w-full relative" :class="isGridMode ? 'overflow-y-scroll' : 'overflow-hidden'"> |  | ||||||
|       <!-- Cover size widget --> |  | ||||||
|       <div v-show="!isSelectionMode && isGridMode" class="fixed bottom-4 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> |  | ||||||
|           <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> |  | ||||||
|       <!-- Experimental Bookshelf Texture --> |  | ||||||
|       <div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40"> |  | ||||||
|         <div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div v-if="!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> |  | ||||||
|         <div class="flex"> |  | ||||||
|           <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> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div v-else-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center"> |  | ||||||
|         <template v-for="(shelf, index) in categorizedShelves"> |  | ||||||
|           <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" /> |  | ||||||
|         </template> |  | ||||||
|         <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> |  | ||||||
|       </div> |  | ||||||
|       <div v-else class="w-full"> |  | ||||||
|         <template v-if="viewMode === 'grid'"> |  | ||||||
|           <div 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-collection-card v-if="isCollections" :key="entity.id" :width="bookCoverWidth" :collection="entity" @click="clickGroup" /> |  | ||||||
|                     <cards-group-card v-else-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 :ref="`book-card-${entity.id}`" :key="entity.id" :is-bookshelf-book="!isSeries" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" @hook:mounted="mountedBookCard(entity)" /> |  | ||||||
|                   </template> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="isCollections || isSeriesGroups ? 'h-6' : 'h-4'" /> |  | ||||||
|               </div> |  | ||||||
|             </template> |  | ||||||
|           </div> |  | ||||||
|         </template> |  | ||||||
|         <template v-else> |  | ||||||
|           <app-book-list :books="entities" /> |  | ||||||
|         </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> |  | ||||||
|           <div v-else class="py-4 capitalize">No {{ showGroups ? page : 'Audiobooks' }}</div> |  | ||||||
|           <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> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     page: String, |  | ||||||
|     selectedSeries: String, |  | ||||||
|     searchResults: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => {} |  | ||||||
|     }, |  | ||||||
|     searchQuery: String, |  | ||||||
|     viewMode: String |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       shelves: [], |  | ||||||
|       currSearchParams: null, |  | ||||||
|       availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220], |  | ||||||
|       selectedSizeIndex: 3, |  | ||||||
|       rowPaddingX: 40, |  | ||||||
|       keywordFilterTimeout: null, |  | ||||||
|       scannerParseSubtitle: false, |  | ||||||
|       wrapperClientWidth: 0, |  | ||||||
|       observer: null, |  | ||||||
|       booksObserved: [], |  | ||||||
|       booksVisible: {} |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     keywordFilter() { |  | ||||||
|       this.checkKeywordFilter() |  | ||||||
|     }, |  | ||||||
|     selectedSeries() { |  | ||||||
|       this.$nextTick(() => { |  | ||||||
|         this.$store.commit('audiobooks/setSelectedSeries', this.selectedSeries) |  | ||||||
|         this.setBookshelfEntities() |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     searchResults() { |  | ||||||
|       this.$nextTick(() => { |  | ||||||
|         this.setBookshelfEntities() |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     '$route.query.filter'() { |  | ||||||
|       if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) { |  | ||||||
|         this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter }) |  | ||||||
|       } else if (!this.$route.query.filter && this.filterBy) { |  | ||||||
|         this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     showExperimentalFeatures() { |  | ||||||
|       return this.$store.state.showExperimentalFeatures |  | ||||||
|     }, |  | ||||||
|     isGridMode() { |  | ||||||
|       return this.viewMode === 'grid' |  | ||||||
|     }, |  | ||||||
|     keywordFilter() { |  | ||||||
|       return this.$store.state.audiobooks.keywordFilter |  | ||||||
|     }, |  | ||||||
|     userAudiobooks() { |  | ||||||
|       return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {} |  | ||||||
|     }, |  | ||||||
|     audiobooks() { |  | ||||||
|       return this.$store.state.audiobooks.audiobooks |  | ||||||
|     }, |  | ||||||
|     sizeMultiplier() { |  | ||||||
|       return this.bookCoverWidth / 120 |  | ||||||
|     }, |  | ||||||
|     bookCoverWidth() { |  | ||||||
|       var coverWidth = this.availableSizes[this.selectedSizeIndex] |  | ||||||
|       return coverWidth |  | ||||||
|     }, |  | ||||||
|     sizeMultiplier() { |  | ||||||
|       return this.bookCoverWidth / 120 |  | ||||||
|     }, |  | ||||||
|     paddingX() { |  | ||||||
|       return 16 * this.sizeMultiplier |  | ||||||
|     }, |  | ||||||
|     bookWidth() { |  | ||||||
|       var coverWidth = this.bookCoverWidth |  | ||||||
|       if (this.page === 'collections') coverWidth *= 2 |  | ||||||
|       var _width = coverWidth + this.paddingX * 2 |  | ||||||
|       return this.showGroups ? _width * 1.6 : _width |  | ||||||
|     }, |  | ||||||
|     isSelectionMode() { |  | ||||||
|       return this.$store.getters['getNumAudiobooksSelected'] |  | ||||||
|     }, |  | ||||||
|     filterBy() { |  | ||||||
|       return this.$store.getters['user/getUserSetting']('filterBy') |  | ||||||
|     }, |  | ||||||
|     orderBy() { |  | ||||||
|       return this.$store.getters['user/getUserSetting']('orderBy') |  | ||||||
|     }, |  | ||||||
|     orderDesc() { |  | ||||||
|       return this.$store.getters['user/getUserSetting']('orderDesc') |  | ||||||
|     }, |  | ||||||
|     showGroups() { |  | ||||||
|       return this.page !== '' && this.page !== 'search' && !this.selectedSeries |  | ||||||
|     }, |  | ||||||
|     isCollections() { |  | ||||||
|       return this.page === 'collections' |  | ||||||
|     }, |  | ||||||
|     isSeries() { |  | ||||||
|       return this.page === 'series' |  | ||||||
|     }, |  | ||||||
|     isSeriesGroups() { |  | ||||||
|       return this.isSeries && !this.selectedSeries |  | ||||||
|     }, |  | ||||||
|     categorizedShelves() { |  | ||||||
|       if (this.page !== 'search') return [] |  | ||||||
|       var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : [] |  | ||||||
|       const shelves = [] |  | ||||||
| 
 |  | ||||||
|       if (audiobookSearchResults.length) { |  | ||||||
|         shelves.push({ |  | ||||||
|           label: 'Books', |  | ||||||
|           books: audiobookSearchResults.map((absr) => absr.audiobook) |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (this.searchResults.series && this.searchResults.series.length) { |  | ||||||
|         var seriesGroups = this.searchResults.series.map((seriesResult) => { |  | ||||||
|           return { |  | ||||||
|             type: 'series', |  | ||||||
|             name: seriesResult.series || '', |  | ||||||
|             books: seriesResult.audiobooks || [] |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         shelves.push({ |  | ||||||
|           label: 'Series', |  | ||||||
|           series: seriesGroups |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (this.searchResults.tags && this.searchResults.tags.length) { |  | ||||||
|         var tagGroups = this.searchResults.tags.map((tagResult) => { |  | ||||||
|           return { |  | ||||||
|             type: 'tags', |  | ||||||
|             name: tagResult.tag || '', |  | ||||||
|             books: tagResult.audiobooks || [] |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         shelves.push({ |  | ||||||
|           label: 'Tags', |  | ||||||
|           series: tagGroups |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return shelves |  | ||||||
|     }, |  | ||||||
|     entities() { |  | ||||||
|       return [] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     showBookshelfTextureModal() { |  | ||||||
|       this.$store.commit('globals/setShowBookshelfTextureModal', true) |  | ||||||
|     }, |  | ||||||
|     editBook(audiobook) { |  | ||||||
|       var bookIds = this.entities.map((e) => e.id) |  | ||||||
|       this.$store.commit('setBookshelfBookIds', bookIds) |  | ||||||
|       this.$store.commit('showEditModal', audiobook) |  | ||||||
|     }, |  | ||||||
|     clickGroup(group) { |  | ||||||
|       if (this.page === 'collections') return |  | ||||||
|       this.$emit('update:selectedSeries', group.name) |  | ||||||
|     }, |  | ||||||
|     clearFilter() { |  | ||||||
|       this.$store.commit('audiobooks/setKeywordFilter', null) |  | ||||||
|       if (this.filterBy !== 'all') { |  | ||||||
|         this.$store.dispatch('user/updateUserSettings', { |  | ||||||
|           filterBy: 'all' |  | ||||||
|         }) |  | ||||||
|       } else { |  | ||||||
|         this.setBookshelfEntities() |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     checkKeywordFilter() { |  | ||||||
|       clearTimeout(this.keywordFilterTimeout) |  | ||||||
|       this.keywordFilterTimeout = setTimeout(() => { |  | ||||||
|         this.setBookshelfEntities() |  | ||||||
|       }, 500) |  | ||||||
|     }, |  | ||||||
|     increaseSize() { |  | ||||||
|       this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1) |  | ||||||
|       this.resize() |  | ||||||
|       this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth }) |  | ||||||
|     }, |  | ||||||
|     decreaseSize() { |  | ||||||
|       this.selectedSizeIndex = Math.max(0, this.selectedSizeIndex - 1) |  | ||||||
|       this.resize() |  | ||||||
|       this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth }) |  | ||||||
|     }, |  | ||||||
|     setBookshelfEntities() { |  | ||||||
|       this.wrapperClientWidth = this.$refs.wrapper.clientWidth |  | ||||||
|       var width = Math.max(0, this.wrapperClientWidth - this.rowPaddingX * 2) |  | ||||||
| 
 |  | ||||||
|       var booksPerRow = Math.floor(width / this.bookWidth) |  | ||||||
| 
 |  | ||||||
|       this.currSearchParams = this.buildSearchParams() |  | ||||||
| 
 |  | ||||||
|       var entities = this.entities |  | ||||||
| 
 |  | ||||||
|       var groups = [] |  | ||||||
|       var currentRow = 0 |  | ||||||
|       var currentGroup = [] |  | ||||||
| 
 |  | ||||||
|       for (let i = 0; i < entities.length; i++) { |  | ||||||
|         var row = Math.floor(i / booksPerRow) |  | ||||||
|         if (row > currentRow) { |  | ||||||
|           groups.push([...currentGroup]) |  | ||||||
|           currentRow = row |  | ||||||
|           currentGroup = [] |  | ||||||
|         } |  | ||||||
|         currentGroup.push(entities[i]) |  | ||||||
|       } |  | ||||||
|       if (currentGroup.length) { |  | ||||||
|         groups.push([...currentGroup]) |  | ||||||
|       } |  | ||||||
|       this.shelves = groups |  | ||||||
|     }, |  | ||||||
|     async init() { |  | ||||||
|       this.checkUpdateSearchParams() |  | ||||||
| 
 |  | ||||||
|       this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0 |  | ||||||
| 
 |  | ||||||
|       var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') |  | ||||||
|       var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize) |  | ||||||
|       if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex |  | ||||||
| 
 |  | ||||||
|       // var isLoading = await this.$store.dispatch('audiobooks/load') |  | ||||||
|       // if (!isLoading) { |  | ||||||
|       //   this.setBookshelfEntities() |  | ||||||
|       // } |  | ||||||
|     }, |  | ||||||
|     resize() { |  | ||||||
|       this.$nextTick(this.setBookshelfEntities) |  | ||||||
|     }, |  | ||||||
|     audiobooksUpdated() { |  | ||||||
|       console.log('[Bookshelf] Audiobooks Updated') |  | ||||||
|       this.setBookshelfEntities() |  | ||||||
|     }, |  | ||||||
|     collectionsUpdated() { |  | ||||||
|       if (!this.isCollections) return |  | ||||||
|       console.log('[Bookshelf] Collections Updated') |  | ||||||
|       this.setBookshelfEntities() |  | ||||||
|     }, |  | ||||||
|     buildSearchParams() { |  | ||||||
|       if (this.page === 'search' || this.page === 'series' || this.page === 'collections') { |  | ||||||
|         return '' |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       let searchParams = new URLSearchParams() |  | ||||||
|       if (this.filterBy && this.filterBy !== 'all') { |  | ||||||
|         searchParams.set('filter', this.filterBy) |  | ||||||
|       } |  | ||||||
|       if (this.orderBy) { |  | ||||||
|         searchParams.set('order', this.orderBy) |  | ||||||
|         searchParams.set('orderdesc', this.orderDesc ? 1 : 0) |  | ||||||
|       } |  | ||||||
|       return searchParams.toString() |  | ||||||
|     }, |  | ||||||
|     checkUpdateSearchParams() { |  | ||||||
|       var newSearchParams = this.buildSearchParams() |  | ||||||
|       var currentQueryString = window.location.search |  | ||||||
| 
 |  | ||||||
|       if (newSearchParams === '') { |  | ||||||
|         return false |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (newSearchParams !== this.currSearchParams || newSearchParams !== currentQueryString) { |  | ||||||
|         let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams |  | ||||||
|         window.history.replaceState({ path: newurl }, '', newurl) |  | ||||||
|         return true |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return false |  | ||||||
|     }, |  | ||||||
|     settingsUpdated(settings) { |  | ||||||
|       var wasUpdated = this.checkUpdateSearchParams() |  | ||||||
|       if (wasUpdated) this.setBookshelfEntities() |  | ||||||
| 
 |  | ||||||
|       if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) { |  | ||||||
|         var index = this.availableSizes.indexOf(settings.bookshelfCoverSize) |  | ||||||
|         if (index >= 0) { |  | ||||||
|           this.selectedSizeIndex = index |  | ||||||
|           this.resize() |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     scan() { |  | ||||||
|       this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId) |  | ||||||
|     }, |  | ||||||
|     mountedBookCard(entity, shouldUnobserve = false) { |  | ||||||
|       if (!this.observer) { |  | ||||||
|         console.error('Observer not loaded', entity.id) |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       var el = document.getElementById(`book-card-${entity.id}`) |  | ||||||
|       if (el) { |  | ||||||
|         if (shouldUnobserve) { |  | ||||||
|           console.warn('Unobserving el', el) |  | ||||||
|           this.observer.unobserve(el) |  | ||||||
|         } |  | ||||||
|         this.observer.observe(el) |  | ||||||
|         this.booksObserved.push(entity.id) |  | ||||||
|         // console.log('Book observed', this.booksObserved.length) |  | ||||||
|       } else { |  | ||||||
|         console.error('Could not get book card', entity.id) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     getBookCard(id) { |  | ||||||
|       if (!this.$refs[id] || !this.$refs[id].length) { |  | ||||||
|         return null |  | ||||||
|       } |  | ||||||
|       return this.$refs[id][0] |  | ||||||
|     }, |  | ||||||
|     observerCallback(entries, observer) { |  | ||||||
|       entries.forEach((entry) => { |  | ||||||
|         var bookId = entry.target.getAttribute('data-bookId') |  | ||||||
|         if (!bookId) { |  | ||||||
|           console.error('Invalid observe no book id', entry) |  | ||||||
|           return |  | ||||||
|         } |  | ||||||
|         var component = this.getBookCard(entry.target.id) |  | ||||||
|         if (component) { |  | ||||||
|           if (entry.isIntersecting) { |  | ||||||
|             if (!this.booksVisible[bookId]) { |  | ||||||
|               this.booksVisible[bookId] = true |  | ||||||
|               component.setShowCard(true) |  | ||||||
|             } |  | ||||||
|           } else if (this.booksVisible[bookId]) { |  | ||||||
|             this.booksVisible[bookId] = false |  | ||||||
|             component.setShowCard(false) |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           console.error('Could not get book card for id', entry.target.id) |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     initIO() { |  | ||||||
|       let observerOptions = { |  | ||||||
|         rootMargin: '0px', |  | ||||||
|         threshold: 0.1 |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       this.observer = new IntersectionObserver(this.observerCallback, observerOptions) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   updated() { |  | ||||||
|     if (this.$refs.wrapper) { |  | ||||||
|       if (this.wrapperClientWidth !== this.$refs.wrapper.clientWidth) { |  | ||||||
|         this.$nextTick(this.setBookshelfEntities) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     window.addEventListener('resize', this.resize) |  | ||||||
|     this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated }) |  | ||||||
|     this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) |  | ||||||
|     this.$store.commit('user/addCollectionsListener', { id: 'bookshelf', meth: this.collectionsUpdated }) |  | ||||||
| 
 |  | ||||||
|     this.init() |  | ||||||
|     this.initIO() |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     window.removeEventListener('resize', this.resize) |  | ||||||
|     this.$store.commit('audiobooks/removeListener', 'bookshelf') |  | ||||||
|     this.$store.commit('user/removeSettingsListener', 'bookshelf') |  | ||||||
|     this.$store.commit('user/removeCollectionsListener', 'bookshelf') |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
| .bookshelfRow { |  | ||||||
|   background-image: var(--bookshelf-texture-img); |  | ||||||
| } |  | ||||||
| .bookshelfDivider { |  | ||||||
|   background: rgb(149, 119, 90); |  | ||||||
|   background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); |  | ||||||
|   box-shadow: 2px 14px 8px #111111aa; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @ -149,7 +149,6 @@ export default { | |||||||
|           }) |           }) | ||||||
|         }) |         }) | ||||||
|       } |       } | ||||||
| 
 |  | ||||||
|       this.shelves = shelves |       this.shelves = shelves | ||||||
|     }, |     }, | ||||||
|     settingsUpdated(settings) {}, |     settingsUpdated(settings) {}, | ||||||
|  | |||||||
| @ -1,40 +1,28 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="relative"> |   <div class="relative"> | ||||||
|     <div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem' }" @scroll="scrolled"> |     <div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled"> | ||||||
|       <div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }"> |       <div class="w-full h-full pt-6"> | ||||||
|         <div v-if="shelf.type === 'books'" class="flex items-center -mb-2"> |         <div v-if="shelf.type === 'books'" class="flex items-center"> | ||||||
|           <template v-for="entity in shelf.entities"> |           <template v-for="(entity, index) in shelf.entities"> | ||||||
|             <cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" :padding-y="24" :book-cover-aspect-ratio="bookCoverAspectRatio" @edit="editBook" /> |             <cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectBook" @edit="editBook" /> | ||||||
|           </template> |           </template> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="shelf.type === 'series'" class="flex items-center -mb-2"> |         <div v-if="shelf.type === 'series'" class="flex items-center"> | ||||||
|           <template v-for="entity in shelf.entities"> |           <template v-for="entity in shelf.entities"> | ||||||
|             <cards-group-card :key="entity.name" is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" @click="$emit('clickSeries', entity)" /> |             <cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" /> | ||||||
|           </template> |           </template> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="shelf.type === 'tags'" class="flex items-center -mb-2"> |         <div v-if="shelf.type === 'tags'" class="flex items-center"> | ||||||
|           <template v-for="entity in shelf.entities"> |           <template v-for="entity in shelf.entities"> | ||||||
|             <nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`"> |             <nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`"> | ||||||
|               <cards-group-card is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" /> |               <cards-group-card is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" /> | ||||||
|             </nuxt-link> |             </nuxt-link> | ||||||
|           </template> |           </template> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="shelf.type === 'authors'" class="flex items-center -mb-2"> |         <div v-if="shelf.type === 'authors'" class="flex items-center"> | ||||||
|           <template v-for="entity in shelf.entities"> |           <template v-for="entity in shelf.entities"> | ||||||
|             <nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.name)}`"> |             <nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.name)}`"> | ||||||
|               <cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6" /> |               <cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" /> | ||||||
|             </nuxt-link> |  | ||||||
|           </template> |  | ||||||
|         </div> |  | ||||||
|         <div v-else-if="shelf.series" class="flex items-center -mb-2"> |  | ||||||
|           <template v-for="entity in shelf.series"> |  | ||||||
|             <cards-group-card is-categorized :key="entity.name" :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" @click="$emit('clickSeries', entity)" /> |  | ||||||
|           </template> |  | ||||||
|         </div> |  | ||||||
|         <div v-else-if="shelf.tags" class="flex items-center -mb-2"> |  | ||||||
|           <template v-for="entity in shelf.tags"> |  | ||||||
|             <nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`"> |  | ||||||
|               <cards-group-card is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" /> |  | ||||||
|             </nuxt-link> |             </nuxt-link> | ||||||
|           </template> |           </template> | ||||||
|         </div> |         </div> | ||||||
| @ -49,10 +37,10 @@ | |||||||
| 
 | 
 | ||||||
|     <div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div> |     <div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div> | ||||||
| 
 | 
 | ||||||
|     <div v-show="canScrollLeft && !isScrolling" class="absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left flex items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft"> |     <div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft"> | ||||||
|       <span class="material-icons text-6xl text-white">chevron_left</span> |       <span class="material-icons text-6xl text-white">chevron_left</span> | ||||||
|     </div> |     </div> | ||||||
|     <div v-show="canScrollRight && !isScrolling" class="absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right flex items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight"> |     <div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight"> | ||||||
|       <span class="material-icons text-6xl text-white">chevron_right</span> |       <span class="material-icons text-6xl text-white">chevron_right</span> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @ -79,7 +67,18 @@ export default { | |||||||
|       updateTimer: null |       updateTimer: null | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |   watch: { | ||||||
|  |     isSelectionMode(newVal) { | ||||||
|  |       this.updateSelectionMode(newVal) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     bookCoverHeight() { | ||||||
|  |       return this.bookCoverWidth * this.bookCoverAspectRatio | ||||||
|  |     }, | ||||||
|  |     shelfHeight() { | ||||||
|  |       return this.bookCoverHeight + 48 | ||||||
|  |     }, | ||||||
|     userAudiobooks() { |     userAudiobooks() { | ||||||
|       return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {} |       return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {} | ||||||
|     }, |     }, | ||||||
| @ -89,6 +88,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|     currentLibraryId() { |     currentLibraryId() { | ||||||
|       return this.$store.state.libraries.currentLibraryId |       return this.$store.state.libraries.currentLibraryId | ||||||
|  |     }, | ||||||
|  |     isSelectionMode() { | ||||||
|  |       return this.$store.getters['getNumAudiobooksSelected'] > 0 | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
| @ -97,6 +99,21 @@ export default { | |||||||
|       this.$store.commit('setBookshelfBookIds', bookIds) |       this.$store.commit('setBookshelfBookIds', bookIds) | ||||||
|       this.$store.commit('showEditModal', audiobook) |       this.$store.commit('showEditModal', audiobook) | ||||||
|     }, |     }, | ||||||
|  |     updateSelectionMode(val) { | ||||||
|  |       var selectedAudiobooks = this.$store.state.selectedAudiobooks | ||||||
|  |       if (this.shelf.type === 'books') { | ||||||
|  |         this.shelf.entities.forEach((ent) => { | ||||||
|  |           var component = this.$refs[`shelf-book-${ent.id}`] | ||||||
|  |           if (!component || !component.length) return | ||||||
|  |           component = component[0] | ||||||
|  |           component.setSelectionMode(val) | ||||||
|  |           component.selected = selectedAudiobooks.includes(ent.id) | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     selectBook(audiobook) { | ||||||
|  |       this.$store.commit('toggleAudiobookSelected', audiobook.id) | ||||||
|  |     }, | ||||||
|     scrolled() { |     scrolled() { | ||||||
|       clearTimeout(this.scrollTimer) |       clearTimeout(this.scrollTimer) | ||||||
|       this.scrollTimer = setTimeout(() => { |       this.scrollTimer = setTimeout(() => { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="bookshelf" class="w-full overflow-y-auto"> |   <div id="bookshelf" class="w-full overflow-y-auto"> | ||||||
|     <template v-for="shelf in totalShelves"> |     <template v-for="shelf in totalShelves"> | ||||||
|       <div :key="shelf" class="w-full px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }"> |       <div :key="shelf" class="w-full px-4 sm:px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }"> | ||||||
|         <!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10"> |         <!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10"> | ||||||
|           <p class="text-white text-2xl">{{ shelf }}</p> |           <p class="text-white text-2xl">{{ shelf }}</p> | ||||||
|         </div> --> |         </div> --> | ||||||
| @ -128,14 +128,21 @@ export default { | |||||||
|       if (this.isCoverSquareAspectRatio) return this.bookWidth |       if (this.isCoverSquareAspectRatio) return this.bookWidth | ||||||
|       return this.bookWidth * 1.6 |       return this.bookWidth * 1.6 | ||||||
|     }, |     }, | ||||||
|  |     shelfPadding() { | ||||||
|  |       if (this.bookshelfWidth < 640) return 32 | ||||||
|  |       return 64 | ||||||
|  |     }, | ||||||
|  |     totalPadding() { | ||||||
|  |       return this.shelfPadding * 2 | ||||||
|  |     }, | ||||||
|     entityWidth() { |     entityWidth() { | ||||||
|       if (this.entityName === 'series') return this.bookWidth * 2 |       if (this.entityName === 'series' || this.entityName === 'collections') { | ||||||
|       if (this.entityName === 'collections') return this.bookWidth * 2 |         if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6 | ||||||
|  |         return this.bookWidth * 2 | ||||||
|  |       } | ||||||
|       return this.bookWidth |       return this.bookWidth | ||||||
|     }, |     }, | ||||||
|     entityHeight() { |     entityHeight() { | ||||||
|       if (this.entityName === 'series') return this.bookHeight |  | ||||||
|       if (this.entityName === 'collections') return this.bookHeight |  | ||||||
|       return this.bookHeight |       return this.bookHeight | ||||||
|     }, |     }, | ||||||
|     shelfDividerHeightIndex() { |     shelfDividerHeightIndex() { | ||||||
| @ -225,7 +232,6 @@ export default { | |||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       if (payload) { |       if (payload) { | ||||||
|         // console.log('Received payload', payload) |  | ||||||
|         if (!this.initialized) { |         if (!this.initialized) { | ||||||
|           this.initialized = true |           this.initialized = true | ||||||
|           this.totalEntities = payload.total |           this.totalEntities = payload.total | ||||||
| @ -237,7 +243,6 @@ export default { | |||||||
|         for (let i = 0; i < payload.results.length; i++) { |         for (let i = 0; i < payload.results.length; i++) { | ||||||
|           var index = i + startIndex |           var index = i + startIndex | ||||||
|           this.entities[index] = payload.results[i] |           this.entities[index] = payload.results[i] | ||||||
| 
 |  | ||||||
|           if (this.entityComponentRefs[index]) { |           if (this.entityComponentRefs[index]) { | ||||||
|             this.entityComponentRefs[index].setEntity(this.entities[index]) |             this.entityComponentRefs[index].setEntity(this.entities[index]) | ||||||
|           } |           } | ||||||
| @ -437,7 +442,7 @@ export default { | |||||||
|       this.mountWindowWidth = window.innerWidth |       this.mountWindowWidth = window.innerWidth | ||||||
|       this.bookshelfHeight = clientHeight |       this.bookshelfHeight = clientHeight | ||||||
|       this.bookshelfWidth = clientWidth |       this.bookshelfWidth = clientWidth | ||||||
|       this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalEntityCardWidth) |       this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth)) | ||||||
|       this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2 |       this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2 | ||||||
|       this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 |       this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 | ||||||
| 
 | 
 | ||||||
| @ -526,10 +531,12 @@ export default { | |||||||
|     this.initListeners() |     this.initListeners() | ||||||
|   }, |   }, | ||||||
|   updated() { |   updated() { | ||||||
|     if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) { |     setTimeout(() => { | ||||||
|       console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth) |       if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) { | ||||||
|       this.rebuild() |         console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth) | ||||||
|     } |         this.rebuild() | ||||||
|  |       } | ||||||
|  |     }, 50) | ||||||
|   }, |   }, | ||||||
|   beforeDestroy() { |   beforeDestroy() { | ||||||
|     this.destroyEntityComponents() |     this.destroyEntityComponents() | ||||||
|  | |||||||
| @ -1,433 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div ref="wrapper" class="relative book-card" :data-bookId="audiobookId" :id="`book-card-${audiobookId}`"> |  | ||||||
|     <template v-if="!showCard"> |  | ||||||
|       <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }"> |  | ||||||
|         <div class="bg-bg flex items-center justify-center p-2" :style="{ height: height + 'px', width: width + 'px' }"> |  | ||||||
|           <p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
|     <template v-else> |  | ||||||
|       <!-- New Book Flag --> |  | ||||||
|       <div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl z-20"> |  | ||||||
|         <div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center"> |  | ||||||
|           <p class="text-center text-sm">New</p> |  | ||||||
|         </div> |  | ||||||
|         <div class="absolute -bottom-4 left-0 triangle-right" /> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }"> |  | ||||||
|         <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"> |  | ||||||
|             <covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" /> |  | ||||||
| 
 |  | ||||||
|             <!-- Hidden SM and DOWN --> |  | ||||||
|             <div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block z-20" :class="overlayWrapperClasslist"> |  | ||||||
|               <div v-show="showPlayButton" class="h-full flex items-center justify-center"> |  | ||||||
|                 <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play"> |  | ||||||
|                   <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|               <div v-show="showReadButton" class="h-full flex items-center justify-center"> |  | ||||||
|                 <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook"> |  | ||||||
|                   <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
| 
 |  | ||||||
|               <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick"> |  | ||||||
|                 <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> |  | ||||||
|               </div> |  | ||||||
| 
 |  | ||||||
|               <div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick"> |  | ||||||
|                 <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span> |  | ||||||
|               </div> |  | ||||||
| 
 |  | ||||||
|               <!-- More Icon --> |  | ||||||
|               <div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore"> |  | ||||||
|                 <span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             <div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> |  | ||||||
|               <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             <!-- EBook Icon --> |  | ||||||
|             <div |  | ||||||
|               v-if="showSmallEBookIcon" |  | ||||||
|               class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200 z-10" |  | ||||||
|               :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }" |  | ||||||
|               @click.stop.prevent="clickReadEBook" |  | ||||||
|             > |  | ||||||
|               <!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> --> |  | ||||||
|               <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div> |  | ||||||
| 
 |  | ||||||
|             <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-20"> |  | ||||||
|               <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300"> |  | ||||||
|                 <span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span> |  | ||||||
|               </div> |  | ||||||
|             </ui-tooltip> |  | ||||||
|           </div> |  | ||||||
|         </nuxt-link> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script> |  | ||||||
| import Vue from 'vue' |  | ||||||
| import MoreMenu from '@/components/widgets/MoreMenu' |  | ||||||
| 
 |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     audiobook: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => null |  | ||||||
|     }, |  | ||||||
|     userProgress: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => null |  | ||||||
|     }, |  | ||||||
|     width: { |  | ||||||
|       type: Number, |  | ||||||
|       default: 120 |  | ||||||
|     }, |  | ||||||
|     paddingY: { |  | ||||||
|       type: Number, |  | ||||||
|       default: 16 |  | ||||||
|     }, |  | ||||||
|     isBookshelfBook: Boolean, |  | ||||||
|     showVolumeNumber: Boolean, |  | ||||||
|     bookCoverAspectRatio: Number |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       showCard: false, |  | ||||||
|       isHovering: false, |  | ||||||
|       isMoreMenuOpen: false, |  | ||||||
|       isProcessingReadUpdate: false, |  | ||||||
|       rescanning: false, |  | ||||||
|       timesVisible: 0 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     showExperimentalFeatures() { |  | ||||||
|       return this.$store.state.showExperimentalFeatures |  | ||||||
|     }, |  | ||||||
|     isNew() { |  | ||||||
|       return this.tags.includes('New') |  | ||||||
|     }, |  | ||||||
|     tags() { |  | ||||||
|       return this.audiobook.tags || [] |  | ||||||
|     }, |  | ||||||
|     audiobookId() { |  | ||||||
|       return this.audiobook.id |  | ||||||
|     }, |  | ||||||
|     hasEbook() { |  | ||||||
|       return this.audiobook.numEbooks |  | ||||||
|     }, |  | ||||||
|     hasTracks() { |  | ||||||
|       return this.audiobook.numTracks |  | ||||||
|     }, |  | ||||||
|     isSelectionMode() { |  | ||||||
|       return !!this.selectedAudiobooks.length |  | ||||||
|     }, |  | ||||||
|     selectedAudiobooks() { |  | ||||||
|       return this.$store.state.selectedAudiobooks |  | ||||||
|     }, |  | ||||||
|     selected() { |  | ||||||
|       return this.$store.getters['getIsAudiobookSelected'](this.audiobookId) |  | ||||||
|     }, |  | ||||||
|     processingBatch() { |  | ||||||
|       return this.$store.state.processingBatch |  | ||||||
|     }, |  | ||||||
|     book() { |  | ||||||
|       return this.audiobook.book || {} |  | ||||||
|     }, |  | ||||||
|     squareAspectRatio() { |  | ||||||
|       return this.bookCoverAspectRatio === 1 |  | ||||||
|     }, |  | ||||||
|     height() { |  | ||||||
|       return this.width * this.bookCoverAspectRatio |  | ||||||
|     }, |  | ||||||
|     sizeMultiplier() { |  | ||||||
|       var baseSize = this.squareAspectRatio ? 192 : 120 |  | ||||||
|       return this.width / baseSize |  | ||||||
|     }, |  | ||||||
|     paddingX() { |  | ||||||
|       return 16 * this.sizeMultiplier |  | ||||||
|     }, |  | ||||||
|     title() { |  | ||||||
|       return this.book.title |  | ||||||
|     }, |  | ||||||
|     playIconFontSize() { |  | ||||||
|       return Math.max(2, 3 * this.sizeMultiplier) |  | ||||||
|     }, |  | ||||||
|     author() { |  | ||||||
|       return this.book.author |  | ||||||
|     }, |  | ||||||
|     authorFL() { |  | ||||||
|       return this.book.authorFL || this.author |  | ||||||
|     }, |  | ||||||
|     authorLF() { |  | ||||||
|       return this.book.authorLF || this.author |  | ||||||
|     }, |  | ||||||
|     authorFormat() { |  | ||||||
|       if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null |  | ||||||
|       return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL |  | ||||||
|     }, |  | ||||||
|     volumeNumber() { |  | ||||||
|       return this.book.volumeNumber || null |  | ||||||
|     }, |  | ||||||
|     orderBy() { |  | ||||||
|       return this.$store.getters['user/getUserSetting']('orderBy') |  | ||||||
|     }, |  | ||||||
|     filterBy() { |  | ||||||
|       return this.$store.getters['user/getUserSetting']('filterBy') |  | ||||||
|     }, |  | ||||||
|     userProgressPercent() { |  | ||||||
|       return this.userProgress ? this.userProgress.progress || 0 : 0 |  | ||||||
|     }, |  | ||||||
|     userIsRead() { |  | ||||||
|       return this.userProgress ? !!this.userProgress.isRead : false |  | ||||||
|     }, |  | ||||||
|     showError() { |  | ||||||
|       return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid |  | ||||||
|     }, |  | ||||||
|     isStreaming() { |  | ||||||
|       return this.$store.getters['getAudiobookIdStreaming'] === this.audiobookId |  | ||||||
|     }, |  | ||||||
|     showReadButton() { |  | ||||||
|       return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook |  | ||||||
|     }, |  | ||||||
|     showPlayButton() { |  | ||||||
|       return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming |  | ||||||
|     }, |  | ||||||
|     showSmallEBookIcon() { |  | ||||||
|       return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook |  | ||||||
|     }, |  | ||||||
|     isMissing() { |  | ||||||
|       return this.audiobook.isMissing |  | ||||||
|     }, |  | ||||||
|     isInvalid() { |  | ||||||
|       return this.audiobook.isInvalid |  | ||||||
|     }, |  | ||||||
|     hasMissingParts() { |  | ||||||
|       return this.audiobook.hasMissingParts |  | ||||||
|     }, |  | ||||||
|     hasInvalidParts() { |  | ||||||
|       return this.audiobook.hasInvalidParts |  | ||||||
|     }, |  | ||||||
|     errorText() { |  | ||||||
|       if (this.isMissing) return 'Audiobook directory is missing!' |  | ||||||
|       else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook' |  | ||||||
|       var txt = '' |  | ||||||
|       if (this.hasMissingParts) { |  | ||||||
|         txt = `${this.hasMissingParts} missing parts.` |  | ||||||
|       } |  | ||||||
|       if (this.hasInvalidParts) { |  | ||||||
|         if (this.hasMissingParts) txt += ' ' |  | ||||||
|         txt += `${this.hasInvalidParts} invalid parts.` |  | ||||||
|       } |  | ||||||
|       return txt || 'Unknown Error' |  | ||||||
|     }, |  | ||||||
|     overlayWrapperClasslist() { |  | ||||||
|       var classes = [] |  | ||||||
|       if (this.isSelectionMode) classes.push('bg-opacity-60') |  | ||||||
|       else classes.push('bg-opacity-40') |  | ||||||
|       if (this.selected) { |  | ||||||
|         classes.push('border-2 border-yellow-400') |  | ||||||
|       } |  | ||||||
|       return classes |  | ||||||
|     }, |  | ||||||
|     userCanUpdate() { |  | ||||||
|       return this.$store.getters['user/getUserCanUpdate'] |  | ||||||
|     }, |  | ||||||
|     userCanDelete() { |  | ||||||
|       return this.$store.getters['user/getUserCanDelete'] |  | ||||||
|     }, |  | ||||||
|     userCanDownload() { |  | ||||||
|       return this.$store.getters['user/getUserCanDownload'] |  | ||||||
|     }, |  | ||||||
|     userIsRoot() { |  | ||||||
|       return this.$store.getters['user/getIsRoot'] |  | ||||||
|     }, |  | ||||||
|     moreMenuItems() { |  | ||||||
|       var items = [ |  | ||||||
|         { |  | ||||||
|           func: 'toggleRead', |  | ||||||
|           text: `Mark as ${this.userIsRead ? 'Not Read' : 'Read'}` |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           func: 'openCollections', |  | ||||||
|           text: 'Add to Collection' |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|       if (this.userCanUpdate) { |  | ||||||
|         if (this.hasTracks) { |  | ||||||
|           items.push({ |  | ||||||
|             func: 'showEditModalTracks', |  | ||||||
|             text: 'Tracks' |  | ||||||
|           }) |  | ||||||
|         } |  | ||||||
|         items.push({ |  | ||||||
|           func: 'showEditModalMatch', |  | ||||||
|           text: 'Match' |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|       if (this.userCanDownload) { |  | ||||||
|         items.push({ |  | ||||||
|           func: 'showEditModalDownload', |  | ||||||
|           text: 'Download' |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|       if (this.userIsRoot) { |  | ||||||
|         items.push({ |  | ||||||
|           func: 'rescan', |  | ||||||
|           text: 'Re-Scan' |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|       return items |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setShowCard(val) { |  | ||||||
|       if (val) this.timesVisible++ |  | ||||||
|       this.showCard = val |  | ||||||
|     }, |  | ||||||
|     selectBtnClick() { |  | ||||||
|       if (this.processingBatch) return |  | ||||||
|       this.$store.commit('toggleAudiobookSelected', this.audiobookId) |  | ||||||
|     }, |  | ||||||
|     clickError(e) { |  | ||||||
|       e.stopPropagation() |  | ||||||
|       this.$router.push(`/audiobook/${this.audiobookId}`) |  | ||||||
|     }, |  | ||||||
|     play() { |  | ||||||
|       this.$store.commit('setStreamAudiobook', this.audiobook) |  | ||||||
|       this.$root.socket.emit('open_stream', this.audiobookId) |  | ||||||
|     }, |  | ||||||
|     editClick() { |  | ||||||
|       // this.$store.commit('showEditModal', this.audiobook) |  | ||||||
|       this.$emit('edit', this.audiobook) |  | ||||||
|     }, |  | ||||||
|     clickCard(e) { |  | ||||||
|       if (this.isSelectionMode) { |  | ||||||
|         e.stopPropagation() |  | ||||||
|         e.preventDefault() |  | ||||||
|         this.selectBtnClick() |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     clickReadEBook() { |  | ||||||
|       this.$store.commit('showEReader', this.audiobook) |  | ||||||
|     }, |  | ||||||
|     toggleRead() { |  | ||||||
|       // More menu func |  | ||||||
|       var updatePayload = { |  | ||||||
|         isRead: !this.userIsRead |  | ||||||
|       } |  | ||||||
|       this.isProcessingReadUpdate = true |  | ||||||
|       this.$axios |  | ||||||
|         .$patch(`/api/me/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'}`) |  | ||||||
|         }) |  | ||||||
|     }, |  | ||||||
|     audiobookScanComplete(result) { |  | ||||||
|       this.rescanning = false |  | ||||||
|       if (!result) { |  | ||||||
|         this.$toast.error(`Re-Scan Failed for "${this.title}"`) |  | ||||||
|       } else if (result === 'UPDATED') { |  | ||||||
|         this.$toast.success(`Re-Scan complete audiobook was updated`) |  | ||||||
|       } else if (result === 'UPTODATE') { |  | ||||||
|         this.$toast.success(`Re-Scan complete audiobook was up to date`) |  | ||||||
|       } else if (result === 'REMOVED') { |  | ||||||
|         this.$toast.error(`Re-Scan complete audiobook was removed`) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     rescan() { |  | ||||||
|       this.rescanning = true |  | ||||||
|       this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete) |  | ||||||
|       this.$root.socket.emit('scan_audiobook', this.audiobookId) |  | ||||||
|     }, |  | ||||||
|     showEditModalTracks() { |  | ||||||
|       // More menu func |  | ||||||
|       this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'tracks' }) |  | ||||||
|     }, |  | ||||||
|     showEditModalMatch() { |  | ||||||
|       // More menu func |  | ||||||
|       this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'match' }) |  | ||||||
|     }, |  | ||||||
|     showEditModalDownload() { |  | ||||||
|       // More menu func |  | ||||||
|       this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' }) |  | ||||||
|     }, |  | ||||||
|     openCollections() { |  | ||||||
|       this.$store.commit('setSelectedAudiobook', this.audiobook) |  | ||||||
|       this.$store.commit('globals/setShowUserCollectionsModal', true) |  | ||||||
|     }, |  | ||||||
|     createMoreMenu() { |  | ||||||
|       if (!this.$refs.moreIcon) return |  | ||||||
| 
 |  | ||||||
|       var ComponentClass = Vue.extend(MoreMenu) |  | ||||||
| 
 |  | ||||||
|       var _this = this |  | ||||||
|       var instance = new ComponentClass({ |  | ||||||
|         propsData: { |  | ||||||
|           items: this.moreMenuItems |  | ||||||
|         }, |  | ||||||
|         created() { |  | ||||||
|           this.$on('action', (func) => { |  | ||||||
|             if (_this[func]) _this[func]() |  | ||||||
|           }) |  | ||||||
|           this.$on('close', () => { |  | ||||||
|             _this.isMoreMenuOpen = false |  | ||||||
|           }) |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|       instance.$mount() |  | ||||||
| 
 |  | ||||||
|       var wrapperBox = this.$refs.moreIcon.getBoundingClientRect() |  | ||||||
|       var el = instance.$el |  | ||||||
| 
 |  | ||||||
|       var elHeight = this.moreMenuItems.length * 28 + 2 |  | ||||||
|       var elWidth = 130 |  | ||||||
| 
 |  | ||||||
|       var bottomOfIcon = wrapperBox.top + wrapperBox.height |  | ||||||
|       var rightOfIcon = wrapperBox.left + wrapperBox.width |  | ||||||
| 
 |  | ||||||
|       var elTop = bottomOfIcon |  | ||||||
|       var elLeft = rightOfIcon |  | ||||||
|       if (bottomOfIcon + elHeight > window.innerHeight - 100) { |  | ||||||
|         elTop = wrapperBox.top - elHeight |  | ||||||
|         elLeft = wrapperBox.left |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (rightOfIcon + elWidth > window.innerWidth - 100) { |  | ||||||
|         elLeft = rightOfIcon - elWidth |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       el.style.top = elTop + 'px' |  | ||||||
|       el.style.left = elLeft + 'px' |  | ||||||
| 
 |  | ||||||
|       this.isMoreMenuOpen = true |  | ||||||
|       document.body.appendChild(el) |  | ||||||
|     }, |  | ||||||
|     clickShowMore() { |  | ||||||
|       this.createMoreMenu() |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     this.showCard = !this.isBookshelfBook |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| @ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="relative"> |   <div class="relative"> | ||||||
|     <div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> |     <div class="rounded-sm h-full relative" :style="{ padding: `0px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> | ||||||
|       <nuxt-link :to="groupTo" class="cursor-pointer"> |       <nuxt-link :to="groupTo" class="cursor-pointer"> | ||||||
|         <div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }"> |         <div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }"> | ||||||
|           <covers-group-cover ref="groupcover" :id="seriesId" :name="groupName" :is-categorized="isCategorized" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" /> |           <covers-group-cover ref="groupcover" :id="seriesId" :name="groupName" :is-categorized="isCategorized" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||||
| @ -38,10 +38,6 @@ export default { | |||||||
|       type: Number, |       type: Number, | ||||||
|       default: 120 |       default: 120 | ||||||
|     }, |     }, | ||||||
|     paddingY: { |  | ||||||
|       type: Number, |  | ||||||
|       default: 24 |  | ||||||
|     }, |  | ||||||
|     isCategorized: Boolean, |     isCategorized: Boolean, | ||||||
|     bookCoverAspectRatio: Number |     bookCoverAspectRatio: Number | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> |   <div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> | ||||||
|     <!-- When cover image does not fill --> |     <!-- When cover image does not fill --> | ||||||
|     <div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> |     <div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> | ||||||
|       <div class="absolute cover-bg" ref="coverBg" /> |       <div class="absolute cover-bg" ref="coverBg" /> | ||||||
| @ -78,7 +78,12 @@ export default { | |||||||
|       default: 192 |       default: 192 | ||||||
|     }, |     }, | ||||||
|     bookCoverAspectRatio: Number, |     bookCoverAspectRatio: Number, | ||||||
|     showVolumeNumber: Boolean |     showVolumeNumber: Boolean, | ||||||
|  |     bookMount: { | ||||||
|  |       // Book can be passed as prop or set with setEntity() | ||||||
|  |       type: Object, | ||||||
|  |       default: () => null | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @ -465,6 +470,11 @@ export default { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     if (this.bookMount) { | ||||||
|  |       this.setEntity(this.bookMount) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> |   <div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> | ||||||
|     <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> |     <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> | ||||||
|     <div class="w-full h-full bg-primary relative rounded overflow-hidden"> |     <div class="w-full h-full bg-primary relative rounded overflow-hidden"> | ||||||
|       <covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" /> |       <covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" /> | ||||||
| @ -10,7 +10,7 @@ | |||||||
|     </div> |     </div> | ||||||
|     <!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40"> |     <!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40"> | ||||||
|     </div> --> |     </div> --> | ||||||
|     <div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> |     <div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> | ||||||
|       <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> |       <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> | ||||||
|         <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> |         <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> | ||||||
|       </div> |       </div> | ||||||
| @ -24,7 +24,12 @@ export default { | |||||||
|     index: Number, |     index: Number, | ||||||
|     width: Number, |     width: Number, | ||||||
|     height: Number, |     height: Number, | ||||||
|     bookCoverAspectRatio: Number |     bookCoverAspectRatio: Number, | ||||||
|  |     isCategorized: Boolean, | ||||||
|  |     seriesMount: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => null | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @ -60,7 +65,7 @@ export default { | |||||||
|       return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}` |       return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}` | ||||||
|     }, |     }, | ||||||
|     seriesId() { |     seriesId() { | ||||||
|       return this.series ? this.$encode(this.series.id) : null |       return this.series ? this.$encode(this.title) : null | ||||||
|     }, |     }, | ||||||
|     hasValidCovers() { |     hasValidCovers() { | ||||||
|       var validCovers = this.books.map((bookItem) => bookItem.book.cover) |       var validCovers = this.books.map((bookItem) => bookItem.book.cover) | ||||||
| @ -69,6 +74,7 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     setEntity(_series) { |     setEntity(_series) { | ||||||
|  |       console.log('setting entity', _series) | ||||||
|       this.series = _series |       this.series = _series | ||||||
|     }, |     }, | ||||||
|     setSelectionMode(val) { |     setSelectionMode(val) { | ||||||
| @ -100,7 +106,11 @@ export default { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() {}, |   mounted() { | ||||||
|  |     if (this.seriesMount) { | ||||||
|  |       this.setEntity(this.seriesMount) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   beforeDestroy() {} |   beforeDestroy() {} | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -71,6 +71,7 @@ export default { | |||||||
| 
 | 
 | ||||||
|       instance.$mount() |       instance.$mount() | ||||||
|       instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)` |       instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)` | ||||||
|  |       instance.$el.classList.add('absolute', 'top-0', 'left-0') | ||||||
|       shelfEl.appendChild(instance.$el) |       shelfEl.appendChild(instance.$el) | ||||||
| 
 | 
 | ||||||
|       if (this.entities[index]) { |       if (this.entities[index]) { | ||||||
|  | |||||||
| @ -143,5 +143,6 @@ export { | |||||||
| export default ({ app }, inject) => { | export default ({ app }, inject) => { | ||||||
|   app.$decode = decode |   app.$decode = decode | ||||||
|   app.$encode = encode |   app.$encode = encode | ||||||
|   app.$isDev = process.env.NODE_ENV !== 'production' |   // app.$isDev = process.env.NODE_ENV !== 'production'
 | ||||||
|  |   inject('isDev', process.env.NODE_ENV !== 'production') | ||||||
| } | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user