Merge pull request #5 from gedoor/master

update
pull/242/head
52fisher 5 years ago committed by GitHub
commit 2e14b51266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 17
      app/build.gradle
  3. 26
      app/proguard-rules.pro
  4. 4
      app/src/debug/res/values/strings.xml
  5. 3
      app/src/main/AndroidManifest.xml
  6. 3
      app/src/main/assets/18PlusList.txt
  7. 20
      app/src/main/assets/readConfig.json
  8. 17
      app/src/main/assets/txtTocRule.json
  9. 89
      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. 18
      app/src/main/java/io/legado/app/constant/AppConst.kt
  47. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  48. 37
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  49. 2
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  50. 2
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  51. 23
      app/src/main/java/io/legado/app/data/entities/Book.kt
  52. 133
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  53. 2
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  54. 3
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  55. 16
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  56. 1
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  57. 46
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  58. 33
      app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt
  59. 47
      app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt
  60. 47
      app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt
  61. 37
      app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt
  62. 4
      app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt
  63. 22
      app/src/main/java/io/legado/app/help/AppConfig.kt
  64. 21
      app/src/main/java/io/legado/app/help/BookHelp.kt
  65. 8
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  66. 15
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  67. 54
      app/src/main/java/io/legado/app/help/SourceHelp.kt
  68. 4
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  69. 7
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  70. 4
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  71. 32
      app/src/main/java/io/legado/app/help/storage/OldReplace.kt
  72. 127
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  73. 1
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  74. 56
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  75. 9
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  76. 3
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  77. 45
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  78. 21
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  79. 35
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  80. 6
      app/src/main/java/io/legado/app/service/DownloadService.kt
  81. 6
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  82. 71
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  83. 64
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  84. 46
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  85. 5
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  86. 1
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  87. 2
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  88. 23
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  89. 4
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  90. 28
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  91. 10
      app/src/main/java/io/legado/app/ui/book/changesource/DiffCallBack.kt
  92. 3
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt
  93. 18
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  94. 31
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  95. 4
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  96. 90
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  97. 22
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  98. 6
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  99. 100
      app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt
  100. 6
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.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

@ -19,7 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android { android {
compileSdkVersion 29 compileSdkVersion 29
flavorDimensions ("version") flavorDimensions("version")
signingConfigs { signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig { myConfig {
@ -56,7 +56,7 @@ android {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
} }
applicationIdSuffix '.release' applicationIdSuffix '.release'
minifyEnabled true minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug { debug {
@ -66,7 +66,6 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
versionNameSuffix 'debug' versionNameSuffix 'debug'
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
variant.outputs.all { variant.outputs.all {
@ -117,11 +116,11 @@ dependencies {
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.3.0' implementation 'com.google.firebase:firebase-core:17.4.3'
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.3.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.1' implementation 'androidx.preference:preference:1.1.1'
@ -154,7 +153,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.5.7' implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
// //
def coroutines_version = '1.3.3' def coroutines_version = '1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -196,3 +195,9 @@ dependencies {
} }
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
// }
//}

@ -58,14 +58,13 @@
# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
# 因为这些子类都有可能被外部调用 # 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity -keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction -keep public class * extends android.app.Application
-keep public class * extends android.app.Service -keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider -keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference -keep public class * extends android.preference.Preference
-keep public class * extends android.view.View -keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
# 保留androidx下的所有类及其内部类 # 保留androidx下的所有类及其内部类
@ -128,16 +127,11 @@
} }
# webView处理,项目中没有使用到webView忽略即可 # webView处理,项目中没有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview { -keepclassmembers class * extends android.webkit.WebViewClient {
public *; public void *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String); public boolean *(android.webkit.WebView, java.lang.String);
} }
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 # 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用
# 记得proguard-android.txt中一定不要加-dontoptimize才起作用 # 记得proguard-android.txt中一定不要加-dontoptimize才起作用
@ -170,20 +164,20 @@
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn com.jeremyliao.liveeventbus.** -dontwarn com.jeremyliao.liveeventbus.**
-keep class com.google.gson.** { *; }
-keep class com.ke.gson.** { *; }
-keep class com.jeremyliao.liveeventbus.** { *; } -keep class com.jeremyliao.liveeventbus.** { *; }
-keep class okhttp3.**{*;} -keep class okhttp3.**{*;}
-keep class okio.**{*;} -keep class okio.**{*;}
-keep class com.hwangjr.rxbus.**{*;} -keep class com.hwangjr.rxbus.**{*;}
-keep class org.conscrypt.**{*;} -keep class org.conscrypt.**{*;}
-keep class com.kunfei.bookshelf.widget.**{*;}
-keep class com.kunfei.bookshelf.bean.**{*;}
-keep class android.support.**{*;} -keep class android.support.**{*;}
-keep class me.grantland.widget.**{*;} -keep class me.grantland.widget.**{*;}
-keep class de.hdodenhof.circleimageview.**{*;} -keep class de.hdodenhof.circleimageview.**{*;}
-keep class tyrant.explosionfield.**{*;} -keep class tyrant.explosionfield.**{*;}
-keep class tyrantgit.explosionfield.**{*;} -keep class tyrantgit.explosionfield.**{*;}
-keep class freemarker.**{*;} -keep class freemarker.**{*;}
-keep class com.gyf.barlibrary.* {*;} -keep class com.gyf.barlibrary.** {*;}
##JSOUP ##JSOUP
-keep class org.jsoup.**{*;} -keep class org.jsoup.**{*;}
-keep class **.xpath.**{*;} -keep class **.xpath.**{*;}
@ -197,7 +191,7 @@
-dontwarn com.jayway.** -dontwarn com.jayway.**
-keep class com.fasterxml.**{*;} -keep class com.fasterxml.**{*;}
-keep class javax.swing..**{*;} -keep class javax.swing.**{*;}
-dontwarn javax.swing.** -dontwarn javax.swing.**
-keep class java.awt.**{*;} -keep class java.awt.**{*;}
-dontwarn java.awt.** -dontwarn java.awt.**
@ -215,10 +209,10 @@
-dontwarn nl.siegmann.epublib.** -dontwarn nl.siegmann.epublib.**
-dontwarn org.xmlpull.** -dontwarn org.xmlpull.**
-keep class nl.siegmann.epublib.**{*;} -keep class nl.siegmann.epublib.**{*;}
-keep class javax.xml.**{*;} -keep class javax.xml**{*;}
-keep class org.xmlpull.**{*;} -keep class org.xmlpull**{*;}
-keep class org.simpleframework.xml.**{*;} -keep class org.simpleframework.xml**{*;}
-dontwarn org.simpleframework.xml.** -dontwarn org.simpleframework.xml.**
-keepclassmembers class * { -keepclassmembers class * {

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

@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app"> package="io.legado.app">
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

@ -0,0 +1,3 @@
OGN5dS5jb20=
c2cwMC54eXo=
aXRyYWZmaWNuZXQuY29t

@ -7,9 +7,7 @@
"bgType": 0, "bgType": 0,
"bgTypeNight": 0, "bgTypeNight": 0,
"darkStatusIcon": true, "darkStatusIcon": true,
"textSize": 24, "darkStatusIconNight": false
"letterSpacing": 0,
"lineSpacingExtra": 10
}, },
{ {
"bgStr": "#DDC090", "bgStr": "#DDC090",
@ -19,9 +17,7 @@
"bgType": 0, "bgType": 0,
"bgTypeNight": 0, "bgTypeNight": 0,
"darkStatusIcon": true, "darkStatusIcon": true,
"textSize": 24, "darkStatusIconNight": false
"letterSpacing": 0,
"lineSpacingExtra": 10
}, },
{ {
"bgStr": "#C2D8AA", "bgStr": "#C2D8AA",
@ -31,9 +27,7 @@
"bgType": 0, "bgType": 0,
"bgTypeNight": 0, "bgTypeNight": 0,
"darkStatusIcon": false, "darkStatusIcon": false,
"textSize": 24, "darkStatusIconNight": false
"letterSpacing": 0,
"lineSpacingExtra": 10
}, },
{ {
"bgStr": "#DBB8E2", "bgStr": "#DBB8E2",
@ -43,9 +37,7 @@
"bgType": 0, "bgType": 0,
"bgTypeNight": 0, "bgTypeNight": 0,
"darkStatusIcon": false, "darkStatusIcon": false,
"textSize": 24, "darkStatusIconNight": false
"letterSpacing": 0,
"lineSpacingExtra": 10
}, },
{ {
"bgStr": "#ABCEE0", "bgStr": "#ABCEE0",
@ -55,8 +47,6 @@
"bgType": 0, "bgType": 0,
"bgTypeNight": 0, "bgTypeNight": 0,
"darkStatusIcon": false, "darkStatusIcon": false,
"textSize": 24, "darkStatusIconNight": false
"letterSpacing": 0,
"lineSpacingExtra": 10
} }
] ]

