This commit is contained in:
Horis
2023-02-16 11:42:35 +08:00
parent d5caabf2cd
commit 5d407f3753
8 changed files with 126 additions and 79 deletions
@@ -302,6 +302,9 @@ object BookHelp {
* 读取章节内容 * 读取章节内容
*/ */
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
if (book.isLocalTxt) {
return LocalBook.getContent(book, bookChapter)
}
val file = downloadDir.getFile( val file = downloadDir.getFile(
cacheFolderName, cacheFolderName,
book.getFolderName(), book.getFolderName(),
@@ -25,6 +25,7 @@ class ContentProcessor private constructor(
companion object { companion object {
private val processors = hashMapOf<String, WeakReference<ContentProcessor>>() private val processors = hashMapOf<String, WeakReference<ContentProcessor>>()
var enableRemoveSameTitle = true
fun get(bookName: String, bookOrigin: String): ContentProcessor { fun get(bookName: String, bookOrigin: String): ContentProcessor {
val processorWr = processors[bookName + bookOrigin] val processorWr = processors[bookName + bookOrigin]
@@ -84,7 +85,7 @@ class ContentProcessor private constructor(
var sameTitleRemoved = false var sameTitleRemoved = false
if (content != "null") { if (content != "null") {
//去除重复标题 //去除重复标题
if (BookHelp.removeSameTitle(book, chapter)) try { if (enableRemoveSameTitle && BookHelp.removeSameTitle(book, chapter)) try {
val name = Pattern.quote(book.name) val name = Pattern.quote(book.name)
var title = Pattern.quote(chapter.title) var title = Pattern.quote(chapter.title)
var matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*") var matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*")
@@ -10,6 +10,7 @@ import io.legado.app.help.book.isLocal
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.localBook.TextFile
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
@@ -77,6 +78,7 @@ object ReadBook : CoroutineScope by MainScope() {
upWebBook(book) upWebBook(book)
lastBookPress = null lastBookPress = null
webBookProgress = null webBookProgress = null
TextFile.txtBuffer = null
synchronized(this) { synchronized(this) {
loadingChapters.clear() loadingChapters.clear()
} }
@@ -95,7 +95,7 @@ object LocalBook {
} }
fun getContent(book: Book, chapter: BookChapter): String? { fun getContent(book: Book, chapter: BookChapter): String? {
val content = try { var content = try {
when { when {
book.isEpub -> { book.isEpub -> {
EpubFile.getContent(book, chapter) EpubFile.getContent(book, chapter)
@@ -114,13 +114,16 @@ object LocalBook {
e.printOnDebug() e.printOnDebug()
AppLog.put("获取本地书籍内容失败\n${e.localizedMessage}", e) AppLog.put("获取本地书籍内容失败\n${e.localizedMessage}", e)
"获取本地书籍内容失败\n${e.localizedMessage}" "获取本地书籍内容失败\n${e.localizedMessage}"
}?.replace("&lt;img", "&lt; img", true) }
content ?: return null if (book.isEpub) {
return kotlin.runCatching { content = content?.replace("&lt;img", "&lt; img", true) ?: return null
Entities.unescape(content) return kotlin.runCatching {
}.onFailure { Entities.unescape(content)
AppLog.put("HTML实体解码失败\n${it.localizedMessage}", it) }.onFailure {
}.getOrElse { content } AppLog.put("HTML实体解码失败\n${it.localizedMessage}", it)
}.getOrDefault(content)
}
return content
} }
fun getCoverPath(book: Book): String { fun getCoverPath(book: Book): String {
@@ -18,23 +18,60 @@ import kotlin.math.min
class TextFile(private val book: Book) { class TextFile(private val book: Book) {
companion object { companion object {
private val padRegex = "^[\\n\\s]+".toRegex()
private const val bufferSize = 8 * 1024 * 1024
var txtBuffer: ByteArray? = null
var bufferStart = -1
var bufferEnd = -1
var bookUrl = ""
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun getChapterList(book: Book): ArrayList<BookChapter> { fun getChapterList(book: Book): ArrayList<BookChapter> {
return TextFile(book).getChapterList() return TextFile(book).getChapterList()
} }
@Synchronized
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun getContent(book: Book, bookChapter: BookChapter): String { fun getContent(book: Book, bookChapter: BookChapter): String {
if (txtBuffer == null
|| bookUrl != book.bookUrl
|| bookChapter.start!! > bufferEnd
|| bookChapter.end!! < bufferStart
) {
bookUrl = book.bookUrl
LocalBook.getBookInputStream(book).use { bis ->
bufferStart = bufferSize * (bookChapter.start!! / bufferSize).toInt()
txtBuffer = ByteArray(min(bufferSize, bis.available() - bufferStart))
bufferEnd = bufferStart + txtBuffer!!.size
bis.skip(bufferStart.toLong())
bis.read(txtBuffer)
}
}
val count = (bookChapter.end!! - bookChapter.start!!).toInt() val count = (bookChapter.end!! - bookChapter.start!!).toInt()
val buffer = ByteArray(count) val buffer = ByteArray(count)
LocalBook.getBookInputStream(book).use { bis ->
bis.skip(bookChapter.start!!) if (bookChapter.start!! < bufferEnd && bookChapter.end!! > bufferEnd
bis.read(buffer) || bookChapter.start!! < bufferStart && bookChapter.end!! > bufferStart
) {
/** 章节内容在缓冲区交界处 */
LocalBook.getBookInputStream(book).use { bis ->
bis.skip(bookChapter.start!!)
bis.read(buffer)
}
} else {
/** 章节内容在缓冲区内 */
txtBuffer!!.copyInto(
buffer,
0,
(bookChapter.start!! - bufferStart).toInt(),
(bookChapter.end!! - bufferStart).toInt()
)
} }
return String(buffer, book.fileCharset()) return String(buffer, book.fileCharset())
.substringAfter(bookChapter.title) .substringAfter(bookChapter.title)
.replace("^[\\n\\s]+".toRegex(), "  ") .replace(padRegex, "  ")
} }
} }
@@ -121,6 +121,7 @@ class ReadBookActivity : BaseReadBookActivity(),
val searchResult = IntentData.get<SearchResult>("searchResult$key") val searchResult = IntentData.get<SearchResult>("searchResult$key")
val searchResultList = IntentData.get<List<SearchResult>>("searchResultList$key") val searchResultList = IntentData.get<List<SearchResult>>("searchResultList$key")
if (searchResult != null && searchResultList != null) { if (searchResult != null && searchResultList != null) {
ContentProcessor.enableRemoveSameTitle = false
viewModel.searchContentQuery = searchResult.query viewModel.searchContentQuery = searchResult.query
binding.searchMenu.upSearchResultList(searchResultList) binding.searchMenu.upSearchResultList(searchResultList)
isShowingSearchResult = true isShowingSearchResult = true
@@ -1005,6 +1006,7 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun exitSearchMenu() { override fun exitSearchMenu() {
if (isShowingSearchResult) { if (isShowingSearchResult) {
isShowingSearchResult = false isShowingSearchResult = false
ContentProcessor.enableRemoveSameTitle = true
binding.searchMenu.invalidate() binding.searchMenu.invalidate()
binding.searchMenu.invisible() binding.searchMenu.invisible()
binding.readView.isTextSelected = false binding.readView.isTextSelected = false
@@ -15,6 +15,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.databinding.ActivitySearchContentBinding import io.legado.app.databinding.ActivitySearchContentBinding
import io.legado.app.help.IntentData import io.legado.app.help.IntentData
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
@@ -144,43 +145,48 @@ class SearchContentActivity :
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun startContentSearch(query: String) { fun startContentSearch(query: String) {
// 按章节搜索内容 // 按章节搜索内容
if (query.isNotBlank()) { if (query.isBlank()) return
searchJob?.cancel() searchJob?.cancel()
adapter.clearItems() adapter.clearItems()
viewModel.searchResultList.clear() viewModel.searchResultList.clear()
viewModel.searchResultCounts = 0 viewModel.searchResultCounts = 0
viewModel.lastQuery = query viewModel.lastQuery = query
searchJob = launch { binding.refreshProgressBar.isAutoLoading = true
kotlin.runCatching { binding.fbStop.visible()
binding.refreshProgressBar.isAutoLoading = true ContentProcessor.enableRemoveSameTitle = false
binding.fbStop.visible() searchJob = launch(IO) {
withContext(IO) { kotlin.runCatching {
appDb.bookChapterDao.getChapterList(viewModel.bookUrl) appDb.bookChapterDao.getChapterList(viewModel.bookUrl).forEach { bookChapter ->
}.forEach { bookChapter -> ensureActive()
ensureActive() val searchResults = if (isLocalBook
val searchResults = withContext(IO) { || viewModel.cacheChapterNames.contains(bookChapter.getFileName())
if (isLocalBook || viewModel.cacheChapterNames.contains(bookChapter.getFileName())) { ) {
viewModel.searchChapter(query, bookChapter) viewModel.searchChapter(query, bookChapter)
} else { } else {
null return@forEach
} }
} ensureActive()
binding.tvCurrentSearchInfo.text = if (searchResults.isNotEmpty()) {
this@SearchContentActivity.getString(R.string.search_content_size) + ": ${viewModel.searchResultCounts}" viewModel.searchResultList.addAll(searchResults)
ensureActive() binding.tvCurrentSearchInfo.post {
if (searchResults != null && searchResults.isNotEmpty()) { binding.tvCurrentSearchInfo.text =
viewModel.searchResultList.addAll(searchResults) this@SearchContentActivity.getString(R.string.search_content_size) + ": ${viewModel.searchResultCounts}"
adapter.addItems(searchResults) adapter.addItems(searchResults)
} }
} }
if (viewModel.searchResultCounts == 0) { }
val noSearchResult = if (viewModel.searchResultCounts == 0) {
SearchResult(resultText = getString(R.string.search_content_empty)) val noSearchResult =
SearchResult(resultText = getString(R.string.search_content_empty))
binding.tvCurrentSearchInfo.post {
adapter.addItem(noSearchResult) adapter.addItem(noSearchResult)
} }
}.onFailure {
AppLog.put("全文搜索出错\n${it.localizedMessage}", it)
} }
}.onFailure {
AppLog.put("全文搜索出错\n${it.localizedMessage}", it)
}
ContentProcessor.enableRemoveSameTitle = true
binding.tvCurrentSearchInfo.post {
binding.fbStop.invisible() binding.fbStop.invisible()
binding.refreshProgressBar.isAutoLoading = false binding.refreshProgressBar.isAutoLoading = false
} }
@@ -41,42 +41,35 @@ class SearchContentViewModel(application: Application) : BaseViewModel(applicati
chapter: BookChapter? chapter: BookChapter?
): List<SearchResult> { ): List<SearchResult> {
val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf() val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()
if (chapter != null) { chapter ?: return searchResultsWithinChapter
book?.let { book -> val book = book ?: return searchResultsWithinChapter
val chapterContent = BookHelp.getContent(book, chapter) val chapterContent = BookHelp.getContent(book, chapter) ?: return searchResultsWithinChapter
val mContent: String coroutineContext.ensureActive()
coroutineContext.ensureActive() chapter.title = when (AppConfig.chineseConverterType) {
if (chapterContent != null) { 1 -> ChineseUtils.t2s(chapter.title)
withContext(Dispatchers.IO) { 2 -> ChineseUtils.s2t(chapter.title)
chapter.title = when (AppConfig.chineseConverterType) { else -> chapter.title
1 -> ChineseUtils.t2s(chapter.title)
2 -> ChineseUtils.s2t(chapter.title)
else -> chapter.title
}
coroutineContext.ensureActive()
mContent = contentProcessor!!.getContent(
book, chapter, chapterContent
).toString()
}
val positions = searchPosition(mContent, query)
positions.forEachIndexed { index, position ->
coroutineContext.ensureActive()
val construct = getResultAndQueryIndex(mContent, position, query)
val result = SearchResult(
resultCountWithinChapter = index,
resultText = construct.second,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
queryIndexInResult = construct.first,
queryIndexInChapter = position
)
searchResultsWithinChapter.add(result)
}
searchResultCounts += searchResultsWithinChapter.size
}
}
} }
coroutineContext.ensureActive()
val mContent = contentProcessor!!.getContent(
book, chapter, chapterContent
).toString()
val positions = searchPosition(mContent, query)
positions.forEachIndexed { index, position ->
coroutineContext.ensureActive()
val construct = getResultAndQueryIndex(mContent, position, query)
val result = SearchResult(
resultCountWithinChapter = index,
resultText = construct.second,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
queryIndexInResult = construct.first,
queryIndexInChapter = position
)
searchResultsWithinChapter.add(result)
}
searchResultCounts += searchResultsWithinChapter.size
return searchResultsWithinChapter return searchResultsWithinChapter
} }