Explorar el Código

[优化] 同步上游更新 (@821938089)

HapeLee hace 10 meses
padre
commit
060ec9ce1a
Se han modificado 44 ficheros con 373 adiciones y 205 borrados
  1. 1 0
      app/src/main/assets/web/help/md/ruleHelp.md
  2. 4 0
      app/src/main/java/io/legado/app/App.kt
  3. 1 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 1 1
      app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt
  5. 1 1
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  6. 30 20
      app/src/main/java/io/legado/app/help/book/ContentHelp.kt
  7. 2 0
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  8. 53 4
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  9. 16 3
      app/src/main/java/io/legado/app/help/config/ThemeConfig.kt
  10. 3 4
      app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt
  11. 31 22
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  12. 9 0
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  13. 8 7
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  14. 14 6
      app/src/main/java/io/legado/app/model/AudioPlay.kt
  15. 9 1
      app/src/main/java/io/legado/app/model/BookCover.kt
  16. 9 11
      app/src/main/java/io/legado/app/model/localBook/PdfFile.kt
  17. 6 0
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  18. 11 9
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  19. 5 4
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  20. 1 1
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  21. 5 5
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt
  22. 5 8
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt
  23. 6 5
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt
  24. 1 1
      app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt
  25. 38 51
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  26. 9 0
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
  27. 5 5
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  28. 2 1
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  29. 3 0
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  30. 12 0
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  31. 3 3
      app/src/main/java/io/legado/app/utils/ACache.kt
  32. 5 1
      app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
  33. 2 2
      app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt
  34. 2 2
      app/src/main/res/menu/book_source.xml
  35. 3 1
      app/src/main/res/values-zh-rHK/strings.xml
  36. 3 1
      app/src/main/res/values-zh-rTW/strings.xml
  37. 3 1
      app/src/main/res/values-zh/strings.xml
  38. 3 1
      app/src/main/res/values/strings.xml
  39. 9 0
      app/src/main/res/xml/pref_config_backup.xml
  40. 11 11
      gradle/libs.versions.toml
  41. 6 6
      modules/rhino/src/main/java/com/script/rhino/RhinoScriptEngine.kt
  42. 2 0
      modules/web/src/components/ToolBar.vue
  43. 4 4
      modules/web/src/store/sourceStore.ts
  44. 16 1
      modules/web/src/utils/souce.ts

+ 1 - 0
app/src/main/assets/web/help/md/ruleHelp.md

@@ -18,6 +18,7 @@
 > `{"example":"https://www.example.com/js/example.js", ...}` 自动复用已经下载的js文件
 > `{"example":"https://www.example.com/js/example.js", ...}` 自动复用已经下载的js文件
 
 
 > 注意此处定义的函数可能会被多个线程同时调用,在函数里的全局变量内容将会共享使用,对其进行修改可能会出现竞争问题
 > 注意此处定义的函数可能会被多个线程同时调用,在函数里的全局变量内容将会共享使用,对其进行修改可能会出现竞争问题
+> 函数内不可声明全局变量,函数外的全局变量不可再赋值,否则会抛出 `无法修改密封对象的属性` 异常
 
 
 * 并发率
 * 并发率
 > 并发限制,单位ms,可填写两种格式
 > 并发限制,单位ms,可填写两种格式

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

@@ -43,6 +43,8 @@ import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 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.ThemeConfig
 import io.legado.app.help.config.ThemeConfig.applyDayNightInit
 import io.legado.app.help.config.ThemeConfig.applyDayNightInit
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.http.Cronet
 import io.legado.app.help.http.Cronet
@@ -152,6 +154,8 @@ class App : Application() {
             RuleBigDataHelp.clearInvalid()
             RuleBigDataHelp.clearInvalid()
             BookHelp.clearInvalidCache()
             BookHelp.clearInvalidCache()
             Backup.clearCache()
             Backup.clearCache()
+            ReadBookConfig.clearBgAndCache()
+            ThemeConfig.clearBg()
             //初始化简繁转换引擎
             //初始化简繁转换引擎
             when (AppConfig.chineseConverterType) {
             when (AppConfig.chineseConverterType) {
                 1 -> {
                 1 -> {

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

@@ -206,7 +206,7 @@ object PreferKey {
     const val notificationsPost = "notificationsPost"
     const val notificationsPost = "notificationsPost"
     const val ignoreBatteryPermission = "ignoreBatteryPermission"
     const val ignoreBatteryPermission = "ignoreBatteryPermission"
     const val paddingDisplayCutouts = "paddingDisplayCutouts"
     const val paddingDisplayCutouts = "paddingDisplayCutouts"
-
+    const val autoCheckNewBackup = "autoCheckNewBackup"
     const val delayBookLoadEnable = "delayBookLoadEnable"
     const val delayBookLoadEnable = "delayBookLoadEnable"
     const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
     const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
     const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
     const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"

+ 1 - 1
app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt

@@ -1,7 +1,7 @@
 package io.legado.app.data.entities.rule
 package io.legado.app.data.entities.rule
 
 
 data class RowUi(
 data class RowUi(
-    var name: String,
+    var name: String = "",
     var type: String = "text",
     var type: String = "text",
     var action: String? = null,
     var action: String? = null,
     var style: FlexChildStyle? = null
     var style: FlexChildStyle? = null

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

@@ -322,7 +322,7 @@ interface JsExtensions : JsEncodeUtils {
     fun downloadFile(url: String): String {
     fun downloadFile(url: String): String {
         rhinoContext.ensureActive()
         rhinoContext.ensureActive()
         val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
         val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
-        val type = UrlUtil.getSuffix(url, analyzeUrl.type)
+        val type = analyzeUrl.type ?: UrlUtil.getSuffix(url)
         val path = FileUtils.getPath(
         val path = FileUtils.getPath(
             File(FileUtils.getCachePath()),
             File(FileUtils.getCachePath()),
             "${MD5Utils.md5Encode16(url)}.${type}"
             "${MD5Utils.md5Encode16(url)}.${type}"

+ 30 - 20
app/src/main/java/io/legado/app/help/book/ContentHelp.kt

@@ -20,13 +20,13 @@ object ContentHelp {
         var p = content1
         var p = content1
             .replace(""".toRegex(), "“")
             .replace(""".toRegex(), "“")
             .replace("[::]['\"‘”“]+".toRegex(), ":“")
             .replace("[::]['\"‘”“]+".toRegex(), ":“")
-            .replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
+            .replace("[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
             .split("\n(\\s*)".toRegex()).toTypedArray()
             .split("\n(\\s*)".toRegex()).toTypedArray()
 
 
         //初始化StringBuilder的长度,在原content的长度基础上做冗余
         //初始化StringBuilder的长度,在原content的长度基础上做冗余
         var buffer = StringBuilder((content1.length * 1.15).toInt())
         var buffer = StringBuilder((content1.length * 1.15).toInt())
         //          章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。
         //          章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。
-        buffer.append(" ")
+        buffer.append("  ")
         if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) {
         if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) {
             // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
             // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
             buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), ""))
             buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), ""))
@@ -34,7 +34,12 @@ object ContentHelp {
 
 
         //如果原文存在分段错误,需要把段落重新黏合
         //如果原文存在分段错误,需要把段落重新黏合
         for (i in 1 until p.size) {
         for (i in 1 until p.size) {
-            if (match(MARK_SENTENCES_END, buffer[buffer.length - 1])) buffer.append("\n")
+            if (match(MARK_SENTENCES_END, buffer.last())
+                || (match(MARK_QUOTATION_RIGHT, buffer.last())
+                        && match(MARK_SENTENCES_END, buffer[buffer.lastIndex - 1]))
+            ) {
+                buffer.append("\n")
+            }
             // 段落开头以外的地方不应该有空格
             // 段落开头以外的地方不应该有空格
             // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
             // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
             buffer.append(p[i].replace("[\u3000\\s]".toRegex(), ""))
             buffer.append(p[i].replace("[\u3000\\s]".toRegex(), ""))
@@ -44,7 +49,7 @@ object ContentHelp {
         //         ”。“处理为”。\n“。不考虑“?”  “!”的情况。
         //         ”。“处理为”。\n“。不考虑“?”  “!”的情况。
         // ”。xxx处理为 ”。\n xxx
         // ”。xxx处理为 ”。\n xxx
         p = buffer.toString()
         p = buffer.toString()
-            .replace("[\"”“]+[\\s]*[\"”“]+".toRegex(), "”\n“")
+            .replace("[\"”“]+\\s*[\"”“]+".toRegex(), "”\n“")
             .replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“")
             .replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“")
             .replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2")
             .replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2")
             .replace(
             .replace(
@@ -60,7 +65,7 @@ object ContentHelp {
         buffer = reduceLength(buffer)
         buffer = reduceLength(buffer)
         content1 = (buffer.toString() //         处理章节头部空格和换行
         content1 = (buffer.toString() //         处理章节头部空格和换行
             .replaceFirst("^\\s+".toRegex(), "")
             .replaceFirst("^\\s+".toRegex(), "")
-            .replace("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
+            .replace("\\s*[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
             .replace("[::][”“\"\\s]+".toRegex(), ":“")
             .replace("[::][”“\"\\s]+".toRegex(), ":“")
             .replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3")
             .replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3")
             .replace("\n(\\s*)".toRegex(), "\n"))
             .replace("\n(\\s*)".toRegex(), "\n"))
@@ -113,13 +118,13 @@ object ContentHelp {
         if (match(MARK_QUOTATION, str[0])) {
         if (match(MARK_QUOTATION, str[0])) {
             val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1
             val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1
             if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
             if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
-                return "${str.substring(0, i)}\n${str.substring(i)}"
+                return "${str.take(i)}\n${str.substring(i)}"
             }
             }
         } else if (match(MARK_QUOTATION, str[length - 1])) {
         } else if (match(MARK_QUOTATION, str[length - 1])) {
             val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false)
             val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false)
             if (i > 1) {
             if (i > 1) {
                 if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
                 if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
-                    return "${str.substring(0, i)}\n${str.substring(i)}"
+                    return "${str.take(i)}\n${str.substring(i)}"
                 }
                 }
             }
             }
         }
         }
@@ -142,8 +147,8 @@ object ContentHelp {
         tigger: Int
         tigger: Int
     ): ArrayList<Int> {
     ): ArrayList<Int> {
         val result = ArrayList<Int>()
         val result = ArrayList<Int>()
-        val arrayEnd = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length - 2, true)
-        val arrayMid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length - 2, true)
+        val arrayEnd = seekIndexes(str, MARK_SENTENCES_END_P, 0, str.length - 2, true)
+        val arrayMid = seekIndexes(str, MARK_SENTENCES_MID, 0, str.length - 2, true)
         if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result
         if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result
         var j = 0
         var j = 0
         var i = min
         var i = min
@@ -347,7 +352,7 @@ object ContentHelp {
             var j = 0
             var j = 0
             var progress = 0
             var progress = 0
             var nextLine = -1
             var nextLine = -1
-            if (insN.size > 0) nextLine = insN[j]
+            if (insN.isNotEmpty()) nextLine = insN[j]
             var gain = 3
             var gain = 3
             var min = 0
             var min = 0
             var trigger = 2
             var trigger = 2
@@ -425,24 +430,24 @@ object ContentHelp {
         var j = 0
         var j = 0
         var progress = 0
         var progress = 0
         var nextLine = -1
         var nextLine = -1
-        if (insN.size > 0) nextLine = insN[j]
+        if (insN.isNotEmpty()) nextLine = insN[j]
         for (i in arrayQuote.indices) {
         for (i in arrayQuote.indices) {
-            val qutoe = arrayQuote[i]
+            val quote = arrayQuote[i]
 
 
 //            把引号前的换行符与内容相间插入
 //            把引号前的换行符与内容相间插入
             while (j < insN.size) {
             while (j < insN.size) {
 
 
 //                如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
 //                如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
-                if (nextLine >= qutoe) break
+                if (nextLine >= quote) break
                 nextLine = insN[j]
                 nextLine = insN[j]
                 buffer.append(string, progress, nextLine + 1)
                 buffer.append(string, progress, nextLine + 1)
                 buffer.append('\n')
                 buffer.append('\n')
                 progress = nextLine + 1
                 progress = nextLine + 1
                 j++
                 j++
             }
             }
-            if (progress < qutoe) {
-                buffer.append(string, progress, qutoe + 1)
-                progress = qutoe + 1
+            if (progress < quote) {
+                buffer.append(string, progress, quote + 1)
+                progress = quote + 1
             }
             }
             if (insQuote[i] && buffer.length > 2) {
             if (insQuote[i] && buffer.length > 2) {
                 if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert(
                 if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert(
@@ -504,7 +509,7 @@ object ContentHelp {
      * @param inOrder 是否按照从前向后的顺序匹配
      * @param inOrder 是否按照从前向后的顺序匹配
      * @return 返回距离构成的ArrayList<Int>
      * @return 返回距离构成的ArrayList<Int>
      */
      */
-    private fun seekIndexs(
+    private fun seekIndexes(
         str: String,
         str: String,
         key: String,
         key: String,
         from: Int,
         from: Int,
@@ -514,14 +519,18 @@ object ContentHelp {
         val list = ArrayList<Int>()
         val list = ArrayList<Int>()
         if (str.length - from < 1) return list
         if (str.length - from < 1) return list
         var i = 0
         var i = 0
-        if (from > i) i = from
+        if (from > 0) i = from
         var t = str.length
         var t = str.length
         if (to > 0) t = min(t, to)
         if (to > 0) t = min(t, to)
         var c: Char
         var c: Char
         while (i < t) {
         while (i < t) {
             c = if (inOrder) str[i] else str[str.length - i - 1]
             c = if (inOrder) str[i] else str[str.length - i - 1]
             if (key.indexOf(c) != -1) {
             if (key.indexOf(c) != -1) {
-                list.add(i)
+                if (list.isNotEmpty() && i - list.last() == 1) {
+                    list[list.lastIndex] = i
+                } else {
+                    list.add(i)
+                }
             }
             }
             i++
             i++
         }
         }
@@ -567,7 +576,7 @@ object ContentHelp {
     private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int {
     private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int {
         if (str.length - from < 1) return -1
         if (str.length - from < 1) return -1
         var i = 0
         var i = 0
-        if (from > i) i = from
+        if (from > 0) i = from
         var t = str.length
         var t = str.length
         if (to > 0) t = min(t, to)
         if (to > 0) t = min(t, to)
         var c: Char
         var c: Char
@@ -609,6 +618,7 @@ object ContentHelp {
 
 
     //  引号
     //  引号
     private const val MARK_QUOTATION = "\"“”"
     private const val MARK_QUOTATION = "\"“”"
+    private const val MARK_QUOTATION_RIGHT = "\"”"
     private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex()
     private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex()
 
 
     //  限制字典的长度
     //  限制字典的长度

+ 2 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -580,6 +580,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
 
 
     val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true)
     val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true)
 
 
+    val autoCheckNewBackup get() = appCtx.getPrefBoolean(PreferKey.autoCheckNewBackup, true)
+
     val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf")
     val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf")
 
 
     val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "official_version")
     val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "official_version")

+ 53 - 4
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -33,6 +33,7 @@ import splitties.init.appCtx
 import java.io.File
 import java.io.File
 import kotlin.String
 import kotlin.String
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.graphics.drawable.toDrawable
+import io.legado.app.utils.FileDoc
 import io.legado.app.utils.putPrefBoolean
 import io.legado.app.utils.putPrefBoolean
 
 
 /**
 /**
@@ -164,6 +165,25 @@ object ReadBookConfig {
         return false
         return false
     }
     }
 
 
+    fun clearBgAndCache() {
+        val bgs = hashSetOf<String>()
+        configList.forEach { config ->
+            repeat(3) {
+                config.getBgPath(it)?.let { path ->
+                    bgs.add(path)
+                }
+            }
+        }
+        appCtx.externalFiles.getFile("bg").listFiles()?.forEach {
+            if (!bgs.contains(it.absolutePath)) {
+                it.delete()
+            }
+        }
+        FileUtils.delete(appCtx.externalCache.getFile("readConfig"))
+        val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
+        FileUtils.delete(configZipPath)
+    }
+
     private fun resetAll() {
     private fun resetAll() {
         DefaultData.readConfigs.let {
         DefaultData.readConfigs.let {
             configList.clear()
             configList.clear()
@@ -541,13 +561,18 @@ object ReadBookConfig {
         val configFile = configDir.getFile(configFileName)
         val configFile = configDir.getFile(configFileName)
         val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
         val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
         if (config.textFont.isNotEmpty()) {
         if (config.textFont.isNotEmpty()) {
-            val fontName = FileUtils.getName(config.textFont)
+            val fontName = config.textFont
             val fontPath =
             val fontPath =
                 FileUtils.getPath(appCtx.externalFiles, "font", fontName)
                 FileUtils.getPath(appCtx.externalFiles, "font", fontName)
-            if (!FileUtils.exist(fontPath)) {
-                configDir.getFile(fontName).copyTo(File(fontPath))
+            val fontFile = configDir.getFile(fontName)
+            if (fontFile.exists()) {
+                if (!FileUtils.exist(fontPath)) {
+                    fontFile.copyTo(File(fontPath))
+                }
+                config.textFont = fontPath
+            } else {
+                config.textFont = ""
             }
             }
-            config.textFont = fontPath
         }
         }
         if (config.bgType == 2) {
         if (config.bgType == 2) {
             val bgName = FileUtils.getName(config.bgStr)
             val bgName = FileUtils.getName(config.bgStr)
@@ -696,6 +721,30 @@ object ReadBookConfig {
         @Transient
         @Transient
         private var initColorInt = false
         private var initColorInt = false
 
 
+        fun getBgPath(bgIndex: Int): String? {
+            val bgType = when (bgIndex) {
+                0 -> bgType
+                1 -> bgTypeNight
+                2 -> bgTypeEInk
+                else -> error("unknown bgIndex: $bgIndex")
+            }
+            if (bgType != 2) {
+                return null
+            }
+            val bgStr = when (bgIndex) {
+                0 -> bgStr
+                1 -> bgStrNight
+                2 -> bgStrEInk
+                else -> error("unknown bgIndex: $bgIndex")
+            }
+            val path = if (bgStr.contains(File.separator)) {
+                bgStr
+            } else {
+                FileUtils.getPath(appCtx.externalFiles, "bg", bgStr)
+            }
+            return path
+        }
+
         private fun initColorInt() {
         private fun initColorInt() {
             textColorIntEInk = textColorEInk.toColorInt()
             textColorIntEInk = textColorEInk.toColorInt()
             textColorIntNight = textColorNight.toColorInt()
             textColorIntNight = textColorNight.toColorInt()

+ 16 - 3
app/src/main/java/io/legado/app/help/config/ThemeConfig.kt

@@ -16,9 +16,11 @@ import io.legado.app.model.BookCover
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
 import io.legado.app.utils.GSON
+import io.legado.app.utils.externalFiles
 import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.fromJsonArray
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getCompatColor
 import io.legado.app.utils.getCompatColor
+import io.legado.app.utils.getFile
 import io.legado.app.utils.getPrefInt
 import io.legado.app.utils.getPrefInt
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.hexString
 import io.legado.app.utils.hexString
@@ -236,9 +238,20 @@ object ThemeConfig {
     /**
     /**
      * 更新主题
      * 更新主题
      */
      */
-//    fun applyTheme(context: Context) = with(context) {
-//
-//    }
+    fun clearBg() {
+        val bgImagePath = appCtx.getPrefString(PreferKey.bgImage)
+        appCtx.externalFiles.getFile(PreferKey.bgImage).listFiles()?.forEach {
+            if (it.absolutePath != bgImagePath) {
+                it.delete()
+            }
+        }
+        val bgImageNPath = appCtx.getPrefString(PreferKey.bgImageN)
+        appCtx.externalFiles.getFile(PreferKey.bgImageN).listFiles()?.forEach {
+            if (it.absolutePath != bgImageNPath) {
+                it.delete()
+            }
+        }
+    }
 
 
     @Keep
     @Keep
     data class Config(
     data class Config(

+ 3 - 4
app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt

@@ -46,9 +46,7 @@ suspend fun OkHttpClient.newCallResponseBody(
     retry: Int = 0,
     retry: Int = 0,
     builder: Request.Builder.() -> Unit
     builder: Request.Builder.() -> Unit
 ): ResponseBody {
 ): ResponseBody {
-    return newCallResponse(retry, builder).let {
-        it.body ?: throw IOException(it.message)
-    }
+    return newCallResponse(retry, builder).body
 }
 }
 
 
 suspend fun OkHttpClient.newCallStrResponse(
 suspend fun OkHttpClient.newCallStrResponse(
@@ -56,7 +54,7 @@ suspend fun OkHttpClient.newCallStrResponse(
     builder: Request.Builder.() -> Unit
     builder: Request.Builder.() -> Unit
 ): StrResponse {
 ): StrResponse {
     return newCallResponse(retry, builder).let {
     return newCallResponse(retry, builder).let {
-        StrResponse(it, it.body?.text() ?: it.message)
+        StrResponse(it, it.body.text())
     }
     }
 }
 }
 
 
@@ -142,6 +140,7 @@ fun Request.Builder.postForm(encodedForm: String) {
     post(encodedForm.toRequestBody(formContentType))
     post(encodedForm.toRequestBody(formContentType))
 }
 }
 
 
+@Suppress("unused")
 fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
 fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
     val formBody = FormBody.Builder()
     val formBody = FormBody.Builder()
     form.forEach {
     form.forEach {

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

@@ -14,9 +14,11 @@ import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
+import io.legado.app.model.BookCover
 import io.legado.app.utils.*
 import io.legado.app.utils.*
 import io.legado.app.utils.compress.ZipUtils
 import io.legado.app.utils.compress.ZipUtils
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.sync.withLock
@@ -29,6 +31,8 @@ import java.text.SimpleDateFormat
 import java.util.*
 import java.util.*
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeUnit
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.coroutineContext
+import androidx.core.content.edit
+import androidx.core.net.toUri
 
 
 /**
 /**
  * 备份
  * 备份
@@ -65,6 +69,7 @@ object Backup {
             ReadBookConfig.configFileName,
             ReadBookConfig.configFileName,
             ReadBookConfig.shareConfigFileName,
             ReadBookConfig.shareConfigFileName,
             ThemeConfig.configFileName,
             ThemeConfig.configFileName,
+            BookCover.configFileName,
             "config.xml"
             "config.xml"
         )
         )
     }
     }
@@ -139,7 +144,7 @@ object Backup {
                     .writeText(it)
                     .writeText(it)
             }
             }
         }
         }
-        coroutineContext.ensureActive()
+        currentCoroutineContext().ensureActive()
         GSON.toJson(ReadBookConfig.configList).let {
         GSON.toJson(ReadBookConfig.configList).let {
             FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
             FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
                 .writeText(it)
                 .writeText(it)
@@ -156,31 +161,35 @@ object Backup {
             FileUtils.createFileIfNotExist(backupPath + File.separator + DirectLinkUpload.ruleFileName)
             FileUtils.createFileIfNotExist(backupPath + File.separator + DirectLinkUpload.ruleFileName)
                 .writeText(GSON.toJson(it))
                 .writeText(GSON.toJson(it))
         }
         }
-        coroutineContext.ensureActive()
+        BookCover.getConfig()?.let {
+            FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
+                .writeText(GSON.toJson(it))
+        }
+        currentCoroutineContext().ensureActive()
         appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
         appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
-            val edit = sp.edit()
-            appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
-                if (BackupConfig.keyIsNotIgnore(key)) {
-                    when (key) {
-                        PreferKey.webDavPassword -> {
-                            edit.putString(key, aes.runCatching {
-                                encryptBase64(value.toString())
-                            }.getOrDefault(value.toString()))
-                        }
+            sp.edit(commit = true) {
+                appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
+                    if (BackupConfig.keyIsNotIgnore(key)) {
+                        when (key) {
+                            PreferKey.webDavPassword -> {
+                                putString(key, aes.runCatching {
+                                    encryptBase64(value.toString())
+                                }.getOrDefault(value.toString()))
+                            }
 
 
-                        else -> when (value) {
-                            is Int -> edit.putInt(key, value)
-                            is Boolean -> edit.putBoolean(key, value)
-                            is Long -> edit.putLong(key, value)
-                            is Float -> edit.putFloat(key, value)
-                            is String -> edit.putString(key, value)
+                            else -> when (value) {
+                                is Int -> putInt(key, value)
+                                is Boolean -> putBoolean(key, value)
+                                is Long -> putLong(key, value)
+                                is Float -> putFloat(key, value)
+                                is String -> putString(key, value)
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
             }
             }
-            edit.commit()
         }
         }
-        coroutineContext.ensureActive()
+        currentCoroutineContext().ensureActive()
         val zipFileName = getNowZipFileName()
         val zipFileName = getNowZipFileName()
         val paths = arrayListOf(*backupFileNames)
         val paths = arrayListOf(*backupFileNames)
         for (i in 0 until paths.size) {
         for (i in 0 until paths.size) {
@@ -200,7 +209,7 @@ object Backup {
                 }
                 }
 
 
                 path.isContentScheme() -> {
                 path.isContentScheme() -> {
-                    copyBackup(context, Uri.parse(path), backupFileName)
+                    copyBackup(context, path.toUri(), backupFileName)
                 }
                 }
 
 
                 else -> {
                 else -> {
@@ -215,7 +224,7 @@ object Backup {
         }
         }
         FileUtils.delete(backupPath)
         FileUtils.delete(backupPath)
         FileUtils.delete(zipFilePath)
         FileUtils.delete(zipFilePath)
-        coroutineContext.ensureActive()
+        currentCoroutineContext().ensureActive()
         ReadBookConfig.getAllPicBgStr().map {
         ReadBookConfig.getAllPicBgStr().map {
             if (it.contains(File.separator)) {
             if (it.contains(File.separator)) {
                 File(it)
                 File(it)
@@ -228,7 +237,7 @@ object Backup {
     }
     }
 
 
     private suspend fun writeListToJson(list: List<Any>, fileName: String, path: String) {
     private suspend fun writeListToJson(list: List<Any>, fileName: String, path: String) {
-        coroutineContext.ensureActive()
+        currentCoroutineContext().ensureActive()
         withContext(IO) {
         withContext(IO) {
             if (list.isNotEmpty()) {
             if (list.isNotEmpty()) {
                 LogUtils.d(TAG, "阅读备份 $fileName 列表大小 ${list.size}")
                 LogUtils.d(TAG, "阅读备份 $fileName 列表大小 ${list.size}")

+ 9 - 0
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -33,6 +33,7 @@ import io.legado.app.help.book.upType
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.help.config.ThemeConfig
+import io.legado.app.model.BookCover
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.ACache
 import io.legado.app.utils.ACache
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
@@ -212,6 +213,14 @@ object Restore {
         }?.onFailure {
         }?.onFailure {
             AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
             AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
         }
         }
+        File(path, BookCover.configFileName).takeIf {
+            it.exists()
+        }?.runCatching {
+            val json = readText()
+            BookCover.saveCoverRule(json)
+        }?.onFailure {
+            AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it)
+        }
         if (!BackupConfig.ignoreReadConfig) {
         if (!BackupConfig.ignoreReadConfig) {
             //恢复阅读界面配置
             //恢复阅读界面配置
             File(path, ReadBookConfig.configFileName).takeIf {
             File(path, ReadBookConfig.configFileName).takeIf {

+ 8 - 7
app/src/main/java/io/legado/app/lib/webdav/WebDav.kt

@@ -16,6 +16,7 @@ import io.legado.app.utils.findNSPrefix
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.toRequestBody
 import io.legado.app.utils.toRequestBody
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import okhttp3.HttpUrl.Companion.toHttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -257,7 +258,7 @@ open class WebDav(
                 method("PROPFIND", requestBody)
                 method("PROPFIND", requestBody)
             }.use { it.isSuccessful }
             }.use { it.isSuccessful }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
         }.getOrDefault(false)
         }.getOrDefault(false)
     }
     }
 
 
@@ -273,7 +274,7 @@ open class WebDav(
                 method("PROPFIND", requestBody)
                 method("PROPFIND", requestBody)
             }.use { it.code != 401 }
             }.use { it.code != 401 }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
         }.getOrDefault(true)
         }.getOrDefault(true)
     }
     }
 
 
@@ -294,7 +295,7 @@ open class WebDav(
                 }
                 }
             }
             }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it)
             AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it)
         }.isSuccess
         }.isSuccess
     }
     }
@@ -351,7 +352,7 @@ open class WebDav(
                 }
                 }
             }
             }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
         }
         }
@@ -372,7 +373,7 @@ open class WebDav(
                 }
                 }
             }
             }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
         }
         }
@@ -393,7 +394,7 @@ open class WebDav(
                 }
                 }
             }
             }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
         }
         }
@@ -424,7 +425,7 @@ open class WebDav(
                 checkResult(it)
                 checkResult(it)
             }
             }
         }.onFailure {
         }.onFailure {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             AppLog.put("WebDav删除失败\n${it.localizedMessage}", it)
             AppLog.put("WebDav删除失败\n${it.localizedMessage}", it)
         }.isSuccess
         }.isSuccess
     }
     }

+ 14 - 6
app/src/main/java/io/legado/app/model/AudioPlay.kt

@@ -142,6 +142,11 @@ object AudioPlay : CoroutineScope by MainScope() {
                     removeLoading(index)
                     removeLoading(index)
                     return
                     return
                 }
                 }
+                if (chapter.isVolume) {
+                    skipTo(index + 1)
+                    removeLoading(index)
+                    return
+                }
                 upLoading(true)
                 upLoading(true)
                 WebBook.getContent(this, bookSource, book, chapter)
                 WebBook.getContent(this, bookSource, book, chapter)
                     .onSuccess { content ->
                     .onSuccess { content ->
@@ -208,7 +213,8 @@ object AudioPlay : CoroutineScope by MainScope() {
         val book = book ?: return
         val book = book ?: return
         durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)
         durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)
         durAudioSize = durChapter?.end?.toInt() ?: 0
         durAudioSize = durChapter?.end?.toInt() ?: 0
-        postEvent(EventBus.AUDIO_SUB_TITLE, durChapter?.title ?: appCtx.getString(R.string.data_loading))
+        val title = durChapter?.title ?: appCtx.getString(R.string.data_loading)
+        postEvent(EventBus.AUDIO_SUB_TITLE, title)
         postEvent(EventBus.AUDIO_SIZE, durAudioSize)
         postEvent(EventBus.AUDIO_SIZE, durAudioSize)
         postEvent(EventBus.AUDIO_PROGRESS, durChapterPos)
         postEvent(EventBus.AUDIO_PROGRESS, durChapterPos)
     }
     }
@@ -260,11 +266,13 @@ object AudioPlay : CoroutineScope by MainScope() {
     fun skipTo(index: Int) {
     fun skipTo(index: Int) {
         Coroutine.async {
         Coroutine.async {
             stopPlay()
             stopPlay()
-            durChapterIndex = index
-            durChapterPos = 0
-            durPlayUrl = ""
-            saveRead()
-            loadPlayUrl()
+            if (index in 0..<simulatedChapterSize) {
+                durChapterIndex = index
+                durChapterPos = 0
+                durPlayUrl = ""
+                saveRead()
+                loadPlayUrl()
+            }
         }
         }
     }
     }
 
 

+ 9 - 1
app/src/main/java/io/legado/app/model/BookCover.kt

@@ -46,6 +46,7 @@ import java.io.File
 object BookCover {
 object BookCover {
 
 
     private const val coverRuleConfigKey = "legadoCoverRuleConfig"
     private const val coverRuleConfigKey = "legadoCoverRuleConfig"
+    const val configFileName = "coverRule.json"
     var drawBookName = true
     var drawBookName = true
         private set
         private set
     var drawBookAuthor = true
     var drawBookAuthor = true
@@ -239,9 +240,12 @@ object BookCover {
     }
     }
 
 
     fun getCoverRule(): CoverRule {
     fun getCoverRule(): CoverRule {
+        return getConfig() ?: DefaultData.coverRule
+    }
+
+    fun getConfig(): CoverRule? {
         return GSON.fromJsonObject<CoverRule>(CacheManager.get(coverRuleConfigKey))
         return GSON.fromJsonObject<CoverRule>(CacheManager.get(coverRuleConfigKey))
             .getOrNull()
             .getOrNull()
-            ?: DefaultData.coverRule
     }
     }
 
 
     suspend fun searchCover(book: Book): String? {
     suspend fun searchCover(book: Book): String? {
@@ -266,6 +270,10 @@ object BookCover {
 
 
     fun saveCoverRule(config: CoverRule) {
     fun saveCoverRule(config: CoverRule) {
         val json = GSON.toJson(config)
         val json = GSON.toJson(config)
+        saveCoverRule(json)
+    }
+
+    fun saveCoverRule(json: String) {
         CacheManager.put(coverRuleConfigKey, json)
         CacheManager.put(coverRuleConfigKey, json)
     }
     }
 
 

+ 9 - 11
app/src/main/java/io/legado/app/model/localBook/PdfFile.kt

@@ -4,6 +4,7 @@ import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.Color
 import android.graphics.pdf.PdfRenderer
 import android.graphics.pdf.PdfRenderer
 import android.os.ParcelFileDescriptor
 import android.os.ParcelFileDescriptor
+import androidx.core.graphics.createBitmap
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 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
@@ -121,18 +122,15 @@ class PdfFile(var book: Book) {
         if (index >= renderer.pageCount) {
         if (index >= renderer.pageCount) {
             return null
             return null
         }
         }
-        return renderer.openPage(index)?.use { page ->
-            Bitmap.createBitmap(
+        return renderer.openPage(index).use { page ->
+            createBitmap(
                 SystemUtils.screenWidthPx,
                 SystemUtils.screenWidthPx,
-                (SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt(),
-                Bitmap.Config.ARGB_8888
-            )
-                .apply {
-                    this.eraseColor(Color.WHITE)
-                    page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
-                }
+                (SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt()
+            ).apply {
+                this.eraseColor(Color.WHITE)
+                page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
+            }
         }
         }
-
     }
     }
 
 
     private fun getContent(chapter: BookChapter): String? =
     private fun getContent(chapter: BookChapter): String? =
@@ -168,7 +166,7 @@ class PdfFile(var book: Book) {
                 null
                 null
             }
             }
 
 
-        } catch (e: Exception) {
+        } catch (_: Exception) {
             return null
             return null
         }
         }
     }
     }

+ 6 - 0
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -308,6 +308,10 @@ abstract class BaseReadAloudService : BaseService(),
     @SuppressLint("WakelockTimeout")
     @SuppressLint("WakelockTimeout")
     @CallSuper
     @CallSuper
     open fun resumeReadAloud() {
     open fun resumeReadAloud() {
+        resumeReadAloudInternal()
+    }
+
+    private fun resumeReadAloudInternal() {
         pause = false
         pause = false
         needResumeOnAudioFocusGain = false
         needResumeOnAudioFocusGain = false
         needResumeOnCallStateIdle = false
         needResumeOnCallStateIdle = false
@@ -674,12 +678,14 @@ abstract class BaseReadAloudService : BaseService(),
 
 
     open fun prevChapter() {
     open fun prevChapter() {
         toLast = false
         toLast = false
+        resumeReadAloudInternal()
         ReadBook.moveToPrevChapter(true, toLast = false)
         ReadBook.moveToPrevChapter(true, toLast = false)
     }
     }
 
 
     open fun nextChapter() {
     open fun nextChapter() {
         ReadBook.upReadTime()
         ReadBook.upReadTime()
         AppLog.putDebug("${ReadBook.curTextChapter?.chapter?.title} 朗读结束跳转下一章并朗读")
         AppLog.putDebug("${ReadBook.curTextChapter?.chapter?.title} 朗读结束跳转下一章并朗读")
+        resumeReadAloudInternal()
         if (!ReadBook.moveToNextChapter(true)) {
         if (!ReadBook.moveToNextChapter(true)) {
             stopSelf()
             stopSelf()
         }
         }

+ 11 - 9
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -37,6 +37,7 @@ import io.legado.app.help.http.okHttpClient
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
+import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.printOnDebug
@@ -46,6 +47,7 @@ import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
@@ -197,8 +199,8 @@ class HttpReadAloudService : BaseReadAloudService(),
             .take(10)
             .take(10)
             .toList()
             .toList()
         contentList.forEach { content ->
         contentList.forEach { content ->
-            coroutineContext.ensureActive()
-            val fileName = md5SpeakFileName(content)
+            currentCoroutineContext().ensureActive()
+            val fileName = md5SpeakFileName(content, textChapter)
             val speakText = content.replace(AppPattern.notReadAloudRegex, "")
             val speakText = content.replace(AppPattern.notReadAloudRegex, "")
             if (speakText.isEmpty()) {
             if (speakText.isEmpty()) {
                 createSilentSound(fileName)
                 createSilentSound(fileName)
@@ -266,8 +268,8 @@ class HttpReadAloudService : BaseReadAloudService(),
             .take(10)
             .take(10)
             .toList()
             .toList()
         contentList.forEach { content ->
         contentList.forEach { content ->
-            coroutineContext.ensureActive()
-            val fileName = md5SpeakFileName(content)
+            currentCoroutineContext().ensureActive()
+            val fileName = md5SpeakFileName(content, textChapter)
             val speakText = content.replace(AppPattern.notReadAloudRegex, "")
             val speakText = content.replace(AppPattern.notReadAloudRegex, "")
             val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
             val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
             val downloader = createDownloader(dataSourceFactory, fileName)
             val downloader = createDownloader(dataSourceFactory, fileName)
@@ -332,10 +334,10 @@ class HttpReadAloudService : BaseReadAloudService(),
                     speakSpeed = speechRate,
                     speakSpeed = speechRate,
                     source = httpTts,
                     source = httpTts,
                     readTimeout = 300 * 1000L,
                     readTimeout = 300 * 1000L,
-                    coroutineContext = coroutineContext
+                    coroutineContext = currentCoroutineContext()
                 )
                 )
                 var response = analyzeUrl.getResponseAwait()
                 var response = analyzeUrl.getResponseAwait()
-                coroutineContext.ensureActive()
+                currentCoroutineContext().ensureActive()
                 val checkJs = httpTts.loginCheckJs
                 val checkJs = httpTts.loginCheckJs
                 if (checkJs?.isNotBlank() == true) {
                 if (checkJs?.isNotBlank() == true) {
                     response = analyzeUrl.evalJS(checkJs, response) as Response
                     response = analyzeUrl.evalJS(checkJs, response) as Response
@@ -353,7 +355,7 @@ class HttpReadAloudService : BaseReadAloudService(),
                         }
                         }
                     }
                     }
                 }
                 }
-                coroutineContext.ensureActive()
+                currentCoroutineContext().ensureActive()
                 response.body.byteStream().let { stream ->
                 response.body.byteStream().let { stream ->
                     downloadErrorNo = 0
                     downloadErrorNo = 0
                     return stream
                     return stream
@@ -396,7 +398,7 @@ class HttpReadAloudService : BaseReadAloudService(),
         return null
         return null
     }
     }
 
 
-    private fun md5SpeakFileName(content: String): String {
+    private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String {
         return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
         return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
                 MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
                 MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
     }
     }
@@ -586,7 +588,7 @@ class HttpReadAloudService : BaseReadAloudService(),
         return servicePendingIntent<HttpReadAloudService>(actionStr)
         return servicePendingIntent<HttpReadAloudService>(actionStr)
     }
     }
 
 
-    inner class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) {
+    class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) {
         override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
         override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
             return C.TIME_UNSET
             return C.TIME_UNSET
         }
         }

+ 5 - 4
app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt

@@ -223,10 +223,11 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
             binding.apply {
             binding.apply {
                 cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
                 cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
                 cbSourceName.text = item.sourceName
                 cbSourceName.text = item.sourceName
-                tvSourceState.text = if (viewModel.checkSources[holder.layoutPosition] != null) {
-                    "已有"
-                } else {
-                    "新增"
+                val localSource = viewModel.checkSources[holder.layoutPosition]
+                tvSourceState.text = when {
+                    localSource == null -> "新增"
+                    item.lastUpdateTime > localSource.lastUpdateTime -> "更新"
+                    else -> "已有"
                 }
                 }
             }
             }
         }
         }

+ 1 - 1
app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt

@@ -188,7 +188,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
             allSources.forEach {
             allSources.forEach {
                 val has = appDb.rssSourceDao.getByKey(it.sourceUrl)
                 val has = appDb.rssSourceDao.getByKey(it.sourceUrl)
                 checkSources.add(has)
                 checkSources.add(has)
-                selectStatus.add(has == null)
+                selectStatus.add(has == null || has.lastUpdateTime < it.lastUpdateTime)
             }
             }
             successLiveData.postValue(allSources.size)
             successLiveData.postValue(allSources.size)
         }
         }

+ 5 - 5
app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt

@@ -17,6 +17,7 @@ import io.legado.app.databinding.DialogChangeCoverBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 /**
 /**
@@ -63,11 +64,10 @@ class ChangeCoverDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_change
 
 
     private fun initData() {
     private fun initData() {
         lifecycleScope.launch {
         lifecycleScope.launch {
-            repeatOnLifecycle(STARTED) {
-                viewModel.dataFlow.conflate().collect {
-                    adapter.setItems(it)
-                    delay(1000)
-                }
+            lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
+            viewModel.dataFlow.conflate().collect {
+                adapter.setItems(it)
+                delay(1000)
             }
             }
         }
         }
     }
     }

+ 5 - 8
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -52,6 +52,7 @@ import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 /**
 /**
@@ -240,15 +241,11 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
         }
         }
 
 
         lifecycleScope.launch {
         lifecycleScope.launch {
-            repeatOnLifecycle(STARTED) {
-                viewModel.searchDataFlow.conflate().collect {
-                    adapter.setItems(it)
-
-                    binding.tvEmptyMsg.isVisible = it.isEmpty()
-                    binding.recyclerView.isVisible = it.isNotEmpty()
+            lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
+            viewModel.searchDataFlow.conflate().collect {
+                adapter.setItems(it)
+                delay(1000)
 
 
-                    delay(1000)
-                }
             }
             }
         }
         }
 
 

+ 6 - 5
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -48,6 +48,7 @@ import io.legado.app.utils.visible
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 
 
@@ -233,11 +234,11 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
             binding.toolBar.menu.applyTint(requireContext())
             binding.toolBar.menu.applyTint(requireContext())
         }
         }
         lifecycleScope.launch {
         lifecycleScope.launch {
-            repeatOnLifecycle(STARTED) {
-                viewModel.searchDataFlow.conflate().collect {
-                    searchBookAdapter.setItems(it)
-                    delay(1000)
-                }
+            lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
+            viewModel.searchDataFlow.conflate().collect {
+                searchBookAdapter.setItems(it)
+                delay(1000)
+
             }
             }
         }
         }
         lifecycleScope.launch {
         lifecycleScope.launch {

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

@@ -33,7 +33,7 @@ class AutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_auto_read)
         val bottomDialog = (activity as ReadBookActivity).bottomDialog++
         val bottomDialog = (activity as ReadBookActivity).bottomDialog++
         if (bottomDialog > 0) {
         if (bottomDialog > 0) {
             dismiss()
             dismiss()
-            return
+            return@run
         }
         }
         initOnChange()
         initOnChange()
         initData()
         initData()

+ 38 - 51
app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt

@@ -26,20 +26,25 @@ import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.file.HandleFileContract
 import io.legado.app.ui.file.HandleFileContract
+import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
 import io.legado.app.utils.GSON
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.SelectImageContract
 import io.legado.app.utils.SelectImageContract
 import io.legado.app.utils.compress.ZipUtils
 import io.legado.app.utils.compress.ZipUtils
+import io.legado.app.utils.createFileIfNotExist
 import io.legado.app.utils.createFileReplace
 import io.legado.app.utils.createFileReplace
 import io.legado.app.utils.createFolderReplace
 import io.legado.app.utils.createFolderReplace
+import io.legado.app.utils.delete
 import io.legado.app.utils.externalCache
 import io.legado.app.utils.externalCache
 import io.legado.app.utils.externalFiles
 import io.legado.app.utils.externalFiles
+import io.legado.app.utils.find
 import io.legado.app.utils.getFile
 import io.legado.app.utils.getFile
 import io.legado.app.utils.inputStream
 import io.legado.app.utils.inputStream
 import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.launch
 import io.legado.app.utils.launch
 import io.legado.app.utils.longToast
 import io.legado.app.utils.longToast
+import io.legado.app.utils.openInputStream
 import io.legado.app.utils.openOutputStream
 import io.legado.app.utils.openOutputStream
 import io.legado.app.utils.outputStream
 import io.legado.app.utils.outputStream
 import io.legado.app.utils.parseToUri
 import io.legado.app.utils.parseToUri
@@ -238,68 +243,37 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
             configDir.createFolderReplace()
             configDir.createFolderReplace()
             val configFile = configDir.getFile("readConfig.json")
             val configFile = configDir.getFile("readConfig.json")
             configFile.createFileReplace()
             configFile.createFileReplace()
-            configFile.writeText(GSON.toJson(ReadBookConfig.getExportConfig()))
-            exportFiles.add(configFile)
+            val config = ReadBookConfig.getExportConfig()
             val fontPath = ReadBookConfig.textFont
             val fontPath = ReadBookConfig.textFont
             if (fontPath.isNotEmpty()) {
             if (fontPath.isNotEmpty()) {
-                val fontName = FileUtils.getName(fontPath)
-                val fontInputStream =
-                    fontPath.parseToUri().inputStream(requireContext()).getOrNull()
+                val fontDoc = FileDoc.fromFile(fontPath)
+                val fontName = fontDoc.name
+                val fontInputStream = fontDoc.openInputStream().getOrNull()
                 fontInputStream?.use {
                 fontInputStream?.use {
                     val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
                     val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
-                    it.copyTo(fontExportFile.outputStream())
-                    exportFiles.add(fontExportFile)
-                }
-            }
-            if (ReadBookConfig.durConfig.bgType == 2) {
-                val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStr)
-                val bgFile = File(ReadBookConfig.durConfig.bgStr)
-                if (bgFile.exists()) {
-                    val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    if (!bgExportFile.exists()) {
-                        bgFile.copyTo(bgExportFile)
-                        exportFiles.add(bgExportFile)
-                    }
-                }
-            }
-            if (ReadBookConfig.durConfig.bgTypeNight == 2) {
-                val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrNight)
-                val bgFile = File(ReadBookConfig.durConfig.bgStrNight)
-                if (bgFile.exists()) {
-                    val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    if (!bgExportFile.exists()) {
-                        bgFile.copyTo(bgExportFile)
-                        exportFiles.add(bgExportFile)
+                    fontExportFile.outputStream().use { out ->
+                        it.copyTo(out)
                     }
                     }
+                    config.textFont = fontName
+                    exportFiles.add(fontExportFile)
                 }
                 }
             }
             }
-            if (ReadBookConfig.durConfig.bgTypeEInk == 2) {
-                val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrEInk)
-                val bgFile = File(ReadBookConfig.durConfig.bgStrEInk)
-                if (bgFile.exists()) {
-                    val bgExportFile = File(FileUtils.getPath(configDir, bgName))
-                    if (!bgExportFile.exists()) {
-                        bgFile.copyTo(bgExportFile)
-                        exportFiles.add(bgExportFile)
-                    }
-                }
+            configFile.writeText(GSON.toJson(config))
+            exportFiles.add(configFile)
+            repeat(3) {
+                val path = ReadBookConfig.durConfig.getBgPath(it) ?: return@repeat
+                val bgExportFile = copyBgImage(path, configDir) ?: return@repeat
+                exportFiles.add(bgExportFile)
             }
             }
             val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
             val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
             if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) {
             if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) {
-                if (uri.isContentScheme()) {
-                    DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc ->
-                        treeDoc.findFile(exportFileName)?.delete()
-                        val out = treeDoc.createFile("", exportFileName)?.openOutputStream()
-                        out?.use {
-                            File(configZipPath).inputStream().use {
-                                it.copyTo(out)
-                            }
-                        }
+                val exportDir = FileDoc.fromDir(uri)
+                exportDir.find(exportFileName)?.delete()
+                val exportFileDoc = exportDir.createFileIfNotExist(exportFileName)
+                exportFileDoc.openOutputStream().getOrThrow().use { out ->
+                    File(configZipPath).inputStream().use {
+                        it.copyTo(out)
                     }
                     }
-                } else {
-                    val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName)
-                    FileUtils.delete(exportPath)
-                    File(configZipPath).copyTo(FileUtils.createFileIfNotExist(exportPath))
                 }
                 }
             }
             }
         }.onSuccess {
         }.onSuccess {
@@ -311,6 +285,19 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
         }
         }
     }
     }
 
 
+    private fun copyBgImage(path: String, configDir: File): File? {
+        val bgName = FileUtils.getName(path)
+        val bgFile = File(path)
+        if (bgFile.exists()) {
+            val bgExportFile = File(FileUtils.getPath(configDir, bgName))
+            if (!bgExportFile.exists()) {
+                bgFile.copyTo(bgExportFile)
+                return bgExportFile
+            }
+        }
+        return null
+    }
+
     @SuppressLint("InflateParams")
     @SuppressLint("InflateParams")
     private fun importNetConfigAlert() {
     private fun importNetConfigAlert() {
         alert("输入地址") {
         alert("输入地址") {

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

@@ -8,6 +8,7 @@ import android.util.Size
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern.noWordCountRegex
 import io.legado.app.constant.AppPattern.noWordCountRegex
+import io.legado.app.constant.PageAnim
 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
@@ -83,6 +84,7 @@ class TextChapterLayout(
     private val useZhLayout = ReadBookConfig.useZhLayout
     private val useZhLayout = ReadBookConfig.useZhLayout
     private val isMiddleTitle = ReadBookConfig.isMiddleTitle
     private val isMiddleTitle = ReadBookConfig.isMiddleTitle
     private val textFullJustify = ReadBookConfig.textFullJustify
     private val textFullJustify = ReadBookConfig.textFullJustify
+    private val pageAnim = book.getPageAnim()
     private val titleSegType = ReadBookConfig.titleSegType
     private val titleSegType = ReadBookConfig.titleSegType
     private val titleSegDistance = ReadBookConfig.titleSegDistance
     private val titleSegDistance = ReadBookConfig.titleSegDistance
     private val titleSegFlag = ReadBookConfig.titleSegFlag
     private val titleSegFlag = ReadBookConfig.titleSegFlag
@@ -476,6 +478,13 @@ class TextChapterLayout(
                 Book.imgStyleFull -> {
                 Book.imgStyleFull -> {
                     width = visibleWidth
                     width = visibleWidth
                     height = size.height * visibleWidth / size.width
                     height = size.height * visibleWidth / size.width
+                    if (pageAnim != PageAnim.scrollPageAnim && height > visibleHeight - durY) {
+                        if (height > visibleHeight) {
+                            width = width * visibleHeight / height
+                            height = visibleHeight
+                        }
+                        prepareNextPageIfNeed(durY + height)
+                    }
                 }
                 }
 
 
                 Book.imgStyleSingle -> {
                 Book.imgStyleSingle -> {

+ 5 - 5
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -103,7 +103,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
     override var sortAscending = true
     override var sortAscending = true
         private set
         private set
     private var snackBar: Snackbar? = null
     private var snackBar: Snackbar? = null
-    private var showDuplicationSource = false
+    private var groupSourcesByDomain = false
     private val hostMap = hashMapOf<String, String>()
     private val hostMap = hashMapOf<String, String>()
     private val qrResult = registerForActivityResult(QrCodeResult()) {
     private val qrResult = registerForActivityResult(QrCodeResult()) {
         it ?: return@registerForActivityResult
         it ?: return@registerForActivityResult
@@ -256,9 +256,9 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 searchView.setQuery(getString(R.string.disabled_explore), true)
                 searchView.setQuery(getString(R.string.disabled_explore), true)
             }
             }
 
 
-            R.id.menu_show_same_source -> {
+            R.id.menu_group_sources_by_domain -> {
                 item.isChecked = !item.isChecked
                 item.isChecked = !item.isChecked
-                showDuplicationSource = item.isChecked
+                groupSourcesByDomain = item.isChecked
                 adapter.showSourceHost = item.isChecked
                 adapter.showSourceHost = item.isChecked
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
@@ -334,7 +334,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 }
                 }
             }.map { data ->
             }.map { data ->
                 hostMap.clear()
                 hostMap.clear()
-                if (showDuplicationSource) {
+                if (groupSourcesByDomain) {
                     data.sortedWith(
                     data.sortedWith(
                         compareBy<BookSourcePart> { getSourceHost(it.bookSourceUrl) == "#" }
                         compareBy<BookSourcePart> { getSourceHost(it.bookSourceUrl) == "#" }
                             .thenBy { getSourceHost(it.bookSourceUrl) }
                             .thenBy { getSourceHost(it.bookSourceUrl) }
@@ -388,7 +388,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
             }.flowOn(IO).conflate().collect { data ->
             }.flowOn(IO).conflate().collect { data ->
                 adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking)
                 adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking)
                 itemTouchCallback.isCanDrag =
                 itemTouchCallback.isCanDrag =
-                    sort == BookSourceSort.Default && !showDuplicationSource
+                    sort == BookSourceSort.Default && !groupSourcesByDomain
                 delay(500)
                 delay(500)
             }
             }
         }
         }

+ 2 - 1
app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt

@@ -55,6 +55,7 @@ import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import splitties.init.appCtx
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.coroutineContext
 import androidx.core.content.edit
 import androidx.core.content.edit
+import kotlinx.coroutines.currentCoroutineContext
 
 
 class BackupConfigFragment : PreferenceFragment(),
 class BackupConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener,
     SharedPreferences.OnSharedPreferenceChangeListener,
@@ -363,7 +364,7 @@ class BackupConfigFragment : PreferenceFragment(),
             context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
             context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
         }
         }
         if (names.isNotEmpty()) {
         if (names.isNotEmpty()) {
-            coroutineContext.ensureActive()
+            currentCoroutineContext().ensureActive()
             withContext(Main) {
             withContext(Main) {
                 context.selector(
                 context.selector(
                     title = context.getString(R.string.select_restore_file),
                     title = context.getString(R.string.select_restore_file),

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

@@ -285,6 +285,9 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
      * 备份同步
      * 备份同步
      */
      */
     private fun backupSync() {
     private fun backupSync() {
+        if (!AppConfig.autoCheckNewBackup) {
+            return
+        }
         lifecycleScope.launch {
         lifecycleScope.launch {
             val lastBackupFile =
             val lastBackupFile =
                 withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch
                 withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch

+ 12 - 0
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt

@@ -10,6 +10,8 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.AppCacheManager
 import io.legado.app.help.AppCacheManager
 import io.legado.app.help.RuleComplete
 import io.legado.app.help.RuleComplete
 import io.legado.app.help.http.CookieStore
 import io.legado.app.help.http.CookieStore
+import io.legado.app.help.source.removeSortCache
+import io.legado.app.model.SharedJsScope
 import io.legado.app.utils.GSON
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getClipText
 import io.legado.app.utils.getClipText
@@ -41,6 +43,16 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
             if (source.sourceName.isBlank() || source.sourceName.isBlank()) {
             if (source.sourceName.isBlank() || source.sourceName.isBlank()) {
                 throw NoStackTraceException(context.getString(R.string.non_null_name_url))
                 throw NoStackTraceException(context.getString(R.string.non_null_name_url))
             }
             }
+            val oldSource = rssSource ?: RssSource()
+            if (!source.equal(oldSource)) {
+                source.lastUpdateTime = System.currentTimeMillis()
+                if (oldSource.sortUrl != source.sortUrl) {
+                    oldSource.removeSortCache()
+                }
+                if (oldSource.jsLib != source.jsLib) {
+                    SharedJsScope.remove(oldSource.jsLib)
+                }
+            }
             rssSource?.let {
             rssSource?.let {
                 appDb.rssSourceDao.delete(it)
                 appDb.rssSourceDao.delete(it)
                 //更新收藏的源地址
                 //更新收藏的源地址

+ 3 - 3
app/src/main/java/io/legado/app/utils/ACache.kt

@@ -457,7 +457,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
      * title 时间计算工具类
      * title 时间计算工具类
      */
      */
     private object Utils {
     private object Utils {
-
+        @Suppress("ConstPropertyName")
         private const val mSeparator = ' '
         private const val mSeparator = ' '
 
 
         /**
         /**
@@ -762,8 +762,8 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
 
 
                 var fileSize: Long = 0
                 var fileSize: Long = 0
                 if (mostLongUsedFile != null) {
                 if (mostLongUsedFile != null) {
-                    fileSize = calculateSize(mostLongUsedFile!!)
-                    if (mostLongUsedFile!!.delete()) {
+                    fileSize = calculateSize(mostLongUsedFile)
+                    if (mostLongUsedFile.delete()) {
                         lastUsageDates.remove(mostLongUsedFile)
                         lastUsageDates.remove(mostLongUsedFile)
                     }
                     }
                 }
                 }

+ 5 - 1
app/src/main/java/io/legado/app/utils/FileDocExtensions.kt

@@ -81,6 +81,10 @@ data class FileDoc(
             return fromUri(path.toUri(), false)
             return fromUri(path.toUri(), false)
         }
         }
 
 
+        fun fromDir(uri: Uri): FileDoc {
+            return fromUri(uri, true)
+        }
+
         fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
         fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
             if (uri.isContentScheme()) {
             if (uri.isContentScheme()) {
                 val doc = if (isDir) {
                 val doc = if (isDir) {
@@ -92,7 +96,7 @@ data class FileDoc(
                         if (it.moveToFirst()) {
                         if (it.moveToFirst()) {
                             val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
                             val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
                             val lUri = it.getString(lUriColum)
                             val lUri = it.getString(lUriColum)
-                            DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!!
+                            DocumentFile.fromSingleUri(appCtx, lUri.toUri())!!
                         } else {
                         } else {
                             DocumentFile.fromSingleUri(appCtx, uri)!!
                             DocumentFile.fromSingleUri(appCtx, uri)!!
                         }
                         }

+ 2 - 2
app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt

@@ -241,7 +241,7 @@ object ZipUtils {
             if (!entryFile.canonicalPath.startsWith(dir.canonicalPath)) {
             if (!entryFile.canonicalPath.startsWith(dir.canonicalPath)) {
                 throw SecurityException("压缩文件只能解压到指定路径")
                 throw SecurityException("压缩文件只能解压到指定路径")
             }
             }
-            if (entry!!.isDirectory) {
+            if (entry.isDirectory) {
                 if (!entryFile.exists()) {
                 if (!entryFile.exists()) {
                     entryFile.mkdirs()
                     entryFile.mkdirs()
                 }
                 }
@@ -286,7 +286,7 @@ object ZipUtils {
             if (entry!!.isDirectory) {
             if (entry!!.isDirectory) {
                 continue
                 continue
             }
             }
-            val fileName = entry!!.name
+            val fileName = entry.name
             if (filter != null && filter.invoke(fileName))
             if (filter != null && filter.invoke(fileName))
                 fileNames.add(fileName)
                 fileNames.add(fileName)
         }
         }

+ 2 - 2
app/src/main/res/menu/book_source.xml

@@ -129,9 +129,9 @@
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
     <item
     <item
-        android:id="@+id/menu_show_same_source"
+        android:id="@+id/menu_group_sources_by_domain"
         android:checkable="true"
         android:checkable="true"
-        android:title="@string/show_same_source"
+        android:title="@string/group_sources_by_domain"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
     <item
     <item

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

@@ -1180,7 +1180,7 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
-    <string name="show_same_source">显示重复书源</string>
+    <string name="group_sources_by_domain">按域名分组显示</string>
     <string name="theme_config">主题配置</string>
     <string name="theme_config">主题配置</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1212,4 +1212,6 @@
     <string name="refresh_explore">刷新发现</string>
     <string name="refresh_explore">刷新发现</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
+    <string name="auto_check_new_backup_t">自动检查新备份</string>
+    <string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
 </resources>
 </resources>

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

@@ -1182,7 +1182,7 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
-    <string name="show_same_source">显示重复书源</string>
+    <string name="group_sources_by_domain">按域名分组显示</string>
     <string name="theme_config">主题配置</string>
     <string name="theme_config">主题配置</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1214,4 +1214,6 @@
     <string name="refresh_explore">刷新发现</string>
     <string name="refresh_explore">刷新发现</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
+    <string name="auto_check_new_backup_t">自动检查新备份</string>
+    <string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
 </resources>
 </resources>

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

@@ -1185,7 +1185,7 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
-    <string name="show_same_source">显示重复书源</string>
+    <string name="group_sources_by_domain">按域名分组显示</string>
     <string name="theme_config">主题配置</string>
     <string name="theme_config">主题配置</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="show_manga_ui">漫画浏览</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
     <string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1344,6 +1344,7 @@
     <string name="underline_height">线段高度</string>
     <string name="underline_height">线段高度</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
+    <string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
     <string name="replace_purify_badge">计数替换净化</string>
     <string name="replace_purify_badge">计数替换净化</string>
     <string name="page_anim_fade">渐变</string>
     <string name="page_anim_fade">渐变</string>
     <string name="show_bookshelf_tab_menu">显示更多分组按钮</string>
     <string name="show_bookshelf_tab_menu">显示更多分组按钮</string>
@@ -1362,4 +1363,5 @@
     <string name="enable_slider_vibrator">滑动进度条时震动</string>
     <string name="enable_slider_vibrator">滑动进度条时震动</string>
     <string name="edit_remark">编辑备注</string>
     <string name="edit_remark">编辑备注</string>
     <string name="book_remark">书籍备注</string>
     <string name="book_remark">书籍备注</string>
+    <string name="auto_check_new_backup_t">自动检查新备份</string>
 </resources>
 </resources>

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

@@ -1188,7 +1188,7 @@
     <string name="read_aloud_read_phone_state_permission_rationale">Reading requires phone state access to pause reading aloud during calls</string>
     <string name="read_aloud_read_phone_state_permission_rationale">Reading requires phone state access to pause reading aloud during calls</string>
     <string name="read_aloud_by_media_button_title">Start Read Aloud via Headset Button</string>
     <string name="read_aloud_by_media_button_title">Start Read Aloud via Headset Button</string>
     <string name="read_aloud_by_media_button_summary">Use headset button to start reading aloud</string>
     <string name="read_aloud_by_media_button_summary">Use headset button to start reading aloud</string>
-    <string name="show_same_source">Show Duplicate Sources</string>
+    <string name="group_sources_by_domain">按域名分组显示</string>
     <string name="theme_config">Theme Configuration</string>
     <string name="theme_config">Theme Configuration</string>
     <string name="show_manga_ui">Manga Viewer</string>
     <string name="show_manga_ui">Manga Viewer</string>
     <string name="disable_manga_scale">Disable Manga Zoom</string>
     <string name="disable_manga_scale">Disable Manga Zoom</string>
@@ -1347,6 +1347,8 @@
     <string name="underline_height">线段高度</string>
     <string name="underline_height">线段高度</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
     <string name="title_bar_mode">菜单标题显示方式</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
     <string name="sure_cache_book">是否确认开始缓存?</string>
+    <string name="auto_check_new_backup_t">自动检查新备份</string>
+    <string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
     <string name="replace_purify_badge">计数替换净化</string>
     <string name="replace_purify_badge">计数替换净化</string>
     <string name="page_anim_fade">渐变</string>
     <string name="page_anim_fade">渐变</string>
     <string name="show_bookshelf_tab_menu">显示更多分组按钮</string>
     <string name="show_bookshelf_tab_menu">显示更多分组按钮</string>

+ 9 - 0
app/src/main/res/xml/pref_config_backup.xml

@@ -60,6 +60,15 @@
             app:allowDividerBelow="false"
             app:allowDividerBelow="false"
             app:iconSpaceReserved="false" />
             app:iconSpaceReserved="false" />
 
 
+        <io.legado.app.lib.prefs.SwitchPreference
+            android:defaultValue="true"
+            android:key="autoCheckNewBackup"
+            android:summary="@string/auto_check_new_backup_s"
+            android:title="@string/auto_check_new_backup_t"
+            app:allowDividerAbove="false"
+            app:allowDividerBelow="false"
+            app:iconSpaceReserved="false" />
+
     </io.legado.app.lib.prefs.PreferenceCategory>
     </io.legado.app.lib.prefs.PreferenceCategory>
 
 
     <io.legado.app.lib.prefs.PreferenceCategory
     <io.legado.app.lib.prefs.PreferenceCategory

+ 11 - 11
gradle/libs.versions.toml

@@ -4,18 +4,18 @@ accompanistWebview = "0.36.0"
 animation = "1.10.0"
 animation = "1.10.0"
 biometric = "1.2.0"
 biometric = "1.2.0"
 coilCompose = "2.7.0"
 coilCompose = "2.7.0"
-composeBom = "2025.12.00"
+composeBom = "2025.12.01"
 constraintlayoutCompose = "1.1.1"
 constraintlayoutCompose = "1.1.1"
 coreSplashscreen = "1.2.0"
 coreSplashscreen = "1.2.0"
 datastorePreferences = "1.2.0"
 datastorePreferences = "1.2.0"
 foundation = "1.10.0"
 foundation = "1.10.0"
-kotlin = "2.2.21"
+kotlin = "2.3.0"
 kotlinxCoroutinesAndroid = "1.10.2"
 kotlinxCoroutinesAndroid = "1.10.2"
 ksp = "2.2.21-2.0.4"
 ksp = "2.2.21-2.0.4"
-agp = "8.13.1"
+agp = "8.13.2"
 appcompat = "1.7.1"
 appcompat = "1.7.1"
 colorpicker = "1.1.0"
 colorpicker = "1.1.0"
-commonsText = "1.14.0"
+commonsText = "1.15.0"
 constraintlayout = "2.2.1"
 constraintlayout = "2.2.1"
 core = "1.17.0"
 core = "1.17.0"
 firebaseBom = "34.6.0"
 firebaseBom = "34.6.0"
@@ -39,25 +39,25 @@ lifecycleViewmodelCompose = "2.10.0"
 liveeventbus = "1.8.14"
 liveeventbus = "1.8.14"
 markdownCompose = "0.5.0"
 markdownCompose = "0.5.0"
 markwon = "4.6.2"
 markwon = "4.6.2"
-material = "1.14.0-alpha07"
-material3 = "1.5.0-alpha10"
+material = "1.14.0-alpha08"
+material3 = "1.5.0-alpha11"
 material3IconsExtended = "1.2.0"
 material3IconsExtended = "1.2.0"
 materialIconsExtended = "1.7.8"
 materialIconsExtended = "1.7.8"
 media = "1.7.1"
 media = "1.7.1"
 media3 = "1.8.0"
 media3 = "1.8.0"
 nanoHttpd = "2.3.1"
 nanoHttpd = "2.3.1"
 navigationCompose = "2.9.6"
 navigationCompose = "2.9.6"
-okhttp = "5.2.0"
+okhttp = "5.3.2"
 palette = "1.0.0"
 palette = "1.0.0"
 preference = "1.2.1"
 preference = "1.2.1"
 protobufJavalite = "4.26.1"
 protobufJavalite = "4.26.1"
 quickChineseTransfer = "0.2.16"
 quickChineseTransfer = "0.2.16"
-room = "2.8.3"
+room = "2.8.4"
 splitties = "3.0.0"
 splitties = "3.0.0"
-rhino = "1.8.0"
+rhino = "1.8.1"
 desugar = "2.1.5"
 desugar = "2.1.5"
 
 
-activity = "1.12.1"
+activity = "1.12.2"
 #kotlinxSerialization = "1.8.0"
 #kotlinxSerialization = "1.8.0"
 startupRuntime = "1.2.0"
 startupRuntime = "1.2.0"
 swiperefreshlayout = "1.2.0"
 swiperefreshlayout = "1.2.0"
@@ -66,7 +66,7 @@ recyclerview = "1.4.0"
 #noinspection GradleDependency
 #noinspection GradleDependency
 uiViewbinding = "1.9.4"
 uiViewbinding = "1.9.4"
 viewpager2 = "1.1.0"
 viewpager2 = "1.1.0"
-webkit = "1.14.0"
+webkit = "1.15.0"
 collection = "1.5.0"
 collection = "1.5.0"
 
 
 zxingLite = "3.3.0"
 zxingLite = "3.3.0"

+ 6 - 6
modules/rhino/src/main/java/com/script/rhino/RhinoScriptEngine.kt

@@ -206,7 +206,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
                 thiz1 = Context.toObject(thiz1, topLevel)
                 thiz1 = Context.toObject(thiz1, topLevel)
             }
             }
             val engineScope = getRuntimeScope(context)
             val engineScope = getRuntimeScope(context)
-            val localScope = if (thiz1 != null) thiz1 as Scriptable else engineScope
+            val localScope = thiz1 ?: engineScope
             val obj = ScriptableObject.getProperty(localScope, name) as? Function
             val obj = ScriptableObject.getProperty(localScope, name) as? Function
                 ?: throw NoSuchMethodException("no such method: $name")
                 ?: throw NoSuchMethodException("no such method: $name")
             var scope = obj.parentScope
             var scope = obj.parentScope
@@ -229,7 +229,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
     override fun <T> getInterface(clazz: Class<T>): T? {
     override fun <T> getInterface(clazz: Class<T>): T? {
         return try {
         return try {
             implementor.getInterface(null, clazz)
             implementor.getInterface(null, clazz)
-        } catch (var3: ScriptException) {
+        } catch (_: ScriptException) {
             null
             null
         }
         }
     }
     }
@@ -240,7 +240,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
         } else {
         } else {
             try {
             try {
                 implementor.getInterface(obj, paramClass)
                 implementor.getInterface(obj, paramClass)
-            } catch (var4: ScriptException) {
+            } catch (_: ScriptException) {
                 null
                 null
             }
             }
         }
         }
@@ -321,7 +321,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
             override fun makeContext(): Context {
             override fun makeContext(): Context {
                 val cx = RhinoContext(this)
                 val cx = RhinoContext(this)
                 cx.languageVersion = Context.VERSION_ES6
                 cx.languageVersion = Context.VERSION_ES6
-                cx.setInterpretedMode(true)
+                cx.isInterpretedMode = true
                 cx.setClassShutter(RhinoClassShutter)
                 cx.setClassShutter(RhinoClassShutter)
                 cx.wrapFactory = RhinoWrapFactory
                 cx.wrapFactory = RhinoWrapFactory
                 cx.instructionObserverThreshold = 10000
                 cx.instructionObserverThreshold = 10000
@@ -393,7 +393,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
         if (System.getSecurityManager() != null) {
         if (System.getSecurityManager() != null) {
             try {
             try {
                 AccessController.checkPermission(AllPermission())
                 AccessController.checkPermission(AllPermission())
-            } catch (var6: AccessControlException) {
+            } catch (_: AccessControlException) {
                 accessContext = AccessController.getContext()
                 accessContext = AccessController.getContext()
             }
             }
         }
         }
@@ -413,7 +413,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
                         obj1 = Context.toObject(obj1, topLevel)
                         obj1 = Context.toObject(obj1, topLevel)
                     }
                     }
                     val engineScope = getRuntimeScope(context)
                     val engineScope = getRuntimeScope(context)
-                    val localScope = if (obj1 != null) obj1 as Scriptable else engineScope
+                    val localScope = obj1 ?: engineScope
                     val methods = clazz.methods
                     val methods = clazz.methods
                     val methodsSize = methods.size
                     val methodsSize = methods.size
                     for (index in 0 until methodsSize) {
                     for (index in 0 until methodsSize) {

+ 2 - 0
modules/web/src/components/ToolBar.vue

@@ -68,6 +68,7 @@
 import API from '@api'
 import API from '@api'
 import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
 import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
 import hotkeys from 'hotkeys-js'
 import hotkeys from 'hotkeys-js'
+import { getSourceName, isInvaildSource, normalizeSource } from '../utils/souce'
 import { getSourceName, isInvaildSource } from '../utils/souce'
 import { getSourceName, isInvaildSource } from '../utils/souce'
 
 
 const store = useSourceStore()
 const store = useSourceStore()
@@ -168,6 +169,7 @@ const redo = () => {
 const saveSource = () => {
 const saveSource = () => {
   const source = store.currentSource
   const source = store.currentSource
   if (isInvaildSource(source)) {
   if (isInvaildSource(source)) {
+    normalizeSource(source)
     API.saveSource(source).then(({ data }) => {
     API.saveSource(source).then(({ data }) => {
       const sourceName = getSourceName(source)
       const sourceName = getSourceName(source)
       if (data.isSuccess) {
       if (data.isSuccess) {

+ 4 - 4
modules/web/src/store/sourceStore.ts

@@ -13,8 +13,8 @@ const emptySource = isBookSource ? emptyBookSource : emptyRssSource
 export const useSourceStore = defineStore('source', {
 export const useSourceStore = defineStore('source', {
   state: () => {
   state: () => {
     return {
     return {
-      bookSources: [] as BookSoure[], // 临时存放所有书源,
-      rssSources: [] as RssSource[], // 临时存放所有订阅源
+      bookSources: shallowRef([] as BookSoure[]), // 临时存放所有书源,
+      rssSources: shallowRef([] as RssSource[]), // 临时存放所有订阅源
       savedSources: [] as Source[], // 批量保存到阅读app成功的源
       savedSources: [] as Source[], // 批量保存到阅读app成功的源
       currentSource: JSON.parse(JSON.stringify(emptySource)) as Source, // 当前编辑的源
       currentSource: JSON.parse(JSON.stringify(emptySource)) as Source, // 当前编辑的源
       currentTab: localStorage.getItem('tabName') || 'editTab',
       currentTab: localStorage.getItem('tabName') || 'editTab',
@@ -51,9 +51,9 @@ export const useSourceStore = defineStore('source', {
     //拉取源后保存
     //拉取源后保存
     saveSources(data: Source[]) {
     saveSources(data: Source[]) {
       if (isBookSource) {
       if (isBookSource) {
-        this.bookSources = data as BookSoure[]
+        this.bookSources = markRaw(data) as BookSoure[]
       } else {
       } else {
-        this.rssSources = data as RssSource[]
+        this.rssSources = markRaw(data) as RssSource[]
       }
       }
     },
     },
     //批量推送
     //批量推送

+ 16 - 1
modules/web/src/utils/souce.ts

@@ -49,12 +49,27 @@ export const convertSourcesToMap = (sources: Source[]): Map<string, Source> => {
   return map
   return map
 }
 }
 
 
+export const normalizeSource = (source: any) => {
+  for (const key in source) {
+    const value = source[key]
+    if (
+      value === '' ||
+      value === null ||
+      (typeof value === 'string' && !value.trim())
+    ) {
+      delete source[key]
+    } else if (value instanceof Object) {
+      normalizeSource(value)
+    }
+  }
+}
+
 export const emptyBookSource = {
 export const emptyBookSource = {
   ruleSearch: {},
   ruleSearch: {},
   ruleBookInfo: {},
   ruleBookInfo: {},
   ruleToc: {},
   ruleToc: {},
   ruleContent: {},
   ruleContent: {},
-  ruleReview: {},
+  //ruleReview: {},
   ruleExplore: {},
   ruleExplore: {},
 } as BookSoure
 } as BookSoure
 export const emptyRssSource = {} as RssSource
 export const emptyRssSource = {} as RssSource