Kaynağa Gözat

同步上游

HapeLee 11 ay önce
ebeveyn
işleme
146b59dc96
28 değiştirilmiş dosya ile 190 ekleme ve 82 silme
  1. 2 0
      app/src/main/java/io/legado/app/App.kt
  2. 1 0
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  3. 28 18
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  4. 2 1
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  5. 1 1
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  6. 2 2
      app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt
  7. 74 25
      app/src/main/java/io/legado/app/model/ReadBook.kt
  8. 3 1
      app/src/main/java/io/legado/app/model/ReadManga.kt
  9. 1 0
      app/src/main/java/io/legado/app/service/ExportBookService.kt
  10. 11 3
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  11. 2 4
      app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt
  12. 1 1
      app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt
  13. 23 9
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  14. 1 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  15. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  16. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  17. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  18. 7 4
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
  19. 2 0
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  20. 3 0
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  21. 2 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  22. 9 3
      app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt
  23. 4 0
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  24. 1 0
      app/src/main/res/values-zh-rHK/strings.xml
  25. 1 0
      app/src/main/res/values-zh-rTW/strings.xml
  26. 1 0
      app/src/main/res/values-zh/strings.xml
  27. 1 0
      app/src/main/res/values/strings.xml
  28. 1 1
      gradle/libs.versions.toml

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

@@ -36,6 +36,7 @@ import io.legado.app.help.AppFreezeMonitor
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.CrashHandler
 import io.legado.app.help.DefaultData
+import io.legado.app.help.DispatchersMonitor
 import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.book.BookHelp
@@ -129,6 +130,7 @@ class App : Application() {
                 .setLogger(EventLogger())
             DefaultData.upVersion()
             AppFreezeMonitor.init(this@App)
+            DispatchersMonitor.init()
             URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient))
             launch { installGmsTlsProvider(appCtx) }
             initRhino()

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

@@ -29,6 +29,7 @@ object AppPattern {
     val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
     val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著")
     val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
+    val fileNameRegex2 = Regex("[\\\\/:*?\"<>|]")
     val splitGroupRegex = Regex("[,;,;]")
     val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)")
 

+ 28 - 18
app/src/main/java/io/legado/app/help/AppWebDav.kt

@@ -25,7 +25,9 @@ import io.legado.app.utils.compress.ZipUtils
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.isJson
+import io.legado.app.utils.normalizeFileName
 import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.runBlocking
 import splitties.init.appCtx
@@ -243,7 +245,7 @@ object AppWebDav {
                 WebDav(putUrl, it).upload(byteArray, "text/plain")
             }
         } catch (e: Exception) {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true)
         }
     }
@@ -257,12 +259,16 @@ object AppWebDav {
                 WebDav(putUrl, it).upload(uri, "text/plain")
             }
         } catch (e: Exception) {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true)
         }
     }
 
-    suspend fun uploadBookProgress(book: Book) {
+    suspend fun uploadBookProgress(
+        book: Book,
+        toast: Boolean = false,
+        onSuccess: (() -> Unit)? = null
+    ) {
         val authorization = authorization ?: return
         if (!AppConfig.syncBookProgress) return
         if (!NetworkUtils.isAvailable()) return
@@ -272,9 +278,10 @@ object AppWebDav {
             val url = getProgressUrl(book.name, book.author)
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             book.syncTime = System.currentTimeMillis()
+            onSuccess?.invoke()
         } catch (e: Exception) {
-            coroutineContext.ensureActive()
-            AppLog.put("上传进度失败\n${e.localizedMessage}", e)
+            currentCoroutineContext().ensureActive()
+            AppLog.put("上传进度失败\n${e.localizedMessage}", e, toast)
         }
     }
 
@@ -288,7 +295,7 @@ object AppWebDav {
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             onSuccess?.invoke()
         } catch (e: Exception) {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("上传进度失败\n${e.localizedMessage}", e)
         }
     }
