# Conflicts:
#	app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
#	app/src/main/res/xml/pref_main.xml
pull/209/head
yangyxd 5 years ago
commit f42564facd
  1. 3
      .gitignore
  2. 48
      app/build.gradle
  3. 2
      app/proguard-rules.pro
  4. 4
      app/src/debug/res/values/strings.xml
  5. 6
      app/src/google/res/values-zh-rHK/strings.xml
  6. 6
      app/src/google/res/values/strings.xml
  7. 3
      app/src/main/AndroidManifest.xml
  8. 69
      app/src/main/assets/txtTocRule.json
  9. 98
      app/src/main/assets/updateLog.md
  10. 39
      app/src/main/assets/web/book.html
  11. 71
      app/src/main/assets/web/bookshelf.html
  12. 4
      app/src/main/assets/web/index.html
  13. 75
      app/src/main/assets/web/index.js
  14. 0
      app/src/main/assets/web/new/bookshelf.css
  15. 46
      app/src/main/assets/web/new/bookshelf.html
  16. 0
      app/src/main/assets/web/new/bookshelf.js
  17. 0
      app/src/main/assets/web/new/css/about.f23c15cb.css
  18. 0
      app/src/main/assets/web/new/css/app.e1c0d2e4.css
  19. 0
      app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css
  20. 0
      app/src/main/assets/web/new/css/detail.42c41bd6.css
  21. 0
      app/src/main/assets/web/new/fonts/element-icons.535877f5.woff
  22. 0
      app/src/main/assets/web/new/fonts/element-icons.732389de.ttf
  23. 0
      app/src/main/assets/web/new/fonts/iconfont.f9a3fb0e.woff
  24. 0
      app/src/main/assets/web/new/fonts/popfont.f39ecc1a.ttf
  25. 0
      app/src/main/assets/web/new/fonts/shelffont.6c094b6d.ttf
  26. 0
      app/src/main/assets/web/new/img/icons/android-chrome-192x192.png
  27. 0
      app/src/main/assets/web/new/img/icons/android-chrome-512x512.png
  28. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-120x120.png
  29. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-152x152.png
  30. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-180x180.png
  31. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-60x60.png
  32. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-76x76.png
  33. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon.png
  34. 0
      app/src/main/assets/web/new/img/icons/favicon-16x16.png
  35. 0
      app/src/main/assets/web/new/img/icons/favicon-32x32.png
  36. 0
      app/src/main/assets/web/new/img/icons/msapplication-icon-144x144.png
  37. 0
      app/src/main/assets/web/new/img/icons/mstile-150x150.png
  38. 0
      app/src/main/assets/web/new/img/icons/safari-pinned-tab.svg
  39. 0
      app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg
  40. 0
      app/src/main/assets/web/new/js/about.2589b5fe.js
  41. 0
      app/src/main/assets/web/new/js/about~detail.08c372e6.js
  42. 0
      app/src/main/assets/web/new/js/app.b25f3cec.js
  43. 0
      app/src/main/assets/web/new/js/chunk-vendors.b3838a2d.js
  44. 0
      app/src/main/assets/web/new/js/detail.043d6e39.js
  45. 0
      app/src/main/assets/web/new/manifest.json
  46. 2
      app/src/main/java/io/legado/app/App.kt
  47. 17
      app/src/main/java/io/legado/app/constant/AppConst.kt
  48. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  49. 27
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  50. 8
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  51. 9
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  52. 11
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  53. 2
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  54. 8
      app/src/main/java/io/legado/app/data/entities/Book.kt
  55. 122
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  56. 4
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  57. 15
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  58. 2
      app/src/main/java/io/legado/app/data/entities/RssStar.kt
  59. 1
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  60. 46
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  61. 33
      app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt
  62. 47
      app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt
  63. 47
      app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt
  64. 37
      app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt
  65. 11
      app/src/main/java/io/legado/app/help/AppConfig.kt
  66. 237
      app/src/main/java/io/legado/app/help/BookHelp.kt
  67. 179
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  68. 74
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  69. 1
      app/src/main/java/io/legado/app/help/http/AjaxWebView.kt
  70. 18
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  71. 10
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  72. 4
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  73. 32
      app/src/main/java/io/legado/app/help/storage/OldReplace.kt
  74. 123
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  75. 22
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  76. 3
      app/src/main/java/io/legado/app/model/Debug.kt
  77. 6
      app/src/main/java/io/legado/app/model/Rss.kt
  78. 61
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  79. 217
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  80. 3
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  81. 11
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  82. 1
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  83. 1
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  84. 3
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  85. 19
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  86. 22
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  87. 84
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  88. 35
      app/src/main/java/io/legado/app/service/DownloadService.kt
  89. 66
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  90. 34
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  91. 5
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  92. 1
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  93. 2
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  94. 4
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  95. 17
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  96. 24
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  97. 4
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  98. 10
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  99. 9
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt
  100. 6
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  101. Some files were not shown because too many files have changed in this diff Show More

3
.gitignore vendored

