Horis 3 лет назад
Родитель
Сommit
12d9b9876a

+ 5 - 0
app/src/main/assets/updateLog.md

@@ -12,6 +12,11 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
 
+**2023/06/07**
+
+* 修复长按菜单全文搜索结果不全或无结果问题
+* 优化全文搜索速度
+
 **2023/06/06**
 **2023/06/06**
 
 
 * 更新cronet: 114.0.5735.57
 * 更新cronet: 114.0.5735.57

+ 5 - 1
app/src/main/java/io/legado/app/constant/AppPattern.kt

@@ -18,7 +18,7 @@ object AppPattern {
     val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著")
     val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著")
     val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
     val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
     val splitGroupRegex = Regex("[,;,;]")
     val splitGroupRegex = Regex("[,;,;]")
-    val titleNumPattern = Pattern.compile("(第)(.+?)(章)")
+    val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)")
 
 
     //书源调试信息中的各种符号
     //书源调试信息中的各种符号
     val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
     val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
@@ -48,4 +48,8 @@ object AppPattern {
     val semicolonRegex = ";".toRegex()
     val semicolonRegex = ";".toRegex()
 
 
     val equalsRegex = "=".toRegex()
     val equalsRegex = "=".toRegex()
+
+    val spaceRegex = "\\s+".toRegex()
+
+    val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
 }
 }

+ 3 - 0
app/src/main/java/io/legado/app/data/dao/BookDao.kt

@@ -68,6 +68,9 @@ interface BookDao {
     @Query("SELECT * FROM books WHERE name = :name and author = :author")
     @Query("SELECT * FROM books WHERE name = :name and author = :author")
     fun getBook(name: String, author: String): Book?
     fun getBook(name: String, author: String): Book?
 
 
+    @Query("SELECT * FROM books WHERE name = :name and origin = :origin")
+    fun getBookByOrigin(name: String, origin: String): Book?
+
     @get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups)")
     @get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups)")
     val noGroupSize: Int
     val noGroupSize: Int
 
 

