# 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. 46
      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. 173
      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. 8
      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. 175
      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. 20
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  87. 54
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  88. 33
      app/src/main/java/io/legado/app/service/DownloadService.kt
  89. 60
      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 /release
/tmp /tmp
node_modules/ node_modules/
/app/app
/app/google
/app/gradle.properties
package-lock.json package-lock.json

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders" apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
androidExtensions { androidExtensions {
@ -19,6 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android { android {
compileSdkVersion 29 compileSdkVersion 29
flavorDimensions("version")
signingConfigs { signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig { myConfig {
@ -37,7 +38,6 @@ android {
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName version versionName version
flavorDimensions "versionCode"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version) project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true multiDexEnabled true
@ -74,13 +74,15 @@ android {
} }
} }
} }
productFlavors{ productFlavors {
app{ app {
manifestPlaceholders = [APP_CHANNEL_VALUE:"app"] dimension "version"
manifestPlaceholders = [APP_CHANNEL_VALUE: "app"]
} }
google{ google {
dimension "version"
applicationId "io.legado.play" applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE:"google"] manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
} }
} }
compileOptions { compileOptions {
@ -108,27 +110,27 @@ kapt {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) 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:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//kotlin //kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase //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' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX //androidX
implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media: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.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox: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 //lifecycle
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
@ -157,19 +159,19 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" 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 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino //JS rhino
implementation 'com.github.gedoor:rhino-android:1.4' implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit //
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' //noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:retrofit:2.7.2'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer //webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'org.nanohttpd:nanohttpd:2.3.1'
@ -182,15 +184,21 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'
//apache //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' implementation 'org.apache.commons:commons-text:1.8'
//MarkDown //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 rx.**
-dontwarn okio.** -dontwarn okio.**
-dontwarn retrofit2.**
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
-dontwarn org.apache.log4j.lf5.viewer.** -dontwarn org.apache.log4j.lf5.viewer.**
-dontnote org.apache.log4j.lf5.viewer.** -dontnote org.apache.log4j.lf5.viewer.**
@ -172,7 +171,6 @@
-dontwarn com.jeremyliao.liveeventbus.** -dontwarn com.jeremyliao.liveeventbus.**
-keep class com.jeremyliao.liveeventbus.** { *; } -keep class com.jeremyliao.liveeventbus.** { *; }
-keep class retrofit2.**{*;}
-keep class okhttp3.**{*;} -keep class okhttp3.**{*;}
-keep class okio.**{*;} -keep class okio.**{*;}
-keep class com.hwangjr.rxbus.**{*;} -keep class com.hwangjr.rxbus.**{*;}

@ -1,4 +1,4 @@
<resources> <resources>
<string name="app_name">阅读.debug</string> <string name="app_name">阅读·D</string>
<string name="receiving_shared_label">阅读.debug·搜索</string> <string name="receiving_shared_label">阅读·D·搜索</string>
</resources> </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" /> android:launchMode="singleTop" />
<!--订阅条目--> <!--订阅条目-->
<activity <activity
android:name=".ui.rss.article.RssArticlesActivity" android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--Rss收藏--> <!--Rss收藏-->
<activity <activity
@ -300,6 +300,7 @@
<data android:mimeType="application/json" /> <data android:mimeType="application/json" />
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".service.CheckSourceService" /> <service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />
<service android:name=".service.WebService" /> <service android:name=".service.WebService" />

@ -1,98 +1,121 @@
[ [
{ {
"id": -1,
"enable": true, "enable": true,
"name": "目录", "name": "目录",
"rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 0 "serialNumber": -17
}, },
{ {
"id": -2,
"enable": false, "enable": false,
"name": "目录(去空白)", "name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1 "serialNumber": -16
}, },
{ {
"id": -3,
"enable": false, "enable": false,
"name": "目录(简介)", "name": "目录(匹配简介)",
"rule": "(?<=[ \\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 2 "serialNumber": -15
}, },
{ {
"id": -4,
"enable": false, "enable": false,
"name": "目录(古典、轻小说备用)", "name": "目录(古典、轻小说备用)",
"rule": "^[  \\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 3 "serialNumber": -14
}, },
{ {
"id": -5,
"enable": false, "enable": false,
"name": "数字(纯数字标题)", "name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$", "rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$",
"serialNumber": 4 "serialNumber": -13
}, },
{ {
"id": -6,
"enable": true, "enable": true,
"name": "数字 分隔符 标题名称", "name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", "rule": "^[  \\t]{0,4}\\d{1,5}[,., 、_—\\-].{1,30}$",
"serialNumber": 5 "serialNumber": -12
}, },
{ {
"id": -7,
"enable": true,
"name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$",
"serialNumber": -11
},
{
"id": -8,
"enable": true, "enable": true,
"name": "正文 标题/序号", "name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"serialNumber": 6 "serialNumber": -10
}, },
{ {
"id": -9,
"enable": true, "enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题", "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}$", "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, "enable": false,
"name": "Chapter(去简介)", "name": "Chapter(去简介)",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", "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, "enable": true,
"name": "特殊符号 序号 标题", "name": "特殊符号 序号 标题",
"rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$", "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"serialNumber": 9 "serialNumber": -7
}, },
{ {
"id": -12,
"enable": false, "enable": false,
"name": "特殊符号 标题(成对)", "name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 10 "serialNumber": -6
}, },
{ {
"id": -13,
"enable":true, "enable":true,
"name": "特殊符号 标题(单个)", "name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 11 "serialNumber": -5
}, },
{ {
"id": -14,
"enable": true, "enable": true,
"name": "章/卷 序号 标题", "name": "章/卷 序号 标题",
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"serialNumber": 12 "serialNumber": -4
}, },
{ {
"id": -15,
"enable":false, "enable":false,
"name": "顶格标题", "name": "顶格标题",
"rule": "^\\S.{1,20}$", "rule": "^\\S.{1,20}$",
"serialNumber": 13 "serialNumber": -3
}, },
{ {
"id": -16,
"enable":false, "enable":false,
"name": "双标题(前向)", "name": "双标题(前向)",
"rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)",
"serialNumber": 14 "serialNumber": -2
}, },
{ {
"id": -17,
"enable":false, "enable":false,
"name": "双标题(后向)", "name": "双标题(后向)",
"rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", "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)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(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** **2020/03/22**
* 添加文件关联 by wqfantexi * 添加文件关联 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> <!DOCTYPE html>
<html lang=en style="padding: 0;height:100%"> <html>
<head> <head>
<meta charset=utf-8> <meta charset="utf-8" />
<meta http-equiv=X-UA-Compatible content="IE=edge"> <title>阅读3.0书架</title>
<meta name=viewport content="width=device-width,initial-scale=1"> <link rel="icon" href="favicon.ico">
<!--[if IE]><link rel="icon" href="favicon.ico" /><![endif]--> <link href="bookshelf.css" rel="stylesheet" />
<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> </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 <body>
JavaScript enabled. Please enable it to continue.</strong></noscript> <button id="top" class="top"></button>
<div id=app></div> <button id="showchapter" class="showchapter"></button>
<script src=js/chunk-vendors.b3838a2d.js></script> <button id="hidebooks" class="hidebooks"></button>
<script src=js/app.b25f3cec.js></script> <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> </body>
</html> </html>

@ -359,8 +359,8 @@
<br>(?i) 前缀表示忽略大小写 <br>(?i) 前缀表示忽略大小写
</div> </div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a> <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> </div>
</div> </div>

@ -36,23 +36,28 @@ const RuleJSON = (() => {
// 搜索规则 // 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = ''); $$('.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] = ''); $$('.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] = ''); $$('.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] = ''); $$('.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] = ''); $$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
ruleJson.ruleContent = JSON.stringify(contentJson); //ruleJson.ruleContent = JSON.stringify(contentJson);
ruleJson.ruleContent = contentJson;
return ruleJson; return ruleJson;
})(); })();
@ -110,38 +115,48 @@ function rule2json() {
// 转换搜索规则 // 转换搜索规则
let searchJson = {}; 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; searchJson[key] = $('#' + 'ruleSearch_' + key).value;
}); });
RuleJSON.ruleSearch = JSON.stringify(searchJson); //RuleJSON.ruleSearch = JSON.stringify(searchJson);
RuleJSON.ruleSearch = searchJson;
// 转换发现规则 // 转换发现规则
let exploreJson = {}; 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; exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
}); });
RuleJSON.ruleExplore = JSON.stringify(exploreJson); //RuleJSON.ruleExplore = JSON.stringify(exploreJson);
RuleJSON.ruleExplore = exploreJson;
// 转换详情页规则 // 转换详情页规则
let bookInfoJson = {}; 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; bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
}); });
RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson); //RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
RuleJSON.ruleBookInfo = bookInfoJson;
// 转换目录规则 // 转换目录规则
let tocJson = {}; 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; tocJson[key] = $('#' + 'ruleToc_' + key).value;
}); });
RuleJSON.ruleToc = JSON.stringify(tocJson); //RuleJSON.ruleToc = JSON.stringify(tocJson);
RuleJSON.ruleToc = tocJson;
// 转换正文规则 // 转换正文规则
let contentJson = {}; 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; 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.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime);
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder); RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
@ -171,40 +186,50 @@ function json2rule(RuleEditor) {
// 转换搜索规则 // 转换搜索规则
if (RuleEditor.ruleSearch) { if (RuleEditor.ruleSearch) {
let searchJson = JSON.parse(RuleEditor.ruleSearch); //let searchJson = JSON.parse(RuleEditor.ruleSearch);
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => { 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] : ''; $('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
}); });
} }
// 转换发现规则 // 转换发现规则
if (RuleEditor.ruleExplore) { if (RuleEditor.ruleExplore) {
let exploreJson = JSON.parse(RuleEditor.ruleExplore); //let exploreJson = JSON.parse(RuleEditor.ruleExplore);
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => { //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] : ''; $('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
}); });
} }
// 转换详情页规则 // 转换详情页规则
if (RuleEditor.ruleBookInfo) { if (RuleEditor.ruleBookInfo) {
let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo); //let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => { //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] : ''; $('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
}); });
} }
// 转换目录规则 // 转换目录规则
if (RuleEditor.ruleToc) { if (RuleEditor.ruleToc) {
let tocJson = JSON.parse(RuleEditor.ruleToc); //let tocJson = JSON.parse(RuleEditor.ruleToc);
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => { //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] : ''; $('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
}); });
} }
// 转换正文规则 // 转换正文规则
if (RuleEditor.ruleContent) { if (RuleEditor.ruleContent) {
let contentJson = JSON.parse(RuleEditor.ruleContent); //let contentJson = JSON.parse(RuleEditor.ruleContent);
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => { //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] : ''; $('#' + '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( .primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600)) getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor( ).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800)) getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
).backgroundColor( ).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground( ).bottomBackground(

@ -41,17 +41,30 @@ object AppConst {
val keyboardToolChars: List<String> by lazy { val keyboardToolChars: List<String> by lazy {
arrayListOf( 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 bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local)) val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio)) 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 notificationIdRead = 1144771
const val notificationIdAudio = 1144772 const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773 const val notificationIdWeb = 1144773
const val notificationIdDownload = 1144774 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 shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect" const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces" const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh"
} }

@ -4,6 +4,7 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
@ -18,7 +19,7 @@ import kotlinx.coroutines.launch
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class], RssStar::class, TxtTocRule::class],
version = 8, version = 12,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -30,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context): AppDatabase { fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addMigrations(migration_10_11, migration_11_12)
.addCallback(object : Callback() { .addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) { override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) } GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
@ -37,6 +39,29 @@ abstract class AppDatabase : RoomDatabase() {
}) })
.build() .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 abstract fun bookDao(): BookDao