@ -1,101 +1,118 @@
[ [
{ {
"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": -17 "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": -16 "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": -15 "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": -14 "serialNumber": -14
}, },
{ {
"id": -5,
"enable": false, "enable": false,
"name": "数字(纯数字标题)", "name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$", "rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$",
"serialNumber": -13 "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": -12 "serialNumber": -12
}, },
{ {
"id": -7,
"enable": true, "enable": true,
"name": "大写数字 分隔符 标题名称", "name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$", "rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$",
"serialNumber": -11 "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": -10 "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": -9 "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 ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"serialNumber": -7 "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": -6 "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": -5 "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": -4 "serialNumber": -4
}, },
{ {
"id": -15,
"enable":false, "enable":false,
"name": "顶格标题", "name": "顶格标题",
"rule": "^\\S.{1,20}$", "rule": "^\\S.{1,20}$",
"serialNumber": -3 "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": -2 "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}$",

@ -3,6 +3,95 @@
* 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 新公众号[开源阅读]()已启用,[开源阅读软件]()备用 * 新公众号[开源阅读]()已启用,[开源阅读软件]()备用
**2020/06/13**
* 优化搜索
**2020/06/12**
* 修复分组变化的bug
**2020/06/10**
* 正文字体的粗细选择增加可以选择细体(Android O生效) by hingbong
* 修复bug
**2020/06/09**
* 修复从发现界面打开已在书架的书时,显示不对的问题
**2020/06/07**
* 优化书源检测,自定义搜索关键词
* 失效书源如果校验为有效会去掉失效标志
**2020/06/06**
* 修复一些bug,包括从阅读界面退出后还是显示红色更新的bug
**2020/06/03**
* zh-TW translation by david082321
* 修复音频播放时播放速度调节会再下一章失效的bug
**2020/05/31**
* 更新到android studio 4.0
* 书源排序添加按url
* 去除朗读通知的进度条
* 修复恢复问题,暂时去除混淆
**2020/05/24**
* 添加自动翻页速度调节
**2020/05/23**
* 添加文字两端对齐配置
**2020/05/20**
* Rss列表增加一种显示样式
**2020/05/18**
* 修复http://alanskycn.gitee.io/书源导入失败问题,被屏蔽UA了
* Rss列表添加样式切换
**2020/05/17**
* 自动翻页功能完成
* 替换规则输入时弹出辅助输入条
**2020/05/10**
* 添加识别rss分组中的频道信息,在菜单中可以切换频道 from yangyxd
* 源管理添加置底,批量置顶,批量置地
* 封面选择本地图片完成
**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/20**
* 优化阅读界面信息显示 * 优化阅读界面信息显示

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

@ -20,7 +20,7 @@ object AppConst {
const val UA_NAME = "User-Agent" const val UA_NAME = "User-Agent"
val userAgent: String by lazy { val userAgent: String by lazy {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
} }
val SCRIPT_ENGINE: ScriptEngine by lazy { val SCRIPT_ENGINE: ScriptEngine by lazy {
@ -41,8 +41,9 @@ 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"
) )
} }
@ -55,4 +56,15 @@ object AppConst {
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()
}
} }

@ -43,4 +43,6 @@ object PreferKey {
const val readStyleSelect = "readStyleSelect" const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces" const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh" const val readBodyToLh = "readBodyToLh"
const val textFullJustify = "textFullJustify"
const val autoReadSpeed = "autoReadSpeed"
} }

@ -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 = 10, version = 13,
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, migration_12_13)
.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,39 @@ 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
"""
)
}
}
private val migration_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0
"""
)
}
}
} }
abstract fun bookDao(): BookDao abstract fun bookDao(): BookDao

