mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Player track chapter tickmarks, highlight current chapter, progress filters, links in stream container
This commit is contained in:
		
							parent
							
								
									baccbaf82a
								
							
						
					
					
						commit
						f4d6e65380
					
				| @ -12,9 +12,6 @@ | ||||
|         <div v-if="chapters.length" class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> | ||||
|           <span class="material-icons text-3xl">format_list_bulleted</span> | ||||
|         </div> | ||||
|         <div v-else class="flex items-center justify-center text-gray-500"> | ||||
|           <span class="material-icons text-3xl">format_list_bulleted</span> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="absolute right-32 top-0 bottom-0"> | ||||
| @ -56,6 +53,11 @@ | ||||
|         <div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" /> | ||||
|         <div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" /> | ||||
|       </div> | ||||
|       <div ref="track" class="w-full h-2 relative overflow-hidden"> | ||||
|         <template v-for="(tick, index) in chapterTicks"> | ||||
|           <div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-50 h-1 pointer-events-none" /> | ||||
|         </template> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Hover timestamp --> | ||||
|       <div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none"> | ||||
| @ -68,7 +70,7 @@ | ||||
| 
 | ||||
|     <audio ref="audio" @pause="paused" @playing="playing" @progress="progress" @timeupdate="timeupdate" @loadeddata="audioLoadedData" /> | ||||
| 
 | ||||
