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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.filletBackground
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
dialog.window?.decorView?.post {
(dialog as AlertDialog).run {
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
}
}
return dialog
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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