@ -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,7 @@ import io.legado.app.utils.splitNotBlank
interface BaseBook { interface BaseBook {
var bookUrl: String var bookUrl: String
var variableMap: HashMap<String, String>? val variableMap: HashMap<String, String>
var kind: String? var kind: String?
var wordCount: String? var wordCount: String?

@ -71,15 +71,17 @@ data class Book(
return bookUrl.hashCode() return bookUrl.hashCode()
} }
@Ignore @delegate:Transient
@delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null override val variableMap by lazy {
get() { GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
if (field == null) { }
field = GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
} override fun putVariable(key: String, value: String) {
return field variableMap[key] = value
} variable = GSON.toJson(variableMap)
}
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
@ -97,11 +99,6 @@ data class Book(
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
override fun putVariable(key: String, value: String) {
variableMap?.put(key, value)
variable = GSON.toJson(variableMap)
}
fun fileCharset(): Charset { fun fileCharset(): Charset {
return charset(charset ?: "UTF-8") return charset(charset ?: "UTF-8")
} }

@ -1,26 +1,21 @@
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 android.text.TextUtils
import androidx.room.Ignore import androidx.room.*
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
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.rule.* import io.legado.app.data.entities.rule.*
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.utils.ACache import io.legado.app.utils.*
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefString
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import 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 +35,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 +54,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 +74,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) {
@ -148,6 +103,13 @@ data class BookSource(
} }
} }
fun removeGroup(group: String) {
bookSourceGroup?.splitNotBlank(",")?.toHashSet()?.let {
it.remove(group)
bookSourceGroup = TextUtils.join(",", it)
}
}
fun getExploreKinds(): ArrayList<ExploreKind>? { fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>() val exploreKinds = arrayListOf<ExploreKind>()
exploreUrl?.let { exploreUrl?.let {
@ -219,4 +181,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)
}
}
} }

@ -21,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 {

@ -24,6 +24,7 @@ data class RssSource(
var sourceGroup: String? = null, var sourceGroup: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var sortUrl: String? = null, var sortUrl: String? = null,
var articleStyle: Int = 0,
//列表规则 //列表规则
var ruleArticles: String? = null, var ruleArticles: String? = null,
var ruleNextPage: String? = null, var ruleNextPage: String? = null,
@ -34,9 +35,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 {

@ -63,21 +63,19 @@ data class SearchBook(
return other.originOrder - this.originOrder return other.originOrder - this.originOrder
} }
@Ignore @delegate:Transient
@delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null override val variableMap by lazy {
get() { GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
if (field == null) { }
field = GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
}
return field
}
override fun putVariable(key: String, value: String) { override fun putVariable(key: String, value: String) {
variableMap?.put(key, value) variableMap[key] = value
variable = GSON.toJson(variableMap) variable = GSON.toJson(variableMap)
} }
@delegate:Transient
@delegate:Ignore @delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
val origins: LinkedHashSet<String> by lazy { linkedSetOf(origin) } val origins: LinkedHashSet<String> by lazy { linkedSetOf(origin) }

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

@ -3,8 +3,8 @@ package io.legado.app.help
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
internal class AdapterDataObserverHeader( internal class AdapterDataObserverHeader(
var adapterDataObserver: RecyclerView.AdapterDataObserver, private var adapterDataObserver: RecyclerView.AdapterDataObserver,
var headerCount: Int private var headerCount: Int
) : RecyclerView.AdapterDataObserver() { ) : RecyclerView.AdapterDataObserver() {
override fun onChanged() { override fun onChanged() {
adapterDataObserver.onChanged() adapterDataObserver.onChanged()

@ -1,7 +1,7 @@
package io.legado.app.help package io.legado.app.help
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -122,25 +122,19 @@ object AppConfig {
} }
var elevation: Int var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1) @SuppressLint("PrivateResource")
get() = App.INSTANCE.getPrefInt(
"elevation",
App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
)
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 readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true)
val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google" val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google"
} }
val Context.channel: String
get() {
try {
val pm = packageManager
val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return appInfo.metaData.getString("channel") ?: ""
} catch (e: Exception) {
e.printStackTrace();
}
return ""
}

@ -196,7 +196,7 @@ 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) {
synchronized(this) { synchronized(this) {
@ -228,9 +228,6 @@ object BookHelp {
} }
} }
} }
if (!c.substringBefore("\n").contains(title)) {
c = "$title\n$c"
}
try { try {
when (AppConfig.chineseConverterType) { when (AppConfig.chineseConverterType) {
1 -> c = HanLP.convertToSimplifiedChinese(c) 1 -> c = HanLP.convertToSimplifiedChinese(c)
@ -241,7 +238,19 @@ object BookHelp {
App.INSTANCE.toast("简繁转换出错") App.INSTANCE.toast("简繁转换出错")
} }
} }
return c val contents = arrayListOf<String>()
.replace("\\s*\\n+\\s*".toRegex(), "\n${ReadBookConfig.bodyIndent}") 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 contents
} }
} }