+ 15 - 3
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -135,6 +135,10 @@ data class Book(
     @IgnoredOnParcel
     @IgnoredOnParcel
     var downloadUrls: List<String>? = null
     var downloadUrls: List<String>? = null
 
 
+    @Ignore
+    @IgnoredOnParcel
+    private var folderName: String? = null
+
     fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
     fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
 
 
     fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
     fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
@@ -237,10 +241,18 @@ data class Book(
     }
     }
 
 
     fun getFolderName(): String {
     fun getFolderName(): String {
+        folderName?.let {
+            return it
+        }
         //防止书名过长,只取9位
         //防止书名过长,只取9位
-        var folderName = name.replace(AppPattern.fileNameRegex, "")
-        folderName = folderName.substring(0, min(9, folderName.length))
-        return folderName + MD5Utils.md5Encode16(bookUrl)
+        folderName = getFolderNameNoCache()
+        return folderName!!
+    }
+
+    fun getFolderNameNoCache(): String {
+        return name.replace(AppPattern.fileNameRegex, "").let {
+            it.substring(0, min(9, it.length)) + MD5Utils.md5Encode16(bookUrl)
+        }
     }
     }
 
 
     fun toSearchBook() = SearchBook(
     fun toSearchBook() = SearchBook(

+ 9 - 2
app/src/main/java/io/legado/app/data/entities/BookChapter.kt

@@ -59,6 +59,12 @@ data class BookChapter(
         GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
         GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
     }
     }
 
 
+    @delegate:Ignore
+    @IgnoredOnParcel
+    private val titleMD5: String by lazy {
+        MD5Utils.md5Encode16(title)
+    }
+
     override fun putVariable(key: String, value: String?): Boolean {
     override fun putVariable(key: String, value: String?): Boolean {
         if (super.putVariable(key, value)) {
         if (super.putVariable(key, value)) {
             variable = GSON.toJson(variableMap)
             variable = GSON.toJson(variableMap)
@@ -145,9 +151,10 @@ data class BookChapter(
 
 
     @Suppress("unused")
     @Suppress("unused")
     fun getFileName(suffix: String = "nb"): String =
     fun getFileName(suffix: String = "nb"): String =
-        String.format("%05d-%s.%s", index, MD5Utils.md5Encode16(title), suffix)
+        String.format("%05d-%s.%s", index, titleMD5, suffix)
+
 
 
     @Suppress("unused")
     @Suppress("unused")
-    fun getFontName(): String = String.format("%05d-%s.ttf", index, MD5Utils.md5Encode16(title))
+    fun getFontName(): String = String.format("%05d-%s.ttf", index, titleMD5)
 }
 }
 
 

+ 20 - 5
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -51,8 +51,19 @@ object BookHelp {
     }
     }
 
 
     fun updateCacheFolder(oldBook: Book, newBook: Book) {
     fun updateCacheFolder(oldBook: Book, newBook: Book) {
-        val oldFolderPath = FileUtils.getPath(downloadDir, cacheFolderName, oldBook.getFolderName())
-        val newFolderPath = FileUtils.getPath(downloadDir, cacheFolderName, newBook.getFolderName())
+        val oldFolderName = oldBook.getFolderNameNoCache()
+        val newFolderName = newBook.getFolderNameNoCache()
+        if (oldFolderName == newFolderName) return
+        val oldFolderPath = FileUtils.getPath(
+            downloadDir,
+            cacheFolderName,
+            oldFolderName
+        )
+        val newFolderPath = FileUtils.getPath(
+            downloadDir,
+            cacheFolderName,
+            newFolderName
+        )
         FileUtils.move(oldFolderPath, newFolderPath)
         FileUtils.move(oldFolderPath, newFolderPath)
     }
     }
 
 
@@ -331,21 +342,25 @@ object BookHelp {
      * 设置是否禁用正文的去除重复标题,针对单个章节
      * 设置是否禁用正文的去除重复标题,针对单个章节
      */
      */
     fun setRemoveSameTitle(book: Book, bookChapter: BookChapter, removeSameTitle: Boolean) {
     fun setRemoveSameTitle(book: Book, bookChapter: BookChapter, removeSameTitle: Boolean) {
+        val fileName = bookChapter.getFileName("nr")
+        val contentProcessor = ContentProcessor.get(book)
         if (removeSameTitle) {
         if (removeSameTitle) {
             val path = FileUtils.getPath(
             val path = FileUtils.getPath(
                 downloadDir,
                 downloadDir,
                 cacheFolderName,
                 cacheFolderName,
                 book.getFolderName(),
                 book.getFolderName(),
-                bookChapter.getFileName(".nr")
+                fileName
             )
             )
+            contentProcessor.removeSameTitleCache.remove(fileName)
             File(path).delete()
             File(path).delete()
         } else {
         } else {
             FileUtils.createFileIfNotExist(
             FileUtils.createFileIfNotExist(
                 downloadDir,
                 downloadDir,
                 cacheFolderName,
                 cacheFolderName,
                 book.getFolderName(),
                 book.getFolderName(),
-                bookChapter.getFileName(".nr")
+                fileName
             )
             )
+            contentProcessor.removeSameTitleCache.add(fileName)
         }
         }
     }
     }
 
 
@@ -357,7 +372,7 @@ object BookHelp {
             downloadDir,
             downloadDir,
             cacheFolderName,
             cacheFolderName,
             book.getFolderName(),
             book.getFolderName(),
-            bookChapter.getFileName(".nr")
+            bookChapter.getFileName("nr")
         )
         )
         return !File(path).exists()
         return !File(path).exists()
     }
     }

+ 18 - 3
app/src/main/java/io/legado/app/help/book/ContentProcessor.kt

@@ -2,6 +2,7 @@ package io.legado.app.help.book
 
 
 import com.github.liuyueyi.quick.transfer.ChineseUtils
 import com.github.liuyueyi.quick.transfer.ChineseUtils
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
+import io.legado.app.constant.AppPattern.spaceRegex
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
@@ -9,6 +10,7 @@ import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.exception.RegexTimeoutException
 import io.legado.app.exception.RegexTimeoutException
 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.utils.escapeRegex
 import io.legado.app.utils.replace
 import io.legado.app.utils.replace
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
@@ -25,7 +27,8 @@ 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(book: Book) = get(book.name, book.origin)
 
 
         fun get(bookName: String, bookOrigin: String): ContentProcessor {
         fun get(bookName: String, bookOrigin: String): ContentProcessor {
             val processorWr = processors[bookName + bookOrigin]
             val processorWr = processors[bookName + bookOrigin]
@@ -47,9 +50,11 @@ class ContentProcessor private constructor(
 
 
     private val titleReplaceRules = CopyOnWriteArrayList<ReplaceRule>()
     private val titleReplaceRules = CopyOnWriteArrayList<ReplaceRule>()
     private val contentReplaceRules = CopyOnWriteArrayList<ReplaceRule>()
     private val contentReplaceRules = CopyOnWriteArrayList<ReplaceRule>()
+    val removeSameTitleCache = hashSetOf<String>()
 
 
     init {
     init {
         upReplaceRules()
         upReplaceRules()
+        upRemoveSameTitle()
     }
     }
 
 
     fun upReplaceRules() {
     fun upReplaceRules() {
@@ -63,6 +68,15 @@ class ContentProcessor private constructor(
         }
         }
     }
     }
 
 
+    fun upRemoveSameTitle() {
+        val book = appDb.bookDao.getBookByOrigin(bookName, bookOrigin) ?: return
+        removeSameTitleCache.clear()
+        val files = BookHelp.getChapterFiles(book).filter {
+            it.endsWith("nr")
+        }
+        removeSameTitleCache.addAll(files)
+    }
+
     fun getTitleReplaceRules(): List<ReplaceRule> {
     fun getTitleReplaceRules(): List<ReplaceRule> {
         return titleReplaceRules
         return titleReplaceRules
     }
     }
@@ -85,9 +99,10 @@ class ContentProcessor private constructor(
         var sameTitleRemoved = false
         var sameTitleRemoved = false
         if (content != "null") {
         if (content != "null") {
             //去除重复标题
             //去除重复标题
-            if (enableRemoveSameTitle && BookHelp.removeSameTitle(book, chapter)) try {
+            val fileName = chapter.getFileName("nr")
+            if (!removeSameTitleCache.contains(fileName)) try {
                 val name = Pattern.quote(book.name)
                 val name = Pattern.quote(book.name)
-                var title = Pattern.quote(chapter.title)
+                var title = chapter.title.escapeRegex().replace(spaceRegex, "\\\\s*")
                 var matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*")
                 var matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*")
                     .matcher(mContent)
                     .matcher(mContent)
                 if (matcher.find()) {
                 if (matcher.find()) {

+ 2 - 0
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -54,6 +54,7 @@ object ReadBook : CoroutineScope by MainScope() {
 
 
     var preDownloadTask: Coroutine<*>? = null
     var preDownloadTask: Coroutine<*>? = null
     val downloadedChapters = hashSetOf<Int>()
     val downloadedChapters = hashSetOf<Int>()
+    var contentProcessor: ContentProcessor? = null
 
 
     //暂时保存跳转前进度
     //暂时保存跳转前进度
     fun saveCurrentBookProcess() {
     fun saveCurrentBookProcess() {
@@ -74,6 +75,7 @@ object ReadBook : CoroutineScope by MainScope() {
         readRecord.bookName = book.name
         readRecord.bookName = book.name
         readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
         readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
         chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
         chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
+        contentProcessor = ContentProcessor.get(book)
         durChapterIndex = book.durChapterIndex
         durChapterIndex = book.durChapterIndex
         durChapterPos = book.durChapterPos
         durChapterPos = book.durChapterPos
         isLocalBook = book.isLocal
         isLocalBook = book.isLocal

+ 18 - 16
app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt

@@ -11,6 +11,7 @@ import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.BookType
 import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.databinding.ActivityBookInfoEditBinding
 import io.legado.app.databinding.ActivityBookInfoEditBinding
+import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
@@ -94,22 +95,23 @@ class BookInfoEditActivity :
     }
     }
 
 
     private fun saveData() = binding.run {
     private fun saveData() = binding.run {
-        viewModel.book?.let { book ->
-            book.name = tieBookName.text?.toString() ?: ""
-            book.author = tieBookAuthor.text?.toString() ?: ""
-            val local = if (book.isLocal) BookType.local else 0
-            book.type = when (spType.selectedItemPosition) {
-                2 -> BookType.image or local
-                1 -> BookType.audio or local
-                else -> BookType.text or local
-            }
-            val customCoverUrl = tieCoverUrl.text?.toString()
-            book.customCoverUrl = if (customCoverUrl == book.coverUrl) null else customCoverUrl
-            book.customIntro = tieBookIntro.text?.toString()
-            viewModel.saveBook(book) {
-                setResult(Activity.RESULT_OK)
-                finish()
-            }
+        val book = viewModel.book ?: return@run
+        val oldBook = book.copy()
+        book.name = tieBookName.text?.toString() ?: ""
+        book.author = tieBookAuthor.text?.toString() ?: ""
+        val local = if (book.isLocal) BookType.local else 0
+        book.type = when (spType.selectedItemPosition) {
+            2 -> BookType.image or local
+            1 -> BookType.audio or local
+            else -> BookType.text or local
+        }
+        val customCoverUrl = tieCoverUrl.text?.toString()
+        book.customCoverUrl = if (customCoverUrl == book.coverUrl) null else customCoverUrl
+        book.customIntro = tieBookIntro.text?.toString()
+        BookHelp.updateCacheFolder(oldBook, book)
+        viewModel.saveBook(book) {
+            setResult(Activity.RESULT_OK)
+            finish()
         }
         }
     }
     }
 
 

+ 50 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -121,7 +121,6 @@ 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
@@ -294,6 +293,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                             item.isVisible = BuildConfig.DEBUG
                             item.isVisible = BuildConfig.DEBUG
                             item.isChecked = AppConfig.enableReview
                             item.isChecked = AppConfig.enableReview
                         }
                         }
+
                         R.id.menu_reverse_content -> item.isVisible = onLine
                         R.id.menu_reverse_content -> item.isVisible = onLine
                     }
                     }
                 }
                 }
@@ -318,6 +318,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     showDialogFragment(ChangeBookSourceDialog(it.name, it.author))
                     showDialogFragment(ChangeBookSourceDialog(it.name, it.author))
                 }
                 }
             }
             }
+
             R.id.menu_chapter_change_source -> launch {
             R.id.menu_chapter_change_source -> launch {
                 val book = ReadBook.book ?: return@launch
                 val book = ReadBook.book ?: return@launch
                 val chapter =
                 val chapter =
@@ -328,6 +329,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title)
                     ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title)
                 )
                 )
             }
             }
