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" /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex pb-8 sm:pb-4 md:pb-2"> | ||||
|       <div class="flex pb-4 md:pb-2"> | ||||
|         <div class="flex-grow" /> | ||||
|         <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"> | ||||
| @ -82,7 +82,7 @@ | ||||
|       <p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> | ||||
|     </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" /> | ||||
|   </div> | ||||
| @ -109,6 +109,9 @@ export default { | ||||
|     return { | ||||
|       hlsInstance: null, | ||||
|       staleHlsInstance: null, | ||||
|       usingNativeAudioPlayer: false, | ||||
|       playOnLoad: false, | ||||
|       startTime: 0, | ||||
|       volume: 1, | ||||
|       playbackRate: 1, | ||||
|       trackWidth: 0, | ||||
| @ -194,7 +197,7 @@ export default { | ||||
|     }, | ||||
|     audioStalled() { | ||||
|       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() { | ||||
|       if (!this.$refs.audio) return | ||||
| @ -555,11 +558,13 @@ export default { | ||||
|       this.playedTrackWidth = ptWidth | ||||
|     }, | ||||
|     audioLoadedMetadata() { | ||||
|       console.log('Audio METADATA Loaded, total duration', this.audioEl.duration) | ||||
|       this.totalDuration = this.audioEl.duration | ||||
|       this.$emit('loaded', this.totalDuration) | ||||
|       if (this.usingNativeAudioPlayer) { | ||||
|         this.audioEl.currentTime = this.startTime | ||||
|         this.play() | ||||
|       } | ||||
|     }, | ||||
|     audioLoadedData() {}, | ||||
|     set(url, currentTime, playOnLoad = false) { | ||||
|       if (this.hlsInstance) { | ||||
|         this.terminateStream() | ||||
| @ -570,6 +575,8 @@ export default { | ||||
|       } | ||||
|       this.listeningTimeSinceLastUpdate = 0 | ||||
| 
 | ||||
|       this.playOnLoad = playOnLoad | ||||
|       this.startTime = currentTime | ||||
|       this.url = url | ||||
|       if (process.env.NODE_ENV === 'development') { | ||||
|         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 | ||||
|       if (!Hls.isSupported()) { | ||||
|         console.warn('HLS is not supported - fallback to using audio element') | ||||
|         this.usingNativeAudioPlayer = true | ||||
|         audio.src = this.src + '?token=' + this.token | ||||
|         audio.currentTime = currentTime | ||||
|         return | ||||
| @ -604,10 +612,10 @@ export default { | ||||
|         // console.log('[HLS] MEDIA ATTACHED') | ||||
|         this.hlsInstance.loadSource(url) | ||||
| 
 | ||||
|         this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, function () { | ||||
|         this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { | ||||
|           console.log('[HLS] Manifest Parsed') | ||||
|           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 | ||||
|     }, | ||||
|     settingsUpdated(settings) {}, | ||||
|  | ||||
| @ -1,40 +1,28 @@ | ||||
| <template> | ||||
|   <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 class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }"> | ||||
|         <div v-if="shelf.type === 'books'" class="flex items-center -mb-2"> | ||||
|           <template v-for="entity 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" /> | ||||
|     <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 pt-6"> | ||||
|         <div v-if="shelf.type === 'books'" class="flex items-center"> | ||||
|           <template v-for="(entity, index) in shelf.entities"> | ||||
|             <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> | ||||
|         </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"> | ||||
|             <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> | ||||
|         </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"> | ||||
|             <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> | ||||
|           </template> | ||||
|         </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"> | ||||
|             <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" /> | ||||
|             </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" /> | ||||
|               <cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" /> | ||||
|             </nuxt-link> | ||||
|           </template> | ||||
|         </div> | ||||
| @ -49,10 +37,10 @@ | ||||
| 
 | ||||
|     <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> | ||||
|     </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> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -79,7 +67,18 @@ export default { | ||||
|       updateTimer: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     isSelectionMode(newVal) { | ||||
|       this.updateSelectionMode(newVal) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     bookCoverHeight() { | ||||
|       return this.bookCoverWidth * this.bookCoverAspectRatio | ||||
|     }, | ||||
|     shelfHeight() { | ||||
|       return this.bookCoverHeight + 48 | ||||
|     }, | ||||
|     userAudiobooks() { | ||||
|       return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {} | ||||
|     }, | ||||
| @ -89,6 +88,9 @@ export default { | ||||
|     }, | ||||
|     currentLibraryId() { | ||||
|       return this.$store.state.libraries.currentLibraryId | ||||
|     }, | ||||
|     isSelectionMode() { | ||||
|       return this.$store.getters['getNumAudiobooksSelected'] > 0 | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -97,6 +99,21 @@ export default { | ||||
|       this.$store.commit('setBookshelfBookIds', bookIds) | ||||
|       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() { | ||||
|       clearTimeout(this.scrollTimer) | ||||
|       this.scrollTimer = setTimeout(() => { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div id="bookshelf" class="w-full overflow-y-auto"> | ||||
|     <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"> | ||||
|           <p class="text-white text-2xl">{{ shelf }}</p> | ||||
|         </div> --> | ||||
| @ -128,14 +128,21 @@ export default { | ||||
|       if (this.isCoverSquareAspectRatio) return this.bookWidth | ||||
|       return this.bookWidth * 1.6 | ||||
|     }, | ||||
|     shelfPadding() { | ||||
|       if (this.bookshelfWidth < 640) return 32 | ||||
|       return 64 | ||||
|     }, | ||||
|     totalPadding() { | ||||
|       return this.shelfPadding * 2 | ||||
|     }, | ||||
|     entityWidth() { | ||||
|       if (this.entityName === 'series') return this.bookWidth * 2 | ||||
|       if (this.entityName === 'collections') return this.bookWidth * 2 | ||||
|       if (this.entityName === 'series' || this.entityName === 'collections') { | ||||
|         if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6 | ||||
|         return this.bookWidth * 2 | ||||
|       } | ||||
|       return this.bookWidth | ||||
|     }, | ||||
|     entityHeight() { | ||||
|       if (this.entityName === 'series') return this.bookHeight | ||||
|       if (this.entityName === 'collections') return this.bookHeight | ||||
|       return this.bookHeight | ||||
|     }, | ||||
|     shelfDividerHeightIndex() { | ||||
| @ -225,7 +232,6 @@ export default { | ||||
|         return | ||||
|       } | ||||
|       if (payload) { | ||||
|         // console.log('Received payload', payload) | ||||
|         if (!this.initialized) { | ||||
|           this.initialized = true | ||||
|           this.totalEntities = payload.total | ||||
| @ -237,7 +243,6 @@ export default { | ||||
|         for (let i = 0; i < payload.results.length; i++) { | ||||
|           var index = i + startIndex | ||||
|           this.entities[index] = payload.results[i] | ||||
| 
 | ||||
|           if (this.entityComponentRefs[index]) { | ||||
|             this.entityComponentRefs[index].setEntity(this.entities[index]) | ||||
|           } | ||||
| @ -437,7 +442,7 @@ export default { | ||||
|       this.mountWindowWidth = window.innerWidth | ||||
|       this.bookshelfHeight = clientHeight | ||||
|       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.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 | ||||
| 
 | ||||
| @ -526,10 +531,12 @@ export default { | ||||
|     this.initListeners() | ||||
|   }, | ||||
|   updated() { | ||||
|     if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) { | ||||
|       console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth) | ||||
|       this.rebuild() | ||||
|     } | ||||
|     setTimeout(() => { | ||||
|       if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) { | ||||
|         console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth) | ||||
|         this.rebuild() | ||||
|       } | ||||
|     }, 50) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     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> | ||||
|   <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"> | ||||
|         <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" /> | ||||
| @ -38,10 +38,6 @@ export default { | ||||
|       type: Number, | ||||
|       default: 120 | ||||
|     }, | ||||
|     paddingY: { | ||||
|       type: Number, | ||||
|       default: 24 | ||||
|     }, | ||||
|     isCategorized: Boolean, | ||||
|     bookCoverAspectRatio: Number | ||||
|   }, | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <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 --> | ||||
|     <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" /> | ||||
| @ -78,7 +78,12 @@ export default { | ||||
|       default: 192 | ||||
|     }, | ||||
|     bookCoverAspectRatio: Number, | ||||
|     showVolumeNumber: Boolean | ||||
|     showVolumeNumber: Boolean, | ||||
|     bookMount: { | ||||
|       // Book can be passed as prop or set with setEntity() | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -465,6 +470,11 @@ export default { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     if (this.bookMount) { | ||||
|       this.setEntity(this.bookMount) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <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="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" /> | ||||
| @ -10,7 +10,7 @@ | ||||
|     </div> | ||||
|     <!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40"> | ||||
|     </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` }"> | ||||
|         <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> | ||||
|       </div> | ||||
| @ -24,7 +24,12 @@ export default { | ||||
|     index: Number, | ||||
|     width: Number, | ||||
|     height: Number, | ||||
|     bookCoverAspectRatio: Number | ||||
|     bookCoverAspectRatio: Number, | ||||
|     isCategorized: Boolean, | ||||
|     seriesMount: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -60,7 +65,7 @@ export default { | ||||
|       return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}` | ||||
|     }, | ||||
|     seriesId() { | ||||
|       return this.series ? this.$encode(this.series.id) : null | ||||
|       return this.series ? this.$encode(this.title) : null | ||||
|     }, | ||||
|     hasValidCovers() { | ||||
|       var validCovers = this.books.map((bookItem) => bookItem.book.cover) | ||||
| @ -69,6 +74,7 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     setEntity(_series) { | ||||
|       console.log('setting entity', _series) | ||||
|       this.series = _series | ||||
|     }, | ||||
|     setSelectionMode(val) { | ||||
| @ -100,7 +106,11 @@ export default { | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {}, | ||||
|   mounted() { | ||||
|     if (this.seriesMount) { | ||||
|       this.setEntity(this.seriesMount) | ||||
|     } | ||||
|   }, | ||||
|   beforeDestroy() {} | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -71,6 +71,7 @@ export default { | ||||
| 
 | ||||
|       instance.$mount() | ||||
|       instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)` | ||||
|       instance.$el.classList.add('absolute', 'top-0', 'left-0') | ||||
|       shelfEl.appendChild(instance.$el) | ||||
| 
 | ||||
|       if (this.entities[index]) { | ||||
|  | ||||
| @ -143,5 +143,6 @@ export { | ||||
| export default ({ app }, inject) => { | ||||
|   app.$decode = decode | ||||
|   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