@ -7,14 +7,20 @@ import android.net.Uri
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isContentPath
import java.io.File import java.io.File
object ImageLoader { object ImageLoader {
/**
* 自动判断path类型
*/
fun load(context: Context, path: String?): RequestBuilder<Drawable> { fun load(context: Context, path: String?): RequestBuilder<Drawable> {
return when { return when {
path.isNullOrEmpty() -> Glide.with(context).load(path) path.isNullOrEmpty() -> Glide.with(context).load(path)
path.startsWith("http", true) -> Glide.with(context).load(path) path.isAbsUrl() -> Glide.with(context).load(path)
path.isContentPath() -> Glide.with(context).load(Uri.parse(path))
else -> try { else -> try {
Glide.with(context).load(File(path)) Glide.with(context).load(File(path))
} catch (e: Exception) { } catch (e: Exception) {

@ -59,6 +59,7 @@ object ReadBookConfig {
val json = configFile.readText() val json = configFile.readText()
return GSON.fromJsonArray(json) return GSON.fromJsonArray(json)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
} }
} }
return null return null
@ -103,6 +104,11 @@ object ReadBookConfig {
} }
//配置写入读取 //配置写入读取
var autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46)
set(value) {
field = value
App.INSTANCE.putPrefInt(PreferKey.autoReadSpeed, value)
}
var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
set(value) { set(value) {
field = value field = value
@ -127,6 +133,7 @@ object ReadBookConfig {
} }
var isScroll = pageAnim == 3 var isScroll = pageAnim == 3
val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true) val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true)
val textFullJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textFullJustify, true)
var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
set(value) { set(value) {
field = value field = value
@ -141,7 +148,7 @@ object ReadBookConfig {
private val config get() = if (shareLayout) getConfig(5) else durConfig private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean var textBold: Int
get() = config.textBold get() = config.textBold
set(value) { set(value) {
config.textBold = value config.textBold = value
@ -288,11 +295,11 @@ object ReadBookConfig {
private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
private var textColor: String = "#3E3D3B",//白天文字颜色 private var textColor: String = "#3E3D3B",//白天文字颜色
private var textColorNight: String = "#ADADAD",//夜间文字颜色 private var textColorNight: String = "#ADADAD",//夜间文字颜色
var textBold: Boolean = false,//是否粗体字 var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体
var textSize: Int = 20,//文字大小 var textSize: Int = 20,//文字大小
var letterSpacing: Float = 0.5f,//字间距 var letterSpacing: Float = 0.1f,//字间距
var lineSpacingExtra: Int = 12,//行间距 var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距 var paragraphSpacing: Int = 4,//段距
var titleMode: Int = 0,//标题居中 var titleMode: Int = 0,//标题居中
var titleSize: Int = 0, var titleSize: Int = 0,
var titleTopSpacing: Int = 0, var titleTopSpacing: Int = 0,

@ -0,0 +1,54 @@
package io.legado.app.help
import android.os.Handler
import android.os.Looper
import io.legado.app.App
import io.legado.app.data.entities.RssSource
import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.splitNotBlank
import org.jetbrains.anko.toast
object SourceHelp {
private val handler = Handler(Looper.getMainLooper())
private val list18Plus by lazy {
try {
return@lazy String(App.INSTANCE.assets.open("18PlusList.txt").readBytes())
.splitNotBlank("\n")
} catch (e: Exception) {
return@lazy arrayOf<String>()
}
}
fun insertRssSource(vararg rssSources: RssSource) {
rssSources.forEach { rssSource ->
if (is18Plus(rssSource.sourceUrl)) {
handler.post {
App.INSTANCE.toast("${rssSource.sourceName}是18+网址,禁止导入.")
}
} else {
App.db.rssSourceDao().insert(rssSource)
}
}
}
private fun is18Plus(url: String?): Boolean {
url ?: return false
if (AppConfig.isGooglePlay) return false
val baseUrl = NetworkUtils.getBaseUrl(url)
baseUrl ?: return false
try {
val host = baseUrl.split("//", ".")
val base64Url = EncoderUtils.base64Encode("${host[host.lastIndex - 1]}.${host.last()}")
list18Plus.forEach {
if (base64Url == it) {
return true
}
}
} catch (e: Exception) {
}
return false
}
}

@ -39,10 +39,6 @@ object CookieStore {
App.db.cookieDao().delete(url) App.db.cookieDao().delete(url)
} }
fun clearCookies() {
}
private fun cookieToMap(cookie: String): MutableMap<String, String> { private fun cookieToMap(cookie: String): MutableMap<String, String> {
val cookieMap = mutableMapOf<String, String>() val cookieMap = mutableMapOf<String, String>()
if (cookie.isBlank()) { if (cookie.isBlank()) {

@ -1,5 +1,6 @@
package io.legado.app.help.http package io.legado.app.help.http
import io.legado.app.constant.AppConst
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
@ -41,7 +42,7 @@ object HttpHelper {
fun simpleGet(url: String, encode: String? = null): String? { fun simpleGet(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl -> NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode) val response = getApiService<HttpGetApi>(baseUrl, encode)
.get(url, mapOf()) .get(url, mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent)))
.execute() .execute()
return response.body() return response.body()
} }
@ -51,7 +52,7 @@ object HttpHelper {
suspend fun simpleGetAsync(url: String, encode: String? = null): String? { suspend fun simpleGetAsync(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl -> NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode) val response = getApiService<HttpGetApi>(baseUrl, encode)
.getAsync(url, mapOf()) .getAsync(url, mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent)))
return response.body() return response.body()
} }
return null return null
@ -61,7 +62,7 @@ object HttpHelper {
NetworkUtils.getBaseUrl(url)?.let { baseUrl -> NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl) return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java) .create(HttpGetApi::class.java)
.getMapByteAsync(url, mapOf(), mapOf()) .getMapByteAsync(url, mapOf(), mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent)))
.body() .body()
} }
return null return null

@ -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 { try {
if (source == null || source?.ruleToc.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,91 @@ 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))
} }
} }
} catch (e: Exception) {
e.printStackTrace()
} }
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 +176,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 +204,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 +278,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
}
} }

@ -126,6 +126,7 @@ object Restore {
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46)
} }
ChapterProvider.upStyle() ChapterProvider.upStyle()
ReadBook.loadContent(resetPageOffset = false) ReadBook.loadContent(resetPageOffset = false)

@ -51,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 {
@ -157,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) {
@ -179,20 +194,19 @@ class AnalyzeUrl(
} }
} }
RequestMethod.POST -> { RequestMethod.POST -> {
bodyTxt?.let { body?.let {
if (it.isJson()) { if (it.isJson()) {
body = RequestBody.create(jsonType, it) requestBody = RequestBody.create(jsonType, it)
} else { } else {
analyzeFields(it) analyzeFields(it)
} }
} ?: let { } ?: let {
body = FormBody.Builder().build() requestBody = FormBody.Builder().build()
} }
} }
} }
} }
/** /**
* 解析QueryMap * 解析QueryMap
*/ */
@ -253,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
@ -277,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)
} }
@ -294,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
@ -307,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?
)
} }