+
             R.id.menu_refresh,
             R.id.menu_refresh,
             R.id.menu_refresh_dur -> {
             R.id.menu_refresh_dur -> {
                 if (ReadBook.bookSource == null) {
                 if (ReadBook.bookSource == null) {
@@ -340,6 +342,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     }
                     }
                 }
                 }
             }
             }
+
             R.id.menu_refresh_after -> {
             R.id.menu_refresh_after -> {
                 if (ReadBook.bookSource == null) {
                 if (ReadBook.bookSource == null) {
                     upContent()
                     upContent()
@@ -351,6 +354,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     }
                     }
                 }
                 }
             }
             }
+
             R.id.menu_refresh_all -> {
             R.id.menu_refresh_all -> {
                 if (ReadBook.bookSource == null) {
                 if (ReadBook.bookSource == null) {
                     upContent()
                     upContent()
@@ -362,6 +366,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     }
                     }
                 }
                 }
             }
             }
+
             R.id.menu_download -> showDownloadDialog()
             R.id.menu_download -> showDownloadDialog()
             R.id.menu_add_bookmark -> addBookmark()
             R.id.menu_add_bookmark -> addBookmark()
             R.id.menu_edit_content -> showDialogFragment(ContentEditDialog())
             R.id.menu_edit_content -> showDialogFragment(ContentEditDialog())