@ -9,4 +9,7 @@
/release
/tmp
node_modules/
/app/app
/app/google
/app/gradle.properties
package-lock.json

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders"
apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'io.fabric'
androidExtensions {
@ -19,6 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android {
compileSdkVersion 29
flavorDimensions("version")
signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
@ -37,7 +38,6 @@ android {
targetSdkVersion 29
versionCode gitCommits
versionName version
flavorDimensions "versionCode"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
@ -74,13 +74,15 @@ android {
}
}
}
productFlavors{
app{
manifestPlaceholders = [APP_CHANNEL_VALUE:"app"]
productFlavors {
app {
dimension "version"
manifestPlaceholders = [APP_CHANNEL_VALUE: "app"]
}
google{
google {
dimension "version"
applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE:"google"]
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
}
}
compileOptions {
@ -108,27 +110,27 @@ kapt {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.2.3'
implementation 'com.google.firebase:firebase-core:17.4.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.google.code.gson:gson:2.8.6'
//lifecycle
def lifecycle_version = '2.2.0'
@ -157,19 +159,19 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino
implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
//
//noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1'
@ -182,15 +184,21 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0'
//apache
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-lang3:3.10'
implementation 'org.apache.commons:commons-text:1.8'
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
implementation 'ru.noties.markwon:core:3.1.0'
//
implementation 'com.github.houbb:opencc4j:1.4.0'
implementation 'com.hankcs:hanlp:portable-1.7.7'
}
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.gms.google-services'
afterEvaluate {
for (Task task : project.tasks.matching { it.name.startsWith('crashlyticsUploadDeobs') }) {
task.enabled = false
}
}

@ -160,7 +160,6 @@
-dontwarn rx.**
-dontwarn okio.**
-dontwarn retrofit2.**
-dontwarn javax.annotation.**
-dontwarn org.apache.log4j.lf5.viewer.**
-dontnote org.apache.log4j.lf5.viewer.**
@ -172,7 +171,6 @@
-dontwarn com.jeremyliao.liveeventbus.**
-keep class com.jeremyliao.liveeventbus.** { *; }
-keep class retrofit2.**{*;}
-keep class okhttp3.**{*;}
-keep class okio.**{*;}
-keep class com.hwangjr.rxbus.**{*;}

@ -1,4 +1,4 @@
<resources>
<string name="app_name">阅读.debug</string>
<string name="receiving_shared_label">阅读.debug·搜索</string>
<string name="app_name">阅读·D</string>
<string name="receiving_shared_label">阅读·D·搜索</string>
</resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閱讀Pro</string>
</resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">阅读Pro</string>
</resources>

@ -257,7 +257,7 @@
android:launchMode="singleTop" />
<!--订阅条目-->
<activity
android:name=".ui.rss.article.RssArticlesActivity"
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--Rss收藏-->
<activity
@ -300,6 +300,7 @@
<data android:mimeType="application/json" />
</intent-filter>
</activity>
<service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" />
<service android:name=".service.WebService" />

@ -1,98 +1,121 @@
[
{
"id": -1,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 0
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": -17
},
{
"id": -2,
"enable": false,
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1
"rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": -16
},
{
"id": -3,
"enable": false,
"name": "目录(简介)",
"rule": "(?<=[ \\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 2
"name": "目录(匹配简介)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": -15
},
{
"id": -4,
"enable": false,
"name": "目录(古典、轻小说备用)",
"rule": "^[  \\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 3
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": -14
},
{
"id": -5,
"enable": false,
"name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$",
"serialNumber": 4
"serialNumber": -13
},
{
"id": -6,
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$",
"serialNumber": 5
"rule": "^[  \\t]{0,4}\\d{1,5}[,., 、_—\\-].{1,30}$",
"serialNumber": -12
},
{
"id": -7,
"enable": true,
"name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$",
"serialNumber": -11
},
{
"id": -8,
"enable": true,
"name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"serialNumber": 6
"serialNumber": -10
},
{
"id": -9,
"enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 7
"serialNumber": -9
},
{
"id": -10,
"enable": false,
"name": "Chapter(去简介)",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 8
"serialNumber": -8
},
{
"id": -11,
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$",
"serialNumber": 9
"rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"serialNumber": -7
},
{
"id": -12,
"enable": false,
"name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 10
"serialNumber": -6
},
{
"id": -13,
"enable":true,
"name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 11
"serialNumber": -5
},
{
"id": -14,
"enable": true,
"name": "章/卷 序号 标题",
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"serialNumber": 12
"serialNumber": -4
},
{
"id": -15,
"enable":false,
"name": "顶格标题",
"rule": "^\\S.{1,20}$",
"serialNumber": 13
"serialNumber": -3
},
{
"id": -16,
"enable":false,
"name": "双标题(前向)",
"rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)",
"serialNumber": 14
"serialNumber": -2
},
{
"id": -17,
"enable":false,
"name": "双标题(后向)",
"rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$",
"serialNumber": 15
"serialNumber": -1
}
]

@ -1,7 +1,101 @@
## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
* 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 新公众号[开源阅读]()已启用,[开源阅读软件]()备用
**2020/05/04**
* 优化txt文件目录解析
**2020/05/03**
* 优化一些界面显示问题
* 订阅源添加style
* 修复一些重复目录的bug
**2020/05/02**
* 修复不停换源的bug
* 修复本地书籍自动换源
* 修复书源校验的一些问题
**2020/05/01**
* 尝试修复朗读时可能错位的bug
* 添加自动换源配置
* 换源添加禁用菜单
**2020/04/29**
* 修复bug
* 订阅界面添加长按菜单
**2020/04/26**
* 添加导入旧的书源转换
* 修复不自动朗读下一章的bug
**2020/04/25**
* 修复翻页按键设置为空时崩溃的bug
* 翻页按键优先自定义按键,可覆盖音量按键
* 写书源时的辅助键盘添加※
* 更改了书源格式,不再需要转义符
**2020/04/24**
* 坚果云最近调整了策略,必须使用应用密码才能备份,用户信息,安全,第三方应用
* text目录规则添加id字段,负值为系统自带规则
* 其它一些优化
**2020/04/20**
* 优化阅读界面信息显示
**2020/04/19**
* 添加阅读界面各种信息设置
**2020/04/18**
* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, hingbong
* 修复更新时间不对的bug
**2020/04/13**
* 去除rss朗读时的引号
**2020/04/13**
* 修复调用webView返回结果多了引号的bug
**2020/04/12**
* 解决无法取消加粗的bug
* 修复换源自动加入书架的bug
**2020/04/09**
* 修复书架刷新闪烁
**2020/04/08**
* 可以隐藏书架未分组
**2020/04/07**
* 书架添加未分组,有未分组书籍时自动显示
* 其它一些优化
**2020/04/04**
* 优化备份逻辑
* 修复订阅分类太多显示不全的bug
* 修复一些分类要手动刷新的问题
**2020/04/02**
* 书架书名和作者作为唯一值
* 添加订阅分类,分类规则和发现一样,分类一::url1 && 分类2::url2
**2020/03/29**
* 添加退出软件后是否响应耳机按键的开关
* 优化书源校验
**2020/03/26**
* 修复txt目录bug
* 最近工作比较忙,只有晚上有时间写软件,bug之类的不要催,白天不回消息
**2020/03/25**
* 修复7.1.1的网络问题,是retrofit2库最新版本的bug,暂时退回上版本
* 去除下载路径的配置,减少错误
* 添加隐藏状态栏是否扩展到刘海
**2020/03/24**
* txt文件第一章之前的文字不再放到简介里
* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别
* 修复文件关联 by wqfantexi
**2020/03/22**
* 添加文件关联 by wqfantexi

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>阅读3.0书架</title>
<link rel="icon" href="favicon.ico">
<link href="book.css" rel="stylesheet" />
</head>
<body>
<button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button>
<div class="nav">
<button id="back">返回</button>
<button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value="" />
<button id="refresh">重新加载</button>
</div>
<div class="allcontent" id="allcontent">
<div id="books" class="books"></div>
<div id="more" class="more">
<div id="info" class="info"></div>
<div class="clear"></div>
<div id="chapter" class="chapter"></div>
<div id="content" class="content"></div>
<div id="page" class="button">
<center><button id='up'>上一章</button><button id='down'>下一章</button></center>
</div>
</div>
</div>
<script src="book.js"></script>
</body>
</html>

@ -1,46 +1,39 @@
<!DOCTYPE html>
<html lang=en style="padding: 0;height:100%">
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<!--[if IE]><link rel="icon" href="favicon.ico" /><![endif]-->
<title>yd-web-tool</title>
<link href=css/about.f23c15cb.css rel=prefetch>
<link href=css/detail.42c41bd6.css rel=prefetch>
<link href=js/about.2589b5fe.js rel=prefetch>
<link href=js/about~detail.08c372e6.js rel=prefetch>
<link href=js/detail.043d6e39.js rel=prefetch>
<link href=css/app.e1c0d2e4.css rel=preload as=style>
<link href=css/chunk-vendors.ad4ff18f.css rel=preload as=style>
<link href=js/app.b25f3cec.js rel=preload as=script>
<link href=js/chunk-vendors.b3838a2d.js rel=preload as=script>
<link href=css/chunk-vendors.ad4ff18f.css rel=stylesheet>
<link href=css/app.e1c0d2e4.css rel=stylesheet>
<link rel=icon type=image/png sizes=32x32 href=img/icons/favicon-32x32.png>
<link rel=icon type=image/png sizes=16x16 href=img/icons/favicon-16x16.png>
<link rel=manifest href=manifest.json>
<meta name=theme-color content=#4DBA87>
<meta name=apple-mobile-web-app-capable content=no>
<meta name=apple-mobile-web-app-status-bar-style content=default>
<meta name=apple-mobile-web-app-title content=yd-web-tool>
<link rel=apple-touch-icon href=img/icons/apple-touch-icon-152x152.png>
<link rel=mask-icon href=img/icons/safari-pinned-tab.svg color=#4DBA87>
<meta name=msapplication-TileImage content=img/icons/msapplication-icon-144x144.png>
<meta name=msapplication-TileColor content=#000000>
<meta charset="utf-8" />
<title>阅读3.0书架</title>
<link rel="icon" href="favicon.ico">
<link href="bookshelf.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.b3838a2d.js></script>
<script src=js/app.b25f3cec.js></script>
<body>
<button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button>
<div class="nav">
<button id="back">返回</button>
<button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value="" />
<button id="refresh">重新加载</button>
</div>
<div class="allcontent" id="allcontent">
<div id="books" class="books"></div>
<div id="more" class="more">
<div id="info" class="info"></div>
<div class="clear"></div>
<div id="chapter" class="chapter"></div>
<div id="content" class="content"></div>
<div id="page" class="button">
<center><button id='up'>上一章</button><button id='down'>下一章</button></center>
</div>
</div>
</div>
<script src="bookshelf.js"></script>
</body>
</html>

@ -359,8 +359,8 @@
<br>(?i) 前缀表示忽略大小写
</div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a>
<a target="_blank" href="book.html">阅读书架(经典)</a>
<a target="_blank" href="bookshelf.html">阅读书架(新潮)</a>
<a target="_blank" href="bookshelf.html">阅读书架(经典)</a>
<a target="_blank" href="new/bookshelf.html">阅读书架(新潮)</a>
</div>
</div>
</div>

@ -36,23 +36,28 @@ const RuleJSON = (() => {
// 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
ruleJson.ruleSearch = JSON.stringify(searchJson);
//ruleJson.ruleSearch = JSON.stringify(searchJson);
ruleJson.ruleSearch = searchJson;
// 发现规则
$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
ruleJson.ruleExplore = JSON.stringify(exploreJson);
//ruleJson.ruleExplore = JSON.stringify(exploreJson);
ruleJson.ruleExplore = exploreJson;
// 详情页规则
$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
//ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
ruleJson.ruleBookInfo = bookInfoJson;
// 目录规则
$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
ruleJson.ruleToc = JSON.stringify(tocJson);
//ruleJson.ruleToc = JSON.stringify(tocJson);
ruleJson.ruleToc = tocJson;
// 正文规则
$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
ruleJson.ruleContent = JSON.stringify(contentJson);
//ruleJson.ruleContent = JSON.stringify(contentJson);
ruleJson.ruleContent = contentJson;
return ruleJson;
})();
@ -110,38 +115,48 @@ function rule2json() {
// 转换搜索规则
let searchJson = {};
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
searchJson[key] = $('#' + 'ruleSearch_' + key).value;
});
RuleJSON.ruleSearch = JSON.stringify(searchJson);
//RuleJSON.ruleSearch = JSON.stringify(searchJson);
RuleJSON.ruleSearch = searchJson;
// 转换发现规则
let exploreJson = {};
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
Object.keys(RuleJSON.ruleExplore).forEach(key => {
exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
});
RuleJSON.ruleExplore = JSON.stringify(exploreJson);
//RuleJSON.ruleExplore = JSON.stringify(exploreJson);
RuleJSON.ruleExplore = exploreJson;
// 转换详情页规则
let bookInfoJson = {};
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
});
RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
//RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
RuleJSON.ruleBookInfo = bookInfoJson;
// 转换目录规则
let tocJson = {};
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
Object.keys(RuleJSON.ruleToc).forEach(key => {
tocJson[key] = $('#' + 'ruleToc_' + key).value;
});
RuleJSON.ruleToc = JSON.stringify(tocJson);
//RuleJSON.ruleToc = JSON.stringify(tocJson);
RuleJSON.ruleToc = tocJson;
// 转换正文规则
let contentJson = {};
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
Object.keys(RuleJSON.ruleContent).forEach(key => {
contentJson[key] = $('#' + 'ruleContent_' + key).value;
});
RuleJSON.ruleContent = JSON.stringify(contentJson);
//RuleJSON.ruleContent = JSON.stringify(contentJson);
RuleJSON.ruleContent = contentJson;
RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime);
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
@ -171,40 +186,50 @@ function json2rule(RuleEditor) {
// 转换搜索规则
if (RuleEditor.ruleSearch) {
let searchJson = JSON.parse(RuleEditor.ruleSearch);
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
//let searchJson = JSON.parse(RuleEditor.ruleSearch);
let searchJson = RuleEditor.ruleSearch;
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
});
}
// 转换发现规则
if (RuleEditor.ruleExplore) {
let exploreJson = JSON.parse(RuleEditor.ruleExplore);
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
//let exploreJson = JSON.parse(RuleEditor.ruleExplore);
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
let exploreJson = RuleEditor.ruleExplore;
Object.keys(RuleJSON.ruleExplore).forEach(key => {
$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
});
}
// 转换详情页规则
if (RuleEditor.ruleBookInfo) {
let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
//let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
let bookInfoJson = RuleEditor.ruleBookInfo;
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
});
}
// 转换目录规则
if (RuleEditor.ruleToc) {
let tocJson = JSON.parse(RuleEditor.ruleToc);
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
//let tocJson = JSON.parse(RuleEditor.ruleToc);
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
let tocJson = RuleEditor.ruleToc;
Object.keys(RuleJSON.ruleToc).forEach(key => {
$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
});
}
// 转换正文规则
if (RuleEditor.ruleContent) {
let contentJson = JSON.parse(RuleEditor.ruleContent);
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
//let contentJson = JSON.parse(RuleEditor.ruleContent);
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
let contentJson = RuleEditor.ruleContent;
Object.keys(RuleJSON.ruleContent).forEach(key => {
$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
});
}

@ -0,0 +1,46 @@
<!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">
<!--[if IE]><link rel="icon" href="favicon.ico" /><![endif]-->
<title>yd-web-tool</title>
<link href=css/about.f23c15cb.css rel=prefetch>
<link href=css/detail.42c41bd6.css rel=prefetch>
<link href=js/about.2589b5fe.js rel=prefetch>
<link href=js/about~detail.08c372e6.js rel=prefetch>
<link href=js/detail.043d6e39.js rel=prefetch>
<link href=css/app.e1c0d2e4.css rel=preload as=style>
<link href=css/chunk-vendors.ad4ff18f.css rel=preload as=style>
<link href=js/app.b25f3cec.js rel=preload as=script>
<link href=js/chunk-vendors.b3838a2d.js rel=preload as=script>
<link href=css/chunk-vendors.ad4ff18f.css rel=stylesheet>
<link href=css/app.e1c0d2e4.css rel=stylesheet>
<link rel=icon type=image/png sizes=32x32 href=img/icons/favicon-32x32.png>
<link rel=icon type=image/png sizes=16x16 href=img/icons/favicon-16x16.png>
<link rel=manifest href=manifest.json>
<meta name=theme-color content=#4DBA87>
<meta name=apple-mobile-web-app-capable content=no>
<meta name=apple-mobile-web-app-status-bar-style content=default>
<meta name=apple-mobile-web-app-title content=yd-web-tool>
<link rel=apple-touch-icon href=img/icons/apple-touch-icon-152x152.png>
<link rel=mask-icon href=img/icons/safari-pinned-tab.svg color=#4DBA87>
<meta name=msapplication-TileImage content=img/icons/msapplication-icon-144x144.png>
<meta name=msapplication-TileColor content=#000000>
</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.b3838a2d.js></script>
<script src=js/app.b25f3cec.js></script>
</body>
</html>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -73,7 +73,7 @@ class App : Application() {
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800))
getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground(

@ -41,17 +41,30 @@ object AppConst {
val keyboardToolChars: List<String> by lazy {
arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
"href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag"
"", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\",
"$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css",
"id", "class", "tag"
)
}
val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
val bookGroupNone = BookGroup(-4, App.INSTANCE.getString(R.string.no_group))
const val notificationIdRead = 1144771
const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773
const val notificationIdDownload = 1144774
val urlOption: String by lazy {
"""
,{
"charset": "",
"method": "POST",
"body": "",
"headers": {"User-Agent": ""}
}
""".trimIndent()
}
}