@ -40,6 +40,7 @@ class AnalyzeTxtFile {
book: Book, book: Book,
pattern: Pattern? pattern: Pattern?
): ArrayList<BookChapter> { ): ArrayList<BookChapter> {
bookStream.seek(0)
val toc = arrayListOf<BookChapter>() val toc = arrayListOf<BookChapter>()
var tocRule: TxtTocRule? = null var tocRule: TxtTocRule? = null
val rulePattern = pattern ?: let { val rulePattern = pattern ?: let {
@ -83,7 +84,8 @@ class AnalyzeTxtFile {
//获取章节内容 //获取章节内容
val chapterContent = blockContent.substring(seekPos, chapterStart) val chapterContent = blockContent.substring(seekPos, chapterStart)
val chapterLength = chapterContent.toByteArray(charset).size val chapterLength = chapterContent.toByteArray(charset).size
if (chapterLength > 30000 && pattern == null) { val lastStart = toc.lastOrNull()?.start ?: 0
if (curOffset + chapterLength - lastStart > 50000 && pattern == null) {
//移除不匹配的规则 //移除不匹配的规则
tocRules.remove(tocRule) tocRules.remove(tocRule)
return analyze(bookStream, book, null) return analyze(bookStream, book, null)
@ -139,6 +141,11 @@ class AnalyzeTxtFile {
//设置指针偏移 //设置指针偏移
seekPos += chapterContent.length seekPos += chapterContent.length
} }
if (seekPos == 0 && length > 50000 && pattern == null) {
//移除不匹配的规则
tocRules.remove(tocRule)
return analyze(bookStream, book, null)
}
} else { //进行本地虚拟分章 } else { //进行本地虚拟分章
//章节在buffer的偏移量 //章节在buffer的偏移量
var chapterOffset = 0 var chapterOffset = 0

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

@ -63,6 +63,7 @@ class AudioPlayService : BaseService(),
private val dsRunnable: Runnable = Runnable { doDs() } private val dsRunnable: Runnable = Runnable { doDs() }
private var mpRunnable: Runnable = Runnable { upPlayProgress() } private var mpRunnable: Runnable = Runnable { upPlayProgress() }
private var bookChapter: BookChapter? = null private var bookChapter: BookChapter? = null
private var playSpeed: Float = 1f
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -95,7 +96,12 @@ class AudioPlayService : BaseService(),
IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
IntentAction.addTimer -> addTimer() IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
IntentAction.adjustProgress -> adjustProgress(intent.getIntExtra("position", position)) IntentAction.adjustProgress -> adjustProgress(
intent.getIntExtra(
"position",
position
)
)
else -> stopSelf() else -> stopSelf()
} }
} }
@ -140,14 +146,18 @@ class AudioPlayService : BaseService(),
if (url.contains(".m3u8", false)) { if (url.contains(".m3u8", false)) {
stopSelf() stopSelf()
} else { } else {
AudioPlayService.pause = pause try {
handler.removeCallbacks(mpRunnable) AudioPlayService.pause = pause
position = mediaPlayer.currentPosition handler.removeCallbacks(mpRunnable)
mediaPlayer.pause() position = mediaPlayer.currentPosition
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) mediaPlayer.pause()
AudioPlay.status = Status.PAUSE upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
postEvent(EventBus.AUDIO_STATE, Status.PAUSE) AudioPlay.status = Status.PAUSE
upNotification() postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
upNotification()
} catch (e: Exception) {
e.printStackTrace()
}
} }
} }
@ -174,12 +184,12 @@ class AudioPlayService : BaseService(),
private fun upSpeed(adjust: Float) { private fun upSpeed(adjust: Float) {
kotlin.runCatching { kotlin.runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
with(mediaPlayer) { playSpeed += adjust
if (isPlaying) { if (mediaPlayer.isPlaying) {
playbackParams = playbackParams.apply { speed += adjust } mediaPlayer.playbackParams =
} mediaPlayer.playbackParams.apply { speed = playSpeed }
postEvent(EventBus.AUDIO_SPEED, playbackParams.speed)
} }
postEvent(EventBus.AUDIO_SPEED, playSpeed)
} }
} }
} }
@ -189,7 +199,11 @@ class AudioPlayService : BaseService(),
*/ */
override fun onPrepared(mp: MediaPlayer?) { override fun onPrepared(mp: MediaPlayer?) {
if (pause) return if (pause) return
mediaPlayer.start() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mediaPlayer.playbackParams = mediaPlayer.playbackParams.apply { speed = playSpeed }
} else {
mediaPlayer.start()
}
mediaPlayer.seekTo(position) mediaPlayer.seekTo(position)
postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration) postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration)
bookChapter?.let { bookChapter?.let {
@ -506,7 +520,6 @@ class AudioPlayService : BaseService(),
) )
builder.setStyle( builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionCompat?.sessionToken)
.setShowActionsInCompactView(0, 1, 2) .setShowActionsInCompactView(0, 1, 2)
) )
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

@ -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,11 +110,9 @@ 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 {
@ -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,6 @@ abstract class BaseReadAloudService : BaseService(),
) )
builder.setStyle( builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionCompat?.sessionToken)
.setShowActionsInCompactView(0, 1, 2) .setShowActionsInCompactView(0, 1, 2)
) )
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

@ -7,20 +7,18 @@ 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.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook import io.legado.app.service.help.CheckSource
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.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 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 tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>() private val allIds = ArrayList<String>()
@ -58,42 +56,37 @@ class CheckSourceService : BaseService() {
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))
for (i in 0 until min(threadCount, allIds.size)) { for (i in 0 until threadCount) {
check() check()
} }
} }
/**
* 检测
*/
private fun check() { private fun check() {
val index = processIndex
synchronized(this) { synchronized(this) {
processIndex++ processIndex++
} }
execute { execute {
if (processIndex < allIds.size) { if (index < allIds.size) {
val sourceUrl = allIds[processIndex] val sourceUrl = allIds[index]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) { if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl) onNext(sourceUrl)
} else { } else {
check(source) CheckSource(source).check(this, searchPool) {
onNext(it)
}
} }
} ?: onNext(sourceUrl) } ?: onNext(sourceUrl)
} }
} }
} }
private fun check(source: BookSource) {
val webBook = WebBook(source)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
}
private fun onNext(sourceUrl: String) { private fun onNext(sourceUrl: String) {
synchronized(this) { synchronized(this) {
check() check()
@ -102,7 +95,7 @@ class CheckSourceService : BaseService() {
checkedIds.size, checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size) getString(R.string.progress_show, checkedIds.size, allIds.size)
) )
if (processIndex >= allIds.size + min(threadCount, allIds.size) - 1) { if (processIndex >= allIds.size + threadCount - 1) {
stopSelf() stopSelf()
} }
} }

@ -30,7 +30,7 @@ class DownloadService : BaseService() {
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 = "正在启动下载"
@ -199,11 +199,11 @@ class DownloadService : BaseService() {
var successCount = 0 //下载成功的条目数量 var successCount = 0 //下载成功的条目数量
fun increaseSuccess() { fun increaseSuccess() {
++successCount; ++successCount
} }
fun increaseFinished() { fun increaseFinished() {
++downloadFinishedCount; ++downloadFinishedCount
} }
} }
} }

@ -76,7 +76,9 @@ class HttpReadAloudService : BaseReadAloudService(),
val file = getSpeakFile(index) val file = getSpeakFile(index)
file.writeBytes(bytes) file.writeBytes(bytes)
if (index == nowSpeak) { if (index == nowSpeak) {
playAudio(FileInputStream(file).fd) @Suppress("BlockingMethodInNonBlockingContext")
val fis = FileInputStream(file)
playAudio(fis.fd)
} }
} }
} else { } else {
@ -204,7 +206,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} else { } else {
nextChapter() nextChapter()
} }
}, 1000) }, 100)
return true return true
} }

