mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #2255 from MxMarx/added-search-epubs
Search epub text
This commit is contained in:
		
						commit
						a9f74ace5a
					
				| @ -40,6 +40,7 @@ export default { | ||||
|       book: null, | ||||
|       /** @type {ePub.Rendition} */ | ||||
|       rendition: null, | ||||
|       chapters: [], | ||||
|       ereaderSettings: { | ||||
|         theme: 'dark', | ||||
|         font: 'serif', | ||||
| @ -68,10 +69,6 @@ export default { | ||||
|     hasNext() { | ||||
|       return !this.rendition?.location?.atEnd | ||||
|     }, | ||||
|     /** @returns {Array<ePub.NavItem>} */ | ||||
|     chapters() { | ||||
|       return this.book?.navigation?.toc || [] | ||||
|     }, | ||||
|     userMediaProgress() { | ||||
|       if (!this.libraryItemId) return | ||||
|       return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) | ||||
| @ -146,6 +143,40 @@ export default { | ||||
|       if (!this.rendition?.manager) return | ||||
|       return this.rendition?.display(href) | ||||
|     }, | ||||
|     /** @returns {object} Returns the chapter that the `position` in the book is in */ | ||||
|     findChapterFromPosition(chapters, position) { | ||||
|       let foundChapter | ||||
|       for (let i = 0; i < chapters.length; i++) { | ||||
|         if (position >= chapters[i].start && (!chapters[i + 1] || position < chapters[i + 1].start)) { | ||||
|           foundChapter = chapters[i] | ||||
|           if (chapters[i].subitems && chapters[i].subitems.length > 0) { | ||||
|             return this.findChapterFromPosition(chapters[i].subitems, position, foundChapter) | ||||
|           } | ||||
|           break | ||||
|         } | ||||
|       } | ||||
|       return foundChapter | ||||
|     }, | ||||
|     /** @returns {Array} Returns an array of chapters that only includes chapters with query results */ | ||||
|     async searchBook(query) { | ||||
|       const chapters = structuredClone(await this.chapters) | ||||
|       const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item)))) | ||||
|       const mergedResults = [].concat(...searchResults) | ||||
| 
 | ||||
|       mergedResults.forEach((chapter) => { | ||||
|         chapter.start = this.book.locations.percentageFromCfi(chapter.cfi) | ||||
|         const foundChapter = this.findChapterFromPosition(chapters, chapter.start) | ||||
|         if (foundChapter) foundChapter.searchResults.push(chapter) | ||||
|       }) | ||||
| 
 | ||||