@ -42,4 +42,5 @@ object PreferKey {
const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh"
}

@ -4,6 +4,7 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.data.dao.*
import io.legado.app.data.entities.*
@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 8,
version = 12,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -30,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addMigrations(migration_10_11, migration_11_12)
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
@ -37,6 +39,29 @@ abstract class AppDatabase : RoomDatabase() {
})
.build()
}
private val migration_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules")
database.execSQL(
"""
CREATE TABLE txtTocRules(id INTEGER NOT NULL,
name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL,
enable INTEGER NOT NULL, PRIMARY KEY (id))
"""
)
}
}
private val migration_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
ALTER TABLE rssSources ADD style TEXT
"""
)
}
}
}
abstract fun bookDao(): BookDao

@ -10,16 +10,16 @@ import io.legado.app.data.entities.BookChapter
@Dao
interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl")
@Query("select * from chapters where bookUrl = :bookUrl order by `index`")
fun observeByBook(bookUrl: String): LiveData<List<BookChapter>>
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%'")
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`")
fun liveDataSearch(bookUrl: String, key: String): LiveData<List<BookChapter>>
@Query("select * from chapters where bookUrl = :bookUrl")
@Query("select * from chapters where bookUrl = :bookUrl order by `index`")
fun getChapterList(bookUrl: String): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end")
@Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end order by `index`")
fun getChapterList(bookUrl: String, start: Int, end: Int): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")

@ -30,6 +30,9 @@ interface BookDao {
@Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>>
@Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroupSize(): LiveData<Int>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>
@ -42,6 +45,12 @@ interface BookDao {
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
fun getBook(bookUrl: String): Book?
@Query("SELECT * FROM books WHERE name = :name and author = :author")
fun getBook(name: String, author: String): Book?
@get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
val noGroupSize: Int
@get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0")
val webBooks: List<Book>

@ -12,17 +12,18 @@ interface RssArticleDao {
fun get(origin: String, link: String): RssArticle?
@Query(
"""select t1.link, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
from rssArticles as t1 left join rssReadRecords as t2
on t1.link = t2.record where origin = :origin order by `order` desc"""
on t1.link = t2.record where origin = :origin and sort = :sort
order by `order` desc"""
)
fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
fun liveByOriginSort(origin: String, sort: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssArticle: RssArticle)
@Query("delete from rssArticles where origin = :origin and `order` < :order")
fun clearOld(origin: String, order: Long)
@Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order")
fun clearOld(origin: String, sort: String, order: Long)
@Update
fun update(vararg rssArticle: RssArticle)

@ -28,4 +28,6 @@ interface TxtTocRuleDao {
@Delete
fun delete(vararg rule: TxtTocRule)
@Query("delete from txtTocRules where id < 0")
fun deleteDefault()
}

@ -4,7 +4,6 @@ import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.utils.GSON
@ -15,9 +14,12 @@ import java.nio.charset.Charset
import kotlin.math.max
@Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
@Entity(
tableName = "books",
primaryKeys = ["name", "author"],
indices = [(Index(value = ["bookUrl"], unique = true))]
)
data class Book(
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)