@ -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,45 @@ 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)
}
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.setOnUtteranceProgressListener(ttsUtteranceListener)
ttsInitFinish = true it.language = Locale.CHINA
play() ttsInitFinish = true
play()
}
} else { } else {
launch { launch {
toast(R.string.tts_init_failed) toast(R.string.tts_init_failed)
@ -58,35 +67,25 @@ 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)
super.play() super.play()
for (i in nowSpeak until contentList.size) { execute {
if (i == 0) { MediaHelp.playSilentSound(this@TTSReadAloudService)
speak(contentList[i], TextToSpeech.QUEUE_FLUSH, AppConst.APP_TAG + i) textToSpeech?.let {
} else { it.speak("", TextToSpeech.QUEUE_FLUSH, null, null)
speak(contentList[i], TextToSpeech.QUEUE_ADD, AppConst.APP_TAG + i) for (i in nowSpeak until contentList.size) {
it.speak(
contentList[i],
TextToSpeech.QUEUE_ADD,
null,
AppConst.APP_TAG + i
)
}
} }
} }
} }
} }
private fun speak(content: String, queueMode: Int, utteranceId: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech?.speak(content, queueMode, null, utteranceId)
} else {
@Suppress("DEPRECATION")
textToSpeech?.speak(
content,
queueMode,
hashMapOf(Pair(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId))
)
}
}
/** /**
* 更新朗读速度 * 更新朗读速度
*/ */
@ -94,7 +93,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,8 +151,8 @@ 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) {

@ -2,34 +2,62 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.service.CheckSourceService import io.legado.app.service.CheckSourceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
object CheckSource { class CheckSource(val source: BookSource) {
fun start(context: Context, sources: LinkedHashSet<BookSource>) { companion object {
if (sources.isEmpty()) { var keyword = "我的"
context.toast(R.string.non_select)
return fun start(context: Context, sources: List<BookSource>) {
} if (sources.isEmpty()) {
val selectedIds: ArrayList<String> = arrayListOf() context.toast(R.string.non_select)
sources.map { return
selectedIds.add(it.bookSourceUrl) }
val selectedIds: ArrayList<String> = arrayListOf()
sources.map {
selectedIds.add(it.bookSourceUrl)
}
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.start
it.putExtra("selectIds", selectedIds)
context.startService(it)
}
} }
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.start fun stop(context: Context) {
it.putExtra("selectIds", selectedIds) Intent(context, CheckSourceService::class.java).let {
context.startService(it) it.action = IntentAction.stop
context.startService(it)
}
} }
} }
fun stop(context: Context) { fun check(
Intent(context, CheckSourceService::class.java).let { scope: CoroutineScope,
it.action = IntentAction.stop context: CoroutineContext,
context.startService(it) onNext: (sourceUrl: String) -> Unit
} ): Coroutine<*> {
val webBook = WebBook(source)
return webBook.searchBook(keyword, scope = scope, context = context)
.onError(Dispatchers.IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onSuccess(Dispatchers.IO) {
source.removeGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(Dispatchers.IO) {
onNext(source.bookSourceUrl)
}
} }
} }

@ -1,11 +1,13 @@
package io.legado.app.service.help package io.legado.app.service.help
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.hankcs.hanlp.HanLP
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
@ -36,9 +38,8 @@ 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)
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
durPageIndex = book.durChapterPos durPageIndex = book.durChapterPos
isLocalBook = book.origin == BookType.local isLocalBook = book.origin == BookType.local
@ -46,20 +47,19 @@ object ReadBook {
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
upWebBook(book, noSource) titleDate.postValue(book.name)
upWebBook(book)
} }
fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) { fun upWebBook(book: Book) {
book ?: return webBook = if (book.origin == BookType.local) {
if (book.origin == BookType.local) { null
webBook = 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 +107,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
@ -149,7 +149,7 @@ object ReadBook {
} }
private fun curPageChanged() { private fun curPageChanged() {
callBack?.upPageProgress() callBack?.pageChanged()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause) readAloud(!BaseReadAloudService.pause)
} }
@ -176,10 +176,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 +293,12 @@ 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( chapter.title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(chapter.title)
2 -> HanLP.convertToTraditionalChinese(chapter.title)
else -> chapter.title
}
val contents = BookHelp.disposeContent(
chapter.title, chapter.title,
book!!.name, book!!.name,
webBook?.bookSource?.bookSourceUrl, webBook?.bookSource?.bookSourceUrl,
@ -302,18 +307,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)
} }
} }
@ -342,7 +350,7 @@ object ReadBook {
interface CallBack { interface CallBack {
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upView() fun upView()
fun upPageProgress() fun pageChanged()
fun contentLoadFinish() fun contentLoadFinish()
} }
} }

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