@@ -371,28 +376,34 @@ class ReadBookActivity : BaseReadBookActivity(),
                 }
                 }
                 loadChapterList(it)
                 loadChapterList(it)
             }
             }
+
             R.id.menu_enable_replace -> changeReplaceRuleState()
             R.id.menu_enable_replace -> changeReplaceRuleState()
             R.id.menu_re_segment -> ReadBook.book?.let {
             R.id.menu_re_segment -> ReadBook.book?.let {
                 it.setReSegment(!it.getReSegment())
                 it.setReSegment(!it.getReSegment())
                 menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment()
                 menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment()
                 ReadBook.loadContent(false)
                 ReadBook.loadContent(false)
             }
             }
+
             R.id.menu_enable_review -> {
             R.id.menu_enable_review -> {
                 AppConfig.enableReview = !AppConfig.enableReview
                 AppConfig.enableReview = !AppConfig.enableReview
                 menu?.findItem(R.id.menu_enable_review)?.isChecked = AppConfig.enableReview
                 menu?.findItem(R.id.menu_enable_review)?.isChecked = AppConfig.enableReview
                 ReadBook.loadContent(false)
                 ReadBook.loadContent(false)
             }
             }
+
             R.id.menu_page_anim -> showPageAnimConfig {
             R.id.menu_page_anim -> showPageAnimConfig {
                 binding.readView.upPageAnim()
                 binding.readView.upPageAnim()
                 ReadBook.loadContent(false)
                 ReadBook.loadContent(false)
             }
             }
+
             R.id.menu_log -> showDialogFragment<AppLogDialog>()
             R.id.menu_log -> showDialogFragment<AppLogDialog>()
             R.id.menu_toc_regex -> showDialogFragment(
             R.id.menu_toc_regex -> showDialogFragment(
                 TxtTocRuleDialog(ReadBook.book?.tocUrl)
                 TxtTocRuleDialog(ReadBook.book?.tocUrl)
             )
             )
+
             R.id.menu_reverse_content -> ReadBook.book?.let {
             R.id.menu_reverse_content -> ReadBook.book?.let {
                 viewModel.reverseContent(it)
                 viewModel.reverseContent(it)
             }
             }
+
             R.id.menu_set_charset -> showCharsetConfig()
             R.id.menu_set_charset -> showCharsetConfig()
             R.id.menu_image_style -> {
             R.id.menu_image_style -> {
                 val imgStyles =
                 val imgStyles =
@@ -405,12 +416,29 @@ class ReadBookActivity : BaseReadBookActivity(),
                     ReadBook.loadContent(false)
                     ReadBook.loadContent(false)
                 }
                 }
             }
             }
