Merge remote-tracking branch 'source/master'

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

@ -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) * [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/) * [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b) * [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
#### IOS-苹果 #### IOS-苹果

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

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

File diff suppressed because it is too large Load Diff

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

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

@ -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** **2022/05/10**
* 更新cronet: 101.0.4951.61 * 更新cronet: 101.0.4951.61

@ -25,7 +25,7 @@
<div> <div>
<div>源类型 :</div> <div>源类型 :</div>
<textarea rows="1" id="bookSourceType" class="base" title="bookSourceType" <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>
<div>源名称 :</div> <div>源名称 :</div>

@ -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; 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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -1,33 +1,26 @@
package io.legado.app.api.controller package io.legado.app.api.controller
import android.net.Uri
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.documentfile.provider.DocumentFile
import io.legado.app.api.ReturnData import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookProgress
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.CacheManager import io.legado.app.help.CacheManager
import io.legado.app.help.ContentProcessor 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.glide.ImageLoader
import io.legado.app.help.storage.AppWebDav import io.legado.app.help.storage.AppWebDav
import io.legado.app.model.BookCover import io.legado.app.model.BookCover
import io.legado.app.model.ReadBook 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.LocalBook
import io.legado.app.model.localBook.UmdFile
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.ui.book.read.page.provider.ImageProvider import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
object BookController { object BookController {
@ -177,7 +170,6 @@ object BookController {
var content: String? = BookHelp.getContent(book, chapter) var content: String? = BookHelp.getContent(book, chapter)
if (content != null) { if (content != null) {
val contentProcessor = ContentProcessor.get(book.name, book.origin) val contentProcessor = ContentProcessor.get(book.name, book.origin)
saveBookReadIndex(book, index)
content = runBlocking { content = runBlocking {
contentProcessor.getContent(book, chapter, content!!, includeTitle = false) contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
.joinToString("\n") .joinToString("\n")
@ -190,7 +182,6 @@ object BookController {
content = runBlocking { content = runBlocking {
WebBook.getContentAwait(this, bookSource, book, chapter).let { WebBook.getContentAwait(this, bookSource, book, chapter).let {
val contentProcessor = ContentProcessor.get(book.name, book.origin) val contentProcessor = ContentProcessor.get(book.name, book.origin)
saveBookReadIndex(book, index)
contentProcessor.getContent(book, chapter, it, includeTitle = false) contentProcessor.getContent(book, chapter, it, includeTitle = false)
.joinToString("\n") .joinToString("\n")
} }
@ -222,20 +213,26 @@ object BookController {
/** /**
* 保存进度 * 保存进度
*/ */
private fun saveBookReadIndex(book: Book, index: Int) { fun saveBookProgress(postData: String?): ReturnData {
book.durChapterIndex = index val returnData = ReturnData()
book.durChapterTime = System.currentTimeMillis() GSON.fromJsonObject<BookProgress>(postData)
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { .onFailure { it.printOnDebug() }
book.durChapterTitle = it.title .getOrNull()?.let { bookProgress ->
} appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
appDb.bookDao.update(book) book.durChapterIndex = bookProgress.durChapterIndex
AppWebDav.uploadBookProgress(book) book.durChapterPos = bookProgress.durChapterPos
if (ReadBook.book?.bookUrl == book.bookUrl) { book.durChapterTitle = bookProgress.durChapterTitle
ReadBook.book = book book.durChapterTime = bookProgress.durChapterTime
ReadBook.durChapterIndex = index appDb.bookDao.update(book)
ReadBook.clearTextChapter() AppWebDav.uploadBookProgress(bookProgress)
ReadBook.loadContent(true) 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() val fileData = parameters["fileData"]?.firstOrNull()
?: return returnData.setErrorMsg("fileData 不能为空") ?: return returnData.setErrorMsg("fileData 不能为空")
kotlin.runCatching { kotlin.runCatching {
val defaultBookTreeUri = AppConfig.defaultBookTreeUri LocalBook.importFileOnLine(fileData, fileName)
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)
}.onFailure { }.onFailure {
return when (it) { return when (it) {
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!") is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,9 +1,11 @@
package io.legado.app.help.http package io.legado.app.help.http
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.help.CacheManager
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.http.cronet.CronetInterceptor import io.legado.app.help.http.cronet.CronetInterceptor
import io.legado.app.help.http.cronet.CronetLoader import io.legado.app.help.http.cronet.CronetLoader
import io.legado.app.utils.NetworkUtils
import okhttp3.* import okhttp3.*
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
@ -23,7 +25,10 @@ val cookieJar by lazy {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookies.forEach { 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}")
} }
} }

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

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

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

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

@ -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
}
}

@ -1,9 +1,11 @@
package io.legado.app.ui.widget.prefs package io.legado.app.lib.prefs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.EditTextPreferenceDialogFragmentCompat import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.filletBackground import io.legado.app.lib.theme.filletBackground
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() { class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setBackgroundDrawable(requireContext().filletBackground) 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 return dialog
} }

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

@ -1,9 +1,11 @@
package io.legado.app.ui.widget.prefs package io.legado.app.lib.prefs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.ListPreferenceDialogFragmentCompat import androidx.preference.ListPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.filletBackground import io.legado.app.lib.theme.filletBackground
class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() { class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
@ -23,6 +25,13 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setBackgroundDrawable(requireContext().filletBackground) 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 return dialog
} }

@ -1,9 +1,11 @@
package io.legado.app.ui.widget.prefs package io.legado.app.lib.prefs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.filletBackground import io.legado.app.lib.theme.filletBackground
class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() { class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
@ -25,6 +27,13 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setBackgroundDrawable(requireContext().filletBackground) 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 return dialog
} }

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

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

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

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

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

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

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

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

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

@ -1,24 +1,29 @@
package io.legado.app.model.localBook package io.legado.app.model.localBook
import android.net.Uri import android.net.Uri
import android.util.Base64
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.script.SimpleBindings import com.script.SimpleBindings
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.appDb 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.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.exception.NoStackTraceException
import io.legado.app.exception.TocEmptyException import io.legado.app.exception.TocEmptyException
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.*
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.regex.Pattern import java.util.regex.Pattern
/**
* 书籍文件导入 目录正文解析
* 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
*/
object LocalBook { object LocalBook {
private val nameAuthorPatterns = arrayOf( private val nameAuthorPatterns = arrayOf(
@ -41,6 +46,20 @@ object LocalBook {
throw FileNotFoundException("${uri.path} 文件不存在") 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) @Throws(Exception::class)
fun getChapterList(book: Book): ArrayList<BookChapter> { fun getChapterList(book: Book): ArrayList<BookChapter> {
val chapters = when { 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 { fun importFile(uri: Uri): Book {
val bookUrl: String val bookUrl: String
val updateTime: Long val updateTime: Long
@ -119,7 +155,10 @@ object LocalBook {
return book return book
} }
fun analyzeNameAuthor(fileName: String): Pair<String, String> { /**
* 从文件分析书籍必要信息书名 作者等
*/
private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
val tempFileName = fileName.substringBeforeLast(".") val tempFileName = fileName.substringBeforeLast(".")
var name: String var name: String
var author: 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
}
} }

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

@ -1,6 +1,8 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import android.text.TextUtils
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
@ -137,14 +139,29 @@ object BookInfo {
Debug.log(bookSource.bookSourceUrl, "${e.localizedMessage}") Debug.log(bookSource.bookSourceUrl, "${e.localizedMessage}")
DebugLog.e("获取封面出错", e) DebugLog.e("获取封面出错", e)
} }
scope.ensureActive() if (book.type != BookType.file) {
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") scope.ensureActive()
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true) Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
if (book.tocUrl == baseUrl) { if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
book.tocHtml = body 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}")
} }
} }

@ -6,6 +6,7 @@ import com.script.ScriptException
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb import io.legado.app.data.appDb
@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
mBook = WebBook.getBookInfoAwait(this, source, mBook) 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 toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
//校验正文 //校验正文

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

@ -2,16 +2,7 @@ package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R
import io.legado.app.base.BaseViewModel 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) { 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)
)
}
}
} }

@ -61,21 +61,7 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
fun importSource(text: String) { fun importSource(text: String) {
execute { execute {
val mText = text.trim() importSourceAwait(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))
}
}.onError { }.onError {
it.printOnDebug() it.printOnDebug()
errorLiveData.postValue(it.localizedMessage ?: "") 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) { private suspend fun importSourceUrl(url: String) {
okHttpClient.newCallResponseBody { okHttpClient.newCallResponseBody {
url(url) url(url)
}.text().let { }.text().let {
allSources.addAll(HttpTTS.fromJsonArray(it).getOrThrow()) importSourceAwait(it)
} }
} }

@ -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()
}
}
}
}
}
}
}

