mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Side rail, book group cards, fix dropdown select
This commit is contained in:
		
							parent
							
								
									94741598af
								
							
						
					
					
						commit
						fcd664c16e
					
				| @ -103,6 +103,10 @@ | ||||
|   box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166; | ||||
| } | ||||
| 
 | ||||
| .box-shadow-book3d { | ||||
|   box-shadow: 4px 1px 8px #11111166, 1px -4px 8px #11111166; | ||||
| } | ||||
| 
 | ||||
| .box-shadow-side { | ||||
|   box-shadow: 4px 0px 4px #11111166; | ||||
| } | ||||
|  | ||||
| @ -63,7 +63,7 @@ export default { | ||||
|   }, | ||||
|   computed: { | ||||
|     showBack() { | ||||
|       return this.$route.name !== 'index' | ||||
|       return this.$route.name !== 'library-id' | ||||
|     }, | ||||
|     user() { | ||||
|       return this.$store.state.user.user | ||||
| @ -114,7 +114,7 @@ export default { | ||||
|       if (this.$route.name === 'audiobook-id-edit') { | ||||
|         this.$router.push(`/audiobook/${this.$route.params.id}`) | ||||
|       } else { | ||||
|         this.$router.push('/') | ||||
|         this.$router.push('/library') | ||||
|       } | ||||
|     }, | ||||
|     cancelSelectionMode() { | ||||
|  | ||||
| @ -16,21 +16,22 @@ | ||||
|         <ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-else class="w-full flex flex-col items-center"> | ||||
|       <template v-for="(shelf, index) in entities"> | ||||
|     <div v-else id="bookshelf" class="w-full flex flex-col items-center"> | ||||
|       <template v-for="(shelf, index) in shelves"> | ||||
|         <div :key="index" class="w-full bookshelfRow relative"> | ||||
|           <div class="flex justify-center items-center"> | ||||
|             <template v-for="entity in shelf"> | ||||
|               <cards-group-card v-if="page !== ''" :key="entity.id" :width="bookCoverWidth" :group="entity" /> | ||||
|               <cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" /> | ||||
|               <!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> --> | ||||
|               <cards-book-card v-else :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" /> | ||||
|             </template> | ||||
|           </div> | ||||
|           <div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> | ||||
|         </div> | ||||
|       </template> | ||||
|       <div v-show="!entities.length" class="w-full py-16 text-center text-xl"> | ||||
|         <div class="py-4">No Audiobooks</div> | ||||
|         <ui-btn v-if="filterBy !== 'all' || keywordFilter" @click="clearFilter">Clear Filter</ui-btn> | ||||
|       <div v-show="!shelves.length" class="w-full py-16 text-center text-xl"> | ||||
|         <div class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div> | ||||
|         <ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -39,13 +40,12 @@ | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     page: String | ||||
|     page: String, | ||||
|     selectedSeries: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       width: 0, | ||||
|       booksPerRow: 0, | ||||
|       entities: [], | ||||
|       shelves: [], | ||||
|       currFilterOrderKey: null, | ||||
|       availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220], | ||||
|       selectedSizeIndex: 3, | ||||
| @ -57,6 +57,11 @@ export default { | ||||
|   watch: { | ||||
|     keywordFilter() { | ||||
|       this.checkKeywordFilter() | ||||
|     }, | ||||
|     selectedSeries() { | ||||
|       this.$nextTick(() => { | ||||
|         this.setBookshelfEntities() | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -89,9 +94,30 @@ export default { | ||||
|     }, | ||||
|     filterBy() { | ||||
|       return this.$store.getters['user/getUserSetting']('filterBy') | ||||
|     }, | ||||
|     showGroups() { | ||||
|       return this.page !== '' && !this.selectedSeries | ||||
|     }, | ||||
|     entities() { | ||||
|       if (this.page === '') { | ||||
|         return this.$store.getters['audiobooks/getFilteredAndSorted']() | ||||
|       } else { | ||||
|         var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']() | ||||
|         if (this.selectedSeries) { | ||||
|           var group = seriesGroups.find((group) => group.name === this.selectedSeries) | ||||
|           return group.books | ||||
|         } | ||||
|         return seriesGroups | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickGroup(group) { | ||||
|       this.$emit('update:selectedSeries', group.name) | ||||
|     }, | ||||
|     changeRotation() { | ||||
|       this.rotation = 'show-right' | ||||
|     }, | ||||
|     clearFilter() { | ||||
|       this.$store.commit('audiobooks/setKeywordFilter', null) | ||||
|       if (this.filterBy !== 'all') { | ||||
| @ -119,22 +145,16 @@ export default { | ||||
|       this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth }) | ||||
|     }, | ||||
|     setBookshelfEntities() { | ||||
|       if (this.page === '') { | ||||
|         var audiobooksSorted = this.$store.getters['audiobooks/getFilteredAndSorted']() | ||||
|         this.currFilterOrderKey = this.filterOrderKey | ||||
|         this.setGroupedBooks(audiobooksSorted) | ||||
|       } else { | ||||
|         var entities = this.$store.getters['audiobooks/getSeriesGroups']() | ||||
|         this.setGroupedBooks(entities) | ||||
|       } | ||||
|     }, | ||||
|     setGroupedBooks(entities) { | ||||
|       var width = Math.max(0, this.$refs.wrapper.clientWidth - this.rowPaddingX * 2) | ||||
|       var booksPerRow = Math.floor(width / this.bookWidth) | ||||
| 
 | ||||
|       var entities = this.entities | ||||
|       var groups = [] | ||||
|       var currentRow = 0 | ||||
|       var currentGroup = [] | ||||
| 
 | ||||
|       for (let i = 0; i < entities.length; i++) { | ||||
|         var row = Math.floor(i / this.booksPerRow) | ||||
|         var row = Math.floor(i / booksPerRow) | ||||
|         if (row > currentRow) { | ||||
|           groups.push([...currentGroup]) | ||||
|           currentRow = row | ||||
| @ -145,23 +165,20 @@ export default { | ||||
|       if (currentGroup.length) { | ||||
|         groups.push([...currentGroup]) | ||||
|       } | ||||
|       this.entities = groups | ||||
|       this.shelves = groups | ||||
|     }, | ||||
|     calculateBookshelf() { | ||||
|       this.width = this.$refs.wrapper.clientWidth | ||||
|       this.width = Math.max(0, this.width - this.rowPaddingX * 2) | ||||
|       var booksPerRow = Math.floor(this.width / this.bookWidth) | ||||
|       this.booksPerRow = booksPerRow | ||||
|     }, | ||||
|     init() { | ||||
|     async init() { | ||||
|       var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') | ||||
|       var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize) | ||||
|       if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex | ||||
|       this.calculateBookshelf() | ||||
| 
 | ||||
|       var isLoading = await this.$store.dispatch('audiobooks/load') | ||||
|       if (!isLoading) { | ||||
|         this.setBookshelfEntities() | ||||
|       } | ||||
|     }, | ||||
|     resize() { | ||||
|       this.$nextTick(() => { | ||||
|         this.calculateBookshelf() | ||||
|         this.setBookshelfEntities() | ||||
|       }) | ||||
|     }, | ||||
| @ -186,17 +203,15 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   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.dispatch('audiobooks/load') | ||||
|     this.init() | ||||
|     window.addEventListener('resize', this.resize) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     window.removeEventListener('resize', this.resize) | ||||
|     this.$store.commit('audiobooks/removeListener', 'bookshelf') | ||||
|     this.$store.commit('user/removeSettingsListener', 'bookshelf') | ||||
|     window.removeEventListener('resize', this.resize) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -1,20 +1,31 @@ | ||||
| <template> | ||||
|   <div class="w-full h-10 relative"> | ||||
|     <div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8"> | ||||
|       <p class="font-book">{{ numShowing }} Audiobooks</p> | ||||
|       <p v-if="!selectedSeries" class="font-book">{{ numShowing }} {{ entityName }}</p> | ||||
|       <div v-else class="flex items-center"> | ||||
|         <div @click="seriesBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer"> | ||||
|           <span class="material-icons text-3xl text-white">west</span> | ||||
|         </div> | ||||
|         <!-- <span class="material-icons text-2xl cursor-pointer" @click="seriesBackArrow">west</span> --> | ||||
|         <p class="pl-4 font-book text-lg"> | ||||
|           {{ selectedSeries }} <span class="ml-3 font-mono text-lg bg-black bg-opacity-30 rounded-lg px-1 py-0.5">{{ numShowing }}</span> | ||||
|         </p> | ||||
|       </div> | ||||
|       <div class="flex-grow" /> | ||||
| 
 | ||||
|       <ui-text-input v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" /> | ||||
| 
 | ||||
|       <controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" /> | ||||
| 
 | ||||
|       <controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" /> | ||||
|       <ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" /> | ||||
|       <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" /> | ||||
|       <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     page: String, | ||||
|     selectedSeries: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       settings: {}, | ||||
| @ -22,8 +33,27 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     showSortFilters() { | ||||
|       return this.page === '' | ||||
|     }, | ||||
|     numShowing() { | ||||
|       return this.$store.getters['audiobooks/getFiltered']().length | ||||
|       if (this.page === '') { | ||||
|         return this.$store.getters['audiobooks/getFiltered']().length | ||||
|       } else { | ||||
|         var groups = this.$store.getters['audiobooks/getSeriesGroups']() | ||||
|         if (this.selectedSeries) { | ||||
|           var group = groups.find((g) => g.name === this.selectedSeries) | ||||
|           if (group) return group.books.length | ||||
|           return 0 | ||||
|         } | ||||
|         return groups.length | ||||
|       } | ||||
|     }, | ||||
|     entityName() { | ||||
|       if (!this.page) return 'Audiobooks' | ||||
|       if (this.page === 'series') return 'Series' | ||||
|       if (this.page === 'collections') return 'Collections' | ||||
|       return '' | ||||
|     }, | ||||
|     _keywordFilter: { | ||||
|       get() { | ||||
| @ -35,6 +65,10 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     seriesBackArrow() { | ||||
|       this.$router.replace('/library/series') | ||||
|       this.$emit('update:selectedSeries', null) | ||||
|     }, | ||||
|     updateOrder() { | ||||
|       this.saveSettings() | ||||
|     }, | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| <template> | ||||
|   <div class="w-20 border-r border-primary bg-bg h-full relative box-shadow-side z-20"> | ||||
|   <div class="w-20 border-r border-primary bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px"> | ||||
|     <nuxt-link to="/library" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Library</p> | ||||
|       <p class="font-book pt-1.5" style="font-size: 1rem">Library</p> | ||||
| 
 | ||||
|       <div v-show="paramId === ''" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" /> | ||||
|       <div v-show="paramId === ''" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link to="/library/series" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
| @ -15,40 +15,40 @@ | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Series</p> | ||||
|       <p class="font-book pt-1.5" style="font-size: 1rem">Series</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'series'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" /> | ||||
|       <div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Collections</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'collections'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" /> | ||||
|     </nuxt-link> | ||||
|       <div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
| 
 | ||||
|     <nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <!-- <nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Tags</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'tags'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" /> | ||||
|     </nuxt-link> | ||||
|       <div v-show="paramId === 'tags'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
| 
 | ||||
|     <nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <!-- <nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||
|         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> | ||||
|       </svg> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.8rem">Authors</p> | ||||
| 
 | ||||
|       <div v-show="paramId === 'authors'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" /> | ||||
|     </nuxt-link> | ||||
|       <div v-show="paramId === 'authors'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> --> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
|  | ||||
| @ -68,7 +68,7 @@ export default { | ||||
|   methods: { | ||||
|     filterByAuthor() { | ||||
|       if (this.$route.name !== 'index') { | ||||
|         this.$router.push('/') | ||||
|         this.$router.push('/library') | ||||
|       } | ||||
|       var settingsUpdate = { | ||||
|         filterBy: `authors.${this.$encode(this.author)}` | ||||
|  | ||||
							
								
								
									
										254
									
								
								client/components/cards/Book3d.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								client/components/cards/Book3d.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,254 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }"> | ||||
|     <div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }"> | ||||
|       <div class="perspective"> | ||||
|         <div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false"> | ||||
|           <div class="book book-1 box-shadow-book3d" ref="front"></div> | ||||
|           <div class="title book-1 pointer-events-none" ref="left"></div> | ||||
|           <div class="bottom book-1 pointer-events-none" ref="bottom"></div> | ||||
|           <div class="book-back book-1 pointer-events-none"> | ||||
|             <div class="text pointer-events-none"> | ||||
|               <h3 class="mb-4">Book Back</h3> | ||||
|               <p> | ||||
|                 <span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span> | ||||
|               </p> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     src: String, | ||||
|     width: { | ||||
|       type: Number, | ||||
|       default: 200 | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       hover: false, | ||||
|       hover2: false, | ||||
|       standardWidth: 200, | ||||
|       standardHeight: 320, | ||||
|       isAttached: true, | ||||
|       pageX: 0, | ||||
|       pageY: 0 | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     src(newVal) { | ||||
|       this.setCover() | ||||
|     }, | ||||
|     width(newVal) { | ||||
|       this.init() | ||||
|     }, | ||||
|     hover(newVal) { | ||||
|       if (newVal) { | ||||
|         this.unattach() | ||||
|       } else { | ||||
|         this.attach() | ||||
|       } | ||||
|       setTimeout(() => { | ||||
|         this.hover2 = newVal | ||||
|       }, 100) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     scaleMultiplier() { | ||||
|       return this.hover2 ? 1.25 : 1 | ||||
|     }, | ||||
|     scale() { | ||||
|       var scale = this.width / this.standardWidth | ||||
|       return scale | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     unattach() { | ||||
|       if (this.$refs.card && this.isAttached) { | ||||
|         var bookshelf = document.getElementById('bookshelf') | ||||
|         if (bookshelf) { | ||||
|           var pos = this.$refs.wrapper.getBoundingClientRect() | ||||
| 
 | ||||
|           this.pageX = pos.x | ||||
|           this.pageY = pos.y | ||||
|           document.body.appendChild(this.$refs.card) | ||||
|           this.$refs.card.style.left = this.pageX + 'px' | ||||
|           this.$refs.card.style.top = this.pageY + 'px' | ||||
|           this.$refs.card.style.zIndex = 50 | ||||
|           this.isAttached = false | ||||
|         } else if (bookshelf) { | ||||
|           console.log(this.pageX, this.pageY) | ||||
|           this.isAttached = false | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     attach() { | ||||
|       if (this.$refs.card && !this.isAttached) { | ||||
|         if (this.$refs.wrapper) { | ||||
|           this.isAttached = true | ||||
| 
 | ||||
|           this.$refs.wrapper.appendChild(this.$refs.card) | ||||
|           this.$refs.card.style.left = '0px' | ||||
|           this.$refs.card.style.top = '0px' | ||||
|         } | ||||
|       } else { | ||||
|         console.log('Is attached already', this.isAttached) | ||||
|       } | ||||
|     }, | ||||
|     init() { | ||||
|       var standardWidth = this.standardWidth | ||||
|       document.documentElement.style.setProperty('--book-w', standardWidth + 'px') | ||||
|       document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px') | ||||
|       document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px') | ||||
|       document.documentElement.style.setProperty('--book-d', 40 + 'px') | ||||
|     }, | ||||
|     setElBg(el) { | ||||
|       el.style.backgroundImage = `url("${this.src}")` | ||||
|       el.style.backgroundSize = 'cover' | ||||
|       el.style.backgroundPosition = 'center center' | ||||
|       el.style.backgroundRepeat = 'no-repeat' | ||||
|     }, | ||||
|     setCover() { | ||||
|       if (this.$refs.front) { | ||||
|         this.setElBg(this.$refs.front) | ||||
|       } | ||||
|       if (this.$refs.bottom) { | ||||
|         this.setElBg(this.$refs.bottom) | ||||
|         this.$refs.bottom.style.backgroundSize = '2000%' | ||||
|         this.$refs.bottom.style.filter = 'blur(1px)' | ||||
|       } | ||||
|       if (this.$refs.left) { | ||||
|         this.setElBg(this.$refs.left) | ||||
|         this.$refs.left.style.backgroundSize = '2000%' | ||||
|         this.$refs.left.style.filter = 'blur(1px)' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.setCover() | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| /* :root { | ||||
|   --book-w: 200px; | ||||
|   --book-h: 320px; | ||||
|   --book-d: 30px; | ||||
|   --book-wx: 201px; | ||||
| } */ | ||||
| /*  | ||||
| .wrap { | ||||
|   width: calc(1.1 * var(--book-w)); | ||||
|   height: calc(1.1 * var(--book-h)); | ||||
|   margin: 0 auto; | ||||
| } | ||||
| .perspective { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| 
 | ||||
|   perspective: 600px; | ||||
|   transform-style: preserve-3d; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .book-wrap { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
|   transform-style: preserve-3d; | ||||
|   transition: 'all ease-out 0.6s'; | ||||
| } | ||||
| 
 | ||||
| .book { | ||||
|   width: var(--book-w); | ||||
|   height: var(--book-h); | ||||
|   background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center; | ||||
|   background-size: cover; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   margin: auto; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .title { | ||||
|   content: ''; | ||||
|   height: var(--book-h); | ||||
|   width: var(--book-d); | ||||
|   position: absolute; | ||||
|   right: 0; | ||||
|   left: calc(var(--book-wx) * -1); | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   margin: auto; | ||||
|   background: #444; | ||||
|   transform: rotateY(-80deg) translateX(-14px); | ||||
| 
 | ||||
|   background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center; | ||||
|   background-size: 5000%; | ||||
|   filter: blur(1px); | ||||
| } | ||||
| 
 | ||||
| .bottom { | ||||
|   content: ''; | ||||
|   height: var(--book-d); | ||||
|   width: var(--book-w); | ||||
|   position: absolute; | ||||
|   right: 0; | ||||
|   bottom: var(--book-h); | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   margin: auto; | ||||
|   background: #444; | ||||
|   transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg); | ||||
| 
 | ||||
|   background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center; | ||||
|   background-size: 5000%; | ||||
|   filter: blur(1px); | ||||
| } | ||||
| 
 | ||||
| .book-back { | ||||
|   width: var(--book-w); | ||||
|   height: var(--book-h); | ||||
|   background-color: #444; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   margin: auto; | ||||
|   cursor: pointer; | ||||
|   transform: rotate(180deg) translateZ(-30px) translateX(5px); | ||||
| } | ||||
| .book-back .text { | ||||
|   transform: rotateX(180deg); | ||||
|   position: absolute; | ||||
|   bottom: 0px; | ||||
|   padding: 20px; | ||||
|   text-align: left; | ||||
|   font-size: 12px; | ||||
| } | ||||
| .book-back .text h3 { | ||||
|   color: #fff; | ||||
| } | ||||
| .book-back .text span { | ||||
|   display: block; | ||||
|   margin-bottom: 20px; | ||||
|   color: #fff; | ||||
| } | ||||
| 
 | ||||
| .book-wrap.rotate { | ||||
|   transform: rotateY(30deg) rotateX(0deg); | ||||
| } | ||||
| .book-wrap.flip { | ||||
|   transform: rotateY(180deg); | ||||
| } */ | ||||
| </style> | ||||
| @ -79,15 +79,7 @@ export default { | ||||
|       return '/book_placeholder.jpg' | ||||
|     }, | ||||
|     fullCoverUrl() { | ||||
|       if (!this.cover || this.cover === this.placeholderUrl) return this.placeholderUrl | ||||
|       if (this.cover.startsWith('http:') || this.cover.startsWith('https:')) return this.cover | ||||
|       try { | ||||
|         var url = new URL(this.cover, document.baseURI) | ||||
|         return url.href + `?token=${this.userToken}&ts=${this.bookLastUpdate}` | ||||
|       } catch (err) { | ||||
|         console.error(err) | ||||
|         return '' | ||||
|       } | ||||
|       return this.$store.getters['audiobooks/getBookCoverSrc'](this.book, this.placeholderUrl) | ||||
|     }, | ||||
|     cover() { | ||||
|       return this.book.cover || this.placeholderUrl | ||||
|  | ||||
| @ -1,13 +1,20 @@ | ||||
| <template> | ||||
|   <div class="relative"> | ||||
|     <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard"> | ||||
|       <nuxt-link :to="`/library`" class="cursor-pointer"> | ||||
|         <div class="w-full relative box-shadow-book bg-primary" :style="{ height: height + 'px', width: height + 'px' }"></div> | ||||
|       <nuxt-link :to="`/library/series?${groupType}=${groupEncode}`" class="cursor-pointer"> | ||||
|         <div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }"> | ||||
|           <cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" /> | ||||
| 
 | ||||
|           <div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'"> | ||||
|             <p class="truncate font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none"> | ||||
|             <p class="font-book text-xl">{{ bookItems.length }}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </nuxt-link> | ||||
|     </div> | ||||
|     <!-- <div :style="{ width: height + 'px', height: height + 'px' }" class="box-shadow-book bg-primary"> | ||||
|     <p class="text-white">{{ groupName }}</p> | ||||
|   </div> --> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -28,6 +35,15 @@ export default { | ||||
|       isHovering: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     width(newVal) { | ||||
|       this.$nextTick(() => { | ||||
|         if (this.$refs.groupcover) { | ||||
|           this.$refs.groupcover.init() | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     _group() { | ||||
|       return this.group || {} | ||||
| @ -41,15 +57,30 @@ export default { | ||||
|     paddingX() { | ||||
|       return 16 * this.sizeMultiplier | ||||
|     }, | ||||
|     books() { | ||||
|     bookItems() { | ||||
|       return this._group.books || [] | ||||
|     }, | ||||
|     groupName() { | ||||
|       return this._group.name || 'No Name' | ||||
|     }, | ||||
|     groupType() { | ||||
|       return this._group.type | ||||
|     }, | ||||
|     groupEncode() { | ||||
|       return this.$encode(this.groupName) | ||||
|     }, | ||||
|     filter() { | ||||
|       return `${this.groupType}.${this.$encode(this.groupName)}` | ||||
|     }, | ||||
|     hasValidCovers() { | ||||
|       var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover) | ||||
|       return !!validCovers.length | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickCard() {} | ||||
|     clickCard() { | ||||
|       this.$emit('click', this.group) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
|  | ||||
							
								
								
									
										139
									
								
								client/components/cards/GroupCover.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								client/components/cards/GroupCover.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative"> | ||||
|     <div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book"> | ||||
|       <p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     name: String, | ||||
|     bookItems: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     }, | ||||
|     width: Number, | ||||
|     height: Number | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       noValidCovers: false, | ||||
|       coverDiv: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     bookItems: { | ||||
|       immediate: true, | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           // ensure wrapper is initialized | ||||
|           this.$nextTick(this.init) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     sizeMultiplier() { | ||||
|       return this.width / 192 | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getCoverUrl(book) { | ||||
|       return this.$store.getters['audiobooks/getBookCoverSrc'](book, '') | ||||
|     }, | ||||
|     async buildCoverImg(src, bgCoverWidth, offsetLeft, forceCoverBg = false) { | ||||
|       var showCoverBg = | ||||
|         forceCoverBg || | ||||
|         (await new Promise((resolve) => { | ||||
|           var image = new Image() | ||||
| 
 | ||||
|           image.onload = () => { | ||||
|             var { naturalWidth, naturalHeight } = image | ||||
|             var aspectRatio = naturalHeight / naturalWidth | ||||
|             var arDiff = Math.abs(aspectRatio - 1.6) | ||||
| 
 | ||||
|             // If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit | ||||
|             if (arDiff > 0.15) { | ||||
|               resolve(true) | ||||
|             } else { | ||||
|               resolve(false) | ||||
|             } | ||||
|           } | ||||
|           image.onerror = (err) => { | ||||
|             console.error(err) | ||||
|             resolve(false) | ||||
|           } | ||||
|           image.src = src | ||||
|         })) | ||||
| 
 | ||||
|       var imgdiv = document.createElement('div') | ||||
|       imgdiv.style.height = this.height + 'px' | ||||
|       imgdiv.style.width = bgCoverWidth + 'px' | ||||
|       imgdiv.style.left = offsetLeft + 'px' | ||||
|       imgdiv.className = 'absolute top-0 box-shadow-book' | ||||
|       imgdiv.style.boxShadow = '-4px 0px 4px #11111166' | ||||
|       // imgdiv.style.transform = 'skew(0deg, 15deg)' | ||||
| 
 | ||||
|       if (showCoverBg) { | ||||
|         var coverbgwrapper = document.createElement('div') | ||||
|         coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full bg-primary' | ||||
| 
 | ||||
|         var coverbg = document.createElement('div') | ||||
|         coverbg.className = 'w-full h-full' | ||||
|         coverbg.style.backgroundImage = `url("${src}")` | ||||
|         coverbg.style.backgroundSize = 'cover' | ||||
|         coverbg.style.backgroundPosition = 'center' | ||||
|         coverbg.style.opacity = 0.25 | ||||
|         coverbg.style.filter = 'blur(1px)' | ||||
| 
 | ||||
|         coverbgwrapper.appendChild(coverbg) | ||||
|         imgdiv.appendChild(coverbgwrapper) | ||||
|       } | ||||
| 
 | ||||
|       var img = document.createElement('img') | ||||
|       img.src = src | ||||
|       img.className = 'absolute top-0 left-0 w-full h-full' | ||||
|       img.style.objectFit = showCoverBg ? 'contain' : 'cover' | ||||
| 
 | ||||
|       imgdiv.appendChild(img) | ||||
|       return imgdiv | ||||
|     }, | ||||
|     async init() { | ||||
|       if (this.coverDiv) { | ||||
|         this.coverDiv.remove() | ||||
|         this.coverDiv = null | ||||
|       } | ||||
|       var validCovers = this.bookItems.map((bookItem) => this.getCoverUrl(bookItem.book)).filter((b) => b !== '') | ||||
|       if (!validCovers.length) { | ||||
|         this.noValidCovers = true | ||||
|         return | ||||
|       } | ||||
|       this.noValidCovers = false | ||||
| 
 | ||||
|       var coverWidth = this.width | ||||
|       var widthPer = this.width | ||||
|       if (validCovers.length > 1) { | ||||
|         coverWidth = this.height / 1.6 | ||||
|         widthPer = (this.width - coverWidth) / (validCovers.length - 1) | ||||
|       } | ||||
| 
 | ||||
|       var outerdiv = document.createElement('div') | ||||
|       outerdiv.className = 'w-full h-full relative' | ||||
| 
 | ||||
|       for (let i = 0; i < validCovers.length; i++) { | ||||
|         var offsetLeft = widthPer * i | ||||
|         var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, validCovers.length === 1) | ||||
|         outerdiv.appendChild(img) | ||||
|       } | ||||
| 
 | ||||
|       if (this.$refs.wrapper) { | ||||
|         this.coverDiv = outerdiv | ||||
|         this.$refs.wrapper.appendChild(outerdiv) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -115,7 +115,10 @@ export default { | ||||
|     clickedOption(e, item) { | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.input = this.textInput ? this.textInput.trim() : null | ||||
|       this.input = item | ||||
| 
 | ||||
|       // this.input = this.textInput ? this.textInput.trim() : null | ||||
|       console.log('Clicked option', item) | ||||
|       if (this.$refs.input) this.$refs.input.blur() | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -53,7 +53,7 @@ export default { | ||||
|       var tooltip = document.createElement('div') | ||||
|       tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs' | ||||
|       tooltip.style.zIndex = 100 | ||||
|       tooltip.style.backgroundColor = 'rgba(0,0,0,0.75)' | ||||
|       tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)' | ||||
|       tooltip.innerHTML = this.text | ||||
| 
 | ||||
|       this.setTooltipPosition(tooltip) | ||||
|  | ||||
| @ -86,7 +86,7 @@ export default { | ||||
|     audiobookRemoved(audiobook) { | ||||
|       if (this.$route.name.startsWith('audiobook')) { | ||||
|         if (this.$route.params.id === audiobook.id) { | ||||
|           this.$router.replace('/') | ||||
|           this.$router.replace('/library') | ||||
|         } | ||||
|       } | ||||
|       this.$store.commit('audiobooks/remove', audiobook) | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| export default function ({ store, redirect, route }) { | ||||
| export default function ({ store, redirect, route, app }) { | ||||
|   // If the user is not authenticated
 | ||||
|   if (!store.state.user.user) { | ||||
|     if (route.name === 'batch') return redirect('/login') | ||||
|     return redirect(`/login?redirect=${route.path}`) | ||||
|     return redirect(`/login?redirect=${route.fullPath}`) | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.1.15", | ||||
|   "version": "1.2.0", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -130,7 +130,7 @@ export default { | ||||
|           this.isProcessing = false | ||||
|           if (data.updates) { | ||||
|             this.$toast.success(`Successfully updated ${data.updates} audiobooks`) | ||||
|             this.$router.replace('/') | ||||
|             this.$router.replace('/library') | ||||
|           } else { | ||||
|             this.$toast.warning('No updates were necessary') | ||||
|           } | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| <template> | ||||
|   <div class="page" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <app-book-shelf-toolbar /> | ||||
|     <!-- <app-book-shelf-toolbar /> --> | ||||
|     <!-- <div class="flex h-full"> | ||||
|       <app-side-rail /> | ||||
|       <div class="flex-grow"> --> | ||||
|     <app-book-shelf /> | ||||
|     <!-- <app-book-shelf /> --> | ||||
|     <!-- </div> --> | ||||
|     <!-- </div> --> | ||||
|   </div> | ||||
| @ -12,6 +12,9 @@ | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   asyncData({ redirect }) { | ||||
|     redirect('/library') | ||||
|   }, | ||||
|   data() { | ||||
|     return {} | ||||
|   }, | ||||
|  | ||||
| @ -3,8 +3,8 @@ | ||||
|     <div class="flex h-full"> | ||||
|       <app-side-rail /> | ||||
|       <div class="flex-grow"> | ||||
|         <app-book-shelf-toolbar /> | ||||
|         <app-book-shelf :page="id || ''" /> | ||||
|         <app-book-shelf-toolbar :page="id || ''" :selected-series.sync="selectedSeries" /> | ||||
|         <app-book-shelf :page="id || ''" :selected-series.sync="selectedSeries" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -12,9 +12,13 @@ | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   asyncData({ params }) { | ||||
|   asyncData({ params, query, store, app }) { | ||||
|     if (query.filter) { | ||||
|       store.dispatch('user/updateUserSettings', { filterBy: query.filter }) | ||||
|     } | ||||
|     return { | ||||
|       id: params.id | ||||
|       id: params.id, | ||||
|       selectedSeries: query.series ? app.$decode(query.series) : null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|  | ||||
| @ -37,7 +37,7 @@ export default { | ||||
|         if (this.$route.query.redirect) { | ||||
|           this.$router.replace(this.$route.query.redirect) | ||||
|         } else { | ||||
|           this.$router.replace('/') | ||||
|           this.$router.replace('/library') | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -124,4 +124,7 @@ Vue.prototype.$decode = decode | ||||
| export { | ||||
|   encode, | ||||
|   decode | ||||
| } | ||||
| export default ({ app }, inject) => { | ||||
|   app.$decode = decode | ||||
| } | ||||
| @ -5,6 +5,7 @@ const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens', | ||||
| 
 | ||||
| export const state = () => ({ | ||||
|   audiobooks: [], | ||||
|   lastLoad: 0, | ||||
|   listeners: [], | ||||
|   genres: [...STANDARD_GENRES], | ||||
|   tags: [], | ||||
| @ -88,29 +89,63 @@ export const getters = { | ||||
|     var _genres = [] | ||||
|     state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres)) | ||||
|     return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) | ||||
|   }, | ||||
|   getBookCoverSrc: (state, getters, rootState, rootGetters) => (book, placeholder = '/book_placeholder.jpg') => { | ||||
|     if (!book || !book.cover || book.cover === placeholder) return placeholder | ||||
|     var cover = book.cover | ||||
| 
 | ||||
|     // Absolute URL covers
 | ||||
|     if (cover.startsWith('http:') || cover.startsWith('https:')) return cover | ||||
| 
 | ||||
|     // Server hosted covers
 | ||||
|     try { | ||||
|       // Ensure cover is refreshed if cached
 | ||||
|       var bookLastUpdate = book.lastUpdate || Date.now() | ||||
|       var userToken = rootGetters['user/getToken'] | ||||
| 
 | ||||
|       var url = new URL(cover, document.baseURI) | ||||
|       return url.href + `?token=${userToken}&ts=${bookLastUpdate}` | ||||
|     } catch (err) { | ||||
|       console.error(err) | ||||
|       return placeholder | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const actions = { | ||||
|   load({ commit, rootState }) { | ||||
|   // Return true if calling load
 | ||||
|   load({ state, commit, rootState }) { | ||||
|     if (!rootState.user || !rootState.user.user) { | ||||
|       console.error('audiobooks/load - User not set') | ||||
|       return | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     // Don't load again if already loaded in the last 5 minutes
 | ||||
|     var lastLoadDiff = Date.now() - state.lastLoad | ||||
|     if (lastLoadDiff < 5 * 60 * 1000) { | ||||
|       // Already up to date
 | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     this.$axios | ||||
|       .$get(`/api/audiobooks`) | ||||
|       .then((data) => { | ||||
|         commit('set', data) | ||||
|         commit('setLastLoad') | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('Failed', error) | ||||
|         commit('set', []) | ||||
|       }) | ||||
|     return true | ||||
|   }, | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const mutations = { | ||||
|   setLastLoad(state) { | ||||
|     state.lastLoad = Date.now() | ||||
|   }, | ||||
|   setKeywordFilter(state, val) { | ||||
|     state.keywordFilter = val | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.1.15", | ||||
|   "version": "1.2.0", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
| @ -8,8 +8,10 @@ | ||||
|     "start": "node index.js", | ||||
|     "client": "cd client && npm install && npm run generate", | ||||
|     "prod": "npm run client && npm install && node prod.js", | ||||
|     "build-win": "cd client && npm run generate && cd .. && pkg -t node12-win-x64 -o ./dist/app .", | ||||
|     "build-linux": "pkg -t node12-linux-arm64 -o ./dist/app ." | ||||
|     "generate": "cd client && npm run generate", | ||||
|     "build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .", | ||||
|     "build-linuxarm": "pkg -t node12-linux-arm64 -o ./dist/linuxarm/audiobookshelf .", | ||||
|     "build-linuxamd": "pkg -t node12-linux-amd64 -o ./dist/linuxamd/audiobookshelf ." | ||||
|   }, | ||||
|   "bin": "prod.js", | ||||
|   "pkg": { | ||||
|  | ||||
| @ -82,9 +82,6 @@ cd audiobookshelf | ||||
| # Directories will be created if they don't exist | ||||
| # Paths are relative to the root directory, so "../Audiobooks" would be a valid path | ||||
| npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH] | ||||
| 
 | ||||
| # You only need to use `npm run prod` the first time, after that use `npm run start` | ||||
| npm run start -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH] | ||||
| ``` | ||||
| 
 | ||||
| ## Contributing | ||||
|  | ||||
| @ -199,6 +199,8 @@ class Server { | ||||
| 
 | ||||
|     // Dynamic routes are not generated on client
 | ||||
|     app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) | ||||
|     app.get('/library/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) | ||||
|     app.get('/library', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) | ||||
| 
 | ||||
|     app.use('/api', this.authMiddleware.bind(this), this.apiController.router) | ||||
|     app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user