+
             R.id.menu_get_progress -> ReadBook.book?.let {
             R.id.menu_get_progress -> ReadBook.book?.let {
                 viewModel.syncBookProgress(it) { progress ->
                 viewModel.syncBookProgress(it) { progress ->
                     sureSyncProgress(progress)
                     sureSyncProgress(progress)
                 }
                 }
             }
             }
-            R.id.menu_same_title_removed -> viewModel.reverseRemoveSameTitle()
+
+            R.id.menu_same_title_removed -> {
+                ReadBook.book?.let {
+                    val contentProcessor = ContentProcessor.get(it)
+                    val textChapter = ReadBook.curTextChapter
+                    if (textChapter != null
+                        && !textChapter.sameTitleRemoved
+                        && !contentProcessor.removeSameTitleCache.contains(
+                            textChapter.chapter.getFileName("nr")
+                        )
+                    ) {
+                        toastOnUi("未找到可移除的重复标题")
+                    }
+                }
+                viewModel.reverseRemoveSameTitle()
+            }
+
             R.id.menu_help -> showReadMenuHelp()
             R.id.menu_help -> showReadMenuHelp()
         }
         }
         return super.onCompatOptionsItemSelected(item)
         return super.onCompatOptionsItemSelected(item)
@@ -477,34 +505,41 @@ class ReadBookActivity : BaseReadBookActivity(),
                     return true
                     return true
                 }
                 }
             }
             }
+
             isNextKey(keyCode) -> {
             isNextKey(keyCode) -> {
                 if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
                 if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
                     binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                     binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                     return true
                     return true
                 }
                 }
             }
             }
+
             keyCode == KeyEvent.KEYCODE_VOLUME_UP -> {
             keyCode == KeyEvent.KEYCODE_VOLUME_UP -> {
                 if (volumeKeyPage(PageDirection.PREV)) {
                 if (volumeKeyPage(PageDirection.PREV)) {
                     return true
                     return true
                 }
                 }
             }
             }
+
             keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> {
             keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> {
                 if (volumeKeyPage(PageDirection.NEXT)) {
                 if (volumeKeyPage(PageDirection.NEXT)) {
                     return true
                     return true
                 }
                 }
             }
             }
+
             keyCode == KeyEvent.KEYCODE_PAGE_UP -> {
             keyCode == KeyEvent.KEYCODE_PAGE_UP -> {
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV)
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV)
                 return true
                 return true
             }
             }
+
             keyCode == KeyEvent.KEYCODE_PAGE_DOWN -> {
             keyCode == KeyEvent.KEYCODE_PAGE_DOWN -> {
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                 return true
                 return true
             }
             }
+
             keyCode == KeyEvent.KEYCODE_SPACE -> {
             keyCode == KeyEvent.KEYCODE_SPACE -> {
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                 binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
                 return true
                 return true
             }
             }
+
             keyCode == KeyEvent.KEYCODE_BACK -> {
             keyCode == KeyEvent.KEYCODE_BACK -> {
                 if (isShowingSearchResult) {
                 if (isShowingSearchResult) {
                     exitSearchMenu()
                     exitSearchMenu()
@@ -544,6 +579,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     return true
                     return true
                 }
                 }
             }
             }
+
             KeyEvent.KEYCODE_BACK -> {
             KeyEvent.KEYCODE_BACK -> {
                 event?.let {
                 event?.let {
                     if ((event.flags and KeyEvent.FLAG_CANCELED_LONG_PRESS == 0)
                     if ((event.flags and KeyEvent.FLAG_CANCELED_LONG_PRESS == 0)
@@ -585,12 +621,14 @@ class ReadBookActivity : BaseReadBookActivity(),
                         event.rawX + cursorLeft.width,
                         event.rawX + cursorLeft.width,
                         event.rawY - cursorLeft.height
                         event.rawY - cursorLeft.height
                     )
                     )
+
                     R.id.cursor_right -> readView.curPage.selectEndMove(
                     R.id.cursor_right -> readView.curPage.selectEndMove(
                         event.rawX - cursorRight.width,
                         event.rawX - cursorRight.width,
                         event.rawY - cursorRight.height
                         event.rawY - cursorRight.height
                     )
                     )
                 }
                 }
             }
             }
+
             MotionEvent.ACTION_UP -> showTextActionMenu()
             MotionEvent.ACTION_UP -> showTextActionMenu()
         }
         }
         return true
         return true