@ -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}")
}
}
}

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

@ -6,12 +6,15 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.ReplaceAnalyzer import io.legado.app.help.ReplaceAnalyzer
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.http.newCallResponseBody import io.legado.app.help.http.newCallResponseBody
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text import io.legado.app.help.http.text
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
@ -83,17 +86,7 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
fun import(text: String) { fun import(text: String) {
execute { execute {
if (text.isAbsUrl()) { importAwait(text.trim())
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)
}
}.onError { }.onError {
errorLiveData.postValue(it.localizedMessage ?: "ERROR") errorLiveData.postValue(it.localizedMessage ?: "ERROR")
}.onSuccess { }.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() { private fun comparisonSource() {
execute { execute {
allRules.forEach { allRules.forEach {

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

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

@ -229,8 +229,12 @@ abstract class BaseReadBookActivity :
customView { alertBinding.root } customView { alertBinding.root }
yesButton { yesButton {
alertBinding.run { alertBinding.run {
val start = editStart.text?.toString()?.toInt() ?: 0 val start = editStart.text!!.toString().let {
val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum 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) CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
} }
} }

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

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

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

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

@ -7,6 +7,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.RuleComplete import io.legado.app.help.RuleComplete
import io.legado.app.help.http.CookieStore
import io.legado.app.help.http.newCallStrResponse import io.legado.app.help.http.newCallStrResponse
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.utils.* 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? { fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
if (autoComplete) { if (autoComplete) {
return RuleComplete.autoComplete(rule, preRule, type) return RuleComplete.autoComplete(rule, preRule, type)

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

@ -97,7 +97,15 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
viewModel.bookData.value?.bookUrl?.let { bookUrl -> viewModel.bookData.value?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) { if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(chapter.getFileName()) 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) mLayoutManager.scrollToPositionWithOffset(scrollPos, 0)
adapter.upDisplayTitles(scrollPos)
} }
} }
override fun clearDisplayTitle() { override fun clearDisplayTitle() {
adapter.clearDisplayTitle() adapter.clearDisplayTitle()
adapter.upDisplayTitle() adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition())
} }
override val scope: CoroutineScope override val scope: CoroutineScope

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

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

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

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

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

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

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

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

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

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

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

@ -6,6 +6,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.RuleComplete import io.legado.app.help.RuleComplete
import io.legado.app.help.http.CookieStore
import io.legado.app.utils.getClipText import io.legado.app.utils.getClipText
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.printOnDebug 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? { fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
if (autoComplete) { if (autoComplete) {
return RuleComplete.autoComplete(rule, preRule, type) return RuleComplete.autoComplete(rule, preRule, type)

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

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

@ -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)
}
}

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

@ -1,20 +1,22 @@
package io.legado.app.utils package io.legado.app.utils
import android.graphics.Color import android.graphics.Color
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import java.util.* import androidx.core.graphics.ColorUtils
import kotlin.math.* import kotlin.math.*
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused", "MemberVisibilityCanBePrivate")
object ColorUtils { object ColorUtils {
fun isColorLight(@ColorInt color: Int): Boolean {
return ColorUtils.calculateLuminance(color) >= 0.5
}
fun intToString(intColor: Int): String { fun intToString(intColor: Int): String {
return String.format("#%06X", 0xFFFFFF and intColor) return String.format("#%06X", 0xFFFFFF and intColor)
} }
fun stripAlpha(@ColorInt color: Int): Int { fun stripAlpha(@ColorInt color: Int): Int {
return -0x1000000 or color return -0x1000000 or color
} }
@ -39,12 +41,6 @@ object ColorUtils {
return shiftColor(color, 1.1f) 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 @ColorInt
fun invertColor(@ColorInt color: Int): Int { fun invertColor(@ColorInt color: Int): Int {
val r = 255 - Color.red(color) val r = 255 - Color.red(color)
@ -81,83 +77,6 @@ object ColorUtils {
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt()) 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 { fun argb(R: Int, G: Int, B: Int): Int {
return argb(Byte.MAX_VALUE.toInt(), R, G, B) 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)) + (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 * 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} * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
*/ */
fun getColorDifference(a: Int, b: Int): Double { fun getColorDifference(a: Int, b: Int): Double {
val r1: Int = Color.red(a) val lab1 = DoubleArray(3)
val g1: Int = Color.green(a) val lab2 = DoubleArray(3)
val b1: Int = Color.blue(a) ColorUtils.colorToLAB(a, lab1)
val r2: Int = Color.red(b) ColorUtils.colorToLAB(b, lab2)
val g2: Int = Color.green(b)
val b2: Int = Color.blue(b)
val lab1 = rgb2lab(r1, g1, b1)
val lab2 = rgb2lab(r2, g2, b2)
return sqrt( return sqrt(
(lab2[0] - lab1[0].toDouble()) (lab2[0] - lab1[0])
.pow(2.0) + (lab2[1] - lab1[1].toDouble()) .pow(2.0) + (lab2[1] - lab1[1])
.pow(2.0) + (lab2[2] - lab1[2].toDouble()) .pow(2.0) + (lab2[2] - lab1[2])
.pow(2.0) .pow(2.0)
) )
} }

@ -331,6 +331,7 @@ fun Context.openFileUri(uri: Uri, type: String? = null) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val Context.isWifiConnect: Boolean val Context.isWifiConnect: Boolean
@SuppressLint("MissingPermission")
get() { get() {
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI) val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
return info?.isConnected == true return info?.isConnected == true
@ -338,7 +339,7 @@ val Context.isWifiConnect: Boolean
val Context.isPad: Boolean val Context.isPad: Boolean
get() { 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 val Context.channel: String

@ -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
}
}

@ -1,11 +1,13 @@
package io.legado.app.utils package io.legado.app.utils
import androidx.core.os.postDelayed
import io.legado.app.exception.RegexTimeoutException import io.legado.app.exception.RegexTimeoutException
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.coroutines.resume 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) val result = regex.replace(charSequence, replacement)
block.resume(result) block.resume(result)
} catch (e: Exception) { } catch (e: Exception) {
block.cancel(e) block.resumeWithException(e)
} }
} }
mainHandler.postDelayed({ mainHandler.postDelayed(timeout) {
if (thread.isAlive) { if (thread.isAlive) {
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}" val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
val exception = RegexTimeoutException(timeoutMsg) val exception = RegexTimeoutException(timeoutMsg)
block.cancel(exception) block.cancel(exception)
appCtx.longToastOnUi(timeoutMsg) appCtx.longToastOnUi(timeoutMsg)
CrashHandler.saveCrashInfo2File(exception) CrashHandler.saveCrashInfo2File(exception)
mainHandler.postDelayed({ mainHandler.postDelayed(3000) {
if (thread.isAlive) { if (thread.isAlive) {
appCtx.restart() appCtx.restart()
} }
}, 3000) }
} }
}, timeout) }
} }
} }

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

@ -33,6 +33,13 @@
android:checked="true" android:checked="true"
android:text="@string/discovery" /> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

@ -45,6 +45,13 @@
android:text="@string/single_url" android:text="@string/single_url"
tools:ignore="TouchTargetSizeCheck" /> 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>
<LinearLayout <LinearLayout

@ -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>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save