@ -1,10 +1,7 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.*
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.userAgent
@ -15,12 +12,12 @@ import io.legado.app.utils.ACache
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefString
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.util.*
import javax.script.SimpleBindings
@Parcelize
@TypeConverters(BookSource.Converters::class)
@Entity(
tableName = "book_sources",
indices = [(Index(value = ["bookSourceUrl"], unique = false))]
@ -40,12 +37,12 @@ data class BookSource(
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, // 发现url
var ruleExplore: String? = null, // 发现规则
var ruleExplore: ExploreRule? = null, // 发现规则
var searchUrl: String? = null, // 搜索url
var ruleSearch: String? = null, // 搜索规则
var ruleBookInfo: String? = null, // 书籍信息页规则
var ruleToc: String? = null, // 目录页规则
var ruleContent: String? = null // 正文页规则
var ruleSearch: SearchRule? = null, // 搜索规则
var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则
var ruleToc: TocRule? = null, // 目录页规则
var ruleContent: ContentRule? = null // 正文页规则
) : Parcelable, JsExtensions {
override fun hashCode(): Int {
@ -59,26 +56,6 @@ data class BookSource(
return false
}
@Ignore
@IgnoredOnParcel
private var searchRuleV: SearchRule? = null
@Ignore
@IgnoredOnParcel
private var exploreRuleV: ExploreRule? = null
@Ignore
@IgnoredOnParcel
private var bookInfoRuleV: BookInfoRule? = null
@Ignore
@IgnoredOnParcel
private var tocRuleV: TocRule? = null
@Ignore
@IgnoredOnParcel
private var contentRuleV: ContentRule? = null
@Throws(Exception::class)
fun getHeaderMap(): Map<String, String> {
val headerMap = HashMap<String, String>()
@ -99,43 +76,23 @@ data class BookSource(
}
fun getSearchRule(): SearchRule {
searchRuleV ?: let {
searchRuleV = GSON.fromJsonObject<SearchRule>(ruleSearch)
searchRuleV ?: let { searchRuleV = SearchRule() }
}
return searchRuleV!!
return ruleSearch ?: SearchRule()
}
fun getExploreRule(): ExploreRule {
exploreRuleV ?: let {
exploreRuleV = GSON.fromJsonObject<ExploreRule>(ruleExplore)
exploreRuleV ?: let { exploreRuleV = ExploreRule() }
}
return exploreRuleV!!
return ruleExplore ?: ExploreRule()
}
fun getBookInfoRule(): BookInfoRule {
bookInfoRuleV ?: let {
bookInfoRuleV = GSON.fromJsonObject<BookInfoRule>(ruleBookInfo)
bookInfoRuleV ?: let { bookInfoRuleV = BookInfoRule() }
}
return bookInfoRuleV!!
return ruleBookInfo ?: BookInfoRule()
}
fun getTocRule(): TocRule {
tocRuleV ?: let {
tocRuleV = GSON.fromJsonObject<TocRule>(ruleToc)
tocRuleV ?: let { tocRuleV = TocRule() }
}
return tocRuleV!!
return ruleToc ?: TocRule()
}
fun getContentRule(): ContentRule {
contentRuleV ?: let {
contentRuleV = GSON.fromJsonObject<ContentRule>(ruleContent)
contentRuleV ?: let { contentRuleV = ContentRule() }
}
return contentRuleV!!
return ruleContent ?: ContentRule()
}
fun addGroup(group: String) {
@ -169,7 +126,7 @@ data class BookSource(
}
}
val b = a.split("(&&|\n)+".toRegex())
b.map { c ->
b.forEach { c ->
val d = c.split("::")
if (d.size > 1)
exploreKinds.add(ExploreKind(d[0], d[1]))
@ -219,4 +176,57 @@ data class BookSource(
var title: String,
var url: String? = null
)
class Converters {
@TypeConverter
fun exploreRuleToString(exploreRule: ExploreRule?): String {
return GSON.toJson(exploreRule)
}
@TypeConverter
fun stringToExploreRule(json: String?): ExploreRule? {
return GSON.fromJsonObject<ExploreRule>(json)
}
@TypeConverter
fun searchRuleToString(searchRule: SearchRule): String {
return GSON.toJson(searchRule)
}
@TypeConverter
fun stringToSearchRule(json: String?): SearchRule? {
return GSON.fromJsonObject<SearchRule>(json)
}
@TypeConverter
fun bookInfoRuleToString(bookInfoRule: BookInfoRule): String {
return GSON.toJson(bookInfoRule)
}
@TypeConverter
fun stringToBookInfoRule(json: String?): BookInfoRule? {
return GSON.fromJsonObject<BookInfoRule>(json)
}
@TypeConverter
fun tocRuleToString(tocRule: TocRule): String {
return GSON.toJson(tocRule)
}
@TypeConverter
fun stringToTocRule(json: String?): TocRule? {
return GSON.fromJsonObject<TocRule>(json)
}
@TypeConverter
fun contentRuleToString(contentRule: ContentRule): String {
return GSON.toJson(contentRule)
}
@TypeConverter
fun stringToContentRule(json: String?): ContentRule? {
return GSON.fromJsonObject<ContentRule>(json)
}
}
}

@ -9,6 +9,7 @@ import androidx.room.Entity
)
data class RssArticle(
var origin: String = "",
var sort: String = "",
var title: String = "",
var order: Long = 0,
var link: String = "",
@ -20,7 +21,7 @@ data class RssArticle(
) {
override fun hashCode(): Int {
return super.hashCode()
return link.hashCode()
}
override fun equals(other: Any?): Boolean {
@ -36,6 +37,7 @@ data class RssArticle(
fun toStar(): RssStar {
return RssStar(
origin = origin,
sort = sort,
title = title,
starTime = System.currentTimeMillis(),
link = link,

@ -23,6 +23,7 @@ data class RssSource(
var sourceIcon: String = "",
var sourceGroup: String? = null,
var enabled: Boolean = true,
var sortUrl: String? = null,
//列表规则
var ruleArticles: String? = null,
var ruleNextPage: String? = null,
@ -33,9 +34,11 @@ data class RssSource(
var ruleImage: String? = null,
var ruleLink: String? = null,
var ruleContent: String? = null,
var style: String? = null,
var header: String? = null,
var enableJs: Boolean = false,
var loadWithBaseUrl: Boolean = false,
var customOrder: Int = 0
) : Parcelable, JsExtensions {
@ -99,4 +102,16 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
}
fun sortUrls(): LinkedHashMap<String, String> {
val sortMap = linkedMapOf<String, String>()
sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c ->
val d = c.split("::")
if (d.size > 1)
sortMap[d[0]] = d[1]
}
if (sortMap.isEmpty()) {
sortMap[""] = sourceUrl
}
return sortMap
}
}

@ -9,6 +9,7 @@ import androidx.room.Entity
)
data class RssStar(
var origin: String = "",
var sort: String = "",
var title: String = "",
var starTime: Long = 0,
var link: String = "",
@ -20,6 +21,7 @@ data class RssStar(
fun toRssArticle(): RssArticle {
return RssArticle(
origin = origin,
sort = sort,
title = title,
link = link,
pubDate = pubDate,

@ -7,6 +7,7 @@ import androidx.room.PrimaryKey
@Entity(tableName = "txtTocRules")
data class TxtTocRule(
@PrimaryKey
var id: Long = System.currentTimeMillis(),
var name: String = "",
var rule: String = "",
var serialNumber: Int = -1,

@ -1,5 +1,8 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class BookInfoRule(
var init: String? = null,
var name: String? = null,
@ -11,4 +14,45 @@ data class BookInfoRule(
var coverUrl: String? = null,
var tocUrl: String? = null,
var wordCount: String? = null
)
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(init)
dest.writeString(name)
dest.writeString(author)
dest.writeString(intro)
dest.writeString(kind)
dest.writeString(lastChapter)
dest.writeString(updateTime)
dest.writeString(coverUrl)
dest.writeString(tocUrl)
dest.writeString(wordCount)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<BookInfoRule> {
override fun createFromParcel(parcel: Parcel): BookInfoRule {
return BookInfoRule(parcel)
}
override fun newArray(size: Int): Array<BookInfoRule?> {
return arrayOfNulls(size)
}
}
}

@ -1,8 +1,39 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class ContentRule(
var content: String? = null,
var nextContentUrl: String? = null,
var webJs: String? = null,
var sourceRegex: String? = null
)
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(content)
dest.writeString(nextContentUrl)
dest.writeString(webJs)
dest.writeString(sourceRegex)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ContentRule> {
override fun createFromParcel(parcel: Parcel): ContentRule {
return ContentRule(parcel)
}
override fun newArray(size: Int): Array<ContentRule?> {
return arrayOfNulls(size)
}
}
}

@ -1,5 +1,8 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class ExploreRule(
override var bookList: String? = null,
override var name: String? = null,
@ -11,4 +14,46 @@ data class ExploreRule(
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : BookListRule
) : BookListRule, Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(bookList)
dest.writeString(name)
dest.writeString(author)
dest.writeString(intro)
dest.writeString(kind)
dest.writeString(lastChapter)
dest.writeString(updateTime)
dest.writeString(bookUrl)
dest.writeString(coverUrl)
dest.writeString(wordCount)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ExploreRule> {
override fun createFromParcel(parcel: Parcel): ExploreRule {
return ExploreRule(parcel)
}
override fun newArray(size: Int): Array<ExploreRule?> {
return arrayOfNulls(size)
}
}
}

@ -1,5 +1,8 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class SearchRule(
override var bookList: String? = null,
override var name: String? = null,
@ -11,4 +14,46 @@ data class SearchRule(
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : BookListRule
) : BookListRule, Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(bookList)
dest.writeString(name)
dest.writeString(author)
dest.writeString(intro)
dest.writeString(kind)
dest.writeString(lastChapter)
dest.writeString(updateTime)
dest.writeString(bookUrl)
dest.writeString(coverUrl)
dest.writeString(wordCount)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SearchRule> {
override fun createFromParcel(parcel: Parcel): SearchRule {
return SearchRule(parcel)
}
override fun newArray(size: Int): Array<SearchRule?> {
return arrayOfNulls(size)
}
}
}

@ -1,5 +1,8 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class TocRule(
var chapterList: String? = null,
var chapterName: String? = null,
@ -7,4 +10,36 @@ data class TocRule(
var isVip: String? = null,
var updateTime: String? = null,
var nextTocUrl: String? = null
)
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(chapterList)
dest.writeString(chapterName)
dest.writeString(chapterUrl)
dest.writeString(isVip)
dest.writeString(updateTime)
dest.writeString(nextTocUrl)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<TocRule> {
override fun createFromParcel(parcel: Parcel): TocRule {
return TocRule(parcel)
}
override fun newArray(size: Int): Array<TocRule?> {
return arrayOfNulls(size)
}
}
}

@ -115,12 +115,23 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
var bookGroupNoneShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupNone", value)
}
var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1)
set(value) {
App.INSTANCE.putPrefInt("elevation", value)
}
val autoChangeSource: Boolean get() = App.INSTANCE.getPrefBoolean("autoChangeSource", true)
val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true)
val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google"
}
val Context.channel: String

@ -1,16 +1,16 @@
package io.legado.app.help
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.*
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.JaccardSimilarity
@ -20,12 +20,9 @@ import kotlin.math.min
object BookHelp {
private const val cacheFolderName = "book_cache"
val downloadPath: String
get() = App.INSTANCE.getPrefString(R.string.pk_download_path)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
private val downloadUri get() = Uri.parse(downloadPath)
private val downloadDir: File =
App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.cacheDir
private fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
@ -40,137 +37,73 @@ object BookHelp {
}
fun clearCache() {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
} else {
FileUtils.deleteFile(
FileUtils.getPath(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName)
)
FileUtils.deleteFile(
FileUtils.getPath(
downloadDir,
subDirs = *arrayOf(cacheFolderName)
)
}
)
}
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFileIfNotExist(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.uri?.writeText(App.INSTANCE, content)
}
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
}
FileUtils.createFileIfNotExist(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
postEvent(EventBus.SAVE_CONTENT, bookChapter)
}
fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>()
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFolderIfNotExist(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.let { bookDoc ->
DocumentUtils.listFiles(App.INSTANCE, bookDoc.uri).forEach {
fileNameList.add(it.name)
}
}
}
} else {
FileUtils.createFolderIfNotExist(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.let {
fileNameList.addAll(it)
}
FileUtils.createFolderIfNotExist(
downloadDir,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.let {
fileNameList.addAll(it)
}
return fileNameList
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
when {
book.isLocalBook() -> {
return true
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
else -> {
return FileUtils.exists(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
return if (book.isLocalBook()) {
true
} else {
FileUtils.exists(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
return false
}
fun getContent(book: Book, bookChapter: BookChapter): String? {
when {
book.isLocalBook() -> {
return AnalyzeTxtFile.getContent(book, bookChapter)
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.uri?.readText(App.INSTANCE)
}
}
else -> {
val file = FileUtils.getFile(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
}
if (book.isLocalBook()) {
return AnalyzeTxtFile.getContent(book, bookChapter)
} else {
val file = FileUtils.getFile(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
}
}
return null
}
fun delContent(book: Book, bookChapter: BookChapter) {
when {
book.isLocalBook() -> return
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.delete()
}
}
else -> {
FileUtils.createFileIfNotExist(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
if (book.isLocalBook()) {
return
} else {
FileUtils.createFileIfNotExist(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
}
@ -242,24 +175,16 @@ object BookHelp {
private var replaceRules: List<ReplaceRule> = arrayListOf()
@Synchronized
fun upReplaceRules(name: String? = null, origin: String? = null) {
if (name != null) {
if (bookName != name || bookOrigin != origin) {
replaceRules = if (origin.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(name)
} else {
App.db.replaceRuleDao().findEnabledByScope(name, origin)
}
bookName = name
bookOrigin = origin
}
} else {
val o = bookOrigin
bookName?.let {
replaceRules = if (o.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(it)
} else {
App.db.replaceRuleDao().findEnabledByScope(it, o)
suspend fun upReplaceRules() {
withContext(IO) {
synchronized(this) {
val o = bookOrigin
bookName?.let {
replaceRules = if (o.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(it)
} else {
App.db.replaceRuleDao().findEnabledByScope(it, o)
}
}
}
}
@ -271,10 +196,20 @@ object BookHelp {
origin: String?,
content: String,
enableReplace: Boolean
): String {
): List<String> {
var c = content
if (enableReplace) {
upReplaceRules(name, origin)
synchronized(this) {
if (bookName != name || bookOrigin != origin) {
bookName = name
bookOrigin = origin
replaceRules = if (origin.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(name)
} else {
App.db.replaceRuleDao().findEnabledByScope(name, origin)
}
}
}
replaceRules.forEach { item ->
item.pattern.let {
if (it.isNotEmpty()) {
@ -293,15 +228,29 @@ object BookHelp {
}
}
}
if (!c.substringBefore("\n").contains(title)) {
c = "$title\n$c"
try {
when (AppConfig.chineseConverterType) {
1 -> c = HanLP.convertToSimplifiedChinese(c)
2 -> c = HanLP.convertToTraditionalChinese(c)
}
} catch (e: Exception) {
withContext(Main) {
App.INSTANCE.toast("简繁转换出错")
}
}
when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(c)
2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c)
val contents = arrayListOf<String>()
c.split("\n").forEach {
val str = it.replace("^\\s+".toRegex(), "")
.replace("\r", "")
if (contents.isEmpty()) {
contents.add(title)
if (str != title && it.isNotEmpty()) {
contents.add("${ReadBookConfig.bodyIndent}$str")
}
} else if (str.isNotEmpty()) {
contents.add("${ReadBookConfig.bodyIndent}$str")
}
}
return c
.replace("\\s*\\n+\\s*".toRegex(), "\n${ReadBookConfig.bodyIndent}")
.replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行
return contents
}
}

@ -27,7 +27,6 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!!
}
val durConfig get() = getConfig(styleSelect)
private val shareConfig get() = getConfig(5)
var bg: Drawable? = null
var bgMeanColor: Int = 0
@ -139,129 +138,145 @@ object ReadBookConfig {
var bodyIndent = " ".repeat(bodyIndentCount)
var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean
get() = if (shareLayout) shareConfig.textBold else durConfig.textBold
set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value
get() = config.textBold
set(value) {
config.textBold = value
}
var textSize: Int
get() = if (shareLayout) shareConfig.textSize else durConfig.textSize
set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value
get() = config.textSize
set(value) {
config.textSize = value
}
var letterSpacing: Float
get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing
set(value) =
if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value
get() = config.letterSpacing
set(value) {
config.letterSpacing = value
}
var lineSpacingExtra: Int
get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra
set(value) =
if (shareLayout) shareConfig.lineSpacingExtra = value
else durConfig.lineSpacingExtra = value
get() = config.lineSpacingExtra
set(value) {
config.lineSpacingExtra = value
}
var paragraphSpacing: Int
get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing
set(value) =
if (shareLayout) shareConfig.paragraphSpacing = value
else durConfig.paragraphSpacing = value
get() = config.paragraphSpacing
set(value) {
config.paragraphSpacing = value
}
var titleMode: Int
get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode
set(value) =
if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value
get() = config.titleMode
set(value) {
config.titleMode = value
}
var titleSize: Int
get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize
set(value) =
if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value
get() = config.titleSize
set(value) {
config.titleSize = value
}
var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing
set(value) =
if (shareLayout) shareConfig.titleTopSpacing = value
else durConfig.titleTopSpacing = value
get() = config.titleTopSpacing
set(value) {
config.titleTopSpacing = value
}
var titleBottomSpacing: Int
get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing
set(value) =
if (shareLayout) shareConfig.titleBottomSpacing = value
else durConfig.titleBottomSpacing = value
get() = config.titleBottomSpacing
set(value) {
config.titleBottomSpacing = value
}
var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom
set(value) =
if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value
get() = config.paddingBottom
set(value) {
config.paddingBottom = value
}
var paddingLeft: Int
get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft
set(value) =
if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value
get() = config.paddingLeft
set(value) {
config.paddingLeft = value
}
var paddingRight: Int
get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight
set(value) =
if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value
get() = config.paddingRight
set(value) {
config.paddingRight = value
}
var paddingTop: Int
get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop
set(value) =
if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value
get() = config.paddingTop
set(value) {
config.paddingTop = value
}
var headerPaddingBottom: Int
get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom
set(value) =
if (shareLayout) shareConfig.headerPaddingBottom = value
else durConfig.headerPaddingBottom = value
get() = config.headerPaddingBottom
set(value) {
config.headerPaddingBottom = value
}
var headerPaddingLeft: Int
get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft
set(value) =
if (shareLayout) shareConfig.headerPaddingLeft = value
else durConfig.headerPaddingLeft = value
get() = config.headerPaddingLeft
set(value) {
config.headerPaddingLeft = value
}
var headerPaddingRight: Int
get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight
set(value) =
if (shareLayout) shareConfig.headerPaddingRight = value
else durConfig.headerPaddingRight = value
get() = config.headerPaddingRight
set(value) {
config.headerPaddingRight = value
}
var headerPaddingTop: Int
get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop
set(value) =
if (shareLayout) shareConfig.headerPaddingTop = value
else durConfig.headerPaddingTop = value
get() = config.headerPaddingTop
set(value) {
config.headerPaddingTop = value
}
var footerPaddingBottom: Int
get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom
set(value) =
if (shareLayout) shareConfig.footerPaddingBottom = value
else durConfig.footerPaddingBottom = value
get() = config.footerPaddingBottom
set(value) {
config.footerPaddingBottom = value
}
var footerPaddingLeft: Int
get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft
set(value) =
if (shareLayout) shareConfig.footerPaddingLeft = value
else durConfig.footerPaddingLeft = value
get() = config.footerPaddingLeft
set(value) {
config.footerPaddingLeft = value
}
var footerPaddingRight: Int
get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight
set(value) =
if (shareLayout) shareConfig.footerPaddingRight = value
else durConfig.footerPaddingRight = value
get() = config.footerPaddingRight
set(value) {
config.footerPaddingRight = value
}
var footerPaddingTop: Int
get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop
set(value) =
if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value
get() = config.footerPaddingTop
set(value) {
config.footerPaddingTop = value
}
var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine
set(value) =
if (shareLayout) shareConfig.showHeaderLine = value
else durConfig.showHeaderLine = value
get() = config.showHeaderLine
set(value) {
config.showHeaderLine = value
}
var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) =
if (shareLayout) shareConfig.showFooterLine = value
else durConfig.showFooterLine = value
get() = config.showFooterLine
set(value) {
config.showFooterLine = value
}
@Keep
class Config(

@ -0,0 +1,74 @@
package io.legado.app.help
import io.legado.app.App
import io.legado.app.R
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefInt
object ReadTipConfig {
val tipArray: Array<String> = App.INSTANCE.resources.getStringArray(R.array.read_tip)
const val none = 0
const val chapterTitle = 1
const val time = 2
const val battery = 3
const val page = 4
const val totalProgress = 5
const val pageAndTotal = 6
val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] }
val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] }
val tipHeaderRightStr: String get() = tipArray.getOrElse(tipHeaderRight) { tipArray[none] }
val tipFooterLeftStr: String get() = tipArray.getOrElse(tipFooterLeft) { tipArray[none] }
val tipFooterMiddleStr: String get() = tipArray.getOrElse(tipFooterMiddle) { tipArray[none] }
val tipFooterRightStr: String get() = tipArray.getOrElse(tipFooterRight) { tipArray[none] }
var tipHeaderLeft: Int
get() = App.INSTANCE.getPrefInt("tipHeaderLeft", time)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderLeft", value)
}
var tipHeaderMiddle: Int
get() = App.INSTANCE.getPrefInt("tipHeaderMiddle", none)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderMiddle", value)
}
var tipHeaderRight: Int
get() = App.INSTANCE.getPrefInt("tipHeaderRight", battery)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderRight", value)
}
var tipFooterLeft: Int
get() = App.INSTANCE.getPrefInt("tipFooterLeft", chapterTitle)
set(value) {
App.INSTANCE.putPrefInt("tipFooterLeft", value)
}
var tipFooterMiddle: Int
get() = App.INSTANCE.getPrefInt("tipFooterMiddle", none)
set(value) {
App.INSTANCE.putPrefInt("tipFooterMiddle", value)
}
var tipFooterRight: Int
get() = App.INSTANCE.getPrefInt("tipFooterRight", pageAndTotal)
set(value) {
App.INSTANCE.putPrefInt("tipFooterRight", value)
}
var hideHeader: Boolean
get() = App.INSTANCE.getPrefBoolean("hideHeader", true)
set(value) {
App.INSTANCE.putPrefBoolean("hideHeader", value)
}
var hideFooter: Boolean
get() = App.INSTANCE.getPrefBoolean("hideFooter", false)
set(value) {
App.INSTANCE.putPrefBoolean("hideFooter", value)
}
}

@ -180,6 +180,7 @@ class AjaxWebView {
mWebView.get()?.evaluateJavascript(mJavaScript) {
if (it.isNotEmpty() && it != "null") {
val content = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
handler.obtainMessage(MSG_SUCCESS, Res(url, content))
.sendToTarget()
handler.removeCallbacks(this)

@ -3,9 +3,11 @@ package io.legado.app.help.http
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import retrofit2.Retrofit
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@ -13,14 +15,12 @@ import kotlin.coroutines.resume
object HttpHelper {
val client: OkHttpClient by lazy {
val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs = ArrayList<ConnectionSpec>()
specs.add(default)
specs.add(ConnectionSpec.COMPATIBLE_TLS)
specs.add(ConnectionSpec.CLEARTEXT)
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)

@ -32,9 +32,7 @@ object Backup {
val lastBackup = context.getPrefLong(PreferKey.lastBackup)
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async {
context.getPrefString(PreferKey.backupPath)?.let {
backup(context, it, true)
}
backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true)
}
}
}
@ -71,7 +69,11 @@ object Backup {
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path), isAuto)
} else {
copyBackup(File(path), isAuto)
if (path.isEmpty()) {
copyBackup(context.getExternalFilesDir(null)!!, false)
} else {
copyBackup(File(path), isAuto)
}
}
}
}

@ -136,14 +136,12 @@ object ImportOldData {
return bookSources.size
}
fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let {
OldReplace.jsonToReplaceRule(jsonItem.jsonString())?.let {
if (it.isValid()){
replaceRules.add(it)
}

@ -0,0 +1,32 @@
package io.legado.app.help.storage
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.*
object OldReplace {
fun jsonToReplaceRule(json: String): ReplaceRule? {
var replaceRule: ReplaceRule? = null
runCatching {
replaceRule = GSON.fromJsonObject<ReplaceRule>(json.trim())
}
runCatching {
if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) {
val jsonItem = Restore.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
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 replaceRule
}
}

@ -1,9 +1,9 @@
package io.legado.app.help.storage
import androidx.annotation.Keep
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.entities.rule.*
import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.*
@ -14,13 +14,15 @@ object OldRule {
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
fun jsonToBookSource(json: String): BookSource? {
var source: BookSource? = null
runCatching {
source = GSON.fromJsonObject<BookSource>(json.trim())
val source = BookSource()
val sourceAny = try {
GSON.fromJsonObject<BookSourceAny>(json.trim())
} catch (e: Exception) {
null
}
runCatching {
if (source == null || source?.searchUrl.isNullOrBlank()) {
source = BookSource().apply {
if (sourceAny?.ruleToc == null) {
source.apply {
val jsonItem = jsonPath.parse(json.trim())
bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: ""
bookSourceName = jsonItem.readString("bookSourceName") ?: ""
@ -37,7 +39,7 @@ object OldRule {
if (exploreUrl.isNullOrBlank()) {
enabledExplore = false
}
val searchRule = SearchRule(
ruleSearch = SearchRule(
bookList = toNewRule(jsonItem.readString("ruleSearchList")),
name = toNewRule(jsonItem.readString("ruleSearchName")),
author = toNewRule(jsonItem.readString("ruleSearchAuthor")),
@ -47,8 +49,7 @@ object OldRule {
coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter"))
)
ruleSearch = GSON.toJson(searchRule)
val exploreRule = ExploreRule(
ruleExplore = ExploreRule(
bookList = toNewRule(jsonItem.readString("ruleFindList")),
name = toNewRule(jsonItem.readString("ruleFindName")),
author = toNewRule(jsonItem.readString("ruleFindAuthor")),
@ -58,8 +59,7 @@ object OldRule {
coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter"))
)
ruleExplore = GSON.toJson(exploreRule)
val bookInfoRule = BookInfoRule(
ruleBookInfo = BookInfoRule(
init = toNewRule(jsonItem.readString("ruleBookInfoInit")),
name = toNewRule(jsonItem.readString("ruleBookName")),
author = toNewRule(jsonItem.readString("ruleBookAuthor")),
@ -69,29 +69,89 @@ object OldRule {
lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")),
tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl"))
)
ruleBookInfo = GSON.toJson(bookInfoRule)
val chapterRule = TocRule(
ruleToc = TocRule(
chapterList = toNewRule(jsonItem.readString("ruleChapterList")),
chapterName = toNewRule(jsonItem.readString("ruleChapterName")),
chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")),
nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext"))
)
ruleToc = GSON.toJson(chapterRule)
var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: ""
if (content.startsWith("$") && !content.startsWith("$.")) {
content = content.substring(1)
}
val contentRule = ContentRule(
ruleContent = ContentRule(
content = content,
nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext"))
)
ruleContent = GSON.toJson(contentRule)
}
} else {
source.bookSourceUrl = sourceAny.bookSourceUrl
source.bookSourceName = sourceAny.bookSourceName
source.bookSourceGroup = sourceAny.bookSourceGroup
source.bookSourceType = sourceAny.bookSourceType
source.bookUrlPattern = sourceAny.bookUrlPattern
source.customOrder = sourceAny.customOrder
source.enabled = sourceAny.enabled
source.enabledExplore = sourceAny.enabledExplore
source.header = sourceAny.header
source.loginUrl = sourceAny.loginUrl
source.lastUpdateTime = sourceAny.lastUpdateTime
source.weight = sourceAny.weight
source.exploreUrl = sourceAny.exploreUrl
source.ruleExplore = if (sourceAny.ruleExplore is String) {
GSON.fromJsonObject(sourceAny.ruleExplore as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleExplore))
}
source.searchUrl = sourceAny.searchUrl
source.ruleSearch = if (sourceAny.ruleSearch is String) {
GSON.fromJsonObject(sourceAny.ruleSearch as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleSearch))
}
source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) {
GSON.fromJsonObject(sourceAny.ruleBookInfo as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleBookInfo))
}
source.ruleToc = if (sourceAny.ruleToc is String) {
GSON.fromJsonObject(sourceAny.ruleToc as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleToc))
}
source.ruleContent = if (sourceAny.ruleContent is String) {
GSON.fromJsonObject(sourceAny.ruleContent as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleContent))
}
}
}
return source
}
@Keep
data class BookSourceAny(
var bookSourceName: String = "", // 名称
var bookSourceGroup: String? = null, // 分组
var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null, // 详情页url正则
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现
var header: String? = null, // 请求头
var loginUrl: String? = null, // 登录地址
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, // 发现url
var ruleExplore: Any? = null, // 发现规则
var searchUrl: String? = null, // 搜索url
var ruleSearch: Any? = null, // 搜索规则
var ruleBookInfo: Any? = null, // 书籍信息页规则
var ruleToc: Any? = null, // 目录页规则
var ruleContent: Any? = null // 正文页规则
)
// default规则适配
// #正则#替换内容 替换成 ##正则##替换内容
// | 替换成 ||
@ -114,8 +174,8 @@ object OldRule {
!newRule.startsWith("//") &&
!newRule.startsWith("##") &&
!newRule.startsWith(":") &&
!newRule.contains("@js:",true) &&
!newRule.contains("<js>",true)
!newRule.contains("@js:", true) &&
!newRule.contains("<js>", true)
) {
if (newRule.contains("#") && !newRule.contains("##")) {
newRule = oldRule.replace("#", "##")
@ -142,10 +202,10 @@ object OldRule {
}
}
if (allinone) {
newRule = "+" + newRule
newRule = "+$newRule"
}
if (reverse) {
newRule = "-" + newRule
newRule = "-$newRule"
}
return newRule
}
@ -216,27 +276,4 @@ object OldRule {
return GSON.toJson(map)
}
fun jsonToReplaceRule(json: String): ReplaceRule? {
var replaceRule: ReplaceRule? = null
runCatching {
replaceRule = GSON.fromJsonObject<ReplaceRule>(json.trim())
}
runCatching {
if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) {
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
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 replaceRule
}
}

@ -3,12 +3,7 @@ package io.legado.app.lib.webdav
import io.legado.app.help.http.HttpHelper
import io.legado.app.lib.webdav.http.Handler
import io.legado.app.lib.webdav.http.HttpAuth
import okhttp3.Credentials
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.*
import org.jsoup.Jsoup
import java.io.File
import java.io.IOException
@ -82,7 +77,7 @@ constructor(urlStr: String) {
this.exists = false
return false
}
response.body?.let {
response.body()?.let {
if (it.string().isNotEmpty()) {
return true
}
@ -102,7 +97,7 @@ constructor(urlStr: String) {
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) {
response.body?.let { body ->
response.body()?.let { body ->
return parseDir(body.string())
}
}
@ -127,7 +122,10 @@ constructor(urlStr: String) {
.url(url)
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
.method("PROPFIND", requestPropsStr.toRequestBody("text/plain".toMediaTypeOrNull()))
.method(
"PROPFIND",
RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
)
HttpAuth.auth?.let {
request.header(
@ -205,9 +203,9 @@ constructor(urlStr: String) {
fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath)
if (!file.exists()) return false
val mediaType = contentType?.toMediaTypeOrNull()
val mediaType = contentType?.let { MediaType.parse(it) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = file.asRequestBody(mediaType)
val fileBody = RequestBody.create(mediaType, file)
httpUrl?.let {
val request = Request.Builder()
.url(it)
@ -241,7 +239,7 @@ constructor(urlStr: String) {
request.header("Authorization", Credentials.basic(it.user, it.pass))
}
try {
return HttpHelper.client.newCall(request.build()).execute().body?.byteStream()
return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {

@ -54,7 +54,8 @@ object Debug {
cancelDebug()
debugSource = rssSource.sourceUrl
log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null)
val sort = rssSource.sortUrls().entries.first()
Rss.getArticles(sort.key, sort.value, rssSource, null)
.onSuccess {
if (it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空")

@ -15,6 +15,8 @@ import kotlin.coroutines.CoroutineContext
object Rss {
fun getArticles(
sortName: String,
sortUrl: String,
rssSource: RssSource,
pageUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT,
@ -22,11 +24,11 @@ object Rss {
): Coroutine<Result> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(
pageUrl ?: rssSource.sourceUrl,
pageUrl ?: sortUrl,
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource)
RssParserByRule.parseXML(sortName, sortUrl, body, rssSource)
}
}

@ -13,9 +13,8 @@ import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.utils.*
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import java.net.URLEncoder
import java.util.*
@ -40,7 +39,7 @@ class AnalyzeUrl(
) : JsExtensions {
companion object {
private val pagePattern = Pattern.compile("<(.*?)>")
private val jsonType = "application/json; charset=utf-8".toMediaTypeOrNull()
private val jsonType = MediaType.parse("application/json; charset=utf-8")
}
private var baseUrl: String = ""
@ -52,8 +51,8 @@ class AnalyzeUrl(
private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>()
private var charset: String? = null
private var bodyTxt: String? = null
private var body: RequestBody? = null
private var body: String? = null
private var requestBody: RequestBody? = null
private var method = RequestMethod.GET
init {
@ -158,15 +157,30 @@ class AnalyzeUrl(
baseUrl = it
}
if (urlArray.size > 1) {
val options = GSON.fromJsonObject<Map<String, String>>(urlArray[1])
options?.let { _ ->
options["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
options["headers"]?.let { headers ->
GSON.fromJsonObject<Map<String, String>>(headers)?.let { headerMap.putAll(it) }
val option = GSON.fromJsonObject<UrlOption>(urlArray[1])
option?.let { _ ->
option.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
option.headers?.let { headers ->
if (headers is Map<*, *>) {
@Suppress("unchecked_cast")
headerMap.putAll(headers as Map<out String, String>)
}
if (headers is String) {
GSON.fromJsonObject<Map<String, String>>(headers)
?.let { headerMap.putAll(it) }
}
}
charset = option.charset
body = if (option.body is String) {
option.body
} else {
GSON.toJson(option.body)
}
option.webView?.let {
if (it.toString().isNotEmpty()) {
useWebView = true
}
}
options["body"]?.let { bodyTxt = it }
options["charset"]?.let { charset = it }
options["webView"]?.let { if (it.isNotEmpty()) useWebView = true }
}
}
when (method) {
@ -180,20 +194,19 @@ class AnalyzeUrl(
}
}
RequestMethod.POST -> {
bodyTxt?.let {
body?.let {
if (it.isJson()) {
body = it.toRequestBody(jsonType)
requestBody = RequestBody.create(jsonType, it)
} else {
analyzeFields(it)
}
} ?: let {
body = FormBody.Builder().build()
requestBody = FormBody.Builder().build()
}
}
}
}
/**
* 解析QueryMap
*/
@ -254,7 +267,7 @@ class AnalyzeUrl(
} else {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset)
.postBody(url, body!!, headerMap)
.postBody(url, requestBody!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
@ -278,7 +291,7 @@ class AnalyzeUrl(
params.requestMethod = method
params.javaScript = jsStr
params.sourceRegex = sourceRegex
params.postData = bodyTxt?.toByteArray()
params.postData = body?.toByteArray()
params.tag = tag
return HttpHelper.ajax(params)
}
@ -295,7 +308,7 @@ class AnalyzeUrl(
} else {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset)
.postBodyAsync(url, body!!, headerMap)
.postBodyAsync(url, requestBody!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
@ -308,4 +321,12 @@ class AnalyzeUrl(
return Res(NetworkUtils.getUrl(res), res.body())
}
data class UrlOption(
val method: String?,
val charset: String?,
val webView: Any?,
val headers: Any?,
val body: Any?
)
}

@ -13,32 +13,46 @@ import java.nio.charset.Charset
import java.util.regex.Matcher
import java.util.regex.Pattern
object AnalyzeTxtFile {
private const val folderName = "bookTxt"
private const val BLANK: Byte = 0x0a
//默认从文件中获取数据的长度
private const val BUFFER_SIZE = 512 * 1024
//没有标题的时候,每个章节的最大长度
private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024
val cacheFolder: File by lazy {
val rootFile = App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.externalCacheDir
?: App.INSTANCE.cacheDir
FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName))
}
class AnalyzeTxtFile {
private val tocRules = arrayListOf<TxtTocRule>()
private lateinit var charset: Charset
@Throws(Exception::class)
fun analyze(context: Context, book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(context, book)
book.charset = EncodingDetect.getEncode(bookFile)
val charset = book.fileCharset()
val toc = arrayListOf<BookChapter>()
charset = book.fileCharset()
val rulePattern = if (book.tocUrl.isNotEmpty()) {
Pattern.compile(book.tocUrl, Pattern.MULTILINE)
} else {
tocRules.addAll(getTocRules())
null
}
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val rulePattern = getTocRule(book, bookStream, charset)
return analyze(bookStream, book, rulePattern)
}
@Throws(Exception::class)
private fun analyze(
bookStream: RandomAccessFile,
book: Book,
pattern: Pattern?
): ArrayList<BookChapter> {
bookStream.seek(0)
val toc = arrayListOf<BookChapter>()
var tocRule: TxtTocRule? = null
val rulePattern = pattern ?: let {
tocRule = getTocRule(bookStream)
tocRule?.let {
Pattern.compile(it.rule, Pattern.MULTILINE)
}
}
//加载章节
val buffer = ByteArray(BUFFER_SIZE)
//获取到的块起始点,在文件中的位置
bookStream.seek(0)
var curOffset: Long = 0
//block的个数
var blockPos = 0
@ -47,13 +61,14 @@ object AnalyzeTxtFile {
var allLength = 0
//获取文件中的数据到buffer,直到没有数据为止
while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) {
++blockPos
while (bookStream.read(buffer).also { length = it } > 0) {
blockPos++
//如果存在Chapter
if (rulePattern != null) { //将数据转换成String
if (rulePattern != null) {
//将数据转换成String, 不能超过length
var blockContent = String(buffer, 0, length, charset)
val lastN = blockContent.lastIndexOf("\n")
if (lastN != 0) {
if (lastN > 0) {
blockContent = blockContent.substring(0, lastN)
length = blockContent.toByteArray(charset).size
allLength += length
@ -66,40 +81,50 @@ object AnalyzeTxtFile {
//如果存在相应章节
while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置
val chapterStart = matcher.start()
//获取章节内容
val chapterContent = blockContent.substring(seekPos, chapterStart)
val chapterLength = chapterContent.toByteArray(charset).size
val lastStart = toc.lastOrNull()?.start ?: 0
if (curOffset + chapterLength - lastStart > 50000 && pattern == null) {
//移除不匹配的规则
tocRules.remove(tocRule)
return analyze(bookStream, book, null)
}
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容
//第一种情况一定是序章 第二种情况是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容
val chapterContent = blockContent.substring(seekPos, chapterStart)
//设置指针偏移
seekPos += chapterContent.length
if (toc.size == 0) { //如果当前没有章节,那么就是序章
if (toc.isEmpty()) { //如果当前没有章节,那么就是序章
//加入简介
book.intro = chapterContent
if (StringUtils.trim(chapterContent).isNotEmpty()) {
val qyChapter = BookChapter()
qyChapter.title = "前言"
qyChapter.start = 0
qyChapter.end = chapterLength.toLong()
toc.add(qyChapter)
}
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = chapterContent.toByteArray(charset).size.toLong()
curChapter.start = chapterLength.toLong()
toc.add(curChapter)
} else { //否则就block分割之后,上一个章节的剩余内容
//获取上一章节
val lastChapter = toc.last()
//将当前段落添加上一章去
lastChapter.end =
lastChapter.end!! + chapterContent.toByteArray(charset).size
lastChapter.end!! + chapterLength.toLong()
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = lastChapter.end
toc.add(curChapter)
}
} else { //是否存在章节
if (toc.size != 0) { //获取章节内容
val chapterContent = blockContent.substring(seekPos, matcher.start())
seekPos += chapterContent.length
} else {
if (toc.isNotEmpty()) { //获取章节内容
//获取上一章节
val lastChapter = toc.last()
lastChapter.end =
lastChapter.start!! + chapterContent.toByteArray(charset).size
lastChapter.start!! + chapterContent.toByteArray(charset).size.toLong()
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
@ -108,11 +133,18 @@ object AnalyzeTxtFile {
} else { //如果章节不存在则创建章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = 0L
curChapter.end = 0L
curChapter.start = 0
curChapter.end = 0
toc.add(curChapter)
}
}
//设置指针偏移
seekPos += chapterContent.length
}
if (seekPos == 0 && length > 50000 && pattern == null) {
//移除不匹配的规则
tocRules.remove(tocRule)
return analyze(bookStream, book, null)
}
} else { //进行本地虚拟分章
//章节在buffer的偏移量
@ -156,7 +188,8 @@ object AnalyzeTxtFile {
//block的偏移点
curOffset += length.toLong()
if (rulePattern != null) { //设置上一章的结尾
if (rulePattern != null) {
//设置上一章的结尾
val lastChapter = toc.last()
lastChapter.end = curOffset
}
@ -175,44 +208,18 @@ object AnalyzeTxtFile {
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
}
book.latestChapterTitle = toc.last().title
book.totalChapterNum = toc.size
System.gc()
System.runFinalization()
return toc
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
bookStream.seek(bookChapter.start ?: 0)
val extent = (bookChapter.end!! - bookChapter.start!!).toInt()
val content = ByteArray(extent)
bookStream.read(content, 0, extent)
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
}
return bookFile
tocRule?.let {
book.tocUrl = it.rule
}
return File(book.bookUrl)
return toc
}
private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? {
if (book.tocUrl.isNotEmpty()) {
return Pattern.compile(book.tocUrl, Pattern.MULTILINE)
}
val tocRules = getTocRules()
var rulePattern: Pattern? = null
private fun getTocRule(bookStream: RandomAccessFile): TxtTocRule? {
var txtTocRule: TxtTocRule? = null
//首先获取128k的数据
val buffer = ByteArray(BUFFER_SIZE / 4)
val length = bookStream.read(buffer, 0, buffer.size)
@ -221,30 +228,72 @@ object AnalyzeTxtFile {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
book.tocUrl = tocRule.rule
rulePattern = pattern
txtTocRule = tocRule
break
}
}
bookStream.seek(0)
return rulePattern
return txtTocRule
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
return getDefaultRules()
companion object {
private const val folderName = "bookTxt"
private const val BLANK: Byte = 0x0a
//默认从文件中获取数据的长度
private const val BUFFER_SIZE = 512 * 1024
//没有标题的时候,每个章节的最大长度
private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024
val cacheFolder: File by lazy {
val rootFile = App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.externalCacheDir
?: App.INSTANCE.cacheDir
FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName))
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val content = ByteArray((bookChapter.end!! - bookChapter.start!!).toInt())
bookStream.seek(bookChapter.start!!)
bookStream.read(content)
return String(content, book.fileCharset())
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
private fun getBookFile(context: Context, book: Book): File {
if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
}
return bookFile
}
return File(book.bookUrl)
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
return getDefaultRules()
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
}
}
return emptyList()
}
return emptyList()
}
}

@ -11,7 +11,7 @@ import java.io.StringReader
object RssParser {
@Throws(XmlPullParserException::class, IOException::class)
fun parseXML(xml: String, sourceUrl: String): Result {
fun parseXML(sortName: String, xml: String, sourceUrl: String): Result {
val articleList = mutableListOf<RssArticle>()
var currentArticle = RssArticle()
@ -87,6 +87,7 @@ object RssParser {
// The item is correctly parsed
insideItem = false
currentArticle.origin = sourceUrl
currentArticle.sort = sortName
articleList.add(currentArticle)
currentArticle = RssArticle()
}

@ -13,7 +13,7 @@ import io.legado.app.utils.NetworkUtils
object RssParserByRule {
@Throws(Exception::class)
fun parseXML(body: String?, rssSource: RssSource): Result {
fun parseXML(sortName: String, sortUrl: String, body: String?, rssSource: RssSource): Result {
val sourceUrl = rssSource.sourceUrl
var nextUrl: String? = null
if (body.isNullOrBlank()) {
@ -28,11 +28,11 @@ object RssParserByRule {
var ruleArticles = rssSource.ruleArticles
if (ruleArticles.isNullOrBlank()) {
Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析")
return RssParser.parseXML(body, sourceUrl)
return RssParser.parseXML(sortName, body, sourceUrl)
} else {
val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(body, rssSource.sourceUrl)
analyzeRule.setContent(body, sortUrl)
var reverse = false
if (ruleArticles.startsWith("-")) {
reverse = true
@ -45,7 +45,7 @@ object RssParserByRule {
Debug.log(sourceUrl, "┌获取下一页链接")
nextUrl = analyzeRule.getString(rssSource.ruleNextPage)
if (nextUrl.isNotEmpty()) {
nextUrl = NetworkUtils.getAbsoluteURL(sourceUrl, nextUrl)
nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl)
}
Debug.log(sourceUrl, "$nextUrl")
}
@ -59,7 +59,8 @@ object RssParserByRule {
sourceUrl, item, analyzeRule, index == 0,
ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink
)?.let {
it.origin = rssSource.sourceUrl
it.sort = sortName
it.origin = sourceUrl
articleList.add(it)
}
}

@ -193,6 +193,7 @@ object BookChapterList {
list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle
if (book.totalChapterNum < list.size) {
book.lastCheckCount = list.size - book.totalChapterNum
book.latestChapterTime = System.currentTimeMillis()
}
book.totalChapterNum = list.size
return list

@ -101,6 +101,7 @@ object BookContent {
}
}
content.deleteCharAt(content.length - 1)
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")

@ -4,6 +4,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.BookListRule
import io.legado.app.help.BookHelp
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
@ -47,7 +48,7 @@ object BookList {
}
val collections: List<Any>
var reverse = false
val bookListRule = when {
val bookListRule: BookListRule = when {
isSearch -> bookSource.getSearchRule()
bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule()
else -> bookSource.getExploreRule()

@ -8,9 +8,14 @@ import io.legado.app.App
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.help.ActivityHelp
import io.legado.app.service.AudioPlayService
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.AudioPlay
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@ -50,11 +55,23 @@ class MediaButtonReceiver : BroadcastReceiver() {
private fun readAloud(context: Context) {
when {
BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) {
ReadAloud.pause(context)
AudioPlay.pause(context)
} else {
ReadAloud.resume(context)
AudioPlay.resume(context)
}
AudioPlayService.isRun -> if (AudioPlayService.pause) {
AudioPlay.resume(context)
} else {
AudioPlay.pause(context)
}
ActivityHelp.isExist(AudioPlayActivity::class.java) ->
postEvent(EventBus.MEDIA_BUTTON, true)
ActivityHelp.isExist(ReadBookActivity::class.java) ->
postEvent(EventBus.MEDIA_BUTTON, true)
else -> {
else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) {
GlobalScope.launch(Main) {
val lastBook: Book? = withContext(IO) {
App.db.bookDao().lastReadBook

@ -44,7 +44,7 @@ abstract class BaseReadAloudService : BaseService(),
private lateinit var audioManager: AudioManager
private var mFocusRequest: AudioFocusRequest? = null
private var broadcastReceiver: BroadcastReceiver? = null
private var mediaSessionCompat: MediaSessionCompat? = null
private lateinit var mediaSessionCompat: MediaSessionCompat
private var title: String = ""
private var subtitle: String = ""
internal val contentList = arrayListOf<String>()
@ -59,6 +59,7 @@ abstract class BaseReadAloudService : BaseService(),
isRun = true
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
mFocusRequest = MediaHelp.getFocusRequest(this)
mediaSessionCompat = MediaSessionCompat(this, "readAloud")
initMediaSession()
initBroadcastReceiver()
upNotification()
@ -72,7 +73,7 @@ abstract class BaseReadAloudService : BaseService(),
unregisterReceiver(broadcastReceiver)
postEvent(EventBus.ALOUD_STATE, Status.STOP)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
mediaSessionCompat?.release()
mediaSessionCompat.release()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -109,11 +110,9 @@ abstract class BaseReadAloudService : BaseService(),
readAloudNumber = textChapter.getReadLength(pageIndex)
contentList.clear()
if (getPrefBoolean(PreferKey.readAloudByPage)) {
for (index in pageIndex..textChapter.lastIndex()) {
for (index in pageIndex..textChapter.lastIndex) {
textChapter.page(index)?.text?.split("\n")?.let {
if (it.isNotEmpty()) {
contentList.addAll(it)
}
contentList.addAll(it)
}
}
} else {
@ -204,7 +203,7 @@ abstract class BaseReadAloudService : BaseService(),
* 更新媒体状态
*/
private fun upMediaSessionPlaybackState(state: Int) {
mediaSessionCompat?.setPlaybackState(
mediaSessionCompat.setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
.setState(state, nowSpeak.toLong(), 1f)
@ -216,13 +215,12 @@ abstract class BaseReadAloudService : BaseService(),
* 初始化MediaSession, 注册多媒体按钮
*/
private fun initMediaSession() {
mediaSessionCompat = MediaSessionCompat(this, "readAloud")
mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent)
}
})
mediaSessionCompat?.setMediaButtonReceiver(
mediaSessionCompat.setMediaButtonReceiver(
PendingIntent.getBroadcast(
this,
0,
@ -235,7 +233,7 @@ abstract class BaseReadAloudService : BaseService(),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
mediaSessionCompat?.isActive = true
mediaSessionCompat.isActive = true
}
/**
@ -325,7 +323,7 @@ abstract class BaseReadAloudService : BaseService(),
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionCompat?.sessionToken)
.setMediaSession(mediaSessionCompat.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

@ -7,20 +7,22 @@ import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors
import kotlin.math.min
class CheckSourceService : BaseService() {
private val threadCount = AppConfig.threadCount
private var threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>()
private var processIndex = 0
@ -42,52 +44,70 @@ class CheckSourceService : BaseService() {
override fun onDestroy() {
super.onDestroy()
task?.cancel()
tasks.clear()
searchPool.close()
}
private fun check(ids: List<String>) {
task?.cancel()
if (allIds.isNotEmpty()) {
toast("已有书源在校验,等完成后再试")
return
}
tasks.clear()
allIds.clear()
checkedIds.clear()
allIds.addAll(ids)
processIndex = 0
threadCount = min(allIds.size, threadCount)
updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
task = execute {
for (i in 0 until threadCount) {
check()
}
}.onError {
toast("校验书源出错:${it.localizedMessage}")
for (i in 0 until threadCount) {
check()
}
}
/**
* 检测
*/
private fun check() {
val index = processIndex
synchronized(this) {
processIndex++
}
if (processIndex < allIds.size) {
val sourceUrl = allIds[processIndex]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
synchronized(this) {
if (processIndex >= allIds.size + threadCount - 1) {
stopSelf()
}
}
execute {
if (index < allIds.size) {
val sourceUrl = allIds[index]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl)
} else {
check(source)
}
} ?: onNext(sourceUrl)
}
}
}
private fun check(source: BookSource) {
val webBook = WebBook(source)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
}
private fun onNext(sourceUrl: String) {
synchronized(this) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
if (processIndex >= allIds.size + threadCount - 1) {
stopSelf()
}
}
}

@ -13,6 +13,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent
@ -25,11 +26,11 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf()
private var tasks = CompositeCoroutine()
private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>();
private val downloadCount = hashMapOf<String, DownloadCount>()
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载"
@ -125,11 +126,7 @@ class DownloadService : BaseService() {
chapter,
scope = this,
context = searchPool
)
//.onStart {
// notificationContent = "启动:" + chapter.title
//}
.onSuccess(IO) { content ->
).onSuccess(IO) { content ->
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
@ -165,7 +162,7 @@ class DownloadService : BaseService() {
tasks.add(task)
task.invokeOnCompletion {
tasks.remove(task)
if (tasks.isEmpty()) {
if (tasks.isEmpty) {
stopSelf()
}
}
@ -192,17 +189,21 @@ class DownloadService : BaseService() {
val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification)
}
}
class DownloadCount{
@Volatile public var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile public var successCount = 0 //下载成功的条目数量
fun increaseSuccess(){
++successCount;
}
class DownloadCount {
@Volatile
var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile
var successCount = 0 //下载成功的条目数量
fun increaseFinished(){
++downloadFinishedCount;
fun increaseSuccess() {
++successCount
}
fun increaseFinished() {
++downloadFinishedCount
}
}
}

@ -1,7 +1,6 @@
package io.legado.app.service
import android.app.PendingIntent
import android.os.Build
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import io.legado.app.R
@ -20,35 +19,46 @@ import java.util.*
class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener {
companion object {
var textToSpeech: TextToSpeech? = null
private var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
fun clearTTS() {
textToSpeech?.stop()
textToSpeech?.shutdown()
textToSpeech?.let {
it.stop()
it.shutdown()
}
textToSpeech = null
ttsInitFinish = false
}
}
private var ttsInitFinish = false
private val ttsUtteranceListener = TTSUtteranceListener()
override fun onCreate() {
super.onCreate()
textToSpeech = TextToSpeech(this, this)
initTts()
upSpeechRate()
}
private fun initTts() {
ttsInitFinish = false
textToSpeech = TextToSpeech(this, this).apply {
setOnUtteranceProgressListener(ttsUtteranceListener)
}
}
override fun onDestroy() {
super.onDestroy()
clearTTS()
}
@Synchronized
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsInitFinish = true
play()
textToSpeech?.let {
it.language = Locale.CHINA
ttsInitFinish = true
play()
}
} else {
launch {
toast(R.string.tts_init_failed)
@ -58,35 +68,21 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
@Synchronized
override fun play() {
if (contentList.isEmpty() || !ttsInitFinish) {
return
}
if (requestFocus()) {
if (contentList.isNotEmpty() && ttsInitFinish && requestFocus()) {
MediaHelp.playSilentSound(this)
super.play()
textToSpeech?.stop()
for (i in nowSpeak until contentList.size) {
if (i == 0) {
speak(contentList[i], TextToSpeech.QUEUE_FLUSH, AppConst.APP_TAG + i)
} else {
speak(contentList[i], TextToSpeech.QUEUE_ADD, AppConst.APP_TAG + i)
}
textToSpeech?.speak(
contentList[i],
TextToSpeech.QUEUE_ADD,
null,
AppConst.APP_TAG + i
)
}
}
}
private fun speak(content: String, queueMode: Int, utteranceId: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech?.speak(content, queueMode, null, utteranceId)
} else {
@Suppress("DEPRECATION")
textToSpeech?.speak(
content,
queueMode,
hashMapOf(Pair(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId))
)
}
}
/**
* 更新朗读速度
*/
@ -94,7 +90,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
if (this.getPrefBoolean("ttsFollowSys", true)) {
if (reset) {
clearTTS()
textToSpeech = TextToSpeech(this, this)
initTts()
}
} else {
textToSpeech?.setSpeechRate((AppConfig.ttsSpeechRate + 5) / 10f)
@ -152,8 +148,8 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
pageIndex++
ReadBook.moveToNextPage()
}
postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1)
}
postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1)
}
override fun onDone(s: String) {

@ -36,7 +36,7 @@ object ReadBook {
var msg: String? = null
private val loadingChapters = arrayListOf<Int>()
fun resetData(book: Book, noSource: (name: String, author: String) -> Unit) {
fun resetData(book: Book) {
this.book = book
titleDate.postValue(book.name)
durChapterIndex = book.durChapterIndex
@ -46,20 +46,19 @@ object ReadBook {
prevTextChapter = null
curTextChapter = null
nextTextChapter = null
upWebBook(book, noSource)
upWebBook(book)
}
fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) {
fun upWebBook(book: Book?) {
book ?: return
if (book.origin == BookType.local) {
webBook = null
webBook = if (book.origin == BookType.local) {
null
} else {
val bookSource = App.db.bookSourceDao().getBookSource(book.origin)
if (bookSource != null) {
webBook = WebBook(bookSource)
WebBook(bookSource)
} else {
webBook = null
noSource.invoke(book.name, book.author)
null
}
}
}
@ -107,7 +106,7 @@ object ReadBook {
fun moveToPrevChapter(upContent: Boolean, toLast: Boolean = true): Boolean {
if (durChapterIndex > 0) {
durPageIndex = if (toLast) prevTextChapter?.lastIndex() ?: 0 else 0
durPageIndex = if (toLast) prevTextChapter?.lastIndex ?: 0 else 0
durChapterIndex--
nextTextChapter = curTextChapter
curTextChapter = prevTextChapter
@ -176,10 +175,10 @@ object ReadBook {
fun durChapterPos(): Int {
curTextChapter?.let {
if (durPageIndex < it.pageSize()) {
if (durPageIndex < it.pageSize) {
return durPageIndex
}
return it.pageSize() - 1
return it.pageSize - 1
}
return durPageIndex
}
@ -293,7 +292,7 @@ object ReadBook {
) {
Coroutine.async {
if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) {
val c = BookHelp.disposeContent(
val contents = BookHelp.disposeContent(
chapter.title,
book!!.name,
webBook?.bookSource?.bookSourceUrl,
@ -302,18 +301,21 @@ object ReadBook {
)
when (chapter.index) {
durChapterIndex -> {
curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
curTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView()
curPageChanged()
callBack?.contentLoadFinish()
}
durChapterIndex - 1 -> {
prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
prevTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
if (upContent) callBack?.upContent(-1, resetPageOffset)
}
durChapterIndex + 1 -> {
nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
nextTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
if (upContent) callBack?.upContent(1, resetPageOffset)
}
}
@ -331,7 +333,7 @@ object ReadBook {
book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = durChapterIndex
book.durChapterPos = durPageIndex
curTextChapter?.let {
App.db.bookChapterDao().getChapter(book.bookUrl, durChapterIndex)?.let {
book.durChapterTitle = it.title
}
App.db.bookDao().update(book)

@ -26,9 +26,10 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
tv_app_summary.post {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text)
val start = spannableString.indexOf("开源阅读软件")
val gzh = getString(R.string.legado_gzh)
val start = spannableString.indexOf(gzh)
spannableString.setSpan(
span, start, start + 6,
span, start, start + gzh.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tv_app_summary.text = spannableString

@ -22,6 +22,7 @@ class AboutFragment : PreferenceFragmentCompat() {
private val qqGroups = linkedMapOf(
Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"),
Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"),
Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"),
Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"),
Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"),
Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"),

@ -35,7 +35,7 @@ class DonateFragment : PreferenceFragmentCompat() {
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
"gzGzh" -> requireContext().sendToClip("开源阅读")
}
return super.onPreferenceTreeClick(preference)
}

@ -11,7 +11,6 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.service.help.AudioPlay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AudioPlayViewModel(application: Application) : BaseViewModel(application) {
@ -93,9 +92,6 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
AudioPlay.book?.let {
book1.order = it.order
App.db.bookDao().delete(it)
}
withContext(Dispatchers.Main) {
}
App.db.bookDao().insert(book1)
AudioPlay.book = book1

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
@ -105,10 +106,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
booksLiveData?.removeObservers(this)
booksLiveData =
when (groupId) {
-1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup()
AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll()
AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal()
AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio()
AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer { list ->
@ -129,22 +130,22 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
.show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = -11
groupId = AppConst.bookGroupNone.groupId
initBookData()
}
R.id.menu_all -> {
title_bar.subtitle = item.title
groupId = -1
groupId = AppConst.bookGroupAll.groupId
initBookData()
}
R.id.menu_local -> {
title_bar.subtitle = item.title
groupId = -2
groupId = AppConst.bookGroupLocal.groupId
initBookData()
}
R.id.menu_audio -> {
title_bar.subtitle = item.title
groupId = -3
groupId = AppConst.bookGroupAudio.groupId
initBookData()
}
else -> if (item.groupId == R.id.menu_group) {

@ -2,6 +2,8 @@ package io.legado.app.ui.book.changesource
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.PopupMenu
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
@ -10,6 +12,7 @@ import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_change_source.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class ChangeSourceAdapter(context: Context, val callBack: CallBack) :
@ -43,10 +46,31 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) :
callBack.changeTo(it)
}
}
holder.itemView.onLongClick {
showMenu(holder.itemView, getItem(holder.layoutPosition))
true
}
}
private fun showMenu(view: View, searchBook: SearchBook?) {
searchBook ?: return
val popupMenu = PopupMenu(context, view)
popupMenu.inflate(R.menu.change_source_item)
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_disable_book_source -> {
callBack.disableSource(searchBook)
removeItem(searchBook)
}
}
true
}
popupMenu.show()
}
interface CallBack {
val bookUrl: String?
fun changeTo(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook)
}
}

@ -185,6 +185,10 @@ class ChangeSourceDialog : BaseDialogFragment(),
override val bookUrl: String?
get() = callBack?.oldBook?.bookUrl
override fun disableSource(searchBook: SearchBook) {
viewModel.disableSource(searchBook)
}
interface CallBack {
val oldBook: Book?
fun changeTo(book: Book)

@ -165,4 +165,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
searchPool.close()
}
fun disableSource(searchBook: SearchBook) {
execute {
App.db.bookSourceDao().getBookSource(searchBook.origin)?.let { source ->
source.enabled = false
App.db.bookSourceDao().update(source)
}
searchBooks.remove(searchBook)
}
}
}

@ -1,5 +1,6 @@
package io.legado.app.ui.book.chapterlist
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
@ -13,7 +14,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModelOfActivity
@ -54,7 +55,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
}
private fun initView() {
ll_chapter_base_info.setBackgroundColor(backgroundColor)
ll_chapter_base_info.setBackgroundColor(bottomBackground)
iv_chapter_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) }
iv_chapter_bottom.onClick {
if (adapter.itemCount > 0) {
@ -66,6 +67,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
}
}
@SuppressLint("SetTextI18n")
private fun initBook() {
launch {
withContext(IO) {
@ -74,7 +76,8 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
initDoc()
book?.let {
durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle
tv_current_chapter_info.text =
"${it.durChapterTitle}(${it.durChapterIndex + 1}/${it.totalChapterNum})"
initCacheFileNames(it)
}
}

@ -99,6 +99,8 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
.isChecked = AppConfig.bookGroupLocalShow
it.findItem(R.id.menu_group_audio)
.isChecked = AppConfig.bookGroupAudioShow
it.findItem(R.id.menu_group_none)
.isChecked = AppConfig.bookGroupNoneShow
}
}
@ -120,6 +122,10 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
AppConfig.bookGroupAudioShow = item.isChecked
callBack?.upGroup()
}
R.id.menu_group_none -> {
item.isChecked = !item.isChecked
AppConfig.bookGroupNoneShow = item.isChecked
}
}
return true
}

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

Loading…
Cancel
Save