@@ -657,6 +695,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                 1 -> binding.readView.aloudStartSelect()
                 1 -> binding.readView.aloudStartSelect()
                 else -> speak(binding.readView.getSelectText())
                 else -> speak(binding.readView.getSelectText())
             }
             }
+
             R.id.menu_bookmark -> binding.readView.curPage.let {
             R.id.menu_bookmark -> binding.readView.curPage.let {
                 val bookmark = it.createBookmark()
                 val bookmark = it.createBookmark()
                 if (bookmark == null) {
                 if (bookmark == null) {
@@ -666,6 +705,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                 }
                 }
                 return true
                 return true
             }
             }
+
             R.id.menu_replace -> {
             R.id.menu_replace -> {
                 val scopes = arrayListOf<String>()
                 val scopes = arrayListOf<String>()
                 ReadBook.book?.name?.let {
                 ReadBook.book?.name?.let {
@@ -683,11 +723,13 @@ class ReadBookActivity : BaseReadBookActivity(),
                 )
                 )
                 return true
                 return true
             }
             }
+
             R.id.menu_search_content -> {
             R.id.menu_search_content -> {
                 viewModel.searchContentQuery = selectedText
                 viewModel.searchContentQuery = selectedText
                 openSearchActivity(selectedText)
                 openSearchActivity(selectedText)
                 return true
                 return true
             }
             }
+
             R.id.menu_dict -> {
             R.id.menu_dict -> {
                 showDialogFragment(DictDialog(selectedText))
                 showDialogFragment(DictDialog(selectedText))
                 return true
                 return true
@@ -1010,7 +1052,6 @@ 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
@@ -1110,6 +1151,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                 ReadAloud.upReadAloudClass()
                 ReadAloud.upReadAloudClass()
                 ReadBook.readAloud()
                 ReadBook.readAloud()
             }
             }
+
             BaseReadAloudService.pause -> ReadAloud.resume(this)
             BaseReadAloudService.pause -> ReadAloud.resume(this)
             else -> ReadAloud.pause(this)
             else -> ReadAloud.pause(this)
         }
         }
@@ -1142,6 +1184,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                         viewModel.saveImage(src, Uri.parse(path))
                         viewModel.saveImage(src, Uri.parse(path))
                     }
                     }
                 }
                 }
+
                 "selectFolder" -> selectImageDir.launch()
                 "selectFolder" -> selectImageDir.launch()
             }
             }
             popupAction.dismiss()
             popupAction.dismiss()
@@ -1164,15 +1207,18 @@ class ReadBookActivity : BaseReadBookActivity(),
                 setCurTextColor(color)
                 setCurTextColor(color)
                 postEvent(EventBus.UP_CONFIG, false)
                 postEvent(EventBus.UP_CONFIG, false)
             }
             }
+
             BG_COLOR -> {
             BG_COLOR -> {
                 setCurBg(0, "#${color.hexString}")
                 setCurBg(0, "#${color.hexString}")
                 postEvent(EventBus.UP_CONFIG, false)
                 postEvent(EventBus.UP_CONFIG, false)
             }
             }
+
             TIP_COLOR -> {
             TIP_COLOR -> {
                 ReadTipConfig.tipColor = color
                 ReadTipConfig.tipColor = color
                 postEvent(EventBus.TIP_COLOR, "")
                 postEvent(EventBus.TIP_COLOR, "")
                 postEvent(EventBus.UP_CONFIG, true)
                 postEvent(EventBus.UP_CONFIG, true)
             }
             }
+
             TIP_DIVIDER_COLOR -> {
             TIP_DIVIDER_COLOR -> {
                 ReadTipConfig.tipDividerColor = color
                 ReadTipConfig.tipDividerColor = color
                 postEvent(EventBus.TIP_COLOR, "")
                 postEvent(EventBus.TIP_COLOR, "")
@@ -1233,6 +1279,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                         lineIndex,
                         lineIndex,
                         charIndex + viewModel.searchContentQuery.length - 1
                         charIndex + viewModel.searchContentQuery.length - 1
                     )
                     )
+
                     1 -> binding.readView.curPage.selectEndMoveIndex(
                     1 -> binding.readView.curPage.selectEndMoveIndex(
                         0, lineIndex + 1, charIndex2
                         0, lineIndex + 1, charIndex2
                     )
                     )

+ 7 - 8
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -155,6 +155,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
                     is SecurityException, is FileNotFoundException -> {
                     is SecurityException, is FileNotFoundException -> {
                         permissionDenialLiveData.postValue(1)
                         permissionDenialLiveData.postValue(1)
                     }
                     }