@ -47,6 +47,7 @@ class AboutFragment : PreferenceFragmentCompat() {
"update_log" -> showUpdateLog() "update_log" -> showUpdateLog()
"check_update" -> openUrl(R.string.latest_release_url) "check_update" -> openUrl(R.string.latest_release_url)
"mail" -> sendMail() "mail" -> sendMail()
"sourceRuleSummary" -> openUrl(R.string.source_rule_url)
"git" -> openUrl(R.string.this_github_url) "git" -> openUrl(R.string.this_github_url)
"home_page" -> openUrl(R.string.home_page_url) "home_page" -> openUrl(R.string.home_page_url)
"license" -> requireContext().openUrl(licenseUrl) "license" -> requireContext().openUrl(licenseUrl)

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

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

@ -2,6 +2,7 @@ package io.legado.app.ui.book.changesource
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
@ -22,6 +23,7 @@ import java.util.concurrent.Executors
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
val handler = Handler()
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
val searchBooksLiveData = MutableLiveData<List<SearchBook>>() val searchBooksLiveData = MutableLiveData<List<SearchBook>>()
var name: String = "" var name: String = ""
@ -29,6 +31,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
private var screenKey: String = "" private var screenKey: String = ""
private val searchBooks = hashSetOf<SearchBook>() private val searchBooks = hashSetOf<SearchBook>()
private var postTime = 0L
private val sendRunnable = Runnable { upAdapter() }
fun initData(arguments: Bundle?) { fun initData(arguments: Bundle?) {
arguments?.let { bundle -> arguments?.let { bundle ->
@ -55,9 +60,17 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
@Synchronized
private fun upAdapter() { private fun upAdapter() {
val books = searchBooks.toList() if (System.currentTimeMillis() >= postTime + 500) {
searchBooksLiveData.postValue(books.sortedBy { it.originOrder }) handler.removeCallbacks(sendRunnable)
postTime = System.currentTimeMillis()
val books = searchBooks.toList()
searchBooksLiveData.postValue(books.sortedBy { it.originOrder })
} else {
handler.removeCallbacks(sendRunnable)
handler.postDelayed(sendRunnable, 500 - System.currentTimeMillis() + postTime)
}
} }
private fun searchFinish(searchBook: SearchBook) { private fun searchFinish(searchBook: SearchBook) {
@ -165,4 +178,15 @@ 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)
upAdapter()
}
}
} }

@ -24,13 +24,11 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.originName != newItem.originName) { return when {
return false oldItem.originName != newItem.originName -> false
} oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { else -> true
return false
} }
return true
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {

@ -72,7 +72,8 @@ class ExploreShowActivity : VMBaseActivity<ExploreShowViewModel>(R.layout.activi
override fun showBookInfo(book: Book) { override fun showBookInfo(book: Book) {
startActivity<BookInfoActivity>( startActivity<BookInfoActivity>(
Pair("bookUrl", book.bookUrl) Pair("name", book.name),
Pair("author", book.author)
) )
} }
} }

@ -12,6 +12,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.service.help.ReadBook
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) { class BookInfoViewModel(application: Application) : BaseViewModel(application) {
@ -22,13 +23,13 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun initData(intent: Intent) { fun initData(intent: Intent) {
execute { execute {
intent.getStringExtra("bookUrl")?.let { val name = intent.getStringExtra("name") ?: ""
App.db.bookDao().getBook(it)?.let { book -> val author = intent.getStringExtra("author") ?: ""
inBookshelf = true App.db.bookDao().getBook(name, author)?.let { book ->
setBook(book) inBookshelf = true
} ?: App.db.searchBookDao().getSearchBook(it)?.toBook()?.let { book -> setBook(book)
setBook(book) } ?: App.db.searchBookDao().getFirstByNameAuthor(name, author)?.toBook()?.let { book ->
} setBook(book)
} }
} }
} }
@ -178,6 +179,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
book.durChapterTitle = it.durChapterTitle book.durChapterTitle = it.durChapterTitle
} }
App.db.bookDao().insert(book) App.db.bookDao().insert(book)
if (ReadBook.book?.name == book.name && ReadBook.book?.author == book.author) {
ReadBook.book = book
}
} }
}.onSuccess { }.onSuccess {
success?.invoke() success?.invoke()

@ -1,6 +1,7 @@
package io.legado.app.ui.book.info.edit package io.legado.app.ui.book.info.edit
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -16,6 +17,9 @@ import org.jetbrains.anko.sdk27.listeners.onClick
class BookInfoEditActivity : class BookInfoEditActivity :
VMBaseActivity<BookInfoEditViewModel>(R.layout.activity_book_info_edit), VMBaseActivity<BookInfoEditViewModel>(R.layout.activity_book_info_edit),
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val resultSelectCover = 132
override val viewModel: BookInfoEditViewModel override val viewModel: BookInfoEditViewModel
get() = getViewModel(BookInfoEditViewModel::class.java) get() = getViewModel(BookInfoEditViewModel::class.java)
@ -47,6 +51,13 @@ class BookInfoEditActivity :
ChangeCoverDialog.show(supportFragmentManager, it.name, it.author) ChangeCoverDialog.show(supportFragmentManager, it.name, it.author)
} }
} }
tv_select_cover.onClick {
selectImage()
}
tv_refresh_cover.onClick {
viewModel.book?.customCoverUrl = tie_cover_url.text?.toString()
upCover()
}
} }
private fun upView(book: Book) { private fun upView(book: Book) {
@ -77,9 +88,29 @@ class BookInfoEditActivity :
} }
} }
private fun selectImage() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, resultSelectCover)
}
override fun coverChangeTo(coverUrl: String) { override fun coverChangeTo(coverUrl: String) {
viewModel.book?.customCoverUrl = coverUrl viewModel.book?.customCoverUrl = coverUrl
tie_cover_url.setText(coverUrl) tie_cover_url.setText(coverUrl)
upCover() upCover()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
resultSelectCover -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
coverChangeTo(uri.toString())
}
}
}
}
}
} }