|     <modals-chapters-modal v-model="showChaptersModal" :chapters="chapters" @select="selectChapter" /> | ||||
|     <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -100,7 +102,8 @@ export default { | ||||
|       totalDuration: 0, | ||||
|       seekedTime: 0, | ||||
|       seekLoading: false, | ||||
|       showChaptersModal: false | ||||
|       showChaptersModal: false, | ||||
|       currentTime: 0 | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -109,6 +112,18 @@ export default { | ||||
|     }, | ||||
|     totalDurationPretty() { | ||||
|       return this.$secondsToTimestamp(this.totalDuration) | ||||
|     }, | ||||
|     chapterTicks() { | ||||
|       return this.chapters.map((chap) => { | ||||
|         var perc = chap.start / this.totalDuration | ||||
|         return { | ||||
|           title: chap.title, | ||||
|           left: perc * this.trackWidth | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     currentChapter() { | ||||
|       return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -175,7 +190,13 @@ export default { | ||||
|         this.$refs.hoverTimestamp.style.left = offsetX - width / 2 + 'px' | ||||
|       } | ||||
|       if (this.$refs.hoverTimestampText) { | ||||
|         this.$refs.hoverTimestampText.innerText = this.$secondsToTimestamp(time) | ||||
|         var hoverText = this.$secondsToTimestamp(time) | ||||
| 
 | ||||
|         var chapter = this.chapters.find((chapter) => chapter.start <= time && time < chapter.end) | ||||
|         if (chapter && chapter.title) { | ||||
|           hoverText += ` - ${chapter.title}` | ||||
|         } | ||||
|         this.$refs.hoverTimestampText.innerText = hoverText | ||||
|       } | ||||
|       if (this.$refs.trackCursor) { | ||||
|         this.$refs.trackCursor.style.opacity = 1 | ||||
| @ -289,7 +310,6 @@ export default { | ||||
|           end: end + offset | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       return ranges | ||||
|     }, | ||||
|     getLastBufferedTime() { | ||||
| @ -334,6 +354,8 @@ export default { | ||||
| 
 | ||||
|       this.updateTimestamp() | ||||
| 
 | ||||
|       this.currentTime = this.audioEl.currentTime | ||||
| 
 | ||||
|       var perc = this.audioEl.currentTime / this.audioEl.duration | ||||
|       var ptWidth = Math.round(perc * this.trackWidth) | ||||
|       if (this.playedTrackWidth === ptWidth) { | ||||
|  | ||||
| @ -10,8 +10,11 @@ | ||||
|     </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">Your Audiobookshelf is empty!</p> | ||||
|       <ui-btn color="success" @click="scan">Scan your Audiobooks</ui-btn> | ||||
|       <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 class="w-full flex flex-col items-center"> | ||||
|       <template v-for="(shelf, index) in groupedBooks"> | ||||
| @ -43,7 +46,8 @@ export default { | ||||
|       availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220], | ||||
|       selectedSizeIndex: 3, | ||||
|       rowPaddingX: 40, | ||||
|       keywordFilterTimeout: null | ||||
|       keywordFilterTimeout: null, | ||||
|       scannerParseSubtitle: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|  | ||||
| @ -42,7 +42,6 @@ export default { | ||||
|       this.saveSettings() | ||||
|     }, | ||||
|     saveSettings() { | ||||
|       this.$store.commit('user/setSettings', this.settings) // Immediate update | ||||
|       this.$store.dispatch('user/updateUserSettings', this.settings) | ||||
|     }, | ||||
|     init() { | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| <template> | ||||
|   <div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary p-4"> | ||||
|     <div class="absolute -top-16 left-4"> | ||||
|     <nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer"> | ||||
|       <cards-book-cover :audiobook="streamAudiobook" :width="88" /> | ||||
|     </div> | ||||
|     </nuxt-link> | ||||
|     <div class="flex items-center pl-24"> | ||||
|       <div> | ||||
|         <h1> | ||||
|           {{ title }} <span v-if="stream" class="text-xs text-gray-400">({{ stream.id }})</span> | ||||
|         </h1> | ||||
|         <p class="text-gray-400 text-sm">by {{ author }}</p> | ||||
|         <nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="hover:underline cursor-pointer"> | ||||
|           {{ title }} <span v-if="stream && $isDev" class="text-xs text-gray-400">({{ stream.id }})</span> | ||||
|         </nuxt-link> | ||||
|         <p class="text-gray-400 text-sm hover:underline cursor-pointer" @click="filterByAuthor">by {{ author }}</p> | ||||
|       </div> | ||||
|       <div class="flex-grow" /> | ||||
|       <span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span> | ||||
| @ -66,6 +66,15 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     filterByAuthor() { | ||||
|       if (this.$route.name !== 'index') { | ||||
|         this.$router.push('/') | ||||
|       } | ||||
|       var settingsUpdate = { | ||||
|         filterBy: `authors.${this.$encode(this.author)}` | ||||
|       } | ||||
|       this.$store.dispatch('user/updateUserSettings', settingsUpdate) | ||||
|     }, | ||||
|     audioPlayerMounted() { | ||||
|       this.audioPlayerReady = true | ||||
|       if (this.stream) { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div class="relative"> | ||||
|     <!-- New Book Flag --> | ||||
|     <div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl"> | ||||
|     <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> | ||||
| @ -65,7 +65,7 @@ export default { | ||||
|   }, | ||||
|   computed: { | ||||
|     isNew() { | ||||
|       return this.tags.includes('new') | ||||
|       return this.tags.includes('New') | ||||
|     }, | ||||
|     tags() { | ||||
|       return this.audiobook.tags || [] | ||||
|  | ||||
| @ -86,6 +86,11 @@ export default { | ||||
|           text: 'Authors', | ||||
|           value: 'authors', | ||||
|           sublist: true | ||||
|         }, | ||||
|         { | ||||
|           text: 'Progress', | ||||
|           value: 'progress', | ||||
|           sublist: true | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
| @ -132,6 +137,9 @@ export default { | ||||
|     authors() { | ||||
|       return this.$store.getters['audiobooks/getUniqueAuthors'] | ||||
|     }, | ||||
|     progress() { | ||||
|       return ['Read', 'Unread', 'In Progress'] | ||||
|     }, | ||||
|     sublistItems() { | ||||
|       return (this[this.sublist] || []).map((item) => { | ||||
|         return { | ||||
|  | ||||
| @ -74,10 +74,10 @@ export default { | ||||
|       this.showMenu = false | ||||
|     }, | ||||
|     leftArrowClick() { | ||||
|       this.rateIndex = Math.max(0, this.rateIndex - 4) | ||||
|       this.rateIndex = Math.max(0, this.rateIndex - 1) | ||||
|     }, | ||||
|     rightArrowClick() { | ||||
|       this.rateIndex = Math.min(this.rates.length - this.numVisible, this.rateIndex + 4) | ||||
|       this.rateIndex = Math.min(this.rates.length - this.numVisible, this.rateIndex + 1) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   <modals-modal v-model="show" :width="500" :height="'unset'"> | ||||
|     <div class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 500px"> | ||||
|       <template v-for="chap in chapters"> | ||||
|         <div :key="chap.id" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg bg-opacity-20 rounded-lg relative" @click="clickChapter(chap)"> | ||||
|         <div :key="chap.id" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-bg bg-opacity-80' : 'bg-opacity-20'" @click="clickChapter(chap)"> | ||||
|           {{ chap.title }} | ||||
|           <span class="flex-grow" /> | ||||
|           <span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span> | ||||
| @ -19,6 +19,10 @@ export default { | ||||
|     chapters: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     }, | ||||
|     currentChapter: { | ||||
|       type: Object, | ||||
|       default: () => null | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
| @ -32,6 +36,9 @@ export default { | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     currentChapterId() { | ||||
|       return this.currentChapter ? this.currentChapter.id : null | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -1,5 +1,14 @@ | ||||
| <template> | ||||
|   <button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click"> | ||||
|   <nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :class="classList"> | ||||
|     <slot /> | ||||
|     <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> | ||||
|       <!-- <span class="material-icons animate-spin">refresh</span> --> | ||||
|       <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> | ||||
|         <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> | ||||
|       </svg> | ||||
|     </div> | ||||
|   </nuxt-link> | ||||
|   <button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click"> | ||||
|     <slot /> | ||||
|     <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> | ||||
|       <!-- <span class="material-icons animate-spin">refresh</span> --> | ||||
| @ -13,6 +22,7 @@ | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     to: String, | ||||
|     color: { | ||||
|       type: String, | ||||
|       default: 'primary' | ||||
|  | ||||
| @ -113,7 +113,6 @@ export default { | ||||
|       this.currentSearch = null | ||||
|     }, | ||||
|     clickedOption(e, item) { | ||||
|       var newValue = this.input === item ? null : item | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.input = this.textInput ? this.textInput.trim() : null | ||||
|  | ||||
| @ -180,7 +180,7 @@ export default { | ||||
|     submitForm() { | ||||
|       if (!this.textInput) return | ||||
| 
 | ||||
|       var cleaned = this.textInput.toLowerCase().trim() | ||||
|       var cleaned = this.textInput.trim() | ||||
|       var matchesItem = this.items.find((i) => { | ||||
|         return i === cleaned | ||||
|       }) | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div ref="box" class="tooltip-box" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||
|   <div ref="box" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||
|     <slot /> | ||||
|   </div> | ||||
| </template> | ||||
| @ -51,8 +51,9 @@ export default { | ||||
|     createTooltip() { | ||||
|       if (!this.$refs.box) return | ||||
|       var tooltip = document.createElement('div') | ||||
|       tooltip.className = 'absolute px-2 bg-black bg-opacity-90 py-1 text-white pointer-events-none text-xs rounded shadow-lg' | ||||
|       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.innerHTML = this.text | ||||
| 
 | ||||
|       this.setTooltipPosition(tooltip) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.1.13", | ||||
|   "version": "1.1.14", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
| @ -25,4 +25,4 @@ | ||||
|     "@nuxtjs/tailwindcss": "^4.2.1", | ||||
|     "postcss": "^8.3.6" | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -49,7 +49,12 @@ | ||||
|           <div class="flex-grow" /> | ||||
|           <div class="w-40 flex flex-col"> | ||||
|             <ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn> | ||||
|             <ui-btn color="primary" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn> | ||||
| 
 | ||||
|             <div class="w-full"> | ||||
|               <ui-tooltip direction="bottom" text="Only scans audiobooks without a cover. Covers will be applied if a close match is found." class="w-full"> | ||||
|                 <ui-btn color="primary" class="w-full" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn> | ||||
|               </ui-tooltip> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -16,12 +16,12 @@ export const getters = { | ||||
|   getAudiobook: (state) => id => { | ||||
|     return state.audiobooks.find(ab => ab.id === id) | ||||
|   }, | ||||
|   getFiltered: (state, getters, rootState) => () => { | ||||
|   getFiltered: (state, getters, rootState, rootGetters) => () => { | ||||
|     var filtered = state.audiobooks | ||||
|     var settings = rootState.user.settings || {} | ||||
|     var filterBy = settings.filterBy || '' | ||||
| 
 | ||||
|     var searchGroups = ['genres', 'tags', 'series', 'authors'] | ||||
|     var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress'] | ||||
|     var group = searchGroups.find(_group => filterBy.startsWith(_group + '.')) | ||||
|     if (group) { | ||||
|       var filter = decode(filterBy.replace(`${group}.`, '')) | ||||
| @ -29,6 +29,16 @@ export const getters = { | ||||
|       else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter)) | ||||
|       else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter) | ||||
|       else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter) | ||||
|       else if (group === 'progress') { | ||||
|         filtered = filtered.filter(ab => { | ||||
|           var userAudiobook = rootGetters['user/getUserAudiobook'](ab.id) | ||||
|           var isRead = userAudiobook && userAudiobook.isRead | ||||
|           if (filter === 'Read' && isRead) return true | ||||
|           if (filter === 'Unread' && !isRead) return true | ||||
|           if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true | ||||
|           return false | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|     if (state.keywordFilter) { | ||||
|       const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrarator'] | ||||
|  | ||||
| @ -44,6 +44,8 @@ export const actions = { | ||||
|     var updatePayload = { | ||||
|       ...payload | ||||
|     } | ||||
|     // Immediately update
 | ||||
|     commit('setSettings', updatePayload) | ||||
|     return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => { | ||||
|       if (result.success) { | ||||
|         commit('setSettings', result.settings) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.1.13", | ||||
|   "version": "1.1.14", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user