Merge pull request #2255 from MxMarx/added-search-epubs

Search epub text
This commit is contained in:
advplyr 2023-10-28 14:35:40 -05:00 committed by GitHub
commit a9f74ace5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 17 deletions

View File

@ -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

View File

@ -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%;
}

View File

@ -68,6 +68,7 @@ export default {
methods: {
clear() {
this.inputValue = ''
this.$emit('clear')
},
focused() {
this.isFocused = true