+
                     else -> {
                     else -> {
                         AppLog.put("LoadTocError:${it.localizedMessage}", it)
                         AppLog.put("LoadTocError:${it.localizedMessage}", it)
                         ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
                         ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
@@ -434,14 +435,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
      */
      */
     fun reverseRemoveSameTitle() {
     fun reverseRemoveSameTitle() {
         execute {
         execute {
-            val book = ReadBook.book
-            val textChapter = ReadBook.curTextChapter
-            if (book != null && textChapter != null) {
-                BookHelp.setRemoveSameTitle(
-                    book, textChapter.chapter, !textChapter.sameTitleRemoved
-                )
-                ReadBook.loadContent(ReadBook.durChapterIndex)
-            }
+            val book = ReadBook.book ?: return@execute
+            val textChapter = ReadBook.curTextChapter ?: return@execute
+            BookHelp.setRemoveSameTitle(
+                book, textChapter.chapter, !textChapter.sameTitleRemoved
+            )
+            ReadBook.loadContent(ReadBook.durChapterIndex)
         }
         }
     }
     }
 
 

+ 21 - 20
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt

@@ -27,8 +27,10 @@ import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.utils.*
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
@@ -47,6 +49,7 @@ class SearchContentActivity :
     }
     }
     private var durChapterIndex = 0
     private var durChapterIndex = 0
     private var searchJob: Job? = null
     private var searchJob: Job? = null
+    private var initJob: Deferred<*>? = null
 
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         val bbg = bottomBackground
         val bbg = bottomBackground
@@ -56,38 +59,37 @@ class SearchContentActivity :
         binding.ivSearchContentTop.setColorFilter(btc)
         binding.ivSearchContentTop.setColorFilter(btc)
         binding.ivSearchContentBottom.setColorFilter(btc)
         binding.ivSearchContentBottom.setColorFilter(btc)
         val searchResultList = IntentData.get<List<SearchResult>>("searchResultList")
         val searchResultList = IntentData.get<List<SearchResult>>("searchResultList")
+        val position = intent.getIntExtra("searchResultIndex", 0)
         val noSearchResult = searchResultList == null
         val noSearchResult = searchResultList == null
         initSearchView(!noSearchResult)
         initSearchView(!noSearchResult)
         initRecyclerView()
         initRecyclerView()
         initView()
         initView()
-        intent.getStringExtra("bookUrl")?.let { bookUrl ->
-            viewModel.initBook(bookUrl) {
-                searchResultList?.let {
-                    viewModel.searchResultList.addAll(it)
-                    viewModel.searchResultCounts = it.size
-                    adapter.setItems(it)
-                    val position = intent.getIntExtra("searchResultIndex", 0)
-                    binding.recyclerView.scrollToPosition(position)
-                }
-                initBook(noSearchResult)
-            }
+        val bookUrl = intent.getStringExtra("bookUrl") ?: return
+        viewModel.initBook(bookUrl) {
+            initSearchResultList(searchResultList, position)
+            initBook(noSearchResult)
         }
         }
     }
     }
 
 
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
         if (ev.action == MotionEvent.ACTION_DOWN) {
         if (ev.action == MotionEvent.ACTION_DOWN) {
-            searchView.post {
-                currentFocus?.let {
-                    if (it is EditText) {
-                        it.clearFocus()
-                        it.hideSoftInput()
-                    }
+            currentFocus?.let {
+                if (it.shouldHideSoftInput(ev)) {
+                    it.hideSoftInput()
                 }
                 }
             }
             }
         }
         }
         return super.dispatchTouchEvent(ev)
         return super.dispatchTouchEvent(ev)
     }
     }
 
 
+    private fun initSearchResultList(list: List<SearchResult>?, position: Int) {
+        list ?: return
+        viewModel.searchResultList.addAll(list)
+        viewModel.searchResultCounts = list.size
+        adapter.setItems(list)
+        binding.recyclerView.scrollToPosition(position)
+    }
+
     private fun initSearchView(clearFocus: Boolean) {
     private fun initSearchView(clearFocus: Boolean) {
         searchView.applyTint(primaryTextColor)
         searchView.applyTint(primaryTextColor)
         searchView.onActionViewExpanded()
         searchView.onActionViewExpanded()
@@ -149,7 +151,7 @@ class SearchContentActivity :
     }
     }
 
 
     private fun initCacheFileNames(book: Book) {
     private fun initCacheFileNames(book: Book) {
-        launch {
+        initJob = async {
             withContext(IO) {
             withContext(IO) {
                 viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book))
                 viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book))
             }
             }
@@ -179,8 +181,8 @@ class SearchContentActivity :
         viewModel.lastQuery = query
         viewModel.lastQuery = query
         binding.refreshProgressBar.isAutoLoading = true
         binding.refreshProgressBar.isAutoLoading = true
         binding.fbStop.visible()
         binding.fbStop.visible()