@@ -298,26 +305,29 @@ object AppWebDav {
     }
 
     private fun getProgressFileName(name: String, author: String): String {
-        return UrlUtil.replaceReservedChar("${name}_${author}") + ".json"
+        return UrlUtil.replaceReservedChar("${name}_${author}".normalizeFileName()) + ".json"
     }
 
     /**
      * 获取书籍进度
      */
     suspend fun getBookProgress(book: Book): BookProgress? {
-        authorization?.let {
-            val url = getProgressUrl(book.name, book.author)
-            kotlin.runCatching {
-                WebDav(url, it).download().let { byteArray ->
-                    val json = String(byteArray)
-                    if (json.isJson()) {
-                        return GSON.fromJsonObject<BookProgress>(json).getOrNull()
-                    }
+        val url = getProgressUrl(book.name, book.author)
+        kotlin.runCatching {
+            val authorization = authorization ?: return null
+            WebDav(url, authorization).download().let { byteArray ->
+                val json = String(byteArray)
+                if (json.isJson()) {
+                    return GSON.fromJsonObject<BookProgress>(json).getOrNull()
+
                 }
-            }.onFailure {
-                coroutineContext.ensureActive()
-                AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it)
+
+
+
             }
+        }.onFailure {
+            currentCoroutineContext().ensureActive()
+            AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it)
         }
         return null
     }

+ 2 - 1
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap
 import kotlin.math.max
 import kotlin.math.min
 import androidx.core.net.toUri
+import io.legado.app.utils.normalizeFileName
 
 
 val Book.isAudio: Boolean
@@ -376,7 +377,7 @@ fun Book.getExportFileName(
         RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix
     }.onFailure {
         AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
-    }.getOrDefault(default)
+    }.getOrDefault(default).normalizeFileName()
 }
 
 // 根据当前日期计算章节总数

+ 1 - 1
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -77,7 +77,7 @@ object Backup {
             "backup${backupDate}-${deviceName}.zip"
         } else {
             "backup${backupDate}.zip"
-        }
+        }.normalizeFileName()
     }
 
     private fun shouldBackup(): Boolean {

+ 2 - 2
app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt

@@ -142,7 +142,7 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
     }
 
     override fun build(): AlertDialog {
-        var dialog = builder.create()
+        val dialog = builder.create()
         if (AppConfig.isEInkMode) {
             dialog.window?.run {
                 val attr = attributes
@@ -156,7 +156,7 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
     }
 
     override fun show(): AlertDialog {
-        var dialog = builder.show()
+        val dialog = builder.show()
         if (AppConfig.isEInkMode) {
             dialog.window?.run {
                 val attr = attributes

+ 74 - 25
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -34,7 +34,9 @@ import io.legado.app.utils.postEvent
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.SupervisorJob
@@ -44,9 +46,13 @@ import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.coroutines.cancellation.CancellationException
 import kotlin.math.max
 import kotlin.math.min
 
@@ -69,6 +75,10 @@ object ReadBook : CoroutineScope by MainScope() {
     var msg: String? = null
     private val loadingChapters = arrayListOf<Int>()
     private val readRecord = ReadRecord()
+    private val chapterLoadingJobs = ConcurrentHashMap<Int, Coroutine<*>>()
+    private val prevChapterLoadingLock = Mutex()
+    private val curChapterLoadingLock = Mutex()
+    private val nextChapterLoadingLock = Mutex()
     var readStartTime: Long = System.currentTimeMillis()
 
     /* 跳转进度前进度记录 */
@@ -210,9 +220,7 @@ object ReadBook : CoroutineScope by MainScope() {
     }
 
     fun clearTextChapter() {
-        prevTextChapter?.cancelLayout()
-        curTextChapter?.cancelLayout()
-        nextTextChapter?.cancelLayout()
+        clearExpiredChapterLoadingJob(true)
         prevTextChapter = null
         curTextChapter = null
         nextTextChapter = null
@@ -224,13 +232,14 @@ object ReadBook : CoroutineScope by MainScope() {
         nextTextChapter?.clearSearchResult()
     }
 
-    fun uploadProgress(successAction: (() -> Unit)? = null) {
+    fun uploadProgress(toast: Boolean = false, successAction: (() -> Unit)? = null) {
         book?.let {
             launch(IO) {
-                AppWebDav.uploadBookProgress(it)
+                AppWebDav.uploadBookProgress(it, toast) {
+                    successAction?.invoke()
+                }
                 ensureActive()
                 it.update()
-                successAction?.invoke()
             }
         }
     }
@@ -324,7 +333,7 @@ object ReadBook : CoroutineScope by MainScope() {
         if (durChapterIndex < simulatedChapterSize - 1) {
             durChapterPos = 0
             durChapterIndex++
-            prevTextChapter?.cancelLayout()
+            clearExpiredChapterLoadingJob()
             prevTextChapter = curTextChapter
             curTextChapter = nextTextChapter
             nextTextChapter = null
@@ -355,7 +364,7 @@ object ReadBook : CoroutineScope by MainScope() {
         if (durChapterIndex < simulatedChapterSize - 1) {
             durChapterPos = 0
             durChapterIndex++
-            prevTextChapter?.cancelLayout()
+            clearExpiredChapterLoadingJob()
             prevTextChapter = curTextChapter
             curTextChapter = nextTextChapter
             nextTextChapter = null
@@ -387,7 +396,7 @@ object ReadBook : CoroutineScope by MainScope() {
         if (durChapterIndex > 0) {
             durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
             durChapterIndex--
-            nextTextChapter?.cancelLayout()
+            clearExpiredChapterLoadingJob()
             nextTextChapter = curTextChapter
             curTextChapter = prevTextChapter
             prevTextChapter = null
@@ -438,10 +447,15 @@ object ReadBook : CoroutineScope by MainScope() {
         }
     }
 
-    fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
+    fun openChapter(
+        index: Int,
+        durChapterPos: Int = 0,
+        upContent: Boolean = true,
+        success: (() -> Unit)? = null
+    ) {
         if (index < chapterSize) {
             clearTextChapter()
-            callBack?.upContent()
+            if (upContent) callBack?.upContent()
             durChapterIndex = index
             ReadBook.durChapterPos = durChapterPos
             saveRead()
@@ -671,6 +685,7 @@ object ReadBook : CoroutineScope by MainScope() {
     /**
      * 内容加载完成
      */
+    @Synchronized
     fun contentLoadFinish(
         book: Book,
         chapter: BookChapter,
@@ -684,7 +699,8 @@ object ReadBook : CoroutineScope by MainScope() {
         if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
             return
         }
-        Coroutine.async {
+        chapterLoadingJobs[chapter.index]?.cancel()
+        val job = Coroutine.async(this, start = CoroutineStart.LAZY) {
             val contentProcessor = ContentProcessor.get(book.name, book.origin)
             val displayTitle = chapter.getDisplayTitle(
                 contentProcessor.getTitleReplaceRules(),
@@ -692,13 +708,16 @@ object ReadBook : CoroutineScope by MainScope() {
             )
             val contents = contentProcessor
                 .getContent(book, chapter, content, includeTitle = false)
+            ensureActive()
             val textChapter = ChapterProvider.getTextChapterAsync(
-                this@ReadBook, book, chapter, displayTitle, contents, simulatedChapterSize
+                this, book, chapter, displayTitle, contents, simulatedChapterSize
             )
             when (val offset = chapter.index - durChapterIndex) {
-                0 -> {
-                    curTextChapter?.cancelLayout()
-                    curTextChapter = textChapter
+                0 -> curChapterLoadingLock.withLock {
+                    withContext(Main) {
+                        ensureActive()
+                        curTextChapter = textChapter
+                    }
                     callBack?.upMenuView()
                     var available = false
                     for (page in textChapter.layoutChannel) {
@@ -721,16 +740,20 @@ object ReadBook : CoroutineScope by MainScope() {
                     callBack?.contentLoadFinish()
                 }
 
-                -1 -> {
-                    prevTextChapter?.cancelLayout()
-                    prevTextChapter = textChapter
+                -1 -> prevChapterLoadingLock.withLock {
+                    withContext(Main) {
+                        ensureActive()
+                        prevTextChapter = textChapter
+                    }
                     textChapter.layoutChannel.receiveAsFlow().collect()
                     if (upContent) callBack?.upContent(offset, resetPageOffset)
                 }
 
-                1 -> {
-                    nextTextChapter?.cancelLayout()
-                    nextTextChapter = textChapter
+                1 -> nextChapterLoadingLock.withLock {
+                    withContext(Main) {
+                        ensureActive()
+                        nextTextChapter = textChapter
+                    }
                     for (page in textChapter.layoutChannel) {
                         if (page.index > 1) {
                             continue
@@ -742,11 +765,16 @@ object ReadBook : CoroutineScope by MainScope() {
 
             return@async
         }.onError {
+            if (it is CancellationException) {
+                return@onError
+            }
             AppLog.put("ChapterProvider ERROR", it)
             appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
         }.onSuccess {
             success?.invoke()
         }
+        chapterLoadingJobs[chapter.index] = job
+        job.start()
     }
 
     suspend fun contentLoadFinishAwait(
@@ -774,7 +802,9 @@ object ReadBook : CoroutineScope by MainScope() {
             when (val offset = chapter.index - durChapterIndex) {
                 0 -> {
                     curTextChapter?.cancelLayout()
-                    curTextChapter = textChapter
+                    withContext(Main) {
+                        curTextChapter = textChapter
+                    }
                     callBack?.upMenuView()
                     var available = false
                     for (page in textChapter.layoutChannel) {
@@ -799,14 +829,18 @@ object ReadBook : CoroutineScope by MainScope() {
 
                 -1 -> {
                     prevTextChapter?.cancelLayout()
-                    prevTextChapter = textChapter
+                    withContext(Main) {
+                        prevTextChapter = textChapter
+                    }
                     textChapter.layoutChannel.receiveAsFlow().collect()
                     if (upContent) callBack?.upContent(offset, resetPageOffset)
                 }
 
                 1 -> {
                     nextTextChapter?.cancelLayout()
-                    nextTextChapter = textChapter
+                    withContext(Main) {
+                        nextTextChapter = textChapter
+                    }
                     for (page in textChapter.layoutChannel) {
                         if (page.index > 1) {
                             continue
@@ -818,6 +852,9 @@ object ReadBook : CoroutineScope by MainScope() {
 
             return
         }.onFailure {
+            if (it is CancellationException) {
+                return@onFailure
+            }
             AppLog.put("ChapterProvider ERROR", it)
             appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
         }
@@ -913,6 +950,17 @@ object ReadBook : CoroutineScope by MainScope() {
         }
     }
 
+    private fun clearExpiredChapterLoadingJob(clearAll: Boolean = false) {
+        val iterator = chapterLoadingJobs.iterator()
+        while (iterator.hasNext()) {
+            val (index, job) = iterator.next()
+            if (clearAll || index !in durChapterIndex - 1..durChapterIndex + 1) {
+                job.cancel()
+                iterator.remove()
+            }
+        }
+    }
+
     fun cancelPreDownloadTask() {
         if (contentLoadFinish) {
             preDownloadTask?.cancel()
@@ -956,6 +1004,7 @@ object ReadBook : CoroutineScope by MainScope() {
         downloadScope.coroutineContext.cancelChildren()
         coroutineContext.cancelChildren()
         ImageProvider.clear()
+        clearExpiredChapterLoadingJob(true)
         if (!CacheBookService.isRun) {
             CacheBook.close()
         }

+ 3 - 1
app/src/main/java/io/legado/app/model/ReadManga.kt

@@ -482,7 +482,9 @@ object ReadManga : CoroutineScope by MainScope() {
     fun uploadProgress(successAction: (() -> Unit)? = null) {
         book?.let {
             launch(IO) {
-                AppWebDav.uploadBookProgress(it)
+                AppWebDav.uploadBookProgress(it) {
+                    successAction?.invoke()
+                }
                 ensureActive()
                 it.update()
                 successAction?.invoke()

+ 1 - 0
app/src/main/java/io/legado/app/service/ExportBookService.kt

@@ -172,6 +172,7 @@ class ExportBookService : BaseService() {
                 val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let {
                     notificationContentText = "导出完成"
                     upExportNotification(true)
+                    stopSelf()
                     return@launch
                 }
                 exportProgress[bookUrl] = 0

+ 11 - 3
app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt

@@ -171,7 +171,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         when (item.itemId) {
             R.id.menu_download,
             R.id.menu_download_after -> {
-                if (!CacheBook.isRun) {
+                if (!CacheBook.isRun) sureCacheBook {
                     adapter.getItems().forEach { book ->
                         val indices = (book.durChapterIndex..book.lastChapterIndex).toList()
                         CacheBook.start(this@CacheActivity, book, indices)
@@ -182,7 +182,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
             }
 
             R.id.menu_download_all -> {
-                if (!CacheBook.isRun) {
+                if (!CacheBook.isRun) sureCacheBook {
                     adapter.getItems().forEach { book ->
                         val indices = (0..book.lastChapterIndex).toList()
                         CacheBook.start(this@CacheActivity, book, indices)
@@ -546,7 +546,15 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
         }
     }
 
-
+    private fun sureCacheBook(action: () -> Unit) {
+        alert(R.string.draw) {
+            setMessage(R.string.sure_cache_book)
+            noButton()
+            yesButton {
+                action.invoke()
+            }
+        }
+    }
 
     private fun getTypeName(): String {
         return exportTypes.getOrElse(AppConfig.exportType) {

+ 2 - 4
app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt

@@ -187,6 +187,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
             binding.llRetry.isGone = true
             ReadManga.loadOrUpContent()
         }
+        binding.flLoading.isVisible = !AppConfig.isEInkMode
         mAdapter.addFooterView {
             ViewLoadMoreBinding.bind(loadMoreView)
         }
@@ -313,10 +314,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
 
     override fun onPostCreate(savedInstanceState: Bundle?) {
         super.onPostCreate(savedInstanceState)
-//        Looper.myQueue().addIdleHandler {
-//            viewModel.initData(intent)
-//            false
-//        }
+        viewModel.initData(intent)
         justInitData = true
     }
 

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt

@@ -19,7 +19,7 @@ import java.security.MessageDigest
  *                  仅当applyBinarization为true时有效。
  */
 class EpaperTransformation(
-    @IntRange(0, 255) private val threshold: Int = 128,
+    @param:IntRange(0, 255) private val threshold: Int = 128,
 ) : BitmapTransformation() {
 
     private val ID get() = "io.legado.app.model.EpaperTransformation.$threshold"

+ 23 - 9
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -671,7 +671,7 @@ class ReadBookActivity : BaseReadBookActivity(),
             }
 
             R.id.menu_cover_progress -> ReadBook.book?.let {
-                ReadBook.uploadProgress { toastOnUi(R.string.upload_book_success) }
+                ReadBook.uploadProgress(true) { toastOnUi(R.string.upload_book_success) }
             }
 
             R.id.menu_same_title_removed -> {
@@ -1415,10 +1415,17 @@ class ReadBookActivity : BaseReadBookActivity(),
                 ReadAloud.upReadAloudClass()
                 val scrollPageAnim = ReadBook.pageAnim() == 3
                 if (scrollPageAnim) {
-                    val line = binding.readView.getCurVisibleFirstLine()
-                    if (line != null) {
-                        ReadBook.durChapterPos = line.chapterPosition
-                        ReadBook.readAloud(startPos = line.pagePosition)
+                    val pos = binding.readView.getReadAloudPos()
+                    if (pos != null) {
+                        val (index, line) = pos
+                        if (ReadBook.durChapterIndex != index) {
+                            ReadBook.openChapter(index, line.chapterPosition, false) {
+                                ReadBook.readAloud(startPos = line.pagePosition)
+                            }
+                        } else {
+                            ReadBook.durChapterPos = line.chapterPosition
+                            ReadBook.readAloud(startPos = line.pagePosition)
+                        }
                     } else {
                         ReadBook.readAloud()
                     }
@@ -1431,10 +1438,17 @@ class ReadBookActivity : BaseReadBookActivity(),
                 val scrollPageAnim = ReadBook.pageAnim() == 3
                 if (scrollPageAnim && pageChanged) {
                     pageChanged = false
-                    val line = binding.readView.getCurVisibleFirstLine()
-                    if (line != null) {
-                        ReadBook.durChapterPos = line.chapterPosition
-                        ReadBook.readAloud(startPos = line.pagePosition)
+                    val pos = binding.readView.getReadAloudPos()
+                    if (pos != null) {
+                        val (index, line) = pos
+                        if (ReadBook.durChapterIndex != index) {
+                            ReadBook.openChapter(index, line.chapterPosition, false) {
+                                ReadBook.readAloud(startPos = line.pagePosition)
+                            }
+                        } else {
+                            ReadBook.durChapterPos = line.chapterPosition
+                            ReadBook.readAloud(startPos = line.pagePosition)
+                        }
                     } else {
                         ReadBook.readAloud()
                     }

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

@@ -321,7 +321,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     }
 
     fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
-        ReadBook.openChapter(index, durChapterPos, success)
+        ReadBook.openChapter(index, durChapterPos, success = success)
     }
 
     fun removeFromBookshelf(success: (() -> Unit)?) {

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -459,7 +459,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         return visiblePage
     }
 
-    fun getCurVisibleFirstLine(): TextLine? {
+    fun getReadAloudPos(): Pair<Int, TextLine>? {
         var relativeOffset: Float
         for (relativePos in 0..2) {
             relativeOffset = relativeOffset(relativePos)
@@ -477,7 +477,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                         lineTop += relativeOffset
                         lineBottom += relativeOffset
                     }
-                    return visibleLine
+                    return textPage.chapterIndex to visibleLine
                 }
             }
         }

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -496,8 +496,8 @@ class PageView(context: Context) : FrameLayout(context) {
         return binding.contentTextView.getCurVisiblePage()
     }
 
-    fun getCurVisibleFirstLine(): TextLine? {
-        return binding.contentTextView.getCurVisibleFirstLine()
+    fun getReadAloudPos(): Pair<Int, TextLine>? {
+        return binding.contentTextView.getReadAloudPos()
     }
 
     fun markAsMainView() {

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -673,8 +673,8 @@ class ReadView(context: Context, attrs: AttributeSet) :
         return curPage.getCurVisiblePage()
     }
 
-    fun getCurVisibleFirstLine(): TextLine? {
-        return curPage.getCurVisibleFirstLine()
+    fun getReadAloudPos(): Pair<Int, TextLine>? {
+        return curPage.getReadAloudPos()
     }
 
     fun invalidateTextPage() {

+ 7 - 4
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -34,6 +34,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
 import java.util.LinkedList
@@ -120,6 +121,8 @@ class TextChapterLayout(
         }.onError {
             exception = it
             onException(it)
+        }.onCancel {
+            channel.cancel()
         }.onFinally {
             isCompleted = true
         }
@@ -304,7 +307,7 @@ class TextChapterLayout(
         var isSetTypedImage = false
         var wordCount = 0
         contents.forEach { content ->
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             if (isTextImageStyle) {
                 //图片样式为文字嵌入类型
                 var text = content.replace(ChapterProvider.srcReplaceChar, "丨") //▣
@@ -342,7 +345,7 @@ class TextChapterLayout(
                     val text = content.replace(ChapterProvider.srcReplaceChar, "丨")
                     val matcher = AppPattern.imgPattern.matcher(text)
                     while (matcher.find()) {
-                        coroutineContext.ensureActive()
+                        currentCoroutineContext().ensureActive()
                         val imgSrc = matcher.group(1)!!
                         var iStyle = imageStyle
                         var isSmallImage = true
@@ -450,7 +453,7 @@ class TextChapterLayout(
             textPage.height += endPadding
         }
         textPage.text = stringBuilder.toString()
-        coroutineContext.ensureActive()
+        currentCoroutineContext().ensureActive()
         onPageCompleted()
         onCompleted()
     }
@@ -915,7 +918,7 @@ class TextChapterLayout(
                     textPage.leftLineSize = textPage.lineSize
                 }
                 textPage.text = stringBuilder.toString()
-                coroutineContext.ensureActive()
+                currentCoroutineContext().ensureActive()
                 onPageCompleted()
                 //新建页面
                 pendingTextPage = TextPage()

+ 2 - 0
app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt

@@ -20,6 +20,7 @@ import io.legado.app.R
 import io.legado.app.constant.PreferKey
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.help.AppFreezeMonitor
+import io.legado.app.help.DispatchersMonitor
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.lib.dialogs.alert
@@ -203,6 +204,7 @@ class OtherConfigFragment : PreferenceFragment(),
                 LogUtils.logDeviceInfo()
                 LiveEventBus.config().enableLogger(AppConfig.recordLog)
                 AppFreezeMonitor.init(appCtx)
+                DispatchersMonitor.init()
             }
 
             PreferKey.processText -> sharedPreferences?.let {

+ 3 - 0
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -29,7 +29,9 @@ import io.legado.app.utils.onEachParallel
 import io.legado.app.utils.postEvent
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flow
@@ -175,6 +177,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
             ReadBook.onChapterListUpdated(book)
             addDownload(source, book)
         }.onFailure {
+            currentCoroutineContext().ensureActive()
             AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
             //这里可能因为时间太长书籍信息已经更改,所以重新获取
             appDb.bookDao.getBook(book.bookUrl)?.let { book ->

+ 2 - 2
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt

@@ -201,7 +201,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
         booksFlowJob?.cancel()
         booksFlowJob = viewLifecycleOwner.lifecycleScope.launch {
 
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
 
                 appDb.bookDao.flowByGroup(groupId)
                     .map { list ->
@@ -250,7 +250,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
         if (!AppConfig.showLastUpdateTime || (bookshelfLayoutMode != 0 && bookshelfLayoutMode != 4)) {
             return
         }
-        upLastUpdateTimeJob = lifecycleScope.launch {
+        upLastUpdateTimeJob = viewLifecycleOwner.lifecycleScope.launch {
             repeatOnLifecycle(Lifecycle.State.RESUMED) {
                 while (isActive) {
                     booksAdapter.upLastUpdateTime()

+ 9 - 3
app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.widget.recycler
 import android.content.Context
 import android.util.AttributeSet
 import android.view.LayoutInflater
+import android.view.View
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import io.legado.app.R
@@ -26,7 +27,7 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
 
     init {
         super.setOnClickListener {
-            if (!showErrorDialog()) {
+            if (!showErrorDialog(it)) {
                 onClickListener?.onClick(it)
             }
         }
@@ -38,7 +39,7 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
-        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+        layoutParams.width = LayoutParams.MATCH_PARENT
     }
 
     fun startLoad() {
@@ -79,12 +80,17 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
         binding.tvText.visible()
     }
 
-    private fun showErrorDialog(): Boolean {
+    private fun showErrorDialog(view: View): Boolean {
         if (errorMsg.isBlank()) {
             return false
         }
         context.alert(R.string.error) {
             setMessage(errorMsg)
+            if (onClickListener != null) {
+                neutralButton(R.string.retry) {
+                    onClickListener?.onClick(view)
+                }
+            }
         }
         return true
     }

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

@@ -140,3 +140,7 @@ fun String.escapeRegex(): String {
 }
 
 fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this)
+
+fun String.normalizeFileName(): String {
+    return replace(AppPattern.fileNameRegex2, "_")
+}

+ 1 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1211,4 +1211,5 @@
     <string name="hide_manga_title">隐藏漫画列表标题</string>
     <string name="refresh_explore">刷新发现</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
+    <string name="sure_cache_book">是否确认开始缓存?</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1213,4 +1213,5 @@
     <string name="hide_manga_title">隐藏漫画列表标题</string>
     <string name="refresh_explore">刷新发现</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
+    <string name="sure_cache_book">是否确认开始缓存?</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -1345,4 +1345,5 @@
     <string name="dotted_line_while">空隙比例</string>
     <string name="underline_height">线段高度</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
+    <string name="sure_cache_book">是否确认开始缓存?</string>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1346,4 +1346,5 @@
     <string name="dotted_line_while">空隙比例</string>
     <string name="underline_height">线段高度</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
+    <string name="sure_cache_book">是否确认开始缓存?</string>
 </resources>

+ 1 - 1
gradle/libs.versions.toml

@@ -169,7 +169,7 @@ liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref =
 material = { module = "com.google.android.material:material", version.ref = "material" }
 
 mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
-
+renderscript-intrinsics-replacement-toolkit = { module = "com.github.TomasValenta:renderscript-intrinsics-replacement-toolkit", version = "8eaa829ddd" }
 zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
 
 [bundles]