@ -10,16 +10,16 @@ import io.legado.app.data.entities.BookChapter
@Dao @Dao
interface BookChapterDao { 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>> 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>> 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> 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> fun getChapterList(bookUrl: String, start: Int, end: Int): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") @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") @Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>> 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||'%'") @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>> fun liveDataSearch(key: String): LiveData<List<Book>>
@ -42,6 +45,12 @@ interface BookDao {
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl") @Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
fun getBook(bookUrl: String): Book? 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") @get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0")
val webBooks: List<Book> val webBooks: List<Book>

@ -12,17 +12,18 @@ interface RssArticleDao {
fun get(origin: String, link: String): RssArticle? fun get(origin: String, link: String): RssArticle?
@Query( @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 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) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssArticle: RssArticle) fun insert(vararg rssArticle: RssArticle)
@Query("delete from rssArticles where origin = :origin and `order` < :order") @Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order")
fun clearOld(origin: String, order: Long) fun clearOld(origin: String, sort: String, order: Long)
@Update @Update
fun update(vararg rssArticle: RssArticle) fun update(vararg rssArticle: RssArticle)

@ -28,4 +28,6 @@ interface TxtTocRuleDao {
@Delete @Delete
fun delete(vararg rule: TxtTocRule) 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.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
@ -15,9 +14,12 @@ import java.nio.charset.Charset
import kotlin.math.max import kotlin.math.max
@Parcelize @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( data class Book(
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents) var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local) var origin: String = BookType.local, // 书源URL(默认BookType.local)

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

@ -23,6 +23,7 @@ data class RssSource(
var sourceIcon: String = "", var sourceIcon: String = "",
var sourceGroup: String? = null, var sourceGroup: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var sortUrl: String? = null,
//列表规则 //列表规则
var ruleArticles: String? = null, var ruleArticles: String? = null,
var ruleNextPage: String? = null, var ruleNextPage: String? = null,
@ -33,9 +34,11 @@ data class RssSource(
var ruleImage: String? = null, var ruleImage: String? = null,
var ruleLink: String? = null, var ruleLink: String? = null,
var ruleContent: String? = null, var ruleContent: String? = null,
var style: String? = null,
var header: String? = null, var header: String? = null,
var enableJs: Boolean = false, var enableJs: Boolean = false,
var loadWithBaseUrl: Boolean = false, var loadWithBaseUrl: Boolean = false,
var customOrder: Int = 0 var customOrder: Int = 0
) : Parcelable, JsExtensions { ) : Parcelable, JsExtensions {
@ -99,4 +102,16 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) 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( data class RssStar(
var origin: String = "", var origin: String = "",
var sort: String = "",
var title: String = "", var title: String = "",
var starTime: Long = 0, var starTime: Long = 0,
var link: String = "", var link: String = "",
@ -20,6 +21,7 @@ data class RssStar(
fun toRssArticle(): RssArticle { fun toRssArticle(): RssArticle {
return RssArticle( return RssArticle(
origin = origin, origin = origin,
sort = sort,
title = title, title = title,
link = link, link = link,
pubDate = pubDate, pubDate = pubDate,

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

@ -1,5 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class BookInfoRule( data class BookInfoRule(
var init: String? = null, var init: String? = null,
var name: String? = null, var name: String? = null,
@ -11,4 +14,45 @@ data class BookInfoRule(
var coverUrl: String? = null, var coverUrl: String? = null,
var tocUrl: String? = null, var tocUrl: String? = null,
var wordCount: String? = null var wordCount: String? = null
) ) : 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 package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class ContentRule( data class ContentRule(
var content: String? = null, var content: String? = null,
var nextContentUrl: String? = null, var nextContentUrl: String? = null,
var webJs: String? = null, var webJs: String? = null,
var sourceRegex: 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 package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class ExploreRule( data class ExploreRule(
override var bookList: String? = null, override var bookList: String? = null,
override var name: String? = null, override var name: String? = null,
@ -11,4 +14,46 @@ data class ExploreRule(
override var bookUrl: String? = null, override var bookUrl: String? = null,
override var coverUrl: String? = null, override var coverUrl: String? = null,
override var wordCount: 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 package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class SearchRule( data class SearchRule(
override var bookList: String? = null, override var bookList: String? = null,
override var name: String? = null, override var name: String? = null,
@ -11,4 +14,46 @@ data class SearchRule(
override var bookUrl: String? = null, override var bookUrl: String? = null,
override var coverUrl: String? = null, override var coverUrl: String? = null,
override var wordCount: 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 package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
data class TocRule( data class TocRule(
var chapterList: String? = null, var chapterList: String? = null,
var chapterName: String? = null, var chapterName: String? = null,
@ -7,4 +10,36 @@ data class TocRule(
var isVip: String? = null, var isVip: String? = null,
var updateTime: String? = null, var updateTime: String? = null,
var nextTocUrl: 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) App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
} }
var bookGroupNoneShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupNone", value)
}
var elevation: Int var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1) get() = App.INSTANCE.getPrefInt("elevation", -1)
set(value) { set(value) {
App.INSTANCE.putPrefInt("elevation", 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 val Context.channel: String

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

@ -27,7 +27,6 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!! GSON.fromJsonArray<Config>(json)!!
} }
val durConfig get() = getConfig(styleSelect) val durConfig get() = getConfig(styleSelect)
private val shareConfig get() = getConfig(5)
var bg: Drawable? = null var bg: Drawable? = null
var bgMeanColor: Int = 0 var bgMeanColor: Int = 0
@ -139,129 +138,145 @@ object ReadBookConfig {
var bodyIndent = " ".repeat(bodyIndentCount) var bodyIndent = " ".repeat(bodyIndentCount)
var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean var textBold: Boolean
get() = if (shareLayout) shareConfig.textBold else durConfig.textBold get() = config.textBold
set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value set(value) {
config.textBold = value
}
var textSize: Int var textSize: Int
get() = if (shareLayout) shareConfig.textSize else durConfig.textSize get() = config.textSize
set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value set(value) {
config.textSize = value
}
var letterSpacing: Float var letterSpacing: Float
get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing get() = config.letterSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value config.letterSpacing = value
}
var lineSpacingExtra: Int var lineSpacingExtra: Int
get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra get() = config.lineSpacingExtra
set(value) = set(value) {
if (shareLayout) shareConfig.lineSpacingExtra = value config.lineSpacingExtra = value
else durConfig.lineSpacingExtra = value }
var paragraphSpacing: Int var paragraphSpacing: Int
get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing get() = config.paragraphSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.paragraphSpacing = value config.paragraphSpacing = value
else durConfig.paragraphSpacing = value }
var titleMode: Int var titleMode: Int
get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode get() = config.titleMode
set(value) = set(value) {
if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value config.titleMode = value
}
var titleSize: Int var titleSize: Int
get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize get() = config.titleSize
set(value) = set(value) {
if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value config.titleSize = value
}
var titleTopSpacing: Int var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing get() = config.titleTopSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.titleTopSpacing = value config.titleTopSpacing = value
else durConfig.titleTopSpacing = value }
var titleBottomSpacing: Int var titleBottomSpacing: Int
get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing get() = config.titleBottomSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.titleBottomSpacing = value config.titleBottomSpacing = value
else durConfig.titleBottomSpacing = value }
var paddingBottom: Int var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom get() = config.paddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value config.paddingBottom = value
}
var paddingLeft: Int var paddingLeft: Int
get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft get() = config.paddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value config.paddingLeft = value
}
var paddingRight: Int var paddingRight: Int
get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight get() = config.paddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value config.paddingRight = value
}
var paddingTop: Int var paddingTop: Int
get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop get() = config.paddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value config.paddingTop = value
}
var headerPaddingBottom: Int var headerPaddingBottom: Int
get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom get() = config.headerPaddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingBottom = value config.headerPaddingBottom = value
else durConfig.headerPaddingBottom = value }
var headerPaddingLeft: Int var headerPaddingLeft: Int
get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft get() = config.headerPaddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingLeft = value config.headerPaddingLeft = value
else durConfig.headerPaddingLeft = value }
var headerPaddingRight: Int var headerPaddingRight: Int
get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight get() = config.headerPaddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingRight = value config.headerPaddingRight = value
else durConfig.headerPaddingRight = value }
var headerPaddingTop: Int var headerPaddingTop: Int
get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop get() = config.headerPaddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingTop = value config.headerPaddingTop = value
else durConfig.headerPaddingTop = value }
var footerPaddingBottom: Int var footerPaddingBottom: Int
get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom get() = config.footerPaddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingBottom = value config.footerPaddingBottom = value
else durConfig.footerPaddingBottom = value }
var footerPaddingLeft: Int var footerPaddingLeft: Int
get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft get() = config.footerPaddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingLeft = value config.footerPaddingLeft = value
else durConfig.footerPaddingLeft = value }
var footerPaddingRight: Int var footerPaddingRight: Int
get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight get() = config.footerPaddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingRight = value config.footerPaddingRight = value
else durConfig.footerPaddingRight = value }
var footerPaddingTop: Int var footerPaddingTop: Int
get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop get() = config.footerPaddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingTop = value config.footerPaddingTop = value
else durConfig.footerPaddingTop = value }
var showHeaderLine: Boolean var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine get() = config.showHeaderLine
set(value) = set(value) {
if (shareLayout) shareConfig.showHeaderLine = value config.showHeaderLine = value
else durConfig.showHeaderLine = value }
var showFooterLine: Boolean var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine get() = config.showFooterLine
set(value) = set(value) {
if (shareLayout) shareConfig.showFooterLine = value config.showFooterLine = value
else durConfig.showFooterLine = value }
@Keep @Keep
class Config( 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) { mWebView.get()?.evaluateJavascript(mJavaScript) {
if (it.isNotEmpty() && it != "null") { if (it.isNotEmpty() && it != "null") {
val content = StringEscapeUtils.unescapeJson(it) val content = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
handler.obtainMessage(MSG_SUCCESS, Res(url, content)) handler.obtainMessage(MSG_SUCCESS, Res(url, content))
.sendToTarget() .sendToTarget()
handler.removeCallbacks(this) 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.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import retrofit2.Retrofit import retrofit2.Retrofit
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -13,14 +15,12 @@ import kotlin.coroutines.resume
object HttpHelper { object HttpHelper {
val client: OkHttpClient by lazy { val client: OkHttpClient by lazy {
val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs = ArrayList<ConnectionSpec>() val specs = arrayListOf(
specs.add(default) ConnectionSpec.MODERN_TLS,
specs.add(ConnectionSpec.COMPATIBLE_TLS) ConnectionSpec.COMPATIBLE_TLS,
specs.add(ConnectionSpec.CLEARTEXT) ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)

@ -32,9 +32,7 @@ object Backup {
val lastBackup = context.getPrefLong(PreferKey.lastBackup) val lastBackup = context.getPrefLong(PreferKey.lastBackup)
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async { Coroutine.async {
context.getPrefString(PreferKey.backupPath)?.let { backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true)
backup(context, it, true)
}
} }
} }
} }
@ -70,12 +68,16 @@ object Backup {
WebDavHelp.backUpWebDav(backupPath) WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) { if (path.isContentPath()) {
copyBackup(context, Uri.parse(path), isAuto) copyBackup(context, Uri.parse(path), isAuto)
} else {
if (path.isEmpty()) {
copyBackup(context.getExternalFilesDir(null)!!, false)
} else { } else {
copyBackup(File(path), isAuto) copyBackup(File(path), isAuto)
} }
} }
} }
} }
}
private fun writeListToJson(list: List<Any>, fileName: String, path: String) { private fun writeListToJson(list: List<Any>, fileName: String, path: String) {
if (list.isNotEmpty()) { if (list.isNotEmpty()) {

@ -136,14 +136,12 @@ object ImportOldData {
return bookSources.size return bookSources.size
} }
fun importOldReplaceRule(json: String): Int { fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>() val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { OldReplace.jsonToReplaceRule(jsonItem.jsonString())?.let {
if (it.isValid()){ if (it.isValid()){
replaceRules.add(it) 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 package io.legado.app.help.storage
import androidx.annotation.Keep
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.BookSource 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.data.entities.rule.*
import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.* import io.legado.app.utils.*
@ -14,13 +14,15 @@ object OldRule {
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
fun jsonToBookSource(json: String): BookSource? { fun jsonToBookSource(json: String): BookSource? {
var source: BookSource? = null val source = BookSource()
runCatching { val sourceAny = try {
source = GSON.fromJsonObject<BookSource>(json.trim()) GSON.fromJsonObject<BookSourceAny>(json.trim())
} catch (e: Exception) {
null
} }
runCatching { runCatching {
if (source == null || source?.searchUrl.isNullOrBlank()) { if (sourceAny?.ruleToc == null) {
source = BookSource().apply { source.apply {
val jsonItem = jsonPath.parse(json.trim()) val jsonItem = jsonPath.parse(json.trim())
bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: "" bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: ""
bookSourceName = jsonItem.readString("bookSourceName") ?: "" bookSourceName = jsonItem.readString("bookSourceName") ?: ""
@ -37,7 +39,7 @@ object OldRule {
if (exploreUrl.isNullOrBlank()) { if (exploreUrl.isNullOrBlank()) {
enabledExplore = false enabledExplore = false
} }
val searchRule = SearchRule( ruleSearch = SearchRule(
bookList = toNewRule(jsonItem.readString("ruleSearchList")), bookList = toNewRule(jsonItem.readString("ruleSearchList")),
name = toNewRule(jsonItem.readString("ruleSearchName")), name = toNewRule(jsonItem.readString("ruleSearchName")),
author = toNewRule(jsonItem.readString("ruleSearchAuthor")), author = toNewRule(jsonItem.readString("ruleSearchAuthor")),
@ -47,8 +49,7 @@ object OldRule {
coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")), coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter")) lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter"))
) )
ruleSearch = GSON.toJson(searchRule) ruleExplore = ExploreRule(
val exploreRule = ExploreRule(
bookList = toNewRule(jsonItem.readString("ruleFindList")), bookList = toNewRule(jsonItem.readString("ruleFindList")),
name = toNewRule(jsonItem.readString("ruleFindName")), name = toNewRule(jsonItem.readString("ruleFindName")),
author = toNewRule(jsonItem.readString("ruleFindAuthor")), author = toNewRule(jsonItem.readString("ruleFindAuthor")),
@ -58,8 +59,7 @@ object OldRule {
coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")), coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter")) lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter"))
) )
ruleExplore = GSON.toJson(exploreRule) ruleBookInfo = BookInfoRule(
val bookInfoRule = BookInfoRule(
init = toNewRule(jsonItem.readString("ruleBookInfoInit")), init = toNewRule(jsonItem.readString("ruleBookInfoInit")),
name = toNewRule(jsonItem.readString("ruleBookName")), name = toNewRule(jsonItem.readString("ruleBookName")),
author = toNewRule(jsonItem.readString("ruleBookAuthor")), author = toNewRule(jsonItem.readString("ruleBookAuthor")),
@ -69,29 +69,89 @@ object OldRule {
lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")), lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")),
tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl")) tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl"))
) )
ruleBookInfo = GSON.toJson(bookInfoRule) ruleToc = TocRule(
val chapterRule = TocRule(
chapterList = toNewRule(jsonItem.readString("ruleChapterList")), chapterList = toNewRule(jsonItem.readString("ruleChapterList")),
chapterName = toNewRule(jsonItem.readString("ruleChapterName")), chapterName = toNewRule(jsonItem.readString("ruleChapterName")),
chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")), chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")),
nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext")) nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext"))
) )
ruleToc = GSON.toJson(chapterRule)
var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: "" var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: ""
if (content.startsWith("$") && !content.startsWith("$.")) { if (content.startsWith("$") && !content.startsWith("$.")) {
content = content.substring(1) content = content.substring(1)
} }
val contentRule = ContentRule( ruleContent = ContentRule(
content = content, content = content,
nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext")) 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 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规则适配 // default规则适配
// #正则#替换内容 替换成 ##正则##替换内容 // #正则#替换内容 替换成 ##正则##替换内容
// | 替换成 || // | 替换成 ||
@ -114,8 +174,8 @@ object OldRule {
!newRule.startsWith("//") && !newRule.startsWith("//") &&
!newRule.startsWith("##") && !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("##")) { if (newRule.contains("#") && !newRule.contains("##")) {
newRule = oldRule.replace("#", "##") newRule = oldRule.replace("#", "##")
@ -142,10 +202,10 @@ object OldRule {
} }
} }
if (allinone) { if (allinone) {
newRule = "+" + newRule newRule = "+$newRule"
} }
if (reverse) { if (reverse) {
newRule = "-" + newRule newRule = "-$newRule"
} }
return newRule return newRule
} }
@ -216,27 +276,4 @@ object OldRule {
return GSON.toJson(map) 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.help.http.HttpHelper
import io.legado.app.lib.webdav.http.Handler import io.legado.app.lib.webdav.http.Handler
import io.legado.app.lib.webdav.http.HttpAuth import io.legado.app.lib.webdav.http.HttpAuth
import okhttp3.Credentials import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -82,7 +77,7 @@ constructor(urlStr: String) {
this.exists = false this.exists = false
return false return false
} }
response.body?.let { response.body()?.let {
if (it.string().isNotEmpty()) { if (it.string().isNotEmpty()) {
return true return true
} }
@ -102,7 +97,7 @@ constructor(urlStr: String) {
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response -> propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
response.body?.let { body -> response.body()?.let { body ->
return parseDir(body.string()) return parseDir(body.string())
} }
} }
@ -127,7 +122,10 @@ constructor(urlStr: String) {
.url(url) .url(url)
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
.method("PROPFIND", requestPropsStr.toRequestBody("text/plain".toMediaTypeOrNull())) .method(
"PROPFIND",
RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
)
HttpAuth.auth?.let { HttpAuth.auth?.let {
request.header( request.header(
@ -205,9 +203,9 @@ constructor(urlStr: String) {
fun upload(localPath: String, contentType: String? = null): Boolean { fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath) val file = File(localPath)
if (!file.exists()) return false if (!file.exists()) return false
val mediaType = contentType?.toMediaTypeOrNull() val mediaType = contentType?.let { MediaType.parse(it) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = file.asRequestBody(mediaType) val fileBody = RequestBody.create(mediaType, file)
httpUrl?.let { httpUrl?.let {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)
@ -241,7 +239,7 @@ constructor(urlStr: String) {
request.header("Authorization", Credentials.basic(it.user, it.pass)) request.header("Authorization", Credentials.basic(it.user, it.pass))
} }
try { try {
return HttpHelper.client.newCall(request.build()).execute().body?.byteStream() return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream()
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {

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

@ -15,6 +15,8 @@ import kotlin.coroutines.CoroutineContext
object Rss { object Rss {
fun getArticles( fun getArticles(
sortName: String,
sortUrl: String,
rssSource: RssSource, rssSource: RssSource,
pageUrl: String? = null, pageUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
@ -22,11 +24,11 @@ object Rss {
): Coroutine<Result> { ): Coroutine<Result> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
pageUrl ?: rssSource.sourceUrl, pageUrl ?: sortUrl,
headerMapF = rssSource.getHeaderMap() headerMapF = rssSource.getHeaderMap()
) )
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body 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.help.http.api.HttpPostApi
import io.legado.app.utils.* import io.legado.app.utils.*
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call import retrofit2.Call
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
@ -40,7 +39,7 @@ class AnalyzeUrl(
) : JsExtensions { ) : JsExtensions {
companion object { companion object {
private val pagePattern = Pattern.compile("<(.*?)>") 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 = "" private var baseUrl: String = ""
@ -52,8 +51,8 @@ class AnalyzeUrl(
private var queryStr: String? = null private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>() private val fieldMap = LinkedHashMap<String, String>()
private var charset: String? = null private var charset: String? = null
private var bodyTxt: String? = null private var body: String? = null
private var body: RequestBody? = null private var requestBody: RequestBody? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
init { init {
@ -158,15 +157,30 @@ class AnalyzeUrl(
baseUrl = it baseUrl = it
} }
if (urlArray.size > 1) { if (urlArray.size > 1) {
val options = GSON.fromJsonObject<Map<String, String>>(urlArray[1]) val option = GSON.fromJsonObject<UrlOption>(urlArray[1])
options?.let { _ -> option?.let { _ ->
options["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST } option.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
options["headers"]?.let { headers -> option.headers?.let { headers ->
GSON.fromJsonObject<Map<String, String>>(headers)?.let { headerMap.putAll(it) } 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) { when (method) {
@ -180,20 +194,19 @@ class AnalyzeUrl(
} }
} }
RequestMethod.POST -> { RequestMethod.POST -> {
bodyTxt?.let { body?.let {
if (it.isJson()) { if (it.isJson()) {
body = it.toRequestBody(jsonType) requestBody = RequestBody.create(jsonType, it)
} else { } else {
analyzeFields(it) analyzeFields(it)
} }
} ?: let { } ?: let {
body = FormBody.Builder().build() requestBody = FormBody.Builder().build()
} }
} }
} }
} }
/** /**
* 解析QueryMap * 解析QueryMap
*/ */
@ -254,7 +267,7 @@ class AnalyzeUrl(
} else { } else {
HttpHelper HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset) .getApiService<HttpPostApi>(baseUrl, charset)
.postBody(url, body!!, headerMap) .postBody(url, requestBody!!, headerMap)
} }
} }
fieldMap.isEmpty() -> HttpHelper fieldMap.isEmpty() -> HttpHelper
@ -278,7 +291,7 @@ class AnalyzeUrl(
params.requestMethod = method params.requestMethod = method
params.javaScript = jsStr params.javaScript = jsStr
params.sourceRegex = sourceRegex params.sourceRegex = sourceRegex
params.postData = bodyTxt?.toByteArray() params.postData = body?.toByteArray()
params.tag = tag params.tag = tag
return HttpHelper.ajax(params) return HttpHelper.ajax(params)
} }
@ -295,7 +308,7 @@ class AnalyzeUrl(
} else { } else {
HttpHelper HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset) .getApiService<HttpPostApi>(baseUrl, charset)
.postBodyAsync(url, body!!, headerMap) .postBodyAsync(url, requestBody!!, headerMap)
} }
} }
fieldMap.isEmpty() -> HttpHelper fieldMap.isEmpty() -> HttpHelper
@ -308,4 +321,12 @@ class AnalyzeUrl(
return Res(NetworkUtils.getUrl(res), res.body()) 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.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
object AnalyzeTxtFile { class AnalyzeTxtFile {
private const val folderName = "bookTxt"
private const val BLANK: Byte = 0x0a private val tocRules = arrayListOf<TxtTocRule>()
//默认从文件中获取数据的长度 private lateinit var charset: Charset
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))
}
@Throws(Exception::class)
fun analyze(context: Context, book: Book): ArrayList<BookChapter> { fun analyze(context: Context, book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(context, book) val bookFile = getBookFile(context, book)
book.charset = EncodingDetect.getEncode(bookFile) book.charset = EncodingDetect.getEncode(bookFile)
val charset = book.fileCharset() charset = book.fileCharset()
val toc = arrayListOf<BookChapter>() val rulePattern = if (book.tocUrl.isNotEmpty()) {
Pattern.compile(book.tocUrl, Pattern.MULTILINE)
} else {
tocRules.addAll(getTocRules())
null
}
//获取文件流 //获取文件流
val bookStream = RandomAccessFile(bookFile, "r") 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) val buffer = ByteArray(BUFFER_SIZE)
//获取到的块起始点,在文件中的位置 //获取到的块起始点,在文件中的位置
bookStream.seek(0)
var curOffset: Long = 0 var curOffset: Long = 0
//block的个数 //block的个数
var blockPos = 0 var blockPos = 0
@ -47,13 +61,14 @@ object AnalyzeTxtFile {
var allLength = 0 var allLength = 0
//获取文件中的数据到buffer,直到没有数据为止 //获取文件中的数据到buffer,直到没有数据为止
while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) { while (bookStream.read(buffer).also { length = it } > 0) {
++blockPos blockPos++
//如果存在Chapter //如果存在Chapter
if (rulePattern != null) { //将数据转换成String if (rulePattern != null) {
//将数据转换成String, 不能超过length
var blockContent = String(buffer, 0, length, charset) var blockContent = String(buffer, 0, length, charset)
val lastN = blockContent.lastIndexOf("\n") val lastN = blockContent.lastIndexOf("\n")
if (lastN != 0) { if (lastN > 0) {
blockContent = blockContent.substring(0, lastN) blockContent = blockContent.substring(0, lastN)
length = blockContent.toByteArray(charset).size length = blockContent.toByteArray(charset).size
allLength += length allLength += length
@ -66,40 +81,50 @@ object AnalyzeTxtFile {
//如果存在相应章节 //如果存在相应章节
while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置
val chapterStart = matcher.start() 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处前面有一段内容 //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容 //第一种情况一定是序章 第二种情况是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容 if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容
val chapterContent = blockContent.substring(seekPos, chapterStart) if (toc.isEmpty()) { //如果当前没有章节,那么就是序章
//设置指针偏移
seekPos += chapterContent.length
if (toc.size == 0) { //如果当前没有章节,那么就是序章
//加入简介 //加入简介
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() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = chapterContent.toByteArray(charset).size.toLong() curChapter.start = chapterLength.toLong()
toc.add(curChapter) toc.add(curChapter)
} else { //否则就block分割之后,上一个章节的剩余内容 } else { //否则就block分割之后,上一个章节的剩余内容
//获取上一章节 //获取上一章节
val lastChapter = toc.last() val lastChapter = toc.last()
//将当前段落添加上一章去 //将当前段落添加上一章去
lastChapter.end = lastChapter.end =
lastChapter.end!! + chapterContent.toByteArray(charset).size lastChapter.end!! + chapterLength.toLong()
//创建当前章节 //创建当前章节
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = lastChapter.end curChapter.start = lastChapter.end
toc.add(curChapter) toc.add(curChapter)
} }
} else { //是否存在章节 } else {
if (toc.size != 0) { //获取章节内容 if (toc.isNotEmpty()) { //获取章节内容
val chapterContent = blockContent.substring(seekPos, matcher.start())
seekPos += chapterContent.length
//获取上一章节 //获取上一章节
val lastChapter = toc.last() val lastChapter = toc.last()
lastChapter.end = lastChapter.end =
lastChapter.start!! + chapterContent.toByteArray(charset).size lastChapter.start!! + chapterContent.toByteArray(charset).size.toLong()
//创建当前章节 //创建当前章节
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
@ -108,11 +133,18 @@ object AnalyzeTxtFile {
} else { //如果章节不存在则创建章节 } else { //如果章节不存在则创建章节
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = 0L curChapter.start = 0
curChapter.end = 0L curChapter.end = 0
toc.add(curChapter) toc.add(curChapter)
} }
} }
//设置指针偏移
seekPos += chapterContent.length
}
if (seekPos == 0 && length > 50000 && pattern == null) {
//移除不匹配的规则
tocRules.remove(tocRule)
return analyze(bookStream, book, null)
} }
} else { //进行本地虚拟分章 } else { //进行本地虚拟分章
//章节在buffer的偏移量 //章节在buffer的偏移量
@ -156,7 +188,8 @@ object AnalyzeTxtFile {
//block的偏移点 //block的偏移点
curOffset += length.toLong() curOffset += length.toLong()
if (rulePattern != null) { //设置上一章的结尾 if (rulePattern != null) {
//设置上一章的结尾
val lastChapter = toc.last() val lastChapter = toc.last()
lastChapter.end = curOffset lastChapter.end = curOffset
} }
@ -175,20 +208,57 @@ object AnalyzeTxtFile {
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "") bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
} }
book.latestChapterTitle = toc.last().title book.latestChapterTitle = toc.last().title
book.totalChapterNum = toc.size
System.gc() System.gc()
System.runFinalization() System.runFinalization()
tocRule?.let {
book.tocUrl = it.rule
}
return toc return toc
} }
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)
val content = String(buffer, 0, length, charset)
for (tocRule in tocRules) {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
txtTocRule = tocRule
break
}
}
return txtTocRule
}
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 { fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book) val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流 //获取文件流
val bookStream = RandomAccessFile(bookFile, "r") val bookStream = RandomAccessFile(bookFile, "r")
bookStream.seek(bookChapter.start ?: 0) val content = ByteArray((bookChapter.end!! - bookChapter.start!!).toInt())
val extent = (bookChapter.end!! - bookChapter.start!!).toInt() bookStream.seek(bookChapter.start!!)
val content = ByteArray(extent) bookStream.read(content)
bookStream.read(content, 0, extent)
return String(content, book.fileCharset()) return String(content, book.fileCharset())
} }
@ -207,29 +277,6 @@ object AnalyzeTxtFile {
return File(book.bookUrl) return File(book.bookUrl)
} }
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
//首先获取128k的数据
val buffer = ByteArray(BUFFER_SIZE / 4)
val length = bookStream.read(buffer, 0, buffer.size)
val content = String(buffer, 0, length, charset)
for (tocRule in tocRules) {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
book.tocUrl = tocRule.rule
rulePattern = pattern
break
}
}
bookStream.seek(0)
return rulePattern
}
private fun getTocRules(): List<TxtTocRule> { private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all val rules = App.db.txtTocRule().all
if (rules.isEmpty()) { if (rules.isEmpty()) {
@ -247,4 +294,6 @@ object AnalyzeTxtFile {
} }
return emptyList() return emptyList()
} }
}
} }

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

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

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

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

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

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

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

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

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

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

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

@ -26,9 +26,10 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
tv_app_summary.post { tv_app_summary.post {
val span = ForegroundColorSpan(accentColor) val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text) 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( spannableString.setSpan(
span, start, start + 6, span, start, start + gzh.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
) )
tv_app_summary.text = spannableString tv_app_summary.text = spannableString

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

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

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

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
@ -105,10 +106,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
booksLiveData?.removeObservers(this) booksLiveData?.removeObservers(this)
booksLiveData = booksLiveData =
when (groupId) { when (groupId) {
-1 -> App.db.bookDao().observeAll() AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal() AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio() AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup() AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId) else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { list -> booksLiveData?.observe(this, Observer { list ->
@ -129,22 +130,22 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
.show(supportFragmentManager, "groupManage") .show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> { R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group) title_bar.subtitle = getString(R.string.no_group)
groupId = -11 groupId = AppConst.bookGroupNone.groupId
initBookData() initBookData()
} }
R.id.menu_all -> { R.id.menu_all -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -1 groupId = AppConst.bookGroupAll.groupId
initBookData() initBookData()
} }
R.id.menu_local -> { R.id.menu_local -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -2 groupId = AppConst.bookGroupLocal.groupId
initBookData() initBookData()
} }
R.id.menu_audio -> { R.id.menu_audio -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -3 groupId = AppConst.bookGroupAudio.groupId
initBookData() initBookData()
} }
else -> if (item.groupId == R.id.menu_group) { 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.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.PopupMenu
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
@ -10,6 +12,7 @@ import io.legado.app.utils.invisible
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_change_source.view.* import kotlinx.android.synthetic.main.item_change_source.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class ChangeSourceAdapter(context: Context, val callBack: CallBack) : class ChangeSourceAdapter(context: Context, val callBack: CallBack) :
@ -43,10 +46,31 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) :
callBack.changeTo(it) 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 { interface CallBack {
val bookUrl: String? val bookUrl: String?
fun changeTo(searchBook: SearchBook) fun changeTo(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook)
} }
} }

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

@ -165,4 +165,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
searchPool.close() 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 package io.legado.app.ui.book.chapterlist
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle 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.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp 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.UpLinearLayoutManager
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.getViewModelOfActivity
@ -54,7 +55,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} }
private fun initView() { 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_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) }
iv_chapter_bottom.onClick { iv_chapter_bottom.onClick {
if (adapter.itemCount > 0) { if (adapter.itemCount > 0) {
@ -66,6 +67,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} }
} }
@SuppressLint("SetTextI18n")
private fun initBook() { private fun initBook() {
launch { launch {
withContext(IO) { withContext(IO) {
@ -74,7 +76,8 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
initDoc() initDoc()
book?.let { book?.let {
durChapterIndex = it.durChapterIndex durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle tv_current_chapter_info.text =
"${it.durChapterTitle}(${it.durChapterIndex + 1}/${it.totalChapterNum})"
initCacheFileNames(it) initCacheFileNames(it)
} }
} }

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

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

Loading…
Cancel
Save