|       let filteredResults = chapters.filter(function f(o) { | ||||
|         if (o.searchResults.length) return true | ||||
|         if (o.subitems.length) { | ||||
|           return (o.subitems = o.subitems.filter(f)).length | ||||
|         } | ||||
|       }) | ||||
|       return filteredResults | ||||
|     }, | ||||
|     keyUp(e) { | ||||
|       const rtl = this.book.package.metadata.direction === 'rtl' | ||||
|       if ((e.keyCode || e.which) == 37) { | ||||
| @ -319,8 +350,77 @@ export default { | ||||
|             this.checkSaveLocations(reader.book.locations.save()) | ||||
|           }) | ||||
|         } | ||||
|         this.getChapters() | ||||
|       }) | ||||
|     }, | ||||
|     getChapters() { | ||||
|       // Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759 | ||||
|       const toc = this.book?.navigation?.toc || [] | ||||
| 
 | ||||
|       const tocTree = [] | ||||
| 
 | ||||
|       const resolveURL = (url, relativeTo) => { | ||||
|         // see https://github.com/futurepress/epub.js/issues/1084 | ||||
|         // HACK-ish: abuse the URL API a little to resolve the path | ||||
|         // the base needs to be a valid URL, or it will throw a TypeError, | ||||
|         // so we just set a random base URI and remove it later | ||||
|         const base = 'https://example.invalid/' | ||||
|         return new URL(url, base + relativeTo).href.replace(base, '') | ||||
|       } | ||||
| 
 | ||||
|       const basePath = this.book.packaging.navPath || this.book.packaging.ncxPath | ||||
| 
 | ||||
|       const createTree = async (toc, parent) => { | ||||
|         const promises = toc.map(async (tocItem, i) => { | ||||
|           const href = resolveURL(tocItem.href, basePath) | ||||
|           const id = href.split('#')[1] | ||||
|           const item = this.book.spine.get(href) | ||||
|           await item.load(this.book.load.bind(this.book)) | ||||
|           const el = id ? item.document.getElementById(id) : item.document.body | ||||
| 
 | ||||
|           const cfi = item.cfiFromElement(el) | ||||
| 
 | ||||
|           parent[i] = { | ||||
|             title: tocItem.label.trim(), | ||||
|             subitems: [], | ||||
|             href, | ||||
|             cfi, | ||||
|             start: this.book.locations.percentageFromCfi(cfi), | ||||
|             end: null, // set by flattenChapters() | ||||
|             id: null, // set by flattenChapters() | ||||
|             searchResults: [] | ||||
|           } | ||||
| 
 | ||||
|           if (tocItem.subitems) { | ||||
|             await createTree(tocItem.subitems, parent[i].subitems) | ||||
|           } | ||||
|         }) | ||||
|         await Promise.all(promises) | ||||
|       } | ||||
|       return createTree(toc, tocTree).then(() => { | ||||
|         this.chapters = tocTree | ||||
|       }) | ||||
|     }, | ||||
|     flattenChapters(chapters) { | ||||
|       // Convert the nested epub chapters into something that looks like audiobook chapters for player-ui | ||||
|       const unwrap = (chapters) => { | ||||
|         return chapters.reduce((acc, chapter) => { | ||||
|           return chapter.subitems ? [...acc, chapter, ...unwrap(chapter.subitems)] : [...acc, chapter] | ||||
|         }, []) | ||||
|       } | ||||
|       let flattenedChapters = unwrap(chapters) | ||||
| 
 | ||||
|       flattenedChapters = flattenedChapters.sort((a, b) => a.start - b.start) | ||||
|       for (let i = 0; i < flattenedChapters.length; i++) { | ||||
|         flattenedChapters[i].id = i | ||||
|         if (i < flattenedChapters.length - 1) { | ||||
|           flattenedChapters[i].end = flattenedChapters[i + 1].start | ||||
|         } else { | ||||
|           flattenedChapters[i].end = 1 | ||||
|         } | ||||
|       } | ||||
|       return flattenedChapters | ||||
|     }, | ||||
|     resize() { | ||||
|       this.windowWidth = window.innerWidth | ||||
|       this.windowHeight = window.innerHeight | ||||
|  | ||||
| @ -26,9 +26,9 @@ | ||||
|     <component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" /> | ||||
| 
 | ||||
|     <!-- TOC side nav --> | ||||
|     <div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> | ||||
|     <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC"> | ||||
|       <div class="p-4 h-full"> | ||||
|     <div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> | ||||
|     <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent> | ||||
|       <div class="flex flex-col p-4 h-full"> | ||||
|         <div class="flex items-center mb-2"> | ||||
|           <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> | ||||
|             <span class="material-icons text-2xl">arrow_back</span> | ||||
| @ -36,13 +36,28 @@ | ||||
| 
 | ||||
|           <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> | ||||
|         </div> | ||||
|         <div class="tocContent"> | ||||
|         <form @submit.prevent="searchBook" @click.stop.prevent> | ||||
|           <ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" /> | ||||
|         </form> | ||||
| 
 | ||||
|         <div class="overflow-y-auto"> | ||||
|           <div v-if="isSearching && !this.searchResults.length" class="w-full h-40 justify-center"> | ||||
|             <p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p> | ||||
|           </div> | ||||
| 
 | ||||
|           <ul> | ||||
|             <li v-for="chapter in chapters" :key="chapter.id" class="py-1"> | ||||
|               <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a> | ||||
|             <li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1"> | ||||
|               <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a> | ||||
|               <div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4"> | ||||
|                 <a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a> | ||||
|               </div> | ||||
| 
 | ||||
|               <ul v-if="chapter.subitems.length"> | ||||
|                 <li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4"> | ||||
|                   <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a> | ||||
|                   <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a> | ||||
|                   <div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4"> | ||||
|                     <a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a> | ||||
|                   </div> | ||||
|                 </li> | ||||
|               </ul> | ||||
|             </li> | ||||
| @ -105,6 +120,9 @@ export default { | ||||
|       touchstartTime: 0, | ||||
|       touchIdentifier: null, | ||||
|       chapters: [], | ||||
|       isSearching: false, | ||||
|       searchResults: [], | ||||
|       searchQuery: '', | ||||
|       tocOpen: false, | ||||
|       showSettings: false, | ||||
|       ereaderSettings: { | ||||
| @ -163,11 +181,11 @@ export default { | ||||
|         font: [ | ||||
|           { | ||||
|             text: 'Sans', | ||||
|             value: 'sans-serif', | ||||
|             value: 'sans-serif' | ||||
|           }, | ||||
|           { | ||||
|             text: 'Serif', | ||||
|             value: 'serif', | ||||
|             value: 'serif' | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
| @ -254,6 +272,10 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     goToChapter(uri) { | ||||
|       this.toggleToC() | ||||
|       this.$refs.readerComponent.goToChapter(uri) | ||||
|     }, | ||||
|     readerMounted() { | ||||
|       if (this.isEpub) { | ||||
|         this.loadEreaderSettings() | ||||
| @ -281,6 +303,15 @@ export default { | ||||
|         this.close() | ||||
|       } | ||||
|     }, | ||||
|     async searchBook() { | ||||
|       if (this.searchQuery.length > 1) { | ||||
|         this.searchResults = await this.$refs.readerComponent.searchBook(this.searchQuery) | ||||
|         this.isSearching = true | ||||
|       } else { | ||||
|         this.isSearching = false | ||||
|         this.searchResults = [] | ||||
|       } | ||||
|     }, | ||||
|     next() { | ||||
|       if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next() | ||||
|     }, | ||||
| @ -359,6 +390,8 @@ export default { | ||||
|     }, | ||||
|     close() { | ||||
|       this.unregisterListeners() | ||||
|       this.isSearching = false | ||||
|       this.searchQuery = '' | ||||
|       this.show = false | ||||
|     } | ||||
|   }, | ||||
| @ -372,10 +405,6 @@ export default { | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| .tocContent { | ||||
|   height: calc(100% - 36px); | ||||
|   overflow-y: auto; | ||||
| } | ||||
| #reader { | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| @ -68,6 +68,7 @@ export default { | ||||
|   methods: { | ||||
|     clear() { | ||||
|       this.inputValue = '' | ||||
|       this.$emit('clear') | ||||
|     }, | ||||
|     focused() { | ||||
|       this.isFocused = true | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user