-        ContentProcessor.enableRemoveSameTitle = false
         searchJob = launch(IO) {
         searchJob = launch(IO) {
+            initJob?.await()
             kotlin.runCatching {
             kotlin.runCatching {
                 appDb.bookChapterDao.getChapterList(viewModel.bookUrl).forEach { bookChapter ->
                 appDb.bookChapterDao.getChapterList(viewModel.bookUrl).forEach { bookChapter ->
                     ensureActive()
                     ensureActive()
@@ -211,7 +213,6 @@ class SearchContentActivity :
             }.onFailure {
             }.onFailure {
                 AppLog.put("全文搜索出错\n${it.localizedMessage}", it)
                 AppLog.put("全文搜索出错\n${it.localizedMessage}", it)
             }
             }
-            ContentProcessor.enableRemoveSameTitle = true
             binding.tvCurrentSearchInfo.post {
             binding.tvCurrentSearchInfo.post {
                 binding.fbStop.invisible()
                 binding.fbStop.invisible()
                 binding.refreshProgressBar.isAutoLoading = false
                 binding.refreshProgressBar.isAutoLoading = false

+ 2 - 3
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt

@@ -18,7 +18,7 @@ import kotlin.coroutines.coroutineContext
 class SearchContentViewModel(application: Application) : BaseViewModel(application) {
 class SearchContentViewModel(application: Application) : BaseViewModel(application) {
     var bookUrl: String = ""
     var bookUrl: String = ""
     var book: Book? = null
     var book: Book? = null
-    private var contentProcessor: ContentProcessor? = null
+    var contentProcessor: ContentProcessor? = null
     var lastQuery: String = ""
     var lastQuery: String = ""
     var searchResultCounts = 0
     var searchResultCounts = 0
     val cacheChapterNames = hashSetOf<String>()
     val cacheChapterNames = hashSetOf<String>()
@@ -38,10 +38,9 @@ class SearchContentViewModel(application: Application) : BaseViewModel(applicati
 
 
     suspend fun searchChapter(
     suspend fun searchChapter(
         query: String,
         query: String,
-        chapter: BookChapter?
+        chapter: BookChapter
     ): List<SearchResult> {
     ): List<SearchResult> {
         val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()
         val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()
-        chapter ?: return searchResultsWithinChapter
         val book = book ?: return searchResultsWithinChapter
         val book = book ?: return searchResultsWithinChapter
         val chapterContent = BookHelp.getContent(book, chapter) ?: return searchResultsWithinChapter
         val chapterContent = BookHelp.getContent(book, chapter) ?: return searchResultsWithinChapter
         coroutineContext.ensureActive()
         coroutineContext.ensureActive()

+ 5 - 0
app/src/main/java/io/legado/app/utils/StringExtensions.kt

@@ -7,6 +7,7 @@ import android.icu.text.Collator
 import android.icu.util.ULocale
 import android.icu.util.ULocale
 import android.net.Uri
 import android.net.Uri
 import android.text.Editable
 import android.text.Editable
+import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern.dataUriRegex
 import io.legado.app.constant.AppPattern.dataUriRegex
 import java.io.File
 import java.io.File
 import java.lang.Character.codePointCount
 import java.lang.Character.codePointCount
@@ -127,3 +128,7 @@ fun CharSequence.toStringArray(): Array<String> {
     }
     }
 }
 }
 
 
+fun String.escapeRegex(): String {
+    return replace(AppPattern.regexCharRegex, "\\\\$0")
+}
+

+ 15 - 1
app/src/main/java/io/legado/app/utils/ViewExtensions.kt

@@ -8,6 +8,7 @@ import android.graphics.Bitmap
 import android.graphics.Canvas
 import android.graphics.Canvas
 import android.os.Build
 import android.os.Build
 import android.text.Html
 import android.text.Html
+import android.view.MotionEvent
 import android.view.View
 import android.view.View
 import android.view.View.*
 import android.view.View.*
 import android.view.inputmethod.InputMethodManager
 import android.view.inputmethod.InputMethodManager
@@ -195,4 +196,17 @@ fun PopupMenu.show(x: Int, y: Int) {
     }.onFailure {
     }.onFailure {
         it.printOnDebug()
         it.printOnDebug()
     }
     }
-}
+}
+
+fun View.shouldHideSoftInput(event: MotionEvent): Boolean {
+    if (this is EditText) {
+        val l = intArrayOf(0, 0)
+        getLocationInWindow(l)
+        val left = l[0]
+        val top = l[1]
+        val bottom = top + getHeight()
+        val right = left + getWidth()
+        return !(event.x > left && event.x < right && event.y > top && event.y < bottom)
+    }
+    return false
+}