Przeglądaj źródła

Merge remote-tracking branch 'source/master'

谢意帆 4 lat temu
rodzic
commit
2f06e144e3
100 zmienionych plików z 2761 dodań i 640 usunięć
  1. 1 0
      README.md
  2. 66 38
      api.md
  3. 6 4
      app/build.gradle
  4. 1654 0
      app/schemas/io.legado.app.data.AppDatabase/49.json
  5. 2 1
      app/src/main/AndroidManifest.xml
  6. 4 0
      app/src/main/assets/help/ruleHelp.md
  7. 19 0
      app/src/main/assets/updateLog.md
  8. 1 1
      app/src/main/assets/web/bookSource/index.html
  9. 0 0
      app/src/main/assets/web/bookshelf/css/about.65a00131.css
  10. 0 0
      app/src/main/assets/web/bookshelf/css/detail.12a39aa7.css
  11. 2 2
      app/src/main/assets/web/bookshelf/index.html
  12. 0 0
      app/src/main/assets/web/bookshelf/js/about.2456ab2e.js
  13. 0 0
      app/src/main/assets/web/bookshelf/js/about.66694f8c.js
  14. 0 0
      app/src/main/assets/web/bookshelf/js/app.a68b7203.js
  15. 0 0
      app/src/main/assets/web/bookshelf/js/chunk-vendors.f36dda93.js
  16. 0 0
      app/src/main/assets/web/bookshelf/js/detail.08ed5833.js
  17. 0 0
      app/src/main/assets/web/bookshelf/js/detail.8e2caf8f.js
  18. 5 1
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  19. 22 63
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  20. 3 2
      app/src/main/java/io/legado/app/constant/BookType.kt
  21. 1 0
      app/src/main/java/io/legado/app/constant/EventBus.kt
  22. 3 2
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  23. 1 0
      app/src/main/java/io/legado/app/data/entities/BaseSource.kt
  24. 4 0
      app/src/main/java/io/legado/app/data/entities/Book.kt
  25. 6 1
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  26. 7 2
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  27. 2 0
      app/src/main/java/io/legado/app/data/entities/HttpTTS.kt
  28. 4 0
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  29. 2 1
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  30. 11 13
      app/src/main/java/io/legado/app/help/ContentProcessor.kt
  31. 5 9
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  32. 19 15
      app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt
  33. 2 0
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  34. 1 1
      app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt
  35. 3 2
      app/src/main/java/io/legado/app/help/http/BackstageWebView.kt
  36. 4 3
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  37. 6 1
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  38. 17 4
      app/src/main/java/io/legado/app/help/storage/AppWebDav.kt
  39. 6 3
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  40. 18 22
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  41. 2 2
      app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt
  42. 36 0
      app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt
  43. 10 1
      app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt
  44. 1 1
      app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt
  45. 10 1
      app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt
  46. 10 1
      app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt
  47. 3 9
      app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt
  48. 1 1
      app/src/main/java/io/legado/app/lib/prefs/Preference.kt
  49. 1 1
      app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt
  50. 1 1
      app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt
  51. 8 9
      app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt
  52. 1 0
      app/src/main/java/io/legado/app/model/BookCover.kt
  53. 4 4
      app/src/main/java/io/legado/app/model/CacheBook.kt
  54. 6 1
      app/src/main/java/io/legado/app/model/Debug.kt
  55. 14 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  56. 115 5
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  57. 2 2
      app/src/main/java/io/legado/app/model/localBook/README.md
  58. 24 7
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  59. 4 1
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  60. 2 2
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  61. 0 52
      app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt
  62. 19 16
      app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt
  63. 132 0
      app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt
  64. 80 0
      app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt
  65. 4 4
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt
  66. 27 11
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt
  67. 16 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  68. 23 0
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  69. 6 2
      app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt
  70. 22 30
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  71. 2 2
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  72. 2 2
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt
  73. 13 4
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  74. 7 0
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  75. 43 33
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt
  76. 11 2
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt
  77. 2 0
      app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
  78. 1 0
      app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt
  79. 7 21
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  80. 3 3
      app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt
  81. 5 4
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  82. 3 3
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  83. 2 2
      app/src/main/java/io/legado/app/ui/config/WelcomeConfigFragment.kt
  84. 5 4
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  85. 6 6
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  86. 4 2
      app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt
  87. 7 4
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  88. 7 0
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  89. 2 1
      app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt
  90. 4 3
      app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt
  91. 0 22
      app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt
  92. 14 8
      app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt
  93. 12 146
      app/src/main/java/io/legado/app/utils/ColorUtils.kt
  94. 2 1
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  95. 61 0
      app/src/main/java/io/legado/app/utils/RandomColor.kt
  96. 7 5
      app/src/main/java/io/legado/app/utils/RegexExtensions.kt
  97. 1 0
      app/src/main/java/io/legado/app/web/HttpServer.kt
  98. 7 0
      app/src/main/res/layout/activity_book_source_edit.xml
  99. 7 0
      app/src/main/res/layout/activity_rss_source_edit.xml
  100. 23 0
      app/src/main/res/layout/item_book_file_import.xml

+ 1 - 0
README.md