@ -63,7 +63,9 @@ object Help {
} }
} }
/**
* 屏幕方向
*/
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
fun setOrientation(activity: Activity) = activity.apply { fun setOrientation(activity: Activity) = activity.apply {
when (AppConfig.requestedDirection) { when (AppConfig.requestedDirection) {

@ -69,6 +69,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
ReadAloudDialog.CallBack, ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack, ChangeSourceDialog.CallBack,
ReadBook.CallBack, ReadBook.CallBack,
AutoReadDialog.CallBack,
TocRegexDialog.CallBack, TocRegexDialog.CallBack,
ReplaceEditDialog.CallBack, ReplaceEditDialog.CallBack,
ColorPickerDialogListener { ColorPickerDialogListener {
@ -85,7 +86,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
private val mHandler = Handler() private val mHandler = Handler()
private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) } private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) }
private val autoPageRunnable: Runnable = Runnable { autoPagePlus() }
override var autoPageProgress = 0
override var isAutoPage = false
private var screenTimeOut: Long = 0 private var screenTimeOut: Long = 0
private var timeBatteryReceiver: TimeBatteryReceiver? = null private var timeBatteryReceiver: TimeBatteryReceiver? = null
override val pageFactory: TextPageFactory get() = page_view.pageFactory override val pageFactory: TextPageFactory get() = page_view.pageFactory
@ -129,6 +132,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
ReadBook.saveRead()
timeBatteryReceiver?.let { timeBatteryReceiver?.let {
unregisterReceiver(it) unregisterReceiver(it)
timeBatteryReceiver = null timeBatteryReceiver = null
@ -238,7 +242,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
onReplaceRuleSave() onReplaceRuleSave()
} }
R.id.menu_book_info -> ReadBook.book?.let { R.id.menu_book_info -> ReadBook.book?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl)) startActivity<BookInfoActivity>(
Pair("name", it.name),
Pair("author", it.author)
)
} }
R.id.menu_toc_regex -> TocRegexDialog.show( R.id.menu_toc_regex -> TocRegexDialog.show(
supportFragmentManager, supportFragmentManager,
@ -280,6 +287,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/ */
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) { when (keyCode) {
getPrefInt(PreferKey.prevKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV)
return true
}
}
getPrefInt(PreferKey.nextKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT)
return true
}
}
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (volumeKeyPage(PageDelegate.Direction.PREV)) { if (volumeKeyPage(PageDelegate.Direction.PREV)) {
return true return true
@ -294,18 +313,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT)
return true return true
} }
getPrefInt(PreferKey.prevKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV)
return true
}
}
getPrefInt(PreferKey.nextKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT)
return true
}
}
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
@ -344,6 +351,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
toast(R.string.read_aloud_pause) toast(R.string.read_aloud_pause)
return true return true
} }
if (isAutoPage) {
autoPageStop()
return true
}
} }
} }
} }
@ -466,6 +477,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
if (getPrefBoolean("volumeKeyPageOnPlay") if (getPrefBoolean("volumeKeyPageOnPlay")
|| BaseReadAloudService.pause || BaseReadAloudService.pause
) { ) {
page_view.pageDelegate?.isCancel = false
page_view.pageDelegate?.keyTurnPage(direction) page_view.pageDelegate?.keyTurnPage(direction)
return true return true
} }
@ -507,18 +519,22 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} else { } else {
tv_chapter_url.gone() tv_chapter_url.gone()
} }
seek_read_page.max = it.pageSize().minus(1) seek_read_page.max = it.pageSize.minus(1)
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
tv_pre.isEnabled = ReadBook.durChapterIndex != 0 tv_pre.isEnabled = ReadBook.durChapterIndex != 0
tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1
} ?: let {
tv_chapter_name.gone()
tv_chapter_url.gone()
} }
} }
} }
/** /**
* 更新进度条 * 页面改变
*/ */
override fun upPageProgress() { override fun pageChanged() {
autoPageProgress = 0
launch { launch {
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
} }
@ -539,10 +555,16 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
override fun clickCenter() { override fun clickCenter() {
if (BaseReadAloudService.isRun) { when {
showReadAloudDialog() BaseReadAloudService.isRun -> {
} else { showReadAloudDialog()
read_menu.runMenuIn() }
isAutoPage -> {
AutoReadDialog().show(supportFragmentManager, "autoRead")
}
else -> {
read_menu.runMenuIn()
}
} }
} }
@ -557,7 +579,33 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 自动翻页 * 自动翻页
*/ */
override fun autoPage() { override fun autoPage() {
if (isAutoPage) {
autoPageStop()
} else {
isAutoPage = true
page_view.upContent()
page_view.upContent(1)
autoPagePlus()
}
read_menu.setAutoPage(isAutoPage)
}
override fun autoPageStop() {
isAutoPage = false
mHandler.removeCallbacks(autoPageRunnable)
page_view.upContent()
}
private fun autoPagePlus() {
mHandler.removeCallbacks(autoPageRunnable)
autoPageProgress++
if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 10) {
autoPageProgress = 0
page_view.fillPage(PageDelegate.Direction.NEXT)
} else {
page_view.invalidate()
}
mHandler.postDelayed(autoPageRunnable, 100)
} }
/** /**

@ -7,6 +7,7 @@ import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
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.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
@ -34,13 +35,19 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} ?: App.db.bookDao().lastReadBook?.let { } ?: App.db.bookDao().lastReadBook?.let {
initBook(it) initBook(it)
} }
}.onFinally {
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
} }
} }
private fun initBook(book: Book) { private fun initBook(book: Book) {
if (ReadBook.book?.bookUrl != book.bookUrl) { if (ReadBook.book?.bookUrl != book.bookUrl) {
ReadBook.resetData(book) { name, author -> ReadBook.resetData(book)
autoChangeSource(name, author) if (!book.isLocalBook() && ReadBook.webBook == null) {
autoChangeSource(book.name, book.author)
return
} }
isInitFinish = true isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
@ -56,14 +63,14 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
ReadBook.loadContent(resetPageOffset = true) ReadBook.loadContent(resetPageOffset = true)
} }
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
} else { } else {
isInitFinish = true isInitFinish = true
ReadBook.book!!.group = book.group
ReadBook.titleDate.postValue(book.name) ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book) { name, author -> ReadBook.upWebBook(book)
autoChangeSource(name, author) if (!book.isLocalBook() && ReadBook.webBook == null) {
autoChangeSource(book.name, book.author)
return
} }
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) { if (ReadBook.chapterSize == 0) {
@ -163,6 +170,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
private fun autoChangeSource(name: String, author: String) { private fun autoChangeSource(name: String, author: String) {
if (!AppConfig.autoChangeSource) return
execute { execute {
App.db.bookSourceDao().allTextEnabled.forEach { source -> App.db.bookSourceDao().allTextEnabled.forEach { source ->
try { try {

@ -140,7 +140,11 @@ class ReadMenu : FrameLayout {
}) })
//自动翻页 //自动翻页
fabAutoPage.onClick { callBack?.autoPage() } fabAutoPage.onClick {
runMenuOut {
callBack?.autoPage()
}
}
//替换 //替换
fabReplaceRule.onClick { callBack?.openReplaceRule() } fabReplaceRule.onClick { callBack?.openReplaceRule() }

@ -0,0 +1,100 @@
package io.legado.app.ui.book.read.config
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help
import kotlinx.android.synthetic.main.dialog_auto_read.*
import org.jetbrains.anko.sdk27.listeners.onClick
class AutoReadDialog : BaseDialogFragment() {
var callBack: CallBack? = null
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes
attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM
it.attributes = attr
it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
callBack = activity as? CallBack
return inflater.inflate(R.layout.dialog_auto_read, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
root_view.setBackgroundColor(requireContext().bottomBackground)
initOnChange()
initData()
initEvent()
}
private fun initData() {
seek_auto_read.progress = ReadBookConfig.autoReadSpeed
}
private fun initOnChange() {
seek_auto_read.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
ReadBookConfig.autoReadSpeed = seek_auto_read.progress
upTtsSpeechRate()
}
})
}
private fun initEvent() {
ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() }
ll_setting.onClick {
ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog")
}
ll_catalog.onClick { callBack?.openChapterList() }
ll_auto_page_stop.onClick {
callBack?.autoPageStop()
dismiss()
}
}
private fun upTtsSpeechRate() {
ReadAloud.upTtsSpeechRate(requireContext())
if (!BaseReadAloudService.pause) {
ReadAloud.pause(requireContext())
ReadAloud.resume(requireContext())
}
}
interface CallBack {
fun showMenuBar()
fun openChapterList()
fun autoPageStop()
}
}

@ -18,6 +18,7 @@ import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.dp
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -38,7 +39,7 @@ class MoreConfigDialog : DialogFragment() {
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM attr.gravity = Gravity.BOTTOM
it.attributes = attr it.attributes = attr
it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 360.dp)
} }
} }
@ -110,6 +111,9 @@ class MoreConfigDialog : DialogFragment() {
Help.setOrientation(it) Help.setOrientation(it)
} }
} }
PreferKey.textFullJustify -> {
postEvent(EventBus.UP_CONFIG, true)
}
} }
} }

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

Loading…
Cancel
Save