@@ -54,6 +54,7 @@ Legado is a free and open source novel reader for Android.
 * [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
 * [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
 * [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
+* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
 
 
 #### IOS-苹果

+ 66 - 38
api.md

@@ -1,30 +1,37 @@
 # 阅读API
+
 ## 对于Web的配置
-您需要先在设置中启用"Web 服务"。  
+
+您需要先在设置中启用"Web 服务"。
+
 ## 使用
+
 ### Web
+
 以下说明假设您的操作在本机进行,且开放端口为1234。  
 如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。
+
 #### 插入单个书源
+
+请求BODY内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
+
 ```
 URL = http://127.0.0.1:1234/saveSource
 Method = POST
 ```
 
-请求BODY内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
-
 #### 插入多个书源or订阅源
 
+请求BODY内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+
 ```
 URL = http://127.0.0.1:1234/saveBookSources
 URL = http://127.0.0.1:1234/saveRssSources
 Method = POST
 ```
 
-请求BODY内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
-
 #### 获取书源
 
 ```
@@ -43,151 +50,172 @@ Method = GET
 
 #### 删除多个书源or订阅源
 
+请求BODY内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+
 ```
 URL = http://127.0.0.1:1234/deleteBookSources
 URL = http://127.0.0.1:1234/deleteRssSources
 Method = POST
 ```
 
+#### 插入书籍
+
 请求BODY内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
 
-#### 插入书籍
 ```
 URL = http://127.0.0.1:1234/saveBook
 Method = POST
 ```
 
-请求BODY内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
-
 #### 获取所有书籍
+
 ```
 URL = http://127.0.0.1:1234/getBookshelf
 Method = GET
 ```
 
-获取APP内的所有书籍。  
+获取APP内的所有书籍。
 
 #### 获取书籍章节列表
+
 ```
 URL = http://127.0.0.1:1234/getChapterList?url=xxx
 Method = GET
 ```
 
-获取指定图书的章节列表。   
+获取指定图书的章节列表。
 
 #### 获取书籍内容
+
 ```
 URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
 Method = GET
 ```
+
 获取指定图书的第`index`章节的文本内容。
 
 #### 获取封面
+
 ```
 URL = http://127.0.0.1:1234/cover?path=xxxxx
 Method = GET
 ```
 
+#### 保存书籍进度
+
+请求BODY内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookProgress.kt)。
+
+```
+URL = http://127.0.0.1:1234/saveBookProgress
+Method = POST
+```
 
 ### Content Provider
+
 * 需声明`io.legado.READ_WRITE`权限
 * `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
 * 以下出现的`providerHost`请自行替换
 
 #### 插入单个书源or订阅源
 
+创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
+
 ```
 URL = content://providerHost/bookSource/insert
 URL = content://providerHost/rssSource/insert
 Method = insert
 ```
 
-创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
-
 #### 插入多个书源or订阅源
 
+创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+
 ```
 URL = content://providerHost/bookSources/insert
 URL = content://providerHost/rssSources/insert
 Method = insert
 ```
 
-创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
-
 #### 获取书源or订阅源
 
+获取指定URL对应的书源信息。  
+用`Cursor.getString(0)`取出返回结果。
+
 ```
 URL = content://providerHost/bookSource/query?url=xxx
 URL = content://providerHost/rssSource/query?url=xxx
 Method = query
 ```
 
-获取指定URL对应的书源信息。  
-用`Cursor.getString(0)`取出返回结果。
-
 #### 获取所有书源or订阅源
 
+获取APP内的所有订阅源。  
+用`Cursor.getString(0)`取出返回结果。
+
 ```
 URL = content://providerHost/bookSources/query
 URL = content://providerHost/rssSources/query
 Method = query
 ```
 
-获取APP内的所有书源。  
-用`Cursor.getString(0)`取出返回结果。
-
 #### 删除多个书源or订阅源
 
+创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+
 ```
 URL = content://providerHost/bookSources/delete
 URL = content://providerHost/rssSources/delete
 Method = delete
 ```
 
+#### 插入书籍
+
 创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
+格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
 
-#### 插入书籍
 ```
 URL = content://providerHost/book/insert
 Method = insert
 ```
 
-创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,  
-格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
-
 #### 获取所有书籍
+
+获取APP内的所有书籍。  
+用`Cursor.getString(0)`取出返回结果。
+
 ```
 URL = content://providerHost/books/query
 Method = query
 ```
 
-获取APP内的所有书籍。  
+#### 获取书籍章节列表
+
+获取指定图书的章节列表。   
 用`Cursor.getString(0)`取出返回结果。
 
-#### 获取书籍章节列表
 ```
 URL = content://providerHost/book/chapter/query?url=xxx
 Method = query
 ```
 
-获取指定图书的章节列表。   
-用`Cursor.getString(0)`取出返回结果。
-
 #### 获取书籍内容
 
+获取指定图书的第`index`章节的文本内容。     
+用`Cursor.getString(0)`取出返回结果。
+
 ```
 URL = content://providerHost/book/content/query?url=xxx&index=1
 Method = query
 ```
-获取指定图书的第`index`章节的文本内容。     
-用`Cursor.getString(0)`取出返回结果。
 
 #### 获取封面
+
 ```
 URL = content://providerHost/book/cover/query?path=xxxx
 Method = query

+ 6 - 4
app/build.gradle

@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-parcelize'
 apply plugin: 'kotlin-kapt'
 apply plugin: 'de.timfreiheit.resourceplaceholders'
-//apply plugin: 'com.google.gms.google-services'
+apply plugin: 'com.google.gms.google-services'
 apply from: 'download.gradle'
 
 static def releaseTime() {
@@ -179,8 +179,10 @@ dependencies {
     androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
 
     //firebase
-    //implementation platform('com.google.firebase:firebase-bom:29.1.0')
-    //implementation 'com.google.firebase:firebase-analytics-ktx'
+    implementation platform('com.google.firebase:firebase-bom:30.0.1')
+    implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
+    implementation platform('com.google.firebase:firebase-bom:30.0.1')
+    implementation 'com.google.firebase:firebase-perf-ktx:20.0.6'
 
     //media
     implementation("androidx.media:media:1.6.0")
@@ -219,7 +221,7 @@ dependencies {
     implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
 
     //Glide
-    def glideVersion = "4.13.1"
+    def glideVersion = "4.13.2"
     implementation("com.github.bumptech.glide:glide:$glideVersion")
     kapt("com.github.bumptech.glide:compiler:$glideVersion")
 

+ 1654 - 0
app/schemas/io.legado.app.data.AppDatabase/49.json

@@ -0,0 +1,1654 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 49,
+    "identityHash": "2a6f5ee3d0ed9ac13f15183a04a4af45",
+    "entities": [
+      {
+        "tableName": "books",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))",
+        "fields": [
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "tocUrl",
+            "columnName": "tocUrl",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "origin",
+            "columnName": "origin",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "originName",
+            "columnName": "originName",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "author",
+            "columnName": "author",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "kind",
+            "columnName": "kind",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customTag",
+            "columnName": "customTag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "coverUrl",
+            "columnName": "coverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customCoverUrl",
+            "columnName": "customCoverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "intro",
+            "columnName": "intro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customIntro",
+            "columnName": "customIntro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "charset",
+            "columnName": "charset",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "group",
+            "columnName": "group",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "latestChapterTitle",
+            "columnName": "latestChapterTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "latestChapterTime",
+            "columnName": "latestChapterTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "lastCheckTime",
+            "columnName": "lastCheckTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "lastCheckCount",
+            "columnName": "lastCheckCount",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "totalChapterNum",
+            "columnName": "totalChapterNum",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "durChapterTitle",
+            "columnName": "durChapterTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "durChapterIndex",
+            "columnName": "durChapterIndex",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "durChapterPos",
+            "columnName": "durChapterPos",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "durChapterTime",
+            "columnName": "durChapterTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "wordCount",
+            "columnName": "wordCount",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "canUpdate",
+            "columnName": "canUpdate",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "originOrder",
+            "columnName": "originOrder",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "readConfig",
+            "columnName": "readConfig",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_books_name_author",
+            "unique": true,
+            "columnNames": [
+              "name",
+              "author"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "book_groups",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))",
+        "fields": [
+          {
+            "fieldPath": "groupId",
+            "columnName": "groupId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "groupName",
+            "columnName": "groupName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "cover",
+            "columnName": "cover",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "show",
+            "columnName": "show",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "groupId"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "book_sources",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))",
+        "fields": [
+          {
+            "fieldPath": "bookSourceUrl",
+            "columnName": "bookSourceUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookSourceName",
+            "columnName": "bookSourceName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookSourceGroup",
+            "columnName": "bookSourceGroup",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "bookSourceType",
+            "columnName": "bookSourceType",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrlPattern",
+            "columnName": "bookUrlPattern",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customOrder",
+            "columnName": "customOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabledExplore",
+            "columnName": "enabledExplore",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "bookSourceComment",
+            "columnName": "bookSourceComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastUpdateTime",
+            "columnName": "lastUpdateTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "respondTime",
+            "columnName": "respondTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "weight",
+            "columnName": "weight",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "exploreUrl",
+            "columnName": "exploreUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleExplore",
+            "columnName": "ruleExplore",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "searchUrl",
+            "columnName": "searchUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleSearch",
+            "columnName": "ruleSearch",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleBookInfo",
+            "columnName": "ruleBookInfo",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleToc",
+            "columnName": "ruleToc",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleContent",
+            "columnName": "ruleContent",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookSourceUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_book_sources_bookSourceUrl",
+            "unique": false,
+            "columnNames": [
+              "bookSourceUrl"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "chapters",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isVolume",
+            "columnName": "isVolume",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "baseUrl",
+            "columnName": "baseUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "index",
+            "columnName": "index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isVip",
+            "columnName": "isVip",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isPay",
+            "columnName": "isPay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "resourceUrl",
+            "columnName": "resourceUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "start",
+            "columnName": "start",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "end",
+            "columnName": "end",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "startFragmentId",
+            "columnName": "startFragmentId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "endFragmentId",
+            "columnName": "endFragmentId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "url",
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_chapters_bookUrl",
+            "unique": false,
+            "columnNames": [
+              "bookUrl"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)"
+          },
+          {
+            "name": "index_chapters_bookUrl_index",
+            "unique": true,
+            "columnNames": [
+              "bookUrl",
+              "index"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "books",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "bookUrl"
+            ],
+            "referencedColumns": [
+              "bookUrl"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "replace_rules",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "group",
+            "columnName": "group",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "pattern",
+            "columnName": "pattern",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "replacement",
+            "columnName": "replacement",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "scope",
+            "columnName": "scope",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "scopeTitle",
+            "columnName": "scopeTitle",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "scopeContent",
+            "columnName": "scopeContent",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "isEnabled",
+            "columnName": "isEnabled",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "isRegex",
+            "columnName": "isRegex",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "timeoutMillisecond",
+            "columnName": "timeoutMillisecond",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "3000"
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "sortOrder",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_replace_rules_id",
+            "unique": false,
+            "columnNames": [
+              "id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "searchBooks",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "origin",
+            "columnName": "origin",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "originName",
+            "columnName": "originName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "author",
+            "columnName": "author",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "kind",
+            "columnName": "kind",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "coverUrl",
+            "columnName": "coverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "intro",
+            "columnName": "intro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "wordCount",
+            "columnName": "wordCount",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "latestChapterTitle",
+            "columnName": "latestChapterTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tocUrl",
+            "columnName": "tocUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "time",
+            "columnName": "time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "originOrder",
+            "columnName": "originOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_searchBooks_bookUrl",
+            "unique": true,
+            "columnNames": [
+              "bookUrl"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)"
+          },
+          {
+            "name": "index_searchBooks_origin",
+            "unique": false,
+            "columnNames": [
+              "origin"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "book_sources",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "origin"
+            ],
+            "referencedColumns": [
+              "bookSourceUrl"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "search_keywords",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))",
+        "fields": [
+          {
+            "fieldPath": "word",
+            "columnName": "word",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "usage",
+            "columnName": "usage",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastUseTime",
+            "columnName": "lastUseTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "word"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_search_keywords_word",
+            "unique": true,
+            "columnNames": [
+              "word"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "cookies",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))",
+        "fields": [
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "cookie",
+            "columnName": "cookie",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "url"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_cookies_url",
+            "unique": true,
+            "columnNames": [
+              "url"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rssSources",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))",
+        "fields": [
+          {
+            "fieldPath": "sourceUrl",
+            "columnName": "sourceUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sourceName",
+            "columnName": "sourceName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sourceIcon",
+            "columnName": "sourceIcon",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sourceGroup",
+            "columnName": "sourceGroup",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sourceComment",
+            "columnName": "sourceComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sortUrl",
+            "columnName": "sortUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "singleUrl",
+            "columnName": "singleUrl",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "articleStyle",
+            "columnName": "articleStyle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ruleArticles",
+            "columnName": "ruleArticles",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleNextPage",
+            "columnName": "ruleNextPage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleTitle",
+            "columnName": "ruleTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rulePubDate",
+            "columnName": "rulePubDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleDescription",
+            "columnName": "ruleDescription",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleImage",
+            "columnName": "ruleImage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleLink",
+            "columnName": "ruleLink",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleContent",
+            "columnName": "ruleContent",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "style",
+            "columnName": "style",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enableJs",
+            "columnName": "enableJs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "loadWithBaseUrl",
+            "columnName": "loadWithBaseUrl",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "customOrder",
+            "columnName": "customOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "sourceUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_rssSources_sourceUrl",
+            "unique": false,
+            "columnNames": [
+              "sourceUrl"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "bookmarks",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))",
+        "fields": [
+          {
+            "fieldPath": "time",
+            "columnName": "time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookName",
+            "columnName": "bookName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookAuthor",
+            "columnName": "bookAuthor",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterPos",
+            "columnName": "chapterPos",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterName",
+            "columnName": "chapterName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookText",
+            "columnName": "bookText",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "time"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_bookmarks_bookName_bookAuthor",
+            "unique": false,
+            "columnNames": [
+              "bookName",
+              "bookAuthor"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rssArticles",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))",
+        "fields": [
+          {
+            "fieldPath": "origin",
+            "columnName": "origin",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sort",
+            "columnName": "sort",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "link",
+            "columnName": "link",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pubDate",
+            "columnName": "pubDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "image",
+            "columnName": "image",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "origin",
+            "link"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rssReadRecords",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))",
+        "fields": [
+          {
+            "fieldPath": "record",
+            "columnName": "record",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "record"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rssStars",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))",
+        "fields": [
+          {
+            "fieldPath": "origin",
+            "columnName": "origin",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sort",
+            "columnName": "sort",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "starTime",
+            "columnName": "starTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "link",
+            "columnName": "link",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pubDate",
+            "columnName": "pubDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "image",
+            "columnName": "image",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "origin",
+            "link"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "txtTocRules",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "rule",
+            "columnName": "rule",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "serialNumber",
+            "columnName": "serialNumber",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enable",
+            "columnName": "enable",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "readRecord",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))",
+        "fields": [
+          {
+            "fieldPath": "deviceId",
+            "columnName": "deviceId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookName",
+            "columnName": "bookName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "readTime",
+            "columnName": "readTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "deviceId",
+            "bookName"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "httpTTS",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "contentType",
+            "columnName": "contentType",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false,
+            "defaultValue": "'0'"
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastUpdateTime",
+            "columnName": "lastUpdateTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "caches",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))",
+        "fields": [
+          {
+            "fieldPath": "key",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value",
+            "columnName": "value",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "deadline",
+            "columnName": "deadline",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_caches_key",
+            "unique": true,
+            "columnNames": [
+              "key"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "ruleSubs",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "customOrder",
+            "columnName": "customOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "autoUpdate",
+            "columnName": "autoUpdate",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "update",
+            "columnName": "update",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "keyboardAssists",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))",
+        "fields": [
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "key",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "value",
+            "columnName": "value",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "serialNo",
+            "columnName": "serialNo",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "type",
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2a6f5ee3d0ed9ac13f15183a04a4af45')"
+    ]
+  }
+}

+ 2 - 1
app/src/main/AndroidManifest.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+    xmlns:tools="http://schemas.android.com/tools"
+    package="io.legado.app">
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

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

@@ -10,7 +10,11 @@
 @Json: json规则,直接写时以$.开头可省略@Json
 : regex规则,不可省略,只可以用在书籍列表和目录列表
 ```
+* 书源类型: 文件
+> 对于类似知轩藏书提供文件整合下载的网站,可以'在书源详情的下载URL规则获取文件链接,支持多个链接,阅读会自动下载并导入
 
+* CookieJar
+> 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站
 * 登录UI
 > 不使用内置webView登录网站,需要使用`登录URL`规则实现登录逻辑,可使用`登录检查JS`检查登录结果
 ```

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

@@ -11,6 +11,25 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
+**2022/05/16**
+
+* 添加firebase性能监测
+* 清除cookie时清除webView的cookie
+
+**2022/05/15**
+
+* 源编辑添加cookieJar选项
+* 源编辑菜单里添加清除cookie,如果之前能用的书源不能用了,可以关闭cookieJar后点下菜单里的清除cookie后再试
+* 书源支持文件类型 by Xwite
+* 启用firebase收集崩溃日志
+* web端阅读实现无限滚动 by Xwite
+* 进入阅读界面时如果本地书籍有更新自动刷新目录
+
+**2022/05/11**
+
+* 修复替换报错的bug
+* 优化目录界面替换
+
 **2022/05/10**
 
 * 更新cronet: 101.0.4951.61

+ 1 - 1
app/src/main/assets/web/bookSource/index.html

@@ -25,7 +25,7 @@
             <div>
                 <div>源类型 :</div>
                 <textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
-                          placeholder="&lt;必填&gt;0:文本 1:音频 2:图片"></textarea>
+                          placeholder="&lt;必填&gt;0:文本 1:音频 2:图片 3:文件(只提供下载的网站)"></textarea>
             </div>
             <div>
                 <div>源名称 :</div>

Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/css/about.65a00131.css


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/css/detail.12a39aa7.css


+ 2 - 2
app/src/main/assets/web/bookshelf/index.html

@@ -1,3 +1,3 @@
-<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.b93f38f9.css" rel="prefetch"><link href="css/detail.7f9a50b2.css" rel="prefetch"><link href="js/about.66694f8c.js" rel="prefetch"><link href="js/detail.08ed5833.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.9f1876f9.js" rel="preload" as="script"><link href="js/chunk-vendors.7abad833.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
+<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.12a39aa7.css" rel="prefetch"><link href="js/about.2456ab2e.js" rel="prefetch"><link href="js/detail.8e2caf8f.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.a68b7203.js" rel="preload" as="script"><link href="js/chunk-vendors.f36dda93.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
         display: none;
-    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.7abad833.js"></script><script src="js/app.9f1876f9.js"></script></body></html>
+    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.f36dda93.js"></script><script src="js/app.a68b7203.js"></script></body></html>

Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/about.2456ab2e.js


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/about.66694f8c.js


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/app.a68b7203.js


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/chunk-vendors.f36dda93.js


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/detail.08ed5833.js


Plik diff jest za duży
+ 0 - 0
app/src/main/assets/web/bookshelf/js/detail.8e2caf8f.js


+ 5 - 1
app/src/main/java/io/legado/app/api/ReaderProvider.kt

@@ -21,7 +21,8 @@ class ReaderProvider : ContentProvider() {
     private enum class RequestCode {
         SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
         SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
-        SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover
+        SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover,
+        SaveBookProgress
     }
 
     private val postBodyKey = "json"
@@ -86,6 +87,9 @@ class ReaderProvider : ContentProvider() {
             RequestCode.SaveBook -> values?.let {
                 BookController.saveBook(values.getAsString(postBodyKey))
             }
+            RequestCode.SaveBookProgress -> values?.let {
+                BookController.saveBookProgress(values.getAsString(postBodyKey))
+            }
             else -> throw IllegalStateException(
                 "Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
             )

+ 22 - 63
app/src/main/java/io/legado/app/api/controller/BookController.kt

@@ -1,33 +1,26 @@
 package io.legado.app.api.controller
 
-import android.net.Uri
-import android.util.Base64
 import androidx.core.graphics.drawable.toBitmap
-import androidx.documentfile.provider.DocumentFile
 import io.legado.app.api.ReturnData
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookProgress
 import io.legado.app.data.entities.BookSource
 import io.legado.app.help.BookHelp
 import io.legado.app.help.CacheManager
 import io.legado.app.help.ContentProcessor
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.help.storage.AppWebDav
 import io.legado.app.model.BookCover
 import io.legado.app.model.ReadBook
-import io.legado.app.model.localBook.EpubFile
 import io.legado.app.model.localBook.LocalBook
-import io.legado.app.model.localBook.UmdFile
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.ui.book.read.page.provider.ImageProvider
 import io.legado.app.utils.*
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import splitties.init.appCtx
-import java.io.File
-import java.io.FileOutputStream
 
 object BookController {
 
@@ -177,7 +170,6 @@ object BookController {
         var content: String? = BookHelp.getContent(book, chapter)
         if (content != null) {
             val contentProcessor = ContentProcessor.get(book.name, book.origin)
-            saveBookReadIndex(book, index)
             content = runBlocking {
                 contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
                     .joinToString("\n")
@@ -190,7 +182,6 @@ object BookController {
             content = runBlocking {
                 WebBook.getContentAwait(this, bookSource, book, chapter).let {
                     val contentProcessor = ContentProcessor.get(book.name, book.origin)
-                    saveBookReadIndex(book, index)
                     contentProcessor.getContent(book, chapter, it, includeTitle = false)
                         .joinToString("\n")
                 }
@@ -222,20 +213,26 @@ object BookController {
     /**
      * 保存进度
      */
-    private fun saveBookReadIndex(book: Book, index: Int) {
-        book.durChapterIndex = index
-        book.durChapterTime = System.currentTimeMillis()
-        appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
-            book.durChapterTitle = it.title
-        }
-        appDb.bookDao.update(book)
-        AppWebDav.uploadBookProgress(book)
-        if (ReadBook.book?.bookUrl == book.bookUrl) {
-            ReadBook.book = book
-            ReadBook.durChapterIndex = index
-            ReadBook.clearTextChapter()
-            ReadBook.loadContent(true)
-        }
+    fun saveBookProgress(postData: String?): ReturnData {
+        val returnData = ReturnData()
+        GSON.fromJsonObject<BookProgress>(postData)
+            .onFailure { it.printOnDebug() }
+            .getOrNull()?.let { bookProgress ->
+                appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
+                    book.durChapterIndex = bookProgress.durChapterIndex
+                    book.durChapterPos = bookProgress.durChapterPos
+                    book.durChapterTitle = bookProgress.durChapterTitle
+                    book.durChapterTime = bookProgress.durChapterTime
+                    appDb.bookDao.update(book)
+                    AppWebDav.uploadBookProgress(bookProgress)
+                    if (ReadBook.book?.bookUrl == book.bookUrl) {
+                        ReadBook.book = book
+                        ReadBook.durChapterIndex = book.durChapterIndex
+                    }
+                    return returnData.setData("")
+                }
+            }
+        return returnData.setErrorMsg("格式不对")
     }
 
     /**
@@ -248,45 +245,7 @@ object BookController {
         val fileData = parameters["fileData"]?.firstOrNull()
             ?: return returnData.setErrorMsg("fileData 不能为空")
         kotlin.runCatching {
-            val defaultBookTreeUri = AppConfig.defaultBookTreeUri
-            if (defaultBookTreeUri.isNullOrBlank()) return returnData.setErrorMsg("没有设置书籍保存位置!")
-            val treeUri = Uri.parse(defaultBookTreeUri)
-            val fileBytes =
-                Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
-            val uri = if (treeUri.isContentScheme()) {
-                val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
-                var doc = treeDoc!!.findFile(fileName)
-                if (doc == null) {
-                    doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
-                        ?: throw SecurityException("Permission Denial")
-                }
-                appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
-                    oStream.write(fileBytes)
-                }
-                doc.uri
-            } else {
-                val treeFile = File(treeUri.path!!)
-                val file = treeFile.getFile(fileName)
-                FileOutputStream(file).use { oStream ->
-                    oStream.write(fileBytes)
-                }
-                Uri.fromFile(file)
-            }
-            val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
-            val book = Book(
-                bookUrl = uri.toString(),
-                name = nameAuthor.first,
-                author = nameAuthor.second,
-                originName = fileName,
-                coverUrl = FileUtils.getPath(
-                    appCtx.externalFiles,
-                    "covers",
-                    "${MD5Utils.md5Encode16(uri.toString())}.jpg"
-                )
-            )
-            if (book.isEpub()) EpubFile.upBookInfo(book)
-            if (book.isUmd()) UmdFile.upBookInfo(book)
-            appDb.bookDao.insert(book)
+            LocalBook.importFileOnLine(fileData, fileName)
         }.onFailure {
             return when (it) {
                 is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")

+ 3 - 2
app/src/main/java/io/legado/app/constant/BookType.kt

@@ -5,11 +5,12 @@ import androidx.annotation.IntDef
 object BookType {
     const val default = 0           // 0 文本
     const val audio = 1             // 1 音频
-    const val image = 2             //图片
+    const val image = 2            // 2 图片
+    const val file = 3               // 3 只提供下载服务的网站
     const val local = "loc_book"
 
     @Target(AnnotationTarget.VALUE_PARAMETER)
     @Retention(AnnotationRetention.SOURCE)
-    @IntDef(default, audio, image)
+    @IntDef(default, audio, image, file)
     annotation class Type
 }

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

@@ -28,4 +28,5 @@ object EventBus {
     const val TIP_COLOR = "tipColor"
     const val SOURCE_CHANGED = "sourceChanged"
     const val SEARCH_RESULT = "searchResult"
+    const val BOOK_URL_CHANGED = "bookUrlChanged"
 }

+ 3 - 2
app/src/main/java/io/legado/app/data/AppDatabase.kt

@@ -20,7 +20,7 @@ val appDb by lazy {
 }
 
 @Database(
-    version = 48,
+    version = 49,
     exportSchema = true,
     entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
         ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -32,7 +32,8 @@ val appDb by lazy {
         AutoMigration(from = 44, to = 45),
         AutoMigration(from = 45, to = 46),
         AutoMigration(from = 46, to = 47),
-        AutoMigration(from = 47, to = 48)
+        AutoMigration(from = 47, to = 48),
+        AutoMigration(from = 48, to = 49)
     ]
 )
 abstract class AppDatabase : RoomDatabase() {

+ 1 - 0
app/src/main/java/io/legado/app/data/entities/BaseSource.kt

@@ -21,6 +21,7 @@ interface BaseSource : JsExtensions {
     var loginUrl: String?       // 登录地址
     var loginUi: String?   // 登录UI
     var header: String?         // 请求头
+    var enabledCookieJar: Boolean?    //启用cookieJar
 
     fun getTag(): String
 

+ 4 - 0
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -154,6 +154,10 @@ data class Book(
     @IgnoredOnParcel
     override var tocHtml: String? = null
 
+    @Ignore
+    @IgnoredOnParcel
+    var downloadUrls: List<String>? = null
+
     fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
 
     fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)

+ 6 - 1
app/src/main/java/io/legado/app/data/entities/BookChapter.kt

@@ -7,6 +7,7 @@ import androidx.room.Ignore
 import androidx.room.Index
 import com.github.liuyueyi.quick.transfer.ChineseUtils
 import io.legado.app.R
+import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.data.appDb
 import io.legado.app.exception.RegexTimeoutException
@@ -15,6 +16,7 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.RuleDataInterface
 import io.legado.app.utils.*
+import kotlinx.coroutines.CancellationException
 import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
 import splitties.init.appCtx
@@ -93,7 +95,7 @@ data class BookChapter(
                 2 -> displayTitle = ChineseUtils.s2t(displayTitle)
             }
         }
-        if (useReplace && replaceRules != null) {
+        if (useReplace && replaceRules != null) kotlin.run {
             replaceRules.forEach { item ->
                 if (item.pattern.isNotEmpty()) {
                     try {
@@ -112,7 +114,10 @@ data class BookChapter(
                     } catch (e: RegexTimeoutException) {
                         item.isEnabled = false
                         appDb.replaceRuleDao.update(item)
+                    } catch (e: CancellationException) {
+                        return@run
                     } catch (e: Exception) {
+                        AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e)
                         appCtx.toastOnUi("${item.name}替换出错")
                     }
                 }

+ 7 - 2
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -13,6 +13,7 @@ import kotlinx.parcelize.Parcelize
 import splitties.init.appCtx
 import java.io.InputStream
 
+@Suppress("unused")
 @Parcelize
 @TypeConverters(BookSource.Converters::class)
 @Entity(
@@ -27,7 +28,7 @@ data class BookSource(
     var bookSourceName: String = "",
     // 分组
     var bookSourceGroup: String? = null,
-    // 类型,0 文本,1 音频, 2 图片
+    // 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
     @BookType.Type
     var bookSourceType: Int = 0,
     // 详情页url正则
@@ -38,6 +39,9 @@ data class BookSource(
     var enabled: Boolean = true,
     // 启用发现
     var enabledExplore: Boolean = true,
+    // 启用okhttp CookieJAr 自动保存每次请求的cookie
+    @ColumnInfo(defaultValue = "0")
+    override var enabledCookieJar: Boolean? = false,
     // 并发率
     override var concurrentRate: String? = null,
     // 请求头
@@ -158,7 +162,7 @@ data class BookSource(
 
     fun removeGroup(groups: String): BookSource {
         bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
-            it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
+            it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
             bookSourceGroup = TextUtils.join(",", it)
         }
         return this
@@ -190,6 +194,7 @@ data class BookSource(
                 && equal(bookSourceComment, source.bookSourceComment)
                 && enabled == source.enabled
                 && enabledExplore == source.enabledExplore
+                && enabledCookieJar == source.enabledCookieJar
                 && equal(header, source.header)
                 && loginUrl == source.loginUrl
                 && equal(exploreUrl, source.exploreUrl)

+ 2 - 0
app/src/main/java/io/legado/app/data/entities/HttpTTS.kt

@@ -24,6 +24,8 @@ data class HttpTTS(
     override var loginUrl: String? = null,
     override var loginUi: String? = null,
     override var header: String? = null,
+    @ColumnInfo(defaultValue = "0")
+    override var enabledCookieJar: Boolean? = false,
     var loginCheckJs: String? = null,
     @ColumnInfo(defaultValue = "0")
     var lastUpdateTime: Long = System.currentTimeMillis()

+ 4 - 0
app/src/main/java/io/legado/app/data/entities/RssSource.kt

@@ -1,6 +1,7 @@
 package io.legado.app.data.entities
 
 import android.os.Parcelable
+import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.Index
 import androidx.room.PrimaryKey
@@ -19,6 +20,8 @@ data class RssSource(
     var sourceGroup: String? = null,
     var sourceComment: String? = null,
     var enabled: Boolean = true,
+    @ColumnInfo(defaultValue = "0")
+    override var enabledCookieJar: Boolean? = false,
     override var concurrentRate: String? = null,    //并发率
     override var header: String? = null,            // 请求头
     override var loginUrl: String? = null,          // 登录地址
@@ -150,6 +153,7 @@ data class RssSource(
                     style = doc.readString("$.style"),
                     enableJs = doc.readBool("$.enableJs") ?: true,
                     loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
+                    enabledCookieJar = doc.readBool("$.enabledCookieJar") ?: false,
                     customOrder = doc.readInt("$.customOrder") ?: 0
                 )
             }

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

@@ -16,5 +16,6 @@ data class BookInfoRule(
     var coverUrl: String? = null,
     var tocUrl: String? = null,
     var wordCount: String? = null,
-    var canReName: String? = null
+    var canReName: String? = null,
+    var downloadUrls: String? = null
 ) : Parcelable

+ 11 - 13
app/src/main/java/io/legado/app/help/ContentProcessor.kt

@@ -12,6 +12,7 @@ import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.utils.msg
 import io.legado.app.utils.replace
 import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.CancellationException
 import splitties.init.appCtx
 import java.lang.ref.WeakReference
 import java.util.concurrent.CopyOnWriteArrayList
@@ -135,7 +136,7 @@ class ContentProcessor private constructor(
         var mContent = content
         getContentReplaceRules().forEach { item ->
             if (item.pattern.isNotEmpty()) {
-                kotlin.runCatching {
+                try {
                     mContent = if (item.isRegex) {
                         mContent.replace(
                             item.pattern.toRegex(),
@@ -145,18 +146,15 @@ class ContentProcessor private constructor(
                     } else {
                         mContent.replace(item.pattern, item.replacement)
                     }
-                }.onFailure {
-                    when (it) {
-                        is RegexTimeoutException -> {
-                            item.isEnabled = false
-                            appDb.replaceRuleDao.update(item)
-                            return item.name + it.msg
-                        }
-                        else -> {
-                            AppLog.put("${item.name}替换出错\n${it.localizedMessage}", it)
-                            appCtx.toastOnUi("${item.name}替换出错")
-                        }
-                    }
+                } catch (e: RegexTimeoutException) {
+                    item.isEnabled = false
+                    appDb.replaceRuleDao.update(item)
+                    return item.name + e.msg
+                } catch (e: CancellationException) {
+                    return mContent
+                } catch (e: Exception) {
+                    AppLog.put("${item.name}替换出错\n替换内容\n${mContent}", e)
+                    appCtx.toastOnUi("${item.name}替换出错")
                 }
             }
         }

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

@@ -197,12 +197,10 @@ interface JsExtensions {
      *js实现读取cookie
      */
     fun getCookie(tag: String, key: String? = null): String {
-        val cookie = CookieStore.getCookie(tag)
-        val cookieMap = CookieStore.cookieToMap(cookie)
         return if (key != null) {
-            cookieMap[key] ?: ""
+            CookieStore.getKey(tag, key)
         } else {
-            cookie
+            CookieStore.getCookie(tag)
         }
     }
 
@@ -470,10 +468,8 @@ interface JsExtensions {
      * @return zip指定文件的数据
      */
     fun getZipByteArrayContent(url: String, path: String): ByteArray? {
-        val bytes = if (url.startsWith("http://") || url.startsWith("https://")) {
-            runBlocking {
-                return@runBlocking okHttpClient.newCallResponseBody { url(url) }.bytes()
-            }
+        val bytes = if (url.isAbsUrl()) {
+            AnalyzeUrl(url, source = getSource()).getByteArray()
         } else {
             StringUtils.hexStringToByte(url)
         }
@@ -517,7 +513,7 @@ interface JsExtensions {
                 str.isAbsUrl() -> runBlocking {
                     var x = CacheManager.getByteArray(key)
                     if (x == null) {
-                        x = okHttpClient.newCallResponseBody { url(str) }.bytes()
+                        x = AnalyzeUrl(str, source = getSource()).getByteArray()
                         x.let {
                             CacheManager.put(key, it)
                         }

+ 19 - 15
app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt

@@ -1,43 +1,47 @@
 package io.legado.app.help
 
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.exception.NoStackTraceException
 import io.legado.app.utils.*
 
 object ReplaceAnalyzer {
 
-    fun jsonToReplaceRules(json: String): List<ReplaceRule> {
-        val replaceRules = mutableListOf<ReplaceRule>()
-        val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
-        for (item in items) {
-            val jsonItem = jsonPath.parse(item)
-            jsonToReplaceRule(jsonItem.jsonString())?.let {
-                if (it.isValid()) {
-                    replaceRules.add(it)
+    fun jsonToReplaceRules(json: String): Result<MutableList<ReplaceRule>> {
+        return kotlin.runCatching {
+            val replaceRules = mutableListOf<ReplaceRule>()
+            val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
+            for (item in items) {
+                val jsonItem = jsonPath.parse(item)
+                jsonToReplaceRule(jsonItem.jsonString()).getOrThrow().let {
+                    if (it.isValid()) {
+                        replaceRules.add(it)
+                    }
                 }
             }
+            replaceRules
         }
-        return replaceRules
     }
 
-    private fun jsonToReplaceRule(json: String): ReplaceRule? {
-        val replaceRule: ReplaceRule? = GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
-        runCatching {
+    fun jsonToReplaceRule(json: String): Result<ReplaceRule> {
+        return runCatching {
+            val replaceRule: ReplaceRule? =
+                GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
             if (replaceRule == null || replaceRule.pattern.isBlank()) {
                 val jsonItem = jsonPath.parse(json.trim())
                 val rule = ReplaceRule()
                 rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
                 rule.pattern = jsonItem.readString("$.regex") ?: ""
-                if (rule.pattern.isEmpty()) return null
+                if (rule.pattern.isEmpty()) throw NoStackTraceException("格式不对")
                 rule.name = jsonItem.readString("$.replaceSummary") ?: ""
                 rule.replacement = jsonItem.readString("$.replacement") ?: ""
                 rule.isRegex = jsonItem.readBool("$.isRegex") == true
                 rule.scope = jsonItem.readString("$.useTo")
                 rule.isEnabled = jsonItem.readBool("$.enable") == true
                 rule.order = jsonItem.readInt("$.serialNumber") ?: 0
-                return rule
+                return@runCatching rule
             }
+            return@runCatching replaceRule
         }
-        return replaceRule
     }
 
 }

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

@@ -150,6 +150,7 @@ object SourceAnalyzer {
                 source.customOrder = sourceAny.customOrder
                 source.enabled = sourceAny.enabled
                 source.enabledExplore = sourceAny.enabledExplore
+                source.enabledCookieJar = sourceAny.enabledCookieJar
                 source.concurrentRate = sourceAny.concurrentRate
                 source.header = sourceAny.header
                 source.loginUrl = when (sourceAny.loginUrl) {
@@ -219,6 +220,7 @@ object SourceAnalyzer {
         var customOrder: Int = 0,                       // 手动排序编号
         var enabled: Boolean = true,                    // 是否启用
         var enabledExplore: Boolean = true,             // 启用发现
+        var enabledCookieJar: Boolean = false,             // 启用CookieJar
         var concurrentRate: String? = null,             // 并发率
         var header: String? = null,                     // 请求头
         var loginUrl: Any? = null,                      // 登录规则

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

@@ -32,7 +32,7 @@ object SourceVerificationHelp {
                 startBrowser(source, url, title, true)
             }
 
-            var waitUserInput: Boolean = false
+            var waitUserInput = false
             while(CacheManager.get(key) == null) {
                 if (!waitUserInput) {
                     AppLog.putDebug("等待返回验证结果...")

+ 3 - 2
app/src/main/java/io/legado/app/help/http/BackstageWebView.kt

@@ -18,6 +18,7 @@ import org.apache.commons.text.StringEscapeUtils
 import splitties.init.appCtx
 import java.lang.ref.WeakReference
 import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
 
 /**
  * 后台webView
@@ -50,14 +51,14 @@ class BackstageWebView(
 
             override fun onError(error: Throwable) {
                 if (!block.isCompleted)
-                    block.cancel(error)
+                    block.resumeWithException(error)
             }
         }
         runOnUI {
             try {
                 load()
             } catch (error: Throwable) {
-                block.cancel(error)
+                block.resumeWithException(error)
             }
         }
     }

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

@@ -5,8 +5,8 @@ package io.legado.app.help.http
 import android.text.TextUtils
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Cookie
-import io.legado.app.help.http.api.CookieManager
 import io.legado.app.help.CacheManager
+import io.legado.app.help.http.api.CookieManager
 import io.legado.app.utils.NetworkUtils
 
 object CookieStore : CookieManager {
@@ -44,7 +44,7 @@ object CookieStore : CookieManager {
         CacheManager.getFromMemory("${domain}_cookie")?.let { return it }
         val cookieBean = appDb.cookieDao.get(domain)
         val cookie = cookieBean?.cookie ?: ""
-        CacheManager.putMemory(url, cookie ?: "")
+        CacheManager.putMemory(url, cookie)
         return cookie
     }
 
@@ -56,8 +56,9 @@ object CookieStore : CookieManager {
 
     override fun removeCookie(url: String) {
         val domain = NetworkUtils.getSubDomain(url)
-        CacheManager.deleteMemory("${domain}_cookie")
         appDb.cookieDao.delete(domain)
+        CacheManager.deleteMemory("${domain}_cookie")
+        android.webkit.CookieManager.getInstance().removeAllCookies(null)
     }
 
     override fun cookieToMap(cookie: String): MutableMap<String, String> {

+ 6 - 1
app/src/main/java/io/legado/app/help/http/HttpHelper.kt

@@ -1,9 +1,11 @@
 package io.legado.app.help.http
 
 import io.legado.app.constant.AppConst
+import io.legado.app.help.CacheManager
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.cronet.CronetInterceptor
 import io.legado.app.help.http.cronet.CronetLoader
+import io.legado.app.utils.NetworkUtils
 import okhttp3.*
 import java.net.InetSocketAddress
 import java.net.Proxy
@@ -23,7 +25,10 @@ val cookieJar by lazy {
 
         override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
             cookies.forEach {
-                CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
+                //CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
+                //临时保存 书源启用cookie选项再添加到数据库
+                val domain = NetworkUtils.getSubDomain(url.toString())
+                CacheManager.putMemory("${domain}_cookieJar", "${it.name}=${it.value}")
             }
         }
 

+ 17 - 4
app/src/main/java/io/legado/app/help/storage/AppWebDav.kt

@@ -194,15 +194,28 @@ object AppWebDav {
         Coroutine.async {
             val bookProgress = BookProgress(book)
             val json = GSON.toJson(bookProgress)
-            val url = getProgressUrl(book)
+            val url = getProgressUrl(book.name, book.author)
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
         }.onError {
             AppLog.put("上传进度失败\n${it.localizedMessage}")
         }
     }
 
-    private fun getProgressUrl(book: Book): String {
-        return bookProgressUrl + book.name + "_" + book.author + ".json"
+    fun uploadBookProgress(bookProgress: BookProgress) {
+        val authorization = authorization ?: return
+        if (!syncBookProgress) return
+        if (!NetworkUtils.isAvailable()) return
+        Coroutine.async {
+            val json = GSON.toJson(bookProgress)
+            val url = getProgressUrl(bookProgress.name, bookProgress.author)
+            WebDav(url, authorization).upload(json.toByteArray(), "application/json")
+        }.onError {
+            AppLog.put("上传进度失败\n${it.localizedMessage}")
+        }
+    }
+
+    private fun getProgressUrl(name: String, author: String): String {
+        return bookProgressUrl + name + "_" + author + ".json"
     }
 
     /**
@@ -210,7 +223,7 @@ object AppWebDav {
      */
     suspend fun getBookProgress(book: Book): BookProgress? {
         authorization?.let {
-            val url = getProgressUrl(book)
+            val url = getProgressUrl(book.name, book.author)
             kotlin.runCatching {
                 WebDav(url, it).download().let { byteArray ->
                     val json = String(byteArray)

+ 6 - 3
app/src/main/java/io/legado/app/help/storage/ImportOldData.kt

@@ -98,9 +98,12 @@ object ImportOldData {
     }
 
     private fun importOldReplaceRule(json: String): Int {
-        val rules = ReplaceAnalyzer.jsonToReplaceRules(json)
-        appDb.replaceRuleDao.insert(*rules.toTypedArray())
-        return rules.size
+        val rules = ReplaceAnalyzer.jsonToReplaceRules(json).getOrNull()
+        rules?.let {
+            appDb.replaceRuleDao.insert(*rules.toTypedArray())
+            return rules.size
+        }
+        return 0
     }
 
     private fun fromOldBooks(json: String): List<Book> {

+ 18 - 22
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -22,40 +22,36 @@ import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import java.io.File
 import java.io.FileInputStream
+import java.io.FileOutputStream
 
 
 object Restore {
 
     suspend fun restore(context: Context, path: String) {
-        withContext(IO) {
+        kotlin.runCatching {
             if (path.isContentScheme()) {
                 DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
-                    for (fileName in Backup.backupFileNames) {
-                        if (doc.name == fileName) {
-                            DocumentUtils.readText(context, doc.uri).let {
-                                FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName")
-                                    .writeText(it)
+                    if (Backup.backupFileNames.contains(doc.name)) {
+                        context.contentResolver.openInputStream(doc.uri)?.use { inputStream ->
+                            val file = File("${Backup.backupPath}${File.separator}${doc.name}")
+                            FileOutputStream(file).use { outputStream ->
+                                inputStream.copyTo(outputStream)
                             }
                         }
                     }
                 }
             } else {
-                try {
-                    val file = File(path)
-                    for (fileName in Backup.backupFileNames) {
-                        file.getFile(fileName).let {
-                            if (it.exists()) {
-                                it.copyTo(
-                                    FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName"),
-                                    true
-                                )
-                            }
-                        }
+                val dir = File(path)
+                for (fileName in Backup.backupFileNames) {
+                    val file = dir.getFile(fileName)
+                    if (file.exists()) {
+                        val target = File("${Backup.backupPath}${File.separator}$fileName")
+                        file.copyTo(target, true)
                     }
-                } catch (e: Exception) {
-                    e.printOnDebug()
                 }
             }
+        }.onFailure {
+            AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)
         }
         restoreDatabase()
         restoreConfig()
@@ -129,7 +125,7 @@ object Restore {
                     ThemeConfig.upConfig()
                 }
             } catch (e: Exception) {
-                e.printOnDebug()
+                AppLog.put("恢复主题出错\n${e.localizedMessage}", e)
             }
             if (!BackupConfig.ignoreReadConfig) {
                 //恢复阅读界面配置
@@ -142,7 +138,7 @@ object Restore {
                         ReadBookConfig.initConfigs()
                     }
                 } catch (e: Exception) {
-                    e.printOnDebug()
+                    AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
                 }
                 try {
                     val file =
@@ -153,7 +149,7 @@ object Restore {
                         ReadBookConfig.initShareConfig()
                     }
                 } catch (e: Exception) {
-                    e.printOnDebug()
+                    AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
                 }
             }
             Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt → app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.content.ContextWrapper
@@ -118,7 +118,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
     }
 
     override fun onBindViewHolder(holder: PreferenceViewHolder) {
-        val v = io.legado.app.ui.widget.prefs.Preference.bindView<ColorPanelView>(
+        val v = io.legado.app.lib.prefs.Preference.bindView<ColorPanelView>(
             context, holder, icon, title, summary, widgetLayoutResource,
             io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
         )

+ 36 - 0
app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt

@@ -0,0 +1,36 @@
+package io.legado.app.lib.prefs
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.preference.EditTextPreference.OnBindEditTextListener
+import androidx.preference.PreferenceViewHolder
+import io.legado.app.R
+import io.legado.app.lib.theme.accentColor
+import io.legado.app.utils.applyTint
+
+class EditTextPreference(context: Context, attrs: AttributeSet) :
+    androidx.preference.EditTextPreference(context, attrs) {
+
+    private var mOnBindEditTextListener: OnBindEditTextListener? = null
+    private val onBindEditTextListener = OnBindEditTextListener { editText ->
+        editText.applyTint(context.accentColor)
+        mOnBindEditTextListener?.onBindEditText(editText)
+    }
+
+    init {
+        // isPersistent = true
+        layoutResource = R.layout.view_preference
+        super.setOnBindEditTextListener(onBindEditTextListener)
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
+        super.onBindViewHolder(holder)
+    }
+
+    override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) {
+        mOnBindEditTextListener = onBindEditTextListener
+    }
+
+}

+ 10 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreferenceDialog.kt → app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt

@@ -1,9 +1,11 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.app.Dialog
 import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
 import androidx.preference.EditTextPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.filletBackground
 
 class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
@@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val dialog = super.onCreateDialog(savedInstanceState)
         dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
+        dialog.window?.decorView?.post {
+            (dialog as AlertDialog).run {
+                getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
+            }
+        }
         return dialog
     }
 

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt → app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.content.ContextWrapper

+ 10 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/ListPreferenceDialog.kt → app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt

@@ -1,9 +1,11 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.app.Dialog
 import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
 import androidx.preference.ListPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.filletBackground
 
 class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
@@ -23,6 +25,13 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val dialog = super.onCreateDialog(savedInstanceState)
         dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
+        dialog.window?.decorView?.post {
+            (dialog as AlertDialog).run {
+                getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
+            }
+        }
         return dialog
     }
 

+ 10 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/MultiSelectListPreferenceDialog.kt → app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt

@@ -1,9 +1,11 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.app.Dialog
 import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
 import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.filletBackground
 
 class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
@@ -25,6 +27,13 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val dialog = super.onCreateDialog(savedInstanceState)
         dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
+        dialog.window?.decorView?.post {
+            (dialog as AlertDialog).run {
+                getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
+                getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
+            }
+        }
         return dialog
     }
 

+ 3 - 9
app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt → app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.util.AttributeSet
@@ -25,14 +25,8 @@ class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference
 
     override fun onBindViewHolder(holder: PreferenceViewHolder) {
         val v = Preference.bindView<TextView>(
-            context,
-            holder,
-            icon,
-            title,
-            summary,
-            widgetLayoutResource,
-            R.id.text_view,
-            isBottomBackground = isBottomBackground
+            context, holder, icon, title, summary, widgetLayoutResource,
+            R.id.text_view, isBottomBackground = isBottomBackground
         )
         if (v is TextView) {
             v.text = entry

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt → app/src/main/java/io/legado/app/lib/prefs/Preference.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.graphics.drawable.Drawable

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt → app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.util.AttributeSet

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt → app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.prefs
+package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.util.AttributeSet

+ 8 - 9
app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt → app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt

@@ -1,13 +1,13 @@
-package io.legado.app.base
+package io.legado.app.lib.prefs.fragment
 
 import android.annotation.SuppressLint
 import androidx.fragment.app.DialogFragment
 import androidx.preference.*
-import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
-import io.legado.app.ui.widget.prefs.ListPreferenceDialog
-import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
+import io.legado.app.lib.prefs.EditTextPreferenceDialog
+import io.legado.app.lib.prefs.ListPreferenceDialog
+import io.legado.app.lib.prefs.MultiSelectListPreferenceDialog
 
-abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
+abstract class PreferenceFragment : PreferenceFragmentCompat() {
 
     private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
 
@@ -34,7 +34,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
             return
         }
 
-        val f: DialogFragment = when (preference) {
+        val dialogFragment: DialogFragment = when (preference) {
             is EditTextPreference -> {
                 EditTextPreferenceDialog.newInstance(preference.getKey())
             }
@@ -54,10 +54,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
             }
         }
         @Suppress("DEPRECATION")
-        f.setTargetFragment(this, 0)
+        dialogFragment.setTargetFragment(this, 0)
 
-        f.show(parentFragmentManager, dialogFragmentTag)
+        dialogFragment.show(parentFragmentManager, dialogFragmentTag)
     }
 
-
 }

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

@@ -150,6 +150,7 @@ object BookCover {
         override var loginUrl: String? = null,
         override var loginUi: String? = null,
         override var header: String? = null,
+        override var enabledCookieJar: Boolean? = false,
     ) : BaseSource {
 
         override fun getTag(): String {

+ 4 - 4
app/src/main/java/io/legado/app/model/CacheBook.kt

@@ -131,9 +131,9 @@ object CacheBook {
 
     class CacheBookModel(var bookSource: BookSource, var book: Book) {
 
-        private val waitDownloadSet = hashSetOf<Int>()
-        private val onDownloadSet = hashSetOf<Int>()
-        private val successDownloadSet = hashSetOf<Int>()
+        private val waitDownloadSet = linkedSetOf<Int>()
+        private val onDownloadSet = linkedSetOf<Int>()
+        private val successDownloadSet = linkedSetOf<Int>()
         private val errorDownloadMap = hashMapOf<Int, Int>()
 
         val waitCount get() = waitDownloadSet.size
@@ -174,7 +174,7 @@ object CacheBook {
             }
             onDownloadSet.remove(index)
             //重试3次
-            if (errorDownloadMap[index] ?: 0 < 3) {
+            if ((errorDownloadMap[index] ?: 0) < 3) {
                 waitDownloadSet.add(index)
             } else {
                 AppLog.put(

+ 6 - 1
app/src/main/java/io/legado/app/model/Debug.kt

@@ -2,6 +2,7 @@ package io.legado.app.model
 
 import android.annotation.SuppressLint
 import io.legado.app.constant.AppPattern
+import io.legado.app.constant.BookType
 import io.legado.app.data.entities.*
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.model.rss.Rss
@@ -238,7 +239,11 @@ object Debug {
             .onSuccess {
                 log(debugSource, "︽详情页解析完成")
                 log(debugSource, showTime = false)
-                tocDebug(scope, bookSource, book)
+                if (book.type != BookType.file) {
+                    tocDebug(scope, bookSource, book)
+                } else {
+                    log(debugSource, "≡文件类书源跳过解析目录", state = 1000)
+                }
             }
             .onError {
                 log(debugSource, it.msg, state = -1)

+ 14 - 4
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt

@@ -69,6 +69,7 @@ class AnalyzeUrl(
     private var retry: Int = 0
     private var useWebView: Boolean = false
     private var webJs: String? = null
+    private val enabledCookieJar = source?.enabledCookieJar ?: false
 
     init {
         if (!mUrl.isDataUrl()) {
@@ -519,17 +520,26 @@ class AnalyzeUrl(
     }
 
     /**
-     *设置cookie urlOption的优先级大于书源保存的cookie
+     *设置cookie 优先级
+     * urlOption临时cookie > 数据库cookie = okhttp CookieJar保存在内存中的cookie
      *@param tag 书源url 缺省为传入的url
      */
     private fun setCookie(tag: String?) {
-        val cookie = CookieStore.getCookie(tag ?: url)
+        val domain = NetworkUtils.getSubDomain(tag ?: url)
+        //书源启用保存cookie时 添加内存中的cookie到数据库
+        if (enabledCookieJar) {
+            val key = "${domain}_cookieJar"
+            CacheManager.getFromMemory(key)?.let {
+                CookieStore.replaceCookie(domain, it)
+                CacheManager.deleteMemory(key)
+            }
+        }
+        val cookie = CookieStore.getCookie(domain)
         if (cookie.isNotEmpty()) {
             val cookieMap = CookieStore.cookieToMap(cookie)
             val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
             cookieMap.putAll(customCookieMap)
-            val newCookie = CookieStore.mapToCookie(cookieMap)
-            newCookie?.let {
+            CookieStore.mapToCookie(cookieMap)?.let {
                 headerMap.put("Cookie", it)
             }
         }

+ 115 - 5
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -1,24 +1,29 @@
 package io.legado.app.model.localBook
 
 import android.net.Uri
+import android.util.Base64
 import androidx.documentfile.provider.DocumentFile
 import com.script.SimpleBindings
 import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.data.appDb
+import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
+import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.TocEmptyException
 import io.legado.app.help.BookHelp
 import io.legado.app.help.config.AppConfig
+import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.utils.*
 import splitties.init.appCtx
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.InputStream
+import java.io.*
 import java.util.regex.Pattern
 
+/**
+ * 书籍文件导入 目录正文解析
+ * 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
+ */
 object LocalBook {
 
     private val nameAuthorPatterns = arrayOf(
@@ -41,6 +46,20 @@ object LocalBook {
         throw FileNotFoundException("${uri.path} 文件不存在")
     }
 
+    fun getLastModified(book: Book): Result<Long> {
+        return kotlin.runCatching {
+            val uri = Uri.parse(book.bookUrl)
+            if (uri.isContentScheme()) {
+                return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified()
+            }
+            val file = File(uri.path!!)
+            if (file.exists()) {
+                return@runCatching File(uri.path!!).lastModified()
+            }
+            throw FileNotFoundException("${uri.path} 文件不存在")
+        }
+    }
+
     @Throws(Exception::class)
     fun getChapterList(book: Book): ArrayList<BookChapter> {
         val chapters = when {
@@ -79,6 +98,23 @@ object LocalBook {
         }
     }
 
+    /**
+     * 下载在线的文件并自动导入到阅读(txt umd epub)
+     * 压缩文件请先提示用户解压
+     */
+    fun importFileOnLine(
+        str: String,
+        fileName: String,
+        source: BaseSource? = null,
+    ): Book {
+        return saveBookFile(str, fileName, source).let {
+            importFile(it)
+        }
+    }
+
+    /**
+     * 导入本地文件
+     */
     fun importFile(uri: Uri): Book {
         val bookUrl: String
         val updateTime: Long
@@ -119,7 +155,10 @@ object LocalBook {
         return book
     }
 
-    fun analyzeNameAuthor(fileName: String): Pair<String, String> {
+    /**
+     * 从文件分析书籍必要信息(书名 作者等)
+     */
+    private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
         val tempFileName = fileName.substringBeforeLast(".")
         var name: String
         var author: String
@@ -171,4 +210,75 @@ object LocalBook {
             }
         }
     }
+
+    /**
+     * 下载在线的文件
+     */
+    fun saveBookFile(
+        str: String,
+        fileName: String,
+        source: BaseSource? = null,
+    ): Uri {
+        val bytes = when {
+            str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
+            str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
+            else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
+        }
+        return saveBookFile(bytes, fileName)
+    }
+
+    /**
+     * 分析下载文件类书源的下载链接的文件后缀
+     * https://www.example.com/download/{fileName}.{type} 含有文件名和后缀
+     * https://www.example.com/download/?fileid=1234, {type: "txt"} 规则设置
+     */
+    fun parseFileSuffix(url: String): String {
+        val analyzeUrl = AnalyzeUrl(url)
+        val urlNoOption = analyzeUrl.url
+        val lastPath = urlNoOption.substringAfterLast("/")
+        val fileType = lastPath.substringAfterLast(".")
+        val type = analyzeUrl.type
+        return type ?: fileType
+    }
+
+    private fun saveBookFile(
+        bytes: ByteArray,
+        fileName: String
+    ): Uri {
+        val defaultBookTreeUri = AppConfig.defaultBookTreeUri
+        if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
+        val treeUri = Uri.parse(defaultBookTreeUri)
+        return if (treeUri.isContentScheme()) {
+            val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
+            var doc = treeDoc!!.findFile(fileName)
+            if (doc == null) {
+                doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
+                    ?: throw SecurityException("Permission Denial")
+            }
+            appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
+                oStream.write(bytes)
+            }
+            doc.uri
+        } else {
+            val treeFile = File(treeUri.path!!)
+            val file = treeFile.getFile(fileName)
+            FileOutputStream(file).use { oStream ->
+                oStream.write(bytes)
+            }
+            Uri.fromFile(file)
+        }
+    }
+
+    //文件类书源 合并在线书籍信息 在线 > 本地
+    fun mergeBook(localBook: Book, onLineBook: Book?): Book {
+        onLineBook ?: return localBook
+        localBook.name = onLineBook.name.ifBlank { localBook.name }
+        localBook.author = onLineBook.author.ifBlank { localBook.author }
+        localBook.coverUrl = onLineBook.coverUrl
+        localBook.intro =
+            if (onLineBook.intro.isNullOrBlank()) localBook.intro else onLineBook.intro
+        localBook.save()
+        return localBook
+    }
+
 }

+ 2 - 2
app/src/main/java/io/legado/app/model/localBook/README.md

@@ -1,7 +1,7 @@
-# 本地书籍解析
+# 书籍文件导入解析
 
 * BaseLocalBookParse.kt 本地书籍解析接口
-* LocalBook.kt 总入口
+* LocalBook.kt 导入解析总入口
 * TextFile.kt 解析txt
 * EpubFile.kt 解析epub
 * UmdFile.kt 解析umd

+ 24 - 7
app/src/main/java/io/legado/app/model/webBook/BookInfo.kt

@@ -1,6 +1,8 @@
 package io.legado.app.model.webBook
 
+import android.text.TextUtils
 import io.legado.app.R
+import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
@@ -137,14 +139,29 @@ object BookInfo {
             Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
             DebugLog.e("获取封面出错", e)
         }
-        scope.ensureActive()
-        Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
-        book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
-        if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
-        if (book.tocUrl == baseUrl) {
-            book.tocHtml = body
+        if (book.type != BookType.file) {
+            scope.ensureActive()
+            Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
+            book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
+            if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
+            if (book.tocUrl == baseUrl) {
+                book.tocHtml = body
+            }
+            Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
+        } else {
+            scope.ensureActive()
+            Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接")
+            book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true)
+            if (book.downloadUrls == null) {
+                Debug.log(bookSource.bookSourceUrl, "└")
+                throw NoStackTraceException("下载链接为空")
+            } else {
+                Debug.log(
+                    bookSource.bookSourceUrl,
+                    "└" + TextUtils.join(",\n", book.downloadUrls!!)
+                )
+            }
         }
-        Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
     }
 
 }

+ 4 - 1
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -6,6 +6,7 @@ import com.script.ScriptException
 import io.legado.app.R
 import io.legado.app.base.BaseService
 import io.legado.app.constant.AppConst
+import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.IntentAction
 import io.legado.app.data.appDb
@@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
                     mBook = WebBook.getBookInfoAwait(this, source, mBook)
                 }
                 //校验目录
-                if (CheckSource.checkCategory) {
+                if (CheckSource.checkCategory &&
+                    source.bookSourceType != BookType.file
+                ) {
                     val toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
                     val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
                     //校验正文

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

@@ -38,7 +38,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     private val ttsFolderPath: String by lazy {
         cacheDir.absolutePath + File.separator + "httpTTS" + File.separator
     }
-    private var speechRate: Int = AppConfig.speechRatePlay
+    private var speechRate: Int = AppConfig.speechRatePlay + 5
     private var downloadTask: Coroutine<*>? = null
     private var playIndexJob: Job? = null
     private var downloadTaskIsActive = false
@@ -301,7 +301,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     override fun upSpeechRate(reset: Boolean) {
         downloadTask?.cancel()
         exoPlayer.stop()
-        speechRate = AppConfig.speechRatePlay
+        speechRate = AppConfig.speechRatePlay + 5
         downloadAudio()
     }
 

+ 0 - 52
app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt

@@ -2,16 +2,7 @@ package io.legado.app.ui.association
 
 import android.app.Application
 import androidx.lifecycle.MutableLiveData
-import io.legado.app.R
 import io.legado.app.base.BaseViewModel
-import io.legado.app.data.appDb
-import io.legado.app.data.entities.TxtTocRule
-import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.ThemeConfig
-import io.legado.app.utils.GSON
-import io.legado.app.utils.fromJsonArray
-import io.legado.app.utils.fromJsonObject
-import io.legado.app.utils.isJsonArray
 
 abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
 
@@ -37,47 +28,4 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
         }
     }
 
-    fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) {
-        execute {
-            if (json.isJsonArray()) {
-                GSON.fromJsonArray<TxtTocRule>(json).getOrThrow()?.let {
-                    appDb.txtTocRuleDao.insert(*it.toTypedArray())
-                } ?: throw NoStackTraceException("格式不对")
-            } else {
-                GSON.fromJsonObject<TxtTocRule>(json).getOrThrow()?.let {
-                    appDb.txtTocRuleDao.insert(it)
-                } ?: throw NoStackTraceException("格式不对")
-            }
-        }.onSuccess {
-            finally.invoke(context.getString(R.string.success), "导入Txt规则成功")
-        }.onError {
-            finally.invoke(
-                context.getString(R.string.error),
-                it.localizedMessage ?: context.getString(R.string.unknown_error)
-            )
-        }
-    }
-
-    fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) {
-        execute {
-            if (json.isJsonArray()) {
-                GSON.fromJsonArray<ThemeConfig.Config>(json).getOrThrow()?.forEach {
-                    ThemeConfig.addConfig(it)
-                }
-            } else {
-                GSON.fromJsonObject<ThemeConfig.Config>(json).getOrThrow()?.let {
-                    ThemeConfig.addConfig(it)
-                }
-            }
-        }.onSuccess {
-            finally.invoke(context.getString(R.string.success), "导入主题成功")
-        }.onError {
-            finally.invoke(
-                context.getString(R.string.error),
-                it.localizedMessage ?: context.getString(R.string.unknown_error)
-            )
-        }
-    }
-
-
 }

+ 19 - 16
app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt

@@ -61,21 +61,7 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
 
     fun importSource(text: String) {
         execute {
-            val mText = text.trim()
-            when {
-                mText.isJsonObject() -> {
-                    HttpTTS.fromJson(mText).getOrThrow().let {
-                        allSources.add(it)
-                    }
-                }
-                mText.isJsonArray() -> HttpTTS.fromJsonArray(mText).getOrThrow().let { items ->
-                    allSources.addAll(items)
-                }
-                mText.isAbsUrl() -> {
-                    importSourceUrl(mText)
-                }
-                else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
-            }
+            importSourceAwait(text.trim())
         }.onError {
             it.printOnDebug()
             errorLiveData.postValue(it.localizedMessage ?: "")
@@ -84,11 +70,28 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
         }
     }
 
+    private suspend fun importSourceAwait(text: String) {
+        when {
+            text.isJsonObject() -> {
+                HttpTTS.fromJson(text).getOrThrow().let {
+                    allSources.add(it)
+                }
+            }
+            text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow().let { items ->
+                allSources.addAll(items)
+            }
+            text.isAbsUrl() -> {
+                importSourceUrl(text)
+            }
+            else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
+        }
+    }
+
     private suspend fun importSourceUrl(url: String) {
         okHttpClient.newCallResponseBody {
             url(url)
         }.text().let {
-            allSources.addAll(HttpTTS.fromJsonArray(it).getOrThrow())
+            importSourceAwait(it)
         }
     }
 

+ 132 - 0
app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt

@@ -0,0 +1,132 @@
+package io.legado.app.ui.association
+
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.net.Uri
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.legado.app.R
+import io.legado.app.base.BaseDialogFragment
+import io.legado.app.base.adapter.ItemViewHolder
+import io.legado.app.base.adapter.RecyclerAdapter
+import io.legado.app.databinding.DialogRecyclerViewBinding
+import io.legado.app.databinding.ItemBookFileImportBinding
+import io.legado.app.help.config.AppConfig
+import io.legado.app.lib.dialogs.alert
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.widget.dialog.WaitDialog
+import io.legado.app.utils.*
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+
+
+/**
+ * 导入在线书籍文件弹出窗口
+ */
+class ImportOnLineBookFileDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) {
+
+
+    private val binding by viewBinding(DialogRecyclerViewBinding::bind)
+    private val viewModel by viewModels<ImportOnLineBookFileViewModel>()
+    private val adapter by lazy { BookFileAdapter(requireContext()) }
+
+    override fun onStart() {
+        super.onStart()
+        setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+    }
+
+    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
+        val bookUrl = arguments?.getString("bookUrl")
+        viewModel.initData(bookUrl)
+        binding.toolBar.setBackgroundColor(primaryColor)
+        binding.toolBar.setTitle(R.string.download_and_import_file)
+        binding.rotateLoading.show()
+        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
+        binding.recyclerView.adapter = adapter
+        viewModel.errorLiveData.observe(this) {
+            binding.rotateLoading.hide()
+            binding.tvMsg.apply {
+                text = it
+                visible()
+            }
+        }
+        viewModel.successLiveData.observe(this) {
+            binding.rotateLoading.hide()
+            if (it > 0) {
+                adapter.setItems(viewModel.allBookFiles)
+            }
+        }
+        viewModel.savedFileUriData.observe(this) {
+            requireContext().openFileUri(it, "*/*")
+        }
+    }
+
+    private fun importFileAndUpdate(url: String, fileName: String) {
+        val waitDialog = WaitDialog(requireContext())
+        waitDialog.show()
+        viewModel.importOnLineBookFile(url, fileName) {
+           waitDialog.dismiss()
+           dismissAllowingStateLoss()
+        }
+    }
+
+    private fun downloadFile(url: String, fileName: String) {
+        val waitDialog = WaitDialog(requireContext())
+        waitDialog.show()
+        viewModel.downloadUrl(url, fileName) {
+            waitDialog.dismiss()
+            dismissAllowingStateLoss()
+    }
+}
+
+    inner class BookFileAdapter(context: Context) :
+        RecyclerAdapter<Triple<String, String, Boolean>
+, ItemBookFileImportBinding>(context) {
+
+        override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding {
+            return ItemBookFileImportBinding.inflate(inflater, parent, false)
+        }
+
+        override fun convert(
+            holder: ItemViewHolder,
+            binding: ItemBookFileImportBinding,
+            item: Triple<String, String, Boolean>,
+            payloads: MutableList<Any>
+        ) {
+            binding.apply {
+                cbFileName.text = item.second
+            }
+        }
+
+        override fun registerListener(
+            holder: ItemViewHolder,
+            binding: ItemBookFileImportBinding
+        ) {
+            binding.apply {
+                cbFileName.setOnClickListener {
+                    val selectFile = viewModel.allBookFiles[holder.layoutPosition]
+                    if (selectFile.third) {
+                        importFileAndUpdate(selectFile.first, selectFile.second)
+                    } else {
+                        alert(
+                            title = getString(R.string.draw),
+                            message = getString(R.string.file_not_supported, selectFile.second)
+                        ) {
+                            okButton {
+                                importFileAndUpdate(selectFile.first, selectFile.second)
+                            }
+                            neutralButton(R.string.open_fun) {
+                                downloadFile(selectFile.first, selectFile.second)
+                            }
+                            cancelButton()
+                        }
+                    }
+                }
+            }
+        }
+
+    }
+
+}

+ 80 - 0
app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt

@@ -0,0 +1,80 @@
+package io.legado.app.ui.association
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import io.legado.app.R
+import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppPattern
+import io.legado.app.constant.AppLog
+import io.legado.app.constant.EventBus
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
+import io.legado.app.exception.NoStackTraceException
+import io.legado.app.model.analyzeRule.AnalyzeRule
+import io.legado.app.model.analyzeRule.AnalyzeUrl
+import io.legado.app.model.localBook.LocalBook
+import io.legado.app.utils.*
+
+class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) {
+
+    val allBookFiles = arrayListOf<Triple<String, String, Boolean>>()
+    val errorLiveData = MutableLiveData<String>()
+    val successLiveData = MutableLiveData<Int>()
+    val savedFileUriData = MutableLiveData<Uri>()
+    var bookSource: BookSource? = null
+
+    fun initData(bookUrl: String?) {
+        execute {
+            bookUrl ?: throw NoStackTraceException("书籍详情页链接为空")
+            val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
+                ?: throw NoStackTraceException("book is null")
+            bookSource = appDb.bookSourceDao.getBookSource(book.origin)
+                ?: throw NoStackTraceException("bookSource is null")
+            val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls
+            val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body
+            val analyzeRule = AnalyzeRule(book, bookSource)
+            analyzeRule.setContent(content).setBaseUrl(bookUrl)
+            val fileName = "${book.name} 作者:${book.author}"
+            analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let {
+                it.forEach { url ->
+                    val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}"
+                    val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
+                    allBookFiles.add(Triple(url, mFileName, isSupportedFile))
+                }
+            } ?: throw NoStackTraceException("下载链接规则解析为空")
+        }.onSuccess {
+            successLiveData.postValue(allBookFiles.size)
+        }.onError {
+            errorLiveData.postValue(it.localizedMessage ?: "")
+            context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}")
+        }
+        
+    }
+
+    fun downloadUrl(url: String, fileName: String, success: () -> Unit) {
+        execute {
+            LocalBook.saveBookFile(url, fileName, bookSource).let {
+                savedFileUriData.postValue(it)
+            }
+        }.onSuccess {
+            success.invoke()
+        }.onError {
+            context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
+        }
+    }
+
+    fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) {
+        execute {
+            LocalBook.importFileOnLine(url, fileName, bookSource).let {
+                postEvent(EventBus.BOOK_URL_CHANGED, it.bookUrl)
+            }
+        }.onSuccess {
+           success.invoke()
+        }.onError {
+            context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
+        }
+    }
+
+}

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

@@ -88,14 +88,14 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
             adapter.notifyDataSetChanged()
             upSelectText()
         }
-        viewModel.errorLiveData.observe(this, {
+        viewModel.errorLiveData.observe(this) {
             binding.rotateLoading.hide()
             binding.tvMsg.apply {
                 text = it
                 visible()
             }
-        })
-        viewModel.successLiveData.observe(this, {
+        }
+        viewModel.successLiveData.observe(this) {
             binding.rotateLoading.hide()
             if (it > 0) {
                 adapter.setItems(viewModel.allRules)
@@ -106,7 +106,7 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
                     visible()
                 }
             }
-        })
+        }
         val source = arguments?.getString("source")
         if (source.isNullOrEmpty()) {
             dismiss()

+ 27 - 11
app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt

@@ -6,12 +6,15 @@ import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppPattern
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.ReplaceAnalyzer
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.text
 import io.legado.app.utils.isAbsUrl
+import io.legado.app.utils.isJsonArray
+import io.legado.app.utils.isJsonObject
 import io.legado.app.utils.splitNotBlank
 
 class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
@@ -83,17 +86,7 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
 
     fun import(text: String) {
         execute {
-            if (text.isAbsUrl()) {
-                okHttpClient.newCallResponseBody {
-                    url(text)
-                }.text("utf-8").let {
-                    val rules = ReplaceAnalyzer.jsonToReplaceRules(it)
-                    allRules.addAll(rules)
-                }
-            } else {
-                val rules = ReplaceAnalyzer.jsonToReplaceRules(text)
-                allRules.addAll(rules)
-            }
+            importAwait(text.trim())
         }.onError {
             errorLiveData.postValue(it.localizedMessage ?: "ERROR")
         }.onSuccess {
@@ -101,6 +94,29 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
         }
     }
 
+    private suspend fun importAwait(text: String) {
+        when {
+            text.isAbsUrl() -> importUrl(text)
+            text.isJsonArray() -> {
+                val rules = ReplaceAnalyzer.jsonToReplaceRules(text).getOrThrow()
+                allRules.addAll(rules)
+            }
+            text.isJsonObject() -> {
+                val rule = ReplaceAnalyzer.jsonToReplaceRule(text).getOrThrow()
+                allRules.add(rule)
+            }
+            else -> throw NoStackTraceException("格式不对")
+        }
+    }
+
+    private suspend fun importUrl(url: String) {
+        okHttpClient.newCallResponseBody {
+            url(url)
+        }.text("utf-8").let {
+            importAwait(it)
+        }
+    }
+
     private fun comparisonSource() {
         execute {
             allRules.forEach {

+ 16 - 3
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -13,6 +13,7 @@ import androidx.activity.viewModels
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.BookType
+import io.legado.app.constant.EventBus
 import io.legado.app.constant.Theme
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
@@ -26,6 +27,7 @@ import io.legado.app.lib.theme.bottomBackground
 import io.legado.app.lib.theme.getPrimaryTextColor
 import io.legado.app.model.BookCover
 import io.legado.app.ui.about.AppLogDialog
+import io.legado.app.ui.association.ImportOnLineBookFileDialog
 import io.legado.app.ui.book.audio.AudioPlayActivity
 import io.legado.app.ui.book.changecover.ChangeCoverDialog
 import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
@@ -245,7 +247,7 @@ class BookInfoActivity :
                 binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
             }
             chapterList.isNullOrEmpty() -> {
-                binding.tvToc.text = getString(R.string.toc_s, getString(R.string.error_load_toc))
+                binding.tvToc.text = if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString(R.string.toc_s, getString(R.string.error_load_toc))
             }
             else -> {
                 viewModel.bookData.value?.let {
@@ -293,8 +295,14 @@ class BookInfoActivity :
             true
         }
         tvRead.setOnClickListener {
-            viewModel.bookData.value?.let {
-                readBook(it)
+            viewModel.bookData.value?.let { book ->
+                if (viewModel.isImportBookOnLine) {
+                    showDialogFragment<ImportOnLineBookFileDialog> {
+                        putString("bookUrl", book.bookUrl)
+                    }
+                } else {
+                    readBook(book)
+                }
             } ?: toastOnUi("Book is null")
         }
         tvShelf.setOnClickListener {
@@ -497,4 +505,9 @@ class BookInfoActivity :
         }
     }
 
+    override fun observeLiveBus() {
+        observeEvent<String>(EventBus.BOOK_URL_CHANGED) {
+            viewModel.changeToLocalBook(it)
+        }
+    }
 }

+ 23 - 0
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -2,11 +2,13 @@ package io.legado.app.ui.book.info
 
 import android.app.Application
 import android.content.Intent
+import android.net.Uri
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppLog
+import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
@@ -30,6 +32,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
     var inBookshelf = false
     var bookSource: BookSource? = null
     private var changeSourceCoroutine: Coroutine<*>? = null
+    var isImportBookOnLine = false
 
     fun initData(intent: Intent) {
         execute {
@@ -73,8 +76,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
             upCoverByRule(book)
             bookSource = if (book.isLocalBook()) null else
                 appDb.bookSourceDao.getBookSource(book.origin)
+            isImportBookOnLine = (bookSource?.bookSourceType ?: BookType.local) == BookType.file
             if (book.tocUrl.isEmpty()) {
                 loadBookInfo(book)
+            } else if (isImportBookOnLine) {
+                chapterListData.postValue(emptyList())
             } else {
                 val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
                 if (chapterList.isNotEmpty()) {
@@ -113,6 +119,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
                     WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
                         .onSuccess(IO) {
                             bookData.postValue(book)
+                            if (isImportBookOnLine) {
+                                appDb.searchBookDao.update(book.toSearchBook())
+                            }
                             if (inBookshelf) {
                                 appDb.bookDao.update(book)
                             }
@@ -141,6 +150,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
                     appDb.bookChapterDao.insert(*it.toTypedArray())
                     chapterListData.postValue(it)
                 }
+            } else if (isImportBookOnLine) {
+                chapterListData.postValue(emptyList())
             } else {
                 bookSource?.let { bookSource ->
                     WebBook.getChapterList(this, bookSource, book)
@@ -282,4 +293,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
             }
         }
     }
+
+    fun changeToLocalBook(bookUrl: String) {
+        appDb.bookDao.getBook(bookUrl)?.let { localBook ->
+            isImportBookOnLine = false
+            inBookshelf = true
+            LocalBook.mergeBook(localBook, bookData.value).let {
+                bookData.postValue(it)
+                loadChapter(it)
+            }
+        }
+    }
+
 }

+ 6 - 2
app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt

@@ -229,8 +229,12 @@ abstract class BaseReadBookActivity :
                 customView { alertBinding.root }
                 yesButton {
                     alertBinding.run {
-                        val start = editStart.text?.toString()?.toInt() ?: 0
-                        val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum
+                        val start = editStart.text!!.toString().let {
+                            if (it.isEmpty()) 0 else it.toInt()
+                        }
+                        val end = editEnd.text!!.toString().let {
+                            if (it.isEmpty()) book.totalChapterNum else it.toInt()
+                        }
                         CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
                     }
                 }

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

@@ -61,41 +61,33 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     }
 
     private fun initBook(book: Book) {
-        if (ReadBook.book?.bookUrl != book.bookUrl) {
-            ReadBook.resetData(book)
-            isInitFinish = true
-            if (ReadBook.chapterSize == 0) {
-                if (book.tocUrl.isEmpty()) {
-                    loadBookInfo(book)
-                } else {
-                    loadChapterList(book)
-                }
+        val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
+        if (isSameBook) ReadBook.upData(book) else ReadBook.resetData(book)
+        isInitFinish = true
+        if (ReadBook.chapterSize == 0) {
+            if (book.tocUrl.isEmpty()) {
+                loadBookInfo(book)
             } else {
-                if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
-                    ReadBook.durChapterIndex = ReadBook.chapterSize - 1
-                }
-                ReadBook.loadContent(resetPageOffset = true)
+                loadChapterList(book)
             }
-            syncBookProgress(book)
-        } else {
-            ReadBook.upData(book)
-            isInitFinish = true
-            if (ReadBook.chapterSize == 0) {
-                if (book.tocUrl.isEmpty()) {
-                    loadBookInfo(book)
-                } else {
-                    loadChapterList(book)
-                }
+        } else if (book.isLocalBook()
+            && LocalBook.getLastModified(book).getOrDefault(0L) > book.latestChapterTime
+        ) {
+            loadChapterList(book)
+        } else if (isSameBook) {
+            if (ReadBook.curTextChapter != null) {
+                ReadBook.callBack?.upContent(resetPageOffset = false)
             } else {
-                if (ReadBook.curTextChapter != null) {
-                    ReadBook.callBack?.upContent(resetPageOffset = false)
-                } else {
-                    ReadBook.loadContent(resetPageOffset = true)
-                }
+                ReadBook.loadContent(resetPageOffset = true)
             }
-            if (!BaseReadAloudService.isRun) {
-                syncBookProgress(book)
+        } else {
+            if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
+                ReadBook.durChapterIndex = ReadBook.chapterSize - 1
             }
+            ReadBook.loadContent(resetPageOffset = isSameBook)
+        }
+        if (!isSameBook || !BaseReadAloudService.isRun) {
+            syncBookProgress(book)
         }
         if (!book.isLocalBook() && ReadBook.bookSource == null) {
             autoChangeSource(book.name, book.author)

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt

@@ -9,10 +9,10 @@ import android.widget.LinearLayout
 import androidx.fragment.app.DialogFragment
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.bottomBackground
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.ReadBook
@@ -67,7 +67,7 @@ class MoreConfigDialog : DialogFragment() {
         (activity as ReadBookActivity).bottomDialog--
     }
 
-    class ReadPreferenceFragment : BasePreferenceFragment(),
+    class ReadPreferenceFragment : PreferenceFragment(),
         SharedPreferences.OnSharedPreferenceChangeListener {
 
         @SuppressLint("RestrictedApi")

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt

@@ -10,12 +10,12 @@ import androidx.fragment.app.DialogFragment
 import androidx.preference.ListPreference
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.help.IntentHelp
 import io.legado.app.lib.dialogs.SelectItem
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.ReadAloud
@@ -54,7 +54,7 @@ class ReadAloudConfigDialog : DialogFragment() {
             .commit()
     }
 
-    class ReadAloudPreferenceFragment : BasePreferenceFragment(),
+    class ReadAloudPreferenceFragment : PreferenceFragment(),
         SpeakEngineDialog.CallBack,
         SharedPreferences.OnSharedPreferenceChangeListener {
 

+ 13 - 4
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -18,6 +18,7 @@ import io.legado.app.databinding.ActivityBookSourceEditBinding
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.alert
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
@@ -47,7 +48,7 @@ class BookSourceEditActivity :
     private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
         it ?: return@registerForActivityResult
         viewModel.importSource(it) { source ->
-            upRecyclerView(source)
+            upSourceView(source)
         }
     }
     private val selectDoc = registerForActivityResult(HandleFileContract()) {
@@ -68,7 +69,7 @@ class BookSourceEditActivity :
         softKeyboardTool.attachToWindow(window)
         initView()
         viewModel.initData(intent) {
-            upRecyclerView()
+            upSourceView()
         }
     }
 
@@ -109,9 +110,10 @@ class BookSourceEditActivity :
                     }
                 }
             }
+            R.id.menu_clear_cookie -> viewModel.clearCookie(getSource().bookSourceUrl)
             R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
             R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
-            R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
+            R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
             R.id.menu_qr_code_camera -> qrCodeResult.launch()
             R.id.menu_share_str -> share(GSON.toJson(getSource()))
             R.id.menu_share_qr -> shareWithQr(
@@ -139,6 +141,7 @@ class BookSourceEditActivity :
         binding.recyclerView.layoutManager = LinearLayoutManager(this)
         binding.recyclerView.adapter = adapter
         binding.tabLayout.setBackgroundColor(backgroundColor)
+        binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
         binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
             override fun onTabReselected(tab: TabLayout.Tab?) {
 
@@ -186,12 +189,14 @@ class BookSourceEditActivity :
         binding.recyclerView.scrollToPosition(0)
     }
 
-    private fun upRecyclerView(source: BookSource? = viewModel.bookSource) {
+    private fun upSourceView(source: BookSource? = viewModel.bookSource) {
         source?.let {
             binding.cbIsEnable.isChecked = it.enabled
             binding.cbIsEnableFind.isChecked = it.enabledExplore
+            binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
             binding.spType.setSelection(
                 when (it.bookSourceType) {
+                    BookType.file -> 3
                     BookType.image -> 2
                     BookType.audio -> 1
                     else -> 0
@@ -261,6 +266,7 @@ class BookSourceEditActivity :
             add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
             add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
+            add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
         }
         //目录页
         val tr = source?.getTocRule()
@@ -295,7 +301,9 @@ class BookSourceEditActivity :
         val source = viewModel.bookSource?.copy() ?: BookSource()
         source.enabled = binding.cbIsEnable.isChecked
         source.enabledExplore = binding.cbIsEnableFind.isChecked
+        source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
         source.bookSourceType = when (binding.spType.selectedItemPosition) {
+            3 -> BookType.file
             2 -> BookType.image
             1 -> BookType.audio
             else -> BookType.default
@@ -389,6 +397,7 @@ class BookSourceEditActivity :
                 "tocUrl" -> bookInfoRule.tocUrl =
                     viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
                 "canReName" -> bookInfoRule.canReName = it.value
+                "downloadUrls" -> bookInfoRule.downloadUrls = viewModel.ruleComplete(it.value, bookInfoRule.init)
             }
         }
         tocEntities.forEach {

+ 7 - 0
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt

@@ -7,6 +7,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.RuleComplete
+import io.legado.app.help.http.CookieStore
 import io.legado.app.help.http.newCallStrResponse
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.utils.*
@@ -95,6 +96,12 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
         }
     }
 
+    fun clearCookie(url: String) {
+        execute {
+            CookieStore.removeCookie(url)
+        }
+    }
+
     fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
         if (autoComplete) {
             return RuleComplete.autoComplete(rule, preRule, type)

+ 43 - 33
app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt

@@ -19,8 +19,10 @@ import io.legado.app.utils.gone
 import io.legado.app.utils.longToastOnUi
 import io.legado.app.utils.visible
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.async
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.withContext
 import java.util.concurrent.ConcurrentHashMap
 
 class ChapterListAdapter(context: Context, val callback: Callback) :
@@ -54,17 +56,10 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
 
         }
 
-    private val replaceRules
-        get() = callback.book?.let {
-            ContentProcessor.get(it.name, it.origin).getTitleReplaceRules()
-        }
-    private val useReplace
-        get() = AppConfig.tocUiUseReplace && callback.book?.getUseReplaceRule() == true
     private var upDisplayTileJob: Coroutine<*>? = null
 
     override fun onCurrentListChanged() {
         super.onCurrentListChanged()
-        upDisplayTitle()
         callback.onListChanged()
     }
 
@@ -73,36 +68,50 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
         displayTitleMap.clear()
     }
 
-    fun upDisplayTitle() {
+    fun upDisplayTitles(startIndex: Int) {
         upDisplayTileJob?.cancel()
         upDisplayTileJob = Coroutine.async(callback.scope) {
-            val replaceRules = replaceRules
-            val useReplace = useReplace
-            getItems().forEach {
-                if (!isActive) {
-                    return@async
+            val book = callback.book ?: return@async
+            val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
+            val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
+            val items = getItems()
+            async {
+                for (i in startIndex until items.size) {
+                    val item = items[i]
+                    if (displayTitleMap[item.title] == null) {
+                        ensureActive()
+                        val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
+                        ensureActive()
+                        displayTitleMap[item.title] = displayTitle
+                        withContext(Main) {
+                            notifyItemChanged(i, true)
+                        }
+                    }
                 }
-                if (displayTitleMap[it.title] == null) {
-                    displayTitleMap[it.title] = it.getDisplayTitle(replaceRules, useReplace)
+            }.start()
+            async {
+                for (i in startIndex downTo 0) {
+                    val item = items[i]
+                    if (displayTitleMap[item.title] == null) {
+                        ensureActive()
+                        val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
+                        ensureActive()
+                        displayTitleMap[item.title] = displayTitle
+                        withContext(Main) {
+                            notifyItemChanged(i, true)
+                        }
+                    }
                 }
-            }
+            }.start()
         }
     }
 
-    override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
-        return ItemChapterListBinding.inflate(inflater, parent, false)
+    private fun getDisplayTitle(chapter: BookChapter): String {
+        return displayTitleMap[chapter.title] ?: chapter.title
     }
 
-    private fun getDisplayTile(chapter: BookChapter): String {
-        var displayTitle = displayTitleMap[chapter.title]
-        if (displayTitle != null) {
-            return displayTitle
-        }
-        displayTitle = runBlocking {
-            chapter.getDisplayTitle(replaceRules, useReplace)
-        }
-        displayTitleMap[chapter.title] = displayTitle
-        return displayTitle
+    override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
+        return ItemChapterListBinding.inflate(inflater, parent, false)
     }
 
     override fun convert(
@@ -120,7 +129,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
                 } else {
                     tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText))
                 }
-                tvChapterName.text = getDisplayTile(item)
+                tvChapterName.text = getDisplayTitle(item)
                 if (item.isVolume) {
                     //卷名,如第一卷 突出显示
                     tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
@@ -138,6 +147,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
                 }
                 upHasCache(binding, isDur, cached)
             } else {
+                tvChapterName.text = getDisplayTitle(item)
                 upHasCache(binding, isDur, cached)
             }
         }
@@ -150,8 +160,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
             }
         }
         holder.itemView.setOnLongClickListener {
-            getItem(holder.layoutPosition)?.let {
-                context.longToastOnUi(getDisplayTile(it))
+            getItem(holder.layoutPosition)?.let { item ->
+                context.longToastOnUi(getDisplayTitle(item))
             }
             true
         }

+ 11 - 2
app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt

@@ -97,7 +97,15 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
             viewModel.bookData.value?.bookUrl?.let { bookUrl ->
                 if (chapter.bookUrl == bookUrl) {
                     adapter.cacheFileNames.add(chapter.getFileName())
-                    adapter.notifyItemChanged(chapter.index, true)
+                    if (viewModel.searchKey.isNullOrEmpty()) {
+                        adapter.notifyItemChanged(chapter.index, true)
+                    } else {
+                        adapter.getItems().forEachIndexed { index, bookChapter ->
+                            if (bookChapter.index == chapter.index) {
+                                adapter.notifyItemChanged(index, true)
+                            }
+                        }
+                    }
                 }
             }
         }
@@ -128,12 +136,13 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
                 }
             }
             mLayoutManager.scrollToPositionWithOffset(scrollPos, 0)
+            adapter.upDisplayTitles(scrollPos)
         }
     }
 
     override fun clearDisplayTitle() {
         adapter.clearDisplayTitle()
-        adapter.upDisplayTitle()
+        adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition())
     }
 
     override val scope: CoroutineScope

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt

@@ -59,10 +59,12 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>() {
             setOnSearchClickListener { tabLayout.gone() }
             setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                 override fun onQueryTextSubmit(query: String): Boolean {
+                    viewModel.searchKey = query
                     return false
                 }
 
                 override fun onQueryTextChange(newText: String): Boolean {
+                    viewModel.searchKey = newText
                     if (tabLayout.selectedTabPosition == 1) {
                         viewModel.startBookmarkSearch(newText)
                     } else {

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt

@@ -12,6 +12,7 @@ class TocViewModel(application: Application) : BaseViewModel(application) {
     var bookData = MutableLiveData<Book>()
     var chapterListCallBack: ChapterListCallBack? = null
     var bookMarkCallBack: BookmarkCallBack? = null
+    var searchKey: String? = null
 
     fun initBook(bookUrl: String) {
         this.bookUrl = bookUrl

+ 7 - 21
app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt

@@ -15,7 +15,6 @@ import androidx.preference.EditTextPreference
 import androidx.preference.ListPreference
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.help.config.AppConfig
@@ -25,7 +24,7 @@ import io.legado.app.help.storage.*
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.permission.Permissions
 import io.legado.app.lib.permission.PermissionsCompat
-import io.legado.app.lib.theme.accentColor
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.document.HandleFileContract
 import io.legado.app.ui.widget.dialog.TextDialog
@@ -33,8 +32,9 @@ import io.legado.app.utils.*
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
+import kotlin.collections.set
 
-class BackupConfigFragment : BasePreferenceFragment(),
+class BackupConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
     private val viewModel by activityViewModels<ConfigViewModel>()
@@ -100,34 +100,18 @@ class BackupConfigFragment : BasePreferenceFragment(),
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         addPreferencesFromResource(R.xml.pref_config_backup)
-        findPreference<EditTextPreference>(PreferKey.webDavUrl)?.let {
-            it.setOnBindEditTextListener { editText ->
-                editText.applyTint(requireContext().accentColor)
-            }
-        }
-        findPreference<EditTextPreference>(PreferKey.webDavAccount)?.let {
-            it.setOnBindEditTextListener { editText ->
-                editText.applyTint(requireContext().accentColor)
-            }
-        }
         findPreference<EditTextPreference>(PreferKey.webDavPassword)?.let {
             it.setOnBindEditTextListener { editText ->
-                editText.applyTint(requireContext().accentColor)
                 editText.inputType =
                     InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
             }
         }
-        findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
-            it.setOnBindEditTextListener { editText ->
-                editText.applyTint(requireContext().accentColor)
-            }
-        }
         upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
         upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
         upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
         upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
         upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
-        findPreference<io.legado.app.ui.widget.prefs.Preference>("web_dav_restore")
+        findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
             ?.onLongClick { restoreDir.launch(); true }
     }
 
@@ -226,7 +210,9 @@ class BackupConfigFragment : BasePreferenceFragment(),
         return super.onPreferenceTreeClick(preference)
     }
 
-
+    /**
+     * 备份忽略设置
+     */
     private fun backupIgnore() {
         val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
             BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false

+ 3 - 3
app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt

@@ -7,16 +7,16 @@ import android.os.Bundle
 import android.view.View
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.PreferKey
 import io.legado.app.lib.dialogs.selector
+import io.legado.app.lib.prefs.SwitchPreference
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.BookCover
-import io.legado.app.ui.widget.prefs.SwitchPreference
 import io.legado.app.utils.*
 import java.io.FileOutputStream
 
-class CoverConfigFragment : BasePreferenceFragment(),
+class CoverConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
     private val requestCodeCover = 111

+ 5 - 4
app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt

@@ -6,16 +6,17 @@ import android.content.SharedPreferences
 import android.content.pm.PackageManager
 import android.os.Bundle
 import android.view.View
+import androidx.core.view.postDelayed
 import androidx.fragment.app.activityViewModels
 import androidx.preference.ListPreference
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.CheckSource
 import io.legado.app.receiver.SharedReceiverActivity
@@ -26,7 +27,7 @@ import io.legado.app.utils.*
 import splitties.init.appCtx
 
 
-class OtherConfigFragment : BasePreferenceFragment(),
+class OtherConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
     private val viewModel by activityViewModels<ConfigViewModel>()
@@ -131,9 +132,9 @@ class OtherConfigFragment : BasePreferenceFragment(),
                 setProcessTextEnable(it.getBoolean(key, true))
             }
             PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
-            PreferKey.language -> listView.postDelayed({
+            PreferKey.language -> listView.postDelayed(1000) {
                 appCtx.restart()
-            }, 1000)
+            }
             PreferKey.userAgent -> listView.post {
                 upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
             }

+ 3 - 3
app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt

@@ -13,7 +13,6 @@ import android.widget.SeekBar
 import androidx.preference.Preference
 import io.legado.app.R
 import io.legado.app.base.AppContextWrapper
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
@@ -24,16 +23,17 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
+import io.legado.app.lib.prefs.ColorPreference
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.number.NumberPickerDialog
-import io.legado.app.ui.widget.prefs.ColorPreference
 import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
 import io.legado.app.utils.*
 import java.io.FileOutputStream
 
 
 @Suppress("SameParameterValue")
-class ThemeConfigFragment : BasePreferenceFragment(),
+class ThemeConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
     private val requestCodeBgLight = 121

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

@@ -7,15 +7,15 @@ import android.os.Bundle
 import android.view.View
 import androidx.preference.Preference
 import io.legado.app.R
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.PreferKey
 import io.legado.app.lib.dialogs.selector
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.BookCover
 import io.legado.app.utils.*
 import java.io.FileOutputStream
 
-class WelcomeConfigFragment : BasePreferenceFragment(),
+class WelcomeConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
     private val requestWelcomeImage = 221

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

@@ -8,6 +8,7 @@ import android.view.KeyEvent
 import android.view.MenuItem
 import android.view.ViewGroup
 import androidx.activity.viewModels
+import androidx.core.view.postDelayed
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentStatePagerAdapter
@@ -87,13 +88,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
         upVersion()
         //自动更新书籍
         if (AppConfig.autoRefreshBook) {
-            binding.viewPagerMain.postDelayed({
+            binding.viewPagerMain.postDelayed(1000) {
                 viewModel.upAllBookToc()
-            }, 1000)
+            }
         }
-        binding.viewPagerMain.postDelayed({
+        binding.viewPagerMain.postDelayed(3000) {
             viewModel.postLoad()
-        }, 3000)
+        }
         launch {
             val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() }
                 ?: return@launch

+ 6 - 6
app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt

@@ -8,13 +8,16 @@ import android.view.View
 import androidx.preference.Preference
 import io.legado.app.R
 import io.legado.app.base.BaseFragment
-import io.legado.app.base.BasePreferenceFragment
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.databinding.FragmentMyConfigBinding
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.lib.dialogs.selector
+import io.legado.app.lib.prefs.NameListPreference
+import io.legado.app.lib.prefs.PreferenceCategory
+import io.legado.app.lib.prefs.SwitchPreference
+import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.service.WebService
 import io.legado.app.ui.about.AboutActivity
@@ -26,9 +29,6 @@ import io.legado.app.ui.config.ConfigActivity
 import io.legado.app.ui.config.ConfigTag
 import io.legado.app.ui.replace.ReplaceRuleActivity
 import io.legado.app.ui.widget.dialog.TextDialog
-import io.legado.app.ui.widget.prefs.NameListPreference
-import io.legado.app.ui.widget.prefs.PreferenceCategory
-import io.legado.app.ui.widget.prefs.SwitchPreference
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 
@@ -40,7 +40,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
         setSupportToolbar(binding.titleBar.toolbar)
         val fragmentTag = "prefFragment"
         var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
-        if (preferenceFragment == null) preferenceFragment = PreferenceFragment()
+        if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment()
         childFragmentManager.beginTransaction()
             .replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit()
     }
@@ -61,7 +61,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
     /**
      * 配置
      */
-    class PreferenceFragment : BasePreferenceFragment(),
+    class MyPreferenceFragment : PreferenceFragment(),
         SharedPreferences.OnSharedPreferenceChangeListener {
 
         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

+ 4 - 2
app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt

@@ -14,6 +14,7 @@ import io.legado.app.base.VMBaseActivity
 import io.legado.app.databinding.ActivityRssArtivlesBinding
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.lib.dialogs.alert
+import io.legado.app.lib.theme.accentColor
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
 import io.legado.app.utils.StartActivityContract
@@ -45,9 +46,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         binding.viewPager.adapter = adapter
         binding.tabLayout.setupWithViewPager(binding.viewPager)
-        viewModel.titleLiveData.observe(this, {
+        binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
+        viewModel.titleLiveData.observe(this) {
             binding.titleBar.title = it
-        })
+        }
         viewModel.initData(intent) {
             upFragments()
         }

+ 7 - 4
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt

@@ -48,7 +48,7 @@ class RssSourceEditActivity :
     private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
         it?.let {
             viewModel.importSource(it) { source: RssSource ->
-                upRecyclerView(source)
+                upSourceView(source)
             }
         }
     }
@@ -57,7 +57,7 @@ class RssSourceEditActivity :
         softKeyboardTool.attachToWindow(window)
         initView()
         viewModel.initData(intent) {
-            upRecyclerView()
+            upSourceView()
         }
     }
 
@@ -130,10 +130,11 @@ class RssSourceEditActivity :
                     }
                 }
             }
+            R.id.menu_clear_cookie -> viewModel.clearCookie(getRssSource().sourceUrl)
             R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
             R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
             R.id.menu_qr_code_camera -> qrCodeResult.launch()
-            R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
+            R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
             R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
             R.id.menu_share_qr -> shareWithQr(
                 GSON.toJson(getRssSource()),
@@ -150,10 +151,11 @@ class RssSourceEditActivity :
         binding.recyclerView.adapter = adapter
     }
 
-    private fun upRecyclerView(source: RssSource? = viewModel.rssSource) {
+    private fun upSourceView(source: RssSource? = viewModel.rssSource) {
         source?.let {
             binding.cbIsEnable.isChecked = source.enabled
             binding.cbSingleUrl.isChecked = source.singleUrl
+            binding.cbIsEnableCookie.isChecked = source.enabledCookieJar == true
             binding.cbEnableJs.isChecked = source.enableJs
             binding.cbEnableBaseUrl.isChecked = source.loadWithBaseUrl
         }
@@ -191,6 +193,7 @@ class RssSourceEditActivity :
         val source = viewModel.rssSource
         source.enabled = binding.cbIsEnable.isChecked
         source.singleUrl = binding.cbSingleUrl.isChecked
+        source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
         source.enableJs = binding.cbEnableJs.isChecked
         source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked
         sourceEntities.forEach {

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

@@ -6,6 +6,7 @@ import io.legado.app.base.BaseViewModel
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.RssSource
 import io.legado.app.help.RuleComplete
+import io.legado.app.help.http.CookieStore
 import io.legado.app.utils.getClipText
 import io.legado.app.utils.msg
 import io.legado.app.utils.printOnDebug
@@ -77,6 +78,12 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
         }
     }
 
+    fun clearCookie(url: String) {
+        execute {
+            CookieStore.removeCookie(url)
+        }
+    }
+
     fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
         if (autoComplete) {
             return RuleComplete.autoComplete(rule, preRule, type)

+ 2 - 1
app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.welcome
 import android.content.Intent
 import android.graphics.drawable.BitmapDrawable
 import android.os.Bundle
+import androidx.core.view.postDelayed
 import io.legado.app.base.BaseActivity
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.Theme
@@ -26,7 +27,7 @@ open class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
         if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) {
             finish()
         } else {
-            binding.root.postDelayed({ startMainActivity() }, 600)
+            binding.root.postDelayed(600) { startMainActivity() }
         }
     }
 

+ 4 - 3
app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt

@@ -7,6 +7,7 @@ import android.util.AttributeSet
 import android.view.View
 import android.view.animation.LinearInterpolator
 import android.widget.Checkable
+import androidx.core.view.postDelayed
 import io.legado.app.R
 import io.legado.app.lib.theme.ThemeStore
 import io.legado.app.utils.dpToPx
@@ -244,7 +245,7 @@ class SmoothCheckBox @JvmOverloads constructor(
         }
         // invalidate
         if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {
-            postDelayed({ this.postInvalidate() }, 10)
+            postDelayed(10) { this.postInvalidate() }
         }
     }
 
@@ -298,10 +299,10 @@ class SmoothCheckBox @JvmOverloads constructor(
     }
 
     private fun drawTickDelayed() {
-        postDelayed({
+        postDelayed(mAnimDuration.toLong()) {
             mTickDrawing = true
             postInvalidate()
-        }, mAnimDuration.toLong())
+        }
     }
 
     companion object {

+ 0 - 22
app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt

@@ -1,22 +0,0 @@
-package io.legado.app.ui.widget.prefs
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.TextView
-import androidx.preference.PreferenceViewHolder
-import io.legado.app.R
-
-class EditTextPreference(context: Context, attrs: AttributeSet) :
-    androidx.preference.EditTextPreference(context, attrs) {
-
-    init {
-        // isPersistent = true
-        layoutResource = R.layout.view_preference
-    }
-
-    override fun onBindViewHolder(holder: PreferenceViewHolder) {
-        Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
-        super.onBindViewHolder(holder)
-    }
-
-}

+ 14 - 8
app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt

@@ -15,6 +15,7 @@ import android.widget.FrameLayout.LayoutParams
 import androidx.appcompat.widget.AppCompatTextView
 import io.legado.app.R
 import io.legado.app.lib.theme.accentColor
+import io.legado.app.utils.ColorUtils
 import io.legado.app.utils.getCompatColor
 import io.legado.app.utils.invisible
 import io.legado.app.utils.visible
@@ -87,8 +88,6 @@ class BadgeView @JvmOverloads constructor(
             setLayoutParams(layoutParams)
         }
 
-        // set default font
-        setTextColor(Color.WHITE)
         //setTypeface(Typeface.DEFAULT_BOLD);
         setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
         setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
@@ -106,6 +105,10 @@ class BadgeView @JvmOverloads constructor(
         minHeight = dip2Px(16f)
     }
 
+    override fun setBackgroundColor(color: Int) {
+        setBackground(radius, color)
+    }
+
     fun setBackground(dipRadius: Float, badgeColor: Int) {
         val radius = dip2Px(dipRadius).toFloat()
         val radiusArray =
@@ -118,10 +121,13 @@ class BadgeView @JvmOverloads constructor(
         val bgDrawable = ShapeDrawable(roundRect)
         bgDrawable.paint.color = badgeColor
         background = bgDrawable
-    }
-
-    fun setBackground(badgeColor: Int) {
-        setBackground(radius, badgeColor)
+        setTextColor(
+            if (ColorUtils.isColorLight(badgeColor)) {
+                Color.BLACK
+            } else {
+                Color.WHITE
+            }
+        )
     }
 
     /**
@@ -142,9 +148,9 @@ class BadgeView @JvmOverloads constructor(
 
     fun setHighlight(highlight: Boolean) {
         if (highlight) {
-            setBackground(context.accentColor)
+            setBackgroundColor(context.accentColor)
         } else {
-            setBackground(context.getCompatColor(R.color.darker_gray))
+            setBackgroundColor(context.getCompatColor(R.color.darker_gray))
         }
     }
 

+ 12 - 146
app/src/main/java/io/legado/app/utils/ColorUtils.kt

@@ -1,20 +1,22 @@
 package io.legado.app.utils
 
 import android.graphics.Color
-
 import androidx.annotation.ColorInt
 import androidx.annotation.FloatRange
-import java.util.*
+import androidx.core.graphics.ColorUtils
 import kotlin.math.*
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 object ColorUtils {
 
+    fun isColorLight(@ColorInt color: Int): Boolean {
+        return ColorUtils.calculateLuminance(color) >= 0.5
+    }
+
     fun intToString(intColor: Int): String {
         return String.format("#%06X", 0xFFFFFF and intColor)
     }
 
-
     fun stripAlpha(@ColorInt color: Int): Int {
         return -0x1000000 or color
     }
@@ -39,12 +41,6 @@ object ColorUtils {
         return shiftColor(color, 1.1f)
     }
 
-    fun isColorLight(@ColorInt color: Int): Boolean {
-        val darkness =
-            1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
-        return darkness < 0.4
-    }
-
     @ColorInt
     fun invertColor(@ColorInt color: Int): Int {
         val r = 255 - Color.red(color)
@@ -81,83 +77,6 @@ object ColorUtils {
         return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
     }
 
-    /**
-     * 按条件的到随机颜色
-     *
-     * @param alpha 透明
-     * @param lower 下边界
-     * @param upper 上边界
-     * @return 颜色值
-     */
-    fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int {
-        return RandomColor(alpha, lower, upper).color
-    }
-
-    /**
-     * @return 获取随机色
-     */
-    fun getRandomColor(): Int {
-        return RandomColor(255, 80, 200).color
-    }
-
-
-    /**
-     * 随机颜色
-     */
-    class RandomColor(alpha: Int, lower: Int, upper: Int) {
-        private var alpha: Int = 0
-        private var lower: Int = 0
-        private var upper: Int = 0
-
-        //随机数是前闭  后开
-        val color: Int
-            get() {
-                val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
-                val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
-                val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
-
-                return Color.argb(getAlpha(), red, green, blue)
-            }
-
-        init {
-            require(upper > lower) { "must be lower < upper" }
-            setAlpha(alpha)
-            setLower(lower)
-            setUpper(upper)
-        }
-
-        private fun getAlpha(): Int {
-            return alpha
-        }
-
-        private fun setAlpha(alpha: Int) {
-            var alpha1 = alpha
-            if (alpha1 > 255) alpha1 = 255
-            if (alpha1 < 0) alpha1 = 0
-            this.alpha = alpha1
-        }
-
-        private fun getLower(): Int {
-            return lower
-        }
-
-        private fun setLower(lower: Int) {
-            var lower1 = lower
-            if (lower1 < 0) lower1 = 0
-            this.lower = lower1
-        }
-
-        private fun getUpper(): Int {
-            return upper
-        }
-
-        private fun setUpper(upper: Int) {
-            var upper1 = upper
-            if (upper1 > 255) upper1 = 255
-            this.upper = upper1
-        }
-    }
-
     fun argb(R: Int, G: Int, B: Int): Int {
         return argb(Byte.MAX_VALUE.toInt(), R, G, B)
     }
@@ -177,72 +96,19 @@ object ColorUtils {
                 + (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))
     }
 
-    fun rgb2lab(R: Int, G: Int, B: Int): IntArray {
-        val x: Float
-        val y: Float
-        val z: Float
-        val fx: Float
-        val fy: Float
-        val fz: Float
-        val xr: Float
-        val yr: Float
-        val zr: Float
-        val eps = 216f / 24389f
-        val k = 24389f / 27f
-        val xr1 = 0.964221f // reference white D50
-        val yr1 = 1.0f
-        val zr1 = 0.825211f
-
-        // RGB to XYZ
-        var r: Float = R / 255f //R 0..1
-        var g: Float = G / 255f //G 0..1
-        var b: Float = B / 255f //B 0..1
-
-        // assuming sRGB (D65)
-        r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat()
-        g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat()
-        b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat()
-        x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b
-        y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b
-        z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b
-
-        // XYZ to Lab
-        xr = x / xr1
-        yr = y / yr1
-        zr = z / zr1
-        fx = if (xr > eps) xr.toDouble().pow(1 / 3.0)
-            .toFloat() else ((k * xr + 16.0) / 116.0).toFloat()
-        fy = if (yr > eps) yr.toDouble().pow(1 / 3.0)
-            .toFloat() else ((k * yr + 16.0) / 116.0).toFloat()
-        fz = if (zr > eps) zr.toDouble().pow(1 / 3.0)
-            .toFloat() else ((k * zr + 16.0) / 116).toFloat()
-        val ls: Float = 116 * fy - 16
-        val `as`: Float = 500 * (fx - fy)
-        val bs: Float = 200 * (fy - fz)
-        val lab = IntArray(3)
-        lab[0] = (2.55 * ls + .5).toInt()
-        lab[1] = (`as` + .5).toInt()
-        lab[2] = (bs + .5).toInt()
-        return lab
-    }
-
     /**
      * Computes the difference between two RGB colors by converting them to the L*a*b scale and
      * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
      */
     fun getColorDifference(a: Int, b: Int): Double {
-        val r1: Int = Color.red(a)
-        val g1: Int = Color.green(a)
-        val b1: Int = Color.blue(a)
-        val r2: Int = Color.red(b)
-        val g2: Int = Color.green(b)
-        val b2: Int = Color.blue(b)
-        val lab1 = rgb2lab(r1, g1, b1)
-        val lab2 = rgb2lab(r2, g2, b2)
+        val lab1 = DoubleArray(3)
+        val lab2 = DoubleArray(3)
+        ColorUtils.colorToLAB(a, lab1)
+        ColorUtils.colorToLAB(b, lab2)
         return sqrt(
-            (lab2[0] - lab1[0].toDouble())
-                .pow(2.0) + (lab2[1] - lab1[1].toDouble())
-                .pow(2.0) + (lab2[2] - lab1[2].toDouble())
+            (lab2[0] - lab1[0])
+                .pow(2.0) + (lab2[1] - lab1[1])
+                .pow(2.0) + (lab2[2] - lab1[2])
                 .pow(2.0)
         )
     }

+ 2 - 1
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -331,6 +331,7 @@ fun Context.openFileUri(uri: Uri, type: String? = null) {
 
 @Suppress("DEPRECATION")
 val Context.isWifiConnect: Boolean
+    @SuppressLint("MissingPermission")
     get() {
         val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
         return info?.isConnected == true
@@ -338,7 +339,7 @@ val Context.isWifiConnect: Boolean
 
 val Context.isPad: Boolean
     get() {
-        return resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE
+        return (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE
     }
 
 val Context.channel: String

+ 61 - 0
app/src/main/java/io/legado/app/utils/RandomColor.kt

@@ -0,0 +1,61 @@
+package io.legado.app.utils
+
+import android.graphics.Color
+import java.util.*
+
+@Suppress("unused")
+class RandomColor(alpha: Int, lower: Int, upper: Int) {
+
+    constructor() : this(255, 80, 200)
+
+    private var alpha: Int = 0
+    private var lower: Int = 0
+    private var upper: Int = 0
+
+    init {
+        require(upper > lower) { "must be lower < upper" }
+        setAlpha(alpha)
+        setLower(lower)
+        setUpper(upper)
+    }
+
+    //随机数是前闭  后开
+    fun build(): Int {
+        val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
+        val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
+        val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
+        return Color.argb(getAlpha(), red, green, blue)
+    }
+
+    private fun getAlpha(): Int {
+        return alpha
+    }
+
+    private fun setAlpha(alpha: Int) {
+        var alpha1 = alpha
+        if (alpha1 > 255) alpha1 = 255
+        if (alpha1 < 0) alpha1 = 0
+        this.alpha = alpha1
+    }
+
+    private fun getLower(): Int {
+        return lower
+    }
+
+    private fun setLower(lower: Int) {
+        var lower1 = lower
+        if (lower1 < 0) lower1 = 0
+        this.lower = lower1
+    }
+
+    private fun getUpper(): Int {
+        return upper
+    }
+
+    private fun setUpper(upper: Int) {
+        var upper1 = upper
+        if (upper1 > 255) upper1 = 255
+        this.upper = upper1
+    }
+
+}

+ 7 - 5
app/src/main/java/io/legado/app/utils/RegexExtensions.kt

@@ -1,11 +1,13 @@
 package io.legado.app.utils
 
+import androidx.core.os.postDelayed
 import io.legado.app.exception.RegexTimeoutException
 import io.legado.app.help.CrashHandler
 import kotlinx.coroutines.suspendCancellableCoroutine
 import splitties.init.appCtx
 import kotlin.concurrent.thread
 import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
 
 /**
  * 带有超时检测的正则替换
@@ -19,23 +21,23 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon
                 val result = regex.replace(charSequence, replacement)
                 block.resume(result)
             } catch (e: Exception) {
-                block.cancel(e)
+                block.resumeWithException(e)
             }
         }
-        mainHandler.postDelayed({
+        mainHandler.postDelayed(timeout) {
             if (thread.isAlive) {
                 val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
                 val exception = RegexTimeoutException(timeoutMsg)
                 block.cancel(exception)
                 appCtx.longToastOnUi(timeoutMsg)
                 CrashHandler.saveCrashInfo2File(exception)
-                mainHandler.postDelayed({
+                mainHandler.postDelayed(3000) {
                     if (thread.isAlive) {
                         appCtx.restart()
                     }
-                }, 3000)
+                }
             }
-        }, timeout)
+        }
     }
 }
 

+ 1 - 0
app/src/main/java/io/legado/app/web/HttpServer.kt

@@ -42,6 +42,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
                         "/saveBookSources" -> BookSourceController.saveSources(postData)
                         "/deleteBookSources" -> BookSourceController.deleteSources(postData)
                         "/saveBook" -> BookController.saveBook(postData)
+                        "/saveBookProgress" -> BookController.saveBookProgress(postData)
                         "/addLocalBook" -> BookController.addLocalBook(session.parameters)
                         "/saveReadConfig" -> BookController.saveWebReadConfig(postData)
                         "/saveRssSource" -> RssSourceController.saveSource(postData)

+ 7 - 0
app/src/main/res/layout/activity_book_source_edit.xml

@@ -33,6 +33,13 @@
             android:checked="true"
             android:text="@string/discovery" />
 
+        <io.legado.app.lib.theme.view.ThemeCheckBox
+            android:id="@+id/cb_is_enable_cookie"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:checked="true"
+            android:text="@string/auto_save_cookie" />
+
         <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"

+ 7 - 0
app/src/main/res/layout/activity_rss_source_edit.xml

@@ -45,6 +45,13 @@
             android:text="@string/single_url"
             tools:ignore="TouchTargetSizeCheck" />
 
+        <io.legado.app.lib.theme.view.ThemeCheckBox
+            android:id="@+id/cb_is_enable_cookie"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:checked="true"
+            android:text="@string/auto_save_cookie" />
+
     </LinearLayout>
 
     <LinearLayout

+ 23 - 0
app/src/main/res/layout/item_book_file_import.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp">
+
+    <TextView
+        android:id="@+id/cb_file_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@color/primaryText"
+        android:textSize="14sp"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="TouchTargetSizeCheck" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików