Merge pull request #1 from gedoor/master

更新
pull/277/head
10bits 4 years ago committed by GitHub
commit b715b6f4fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      app/build.gradle
  2. 26
      app/proguard-rules.pro
  3. 5
      app/src/debug/res/values-zh-rHK/strings.xml
  4. 5
      app/src/debug/res/values-zh-rTW/strings.xml
  5. 4
      app/src/debug/res/values-zh/strings.xml
  6. 4
      app/src/debug/res/values/strings.xml
  7. 6
      app/src/google/res/values-zh-rCN/strings.xml
  8. 2
      app/src/google/res/values/strings.xml
  9. 9
      app/src/main/AndroidManifest.xml
  10. 20
      app/src/main/assets/readConfig.json
  11. 55
      app/src/main/assets/txtTocRule.json
  12. 140
      app/src/main/assets/updateLog.md
  13. 176
      app/src/main/assets/web/new/bookshelf.css
  14. 49
      app/src/main/assets/web/new/bookshelf.html
  15. 203
      app/src/main/assets/web/new/bookshelf.js
  16. 2
      app/src/main/assets/web/new/css/about.8c965d87.css
  17. 2
      app/src/main/assets/web/new/css/detail.fb767a87.css
  18. BIN
      app/src/main/assets/web/new/favicon.ico
  19. 1
      app/src/main/assets/web/new/js/about.2589b5fe.js
  20. 1
      app/src/main/assets/web/new/js/about.a0534951.js
  21. 0
      app/src/main/assets/web/new/js/about~detail.47586100.js
  22. 1
      app/src/main/assets/web/new/js/app.a7aae935.js
  23. 1
      app/src/main/assets/web/new/js/app.b25f3cec.js
  24. 33
      app/src/main/assets/web/new/js/chunk-vendors.b3838a2d.js
  25. 33
      app/src/main/assets/web/new/js/chunk-vendors.c98251cd.js
  26. 1
      app/src/main/assets/web/new/js/detail.043d6e39.js
  27. 1
      app/src/main/assets/web/new/js/detail.ff471d08.js
  28. 74
      app/src/main/assets/web/new/precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js
  29. 34
      app/src/main/assets/web/new/service-worker.js
  30. 89
      app/src/main/java/io/legado/app/App.kt
  31. 5
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  32. 2
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  33. 15
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  34. 40
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  35. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  36. 6
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  37. 6
      app/src/main/java/io/legado/app/data/dao/CookieDao.kt
  38. 2
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  39. 2
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  40. 61
      app/src/main/java/io/legado/app/data/entities/Book.kt
  41. 25
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  42. 16
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  43. 46
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  44. 36
      app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt
  45. 47
      app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt
  46. 47
      app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt
  47. 37
      app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt
  48. 4
      app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt
  49. 15
      app/src/main/java/io/legado/app/help/AppConfig.kt
  50. 9
      app/src/main/java/io/legado/app/help/BookHelp.kt
  51. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  52. 2
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  53. 46
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  54. 39
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  55. 19
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  56. 7
      app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt
  57. 143
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  58. 42
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  59. 15
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  60. 14
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  61. 5
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  62. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt
  63. 27
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  64. 185
      app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt
  65. 28
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  66. 7
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  67. 42
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  68. 1
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  69. 19
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  70. 215
      app/src/main/java/io/legado/app/service/DownloadService.kt
  71. 6
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  72. 27
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  73. 64
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  74. 12
      app/src/main/java/io/legado/app/service/help/Download.kt
  75. 34
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  76. 22
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  77. 6
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  78. 7
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  79. 1
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  80. 18
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  81. 10
      app/src/main/java/io/legado/app/ui/book/changesource/DiffCallBack.kt
  82. 83
      app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt
  83. 3
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt
  84. 7
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  85. 7
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  86. 8
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  87. 48
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  88. 45
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  89. 4
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt
  90. 9
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  91. 32
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  92. 43
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  93. 11
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  94. 12
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  95. 3
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  96. 5
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  97. 55
      app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt
  98. 6
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt
  99. 87
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  100. 25
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -45,7 +45,8 @@ android {
annotationProcessorOptions {
arguments = [
"room.incremental" : "true",
"room.expandProjection": "true"
"room.expandProjection": "true",
"room.schemaLocation" : "$projectDir/schemas".toString()
]
}
}
@ -56,7 +57,7 @@ android {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.release'
minifyEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
@ -66,7 +67,7 @@ android {
applicationIdSuffix '.debug'
versionNameSuffix 'debug'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
android.applicationVariants.all { variant ->
variant.outputs.all {
@ -87,7 +88,7 @@ android {
}
compileOptions {
// Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -109,6 +110,7 @@ kapt {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.2.0'
@ -117,16 +119,16 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.4.1'
implementation 'com.google.firebase:firebase-core:17.4.4'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//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.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0'
@ -143,7 +145,7 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version"
//paging
implementation 'androidx.paging:paging-runtime:2.1.2'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
//anko
def anko_version = '0.10.8'
@ -154,7 +156,7 @@ dependencies {
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-android:$coroutines_version"
@ -169,6 +171,7 @@ dependencies {
//
//noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
//Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
@ -193,12 +196,14 @@ dependencies {
//
implementation 'com.hankcs:hanlp:portable-1.7.7'
}
//epub
implementation('com.positiondev.epublib:epublib-core:3.1') {
exclude group: 'org.slf4j'
exclude group: 'xmlpull'
}
apply plugin: 'com.google.gms.google-services'
//E-Ink
//implementation 'fadeapp.widgets:scrollless-recyclerView:1.0.2'
}
afterEvaluate {
for (Task task : project.tasks.matching { it.name.startsWith('crashlyticsUploadDeobs') }) {
task.enabled = false
}
}
apply plugin: 'com.google.gms.google-services'

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

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜索</string>
</resources>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜索</string>
</resources>

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

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

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

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

@ -240,6 +240,10 @@
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind"
android:launchMode="singleTop" />
<!--RSS条目-->
<activity
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--RSS阅读-->
<activity
android:name=".ui.rss.read.ReadRssActivity"
@ -258,10 +262,6 @@
<activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" />
<!--订阅条目-->
<activity
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--Rss收藏-->
<activity
android:name=".ui.rss.favorites.RssFavoritesActivity"
@ -301,6 +301,7 @@
<data android:scheme="content"/>
<data android:mimeType="text/plain" />
<data android:mimeType="application/json" />
<data android:mimeType="application/epub" />
</intent-filter>
</activity>

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

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

@ -3,6 +3,124 @@
* 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 新公众号[开源阅读]()已启用,[开源阅读软件]()备用
**2020/07/19**
* 添加自定义默认封面
* 修复封面选择本地图片时书架不显示的bug
**2020/07/14**
* 添加英文语言 by [52fisher](https://github.com/52fisher)
**2020/07/13**
* 在线阅读图片支持测试成功,最好把替换净化关了,防止图片url不对
* 书源保留img标签就行,@html自动保留标签
**2020/07/12**
* epub显示图片,未完善
* 在线阅读也支持图片,还未测试
**2020/07/11**
* epub可以正确识别书名和作者了
* epub封面正确显示
**2020/07/10**
* 修复一些窗口再墨水屏上背景透明的问题
* 添加epub支持
* web阅读时记住进度
* 导入书源时系统文件选择器可以选择json文件
**2020/07/06**
* 优化下载
**2020/07/05**
* 修复夜间模式底栏颜色调整无效的bug
* 【web看书】加了翻页、排序等 by [Celeter](https://github.com/Celeter)
* 两部xx' is recognized as a title by [52fisher](https://github.com/52fisher)
* 添加下载错误日志,从下载菜单浏览
* 修复vip标识引发的bug
**2020/07/04**
* 修复滚动bug
* 其它一些优化
**2020/07/03**
* 修复关闭两端对齐是朗读高亮不准确的bug
* 添加文字底部对齐开关
**2020/06/25**
* E-Ink模式合并到主题模式里, E-Ink模式不能修改阅读界面背景和文字颜色
* 添加判断,防止背景透明引起重影,花屏问题
**2020/06/22**
* 修复xpath获取正文多了许多逗号的bug
* 修复检验有效书源移除失效分组失败的bug
**2020/06/21**
* 双击书架图标返回顶部
**2020/06/20**
* 适配NavigationBar
**2020/06/19**
* 修复eInk bug
* 修复分组下载bug
* 导入本地添加滚动条
**2020/06/18**
* fadeapp.widgets:scrollless-recyclerView导致有些手机重影,暂时去除
* 下载界面添加分组
* 修复eInk bug
**2020/06/17**
* 修复更新书架时更新禁止更新的问题
* 修复导入旧版本数据问题
**2020/06/16**
* 刷新时只刷新当前书架
* 修复恢复备份需要退出重进的问题
* 保存打开 E-Ink 模式前的主题、翻页动画,关闭后恢复之前的配置, 现在可以切着玩了
* 修复因繁体语言导致的崩溃bug
**2020/06/15**
* 添加 E-Ink 模式 by [Modificator](https://github.com/Modificator)
* 修复发现打开书时可能的错误
**2020/06/14**
* 修复txt文件目录识别
* 书源分组添加已启用已禁用
**2020/06/13**
* 优化搜索
**2020/06/12**
* 修复分组变化的bug
**2020/06/10**
* 正文字体的粗细选择增加可以选择细体(Android O生效) by [hingbong](https://github.com/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**
* 添加文字两端对齐配置
@ -18,7 +136,7 @@
* 替换规则输入时弹出辅助输入条
**2020/05/10**
* 添加识别rss分组中的频道信息,在菜单中可以切换频道 from yangyxd
* 添加识别rss分组中的频道信息,在菜单中可以切换频道 from [yangyxd](https://github.com/yangyxd)
* 源管理添加置底,批量置顶,批量置地
* 封面选择本地图片完成
@ -66,7 +184,7 @@
* 添加阅读界面各种信息设置
**2020/04/18**
* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, hingbong
* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, [hingbong](https://github.com/hingbong)
* 修复更新时间不对的bug
**2020/04/13**
@ -114,10 +232,10 @@
**2020/03/24**
* txt文件第一章之前的文字不再放到简介里
* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别
* 修复文件关联 by wqfantexi
* 修复文件关联 by [wqfantexi](https://github.com/wqfantexi)
**2020/03/22**
* 添加文件关联 by wqfantexi
* 添加文件关联 by [wqfantexi](https://github.com/wqfantexi)
* 手动排序可以了,在书架整理里面拖动排序
* 删除分组时同时删除书籍里的分组信息,下次添加新分组时不会自动出现在分组内
* 修复换源丢失分组信息的bug
@ -132,14 +250,14 @@
* 修复rss请求头无效bug
**2020/03/19**
* 美化界面我的 by yangyxd
* 美化界面我的 by [yangyxd](https://github.com/yangyxd)
* 优化搜索
**2020/03/18**
* 尝试修复搜索时崩溃
* 解决看过书籍的移到顶部需要向上滚动才能看到的bug
* 只有再书源被删除找不到书源时才会自动换源
* 美化界面 by yangyxd
* 美化界面 by [yangyxd](https://github.com/yangyxd)
* 订阅后台播放
**2020/03/16**
@ -164,8 +282,8 @@
**2020/03/11**
* 修复调节上边距时下边距一起动的bug
* 适配沚水的web阅读 by 六月
* 分组管理页面调整 by yangyxd
* 适配沚水的web阅读 by [六月](https://github.com/Celeter)
* 分组管理页面调整 by [yangyxd](https://github.com/yangyxd)
**2020/03/10**
* 优化文字选择菜单弹出位置
@ -174,13 +292,13 @@
**2020/03/09**
* 底部文字对齐
* 主题添加阴影调节 by yangyxd
* 主题添加阴影调节 by [yangyxd](https://github.com/yangyxd)
**2020/03/08**
* 订阅长按保存图片
* 订阅全屏播放
* 书架全部分组可以隐藏了
* 内置web书架基本能用了 by 六月
* 内置web书架基本能用了 by [六月](https://github.com/Celeter)
* 书架整理加入未分组
* 显示总进度
* 隐藏状态栏时,标题显示在上方
@ -252,7 +370,7 @@
**2020/02/26**
* 修复仿真翻页
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by hingbong
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by [hingbong](https://github.com/hingbong)
**2020/02/25**
* 优化文本选择和滚动,感觉很完美了

@ -1,176 +0,0 @@
html, body {
height: 100%;
margin: 0;
}
.hide {
display: none;
}
.top, .showchapter, .hidebooks {
width: 60px;
height: 50px;
position: absolute;
right: 30px;
bottom: 30px;
color: black;
font-size: 28px;
background-color: #ddd;
opacity: 0.85;
}
.top {
bottom: 150px;
}
.showchapter {
bottom: 90px;
bottom: 90px;
}
.address {
width: 270px;
}
.nav {
border-bottom: solid 1px #ccc;
}
input, button {
width: 110px;
line-height: 34px;
background-color: #eee;
color: #555;
border: none;
margin: 10px 5px;
font-weight: 500;
border-radius: 2px;
outline: none;
cursor: pointer;
}
input {
padding: 0 10px;
cursor: text;
}
input:hover, button:hover {
border-color: #aaa;
background-color: #efefef;
color: #222;
outline: solid 1px #ccc;
}
.allcontent {
height: calc(100% - 60px);
}
.allscreen {
height: 100%
}
.books > div {
display: inline-block;
margin: 10px;
vertical-align: top;
border: solid 1px #ddd;
}
.read > .books {
width: 420px;
float: left;
height: 100%;
overflow: auto;
border-right: solid 1px #ccc;
}
.read > .books > div {
margin-right: 0;
border-right: none;
}
.more {
overflow-y: auto;
height: 100%;
display: none;
}
.read .more {
display: block;
}
.books > div > img {
width: 120px;
height: 180px;
float: left;
margin-right: 10px;
cursor: pointer;
}
.info {
padding: 10px 20px 0 20px;
width: 600px;
margin: 0 auto;
}
.info > img {
width: 600px;
height: 900px;
}
.info p {
line-height: 1.5;
text-align: justify;
margin: 0;
}
.books tr:nth-child(n+2) td {
border-top: solid 1px #999;
}
.books td:nth-child(1) {
vertical-align: top;
width: 50px;
}
.books td:nth-child(2) {
vertical-align: top;
width: 200px;
}
.clear {
clear: both;
}
.chapter {
margin: 10px;
max-height: 500px;
overflow-y: auto;
border-top: solid 1px #333;
border-bottom: solid 1px #333;
}
.chapter button {
width: 230px;
text-align: left;
text-indent: 14px;
margin: 10px 4px;
}
.content {
padding: 20px;
text-align: justify;
min-height: 1000px;
padding-bottom: 200px;
}
.content h2 {
font-family: "Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu;
font-weight: 500;
text-align: center;
line-height: 100px;
font-size: 40px;
margin: 0;
}

@ -1,46 +1,3 @@
<!DOCTYPE html>
<html lang=en style="padding: 0;height:100%">
<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<!--[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>
<!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.8c965d87.css rel=prefetch><link href=css/detail.fb767a87.css rel=prefetch><link href=js/about.a0534951.js rel=prefetch><link href=js/about~detail.47586100.js rel=prefetch><link href=js/detail.ff471d08.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.a7aae935.js rel=preload as=script><link href=js/chunk-vendors.c98251cd.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.c98251cd.js></script><script src=js/app.a7aae935.js></script></body></html>

@ -1,203 +0,0 @@
var $ = document.querySelector.bind(document)
, $$ = document.querySelectorAll.bind(document)
, $c = document.createElement.bind(document)
, randomImg = "http://api.mtyqx.cn/api/random.php"
, randomImg2 = "http://img.xjh.me/random_img.php"
, books
;
var now_chapter = -1;
var sum_chapter = 0;
var formatTime = value => {
return new Date(value).toLocaleString('zh-CN', {
hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
}).replace(/\//g, "-");
};
var apiMap = {
"getBookshelf": "/getBookshelf",
"getChapterList": "/getChapterList",
"getBookContent": "/getBookContent",
"saveBook": "/saveBook"
};
var apiAddress = (apiName, url, index) => {
let address = $('#address').value || window.location.host;
if (!(/^http|^\/\//).test(address)) {
address = "//" + address;
}
if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) {
address += ":1122";
}
localStorage.setItem('address', address);
if (apiName == "getBookContent") {
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "") + "&index=" + index;
}
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
};
var init = () => {
$('#allcontent').classList.remove("read");
$('#books').innerHTML = "";
fetch(apiAddress("getBookshelf"), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(getBookshelf.errorMsg);
return;
}
books = data.data;
books.forEach((book, i) => {
let bookDiv = $c("div");
let img = $c("img");
img.src = book.coverUrl || randomImg;
img.setAttribute("data-series-num", i);
bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.name}</td></tr>
<tr><td>作者</td><td>${book.author}</td></tr>
<tr><td>阅读</td><td>${book.durChapterTitle}<br>${formatTime(book.durChapterTime)}</td></tr>
<tr><td>更新</td><td>${book.latestChapterTitle}<br>${formatTime(book.latestChapterTime)}</td></tr>
<tr><td>来源</td><td>${book.origin}</td></tr>
</tbody></table>`;
$('#books').appendChild(bookDiv);
});
$$('#books img').forEach(bookImg =>
bookImg.addEventListener("click", () => {
now_chapter = -1;
sum_chapter = 0;
$('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}">
<p>  来源${book.origin}</p>
<p>  书名${book.name}</p>
<p>  作者${book.author}</p>
<p>阅读章节${book.durChapterName}</p>
<p>阅读时间${formatTime(book.durChapterTime)}</p>
<p>最新章节${book.latestChapterTitle}</p>
<p>检查时间${formatTime(book.lastCheckTime)}</p>
<p>  简介${book.intro.trim().replace(/\n/g, "<br>")}</p>`;
window.location.hash = "";
window.location.hash = "#info";
$("#content").innerHTML = "章节列表加载中...";
$("#chapter").innerHTML = "";
fetch(apiAddress("getChapterList", book.bookUrl), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "章节列表加载失败!";
return;
}
data.data.forEach(chapter => {
let ch = $c("button");
ch.setAttribute("data-url", chapter.bookUrl);
ch.setAttribute("data-index", chapter.index);
ch.setAttribute("title", chapter.title);
ch.innerHTML = chapter.title.length > 15 ? chapter.title.substring(0, 14) + "..." : chapter.title;
$("#chapter").appendChild(ch);
});
sum_chapter = data.data.length;
$('#chapter').scrollTop = 0;
$("#content").innerHTML = "章节列表加载完成!";
});
}));
});
};
$("#back").addEventListener("click", () => {
if (window.location.hash === "#content") {
window.location.hash = "#chapter";
} else if (window.location.hash === "#chapter") {
window.location.hash = "#info";
} else {
$('#allcontent').classList.remove("read");
}
});
$("#refresh").addEventListener("click", init);
$('#hidebooks').addEventListener("click", () => {
$("#books").classList.toggle("hide");
$(".nav").classList.toggle("hide");
$("#allcontent").classList.toggle("allscreen");
});
$('#top').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#info";
});
$('#showchapter').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#chapter";
});
$('#up').addEventListener('click', e => {
if (now_chapter > 0) {
now_chapter--;
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent);
} else if (now_chapter == 0) {
alert("已经是第一章了^_^!")
} else {
}
});
$('#down').addEventListener('click', e => {
if (now_chapter > -1) {
if (now_chapter < sum_chapter - 1) {
now_chapter++;
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('[data-index="' + now_chapter + '"]').dispatchEvent(clickEvent);
} else {
alert("已经是最后一章了^_^!")
}
}
});
$('#chapter').addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
var url = e.target.getAttribute("data-url");
var index = e.target.getAttribute("data-index");
var name = e.target.getAttribute("title");
if (!url) {
alert("未取得书籍地址");
}
if (!index && (0 != index)) {
alert("未取得章节索引");
}
now_chapter = parseInt(index);
$("#content").innerHTML = "<p>" + name + " 加载中...</p>";
fetch(apiAddress("getBookContent", url, index), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "<p>" + name + " 加载失败!</p>";
return;
}
var content = data.data.trim().split("\n\n");
if (content.length === 2) {
$("#content").innerHTML = `<h2>${content[0]}</h2>  (全文 ${content[1].length} 字)<br><br>  ` + content[1].trim().replace(/\n/g, "<br><br>");
} else {
$("#content").innerHTML = `<h2>${name || e.target.innerHTML}</h2>  (全文 ${data.data.length} 字)<br><br>  ` + data.data.trim().replace(/\n/g, "<br><br>");
}
window.location.hash = "";
window.location.hash = "#content";
});
}
});
$('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host);
if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {
$('#address').value = localStorage.getItem('address');
}
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,74 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "80a8de284bb3fa9a4a9f",
"url": "css/about.8c965d87.css"
},
{
"revision": "9da3e990110565bfa57c",
"url": "css/app.e1c0d2e4.css"
},
{
"revision": "c900d6091039998c94b9",
"url": "css/chunk-vendors.ad4ff18f.css"
},
{
"revision": "607ffe83acdcd9c9180e",
"url": "css/detail.fb767a87.css"
},
{
"revision": "535877f50039c0cb49a6196a5b7517cd",
"url": "fonts/element-icons.535877f5.woff"
},
{
"revision": "732389ded34cb9c52dd88271f1345af9",
"url": "fonts/element-icons.732389de.ttf"
},
{
"revision": "f9a3fb0e145017e166dd4d91d9280cc4",
"url": "fonts/iconfont.f9a3fb0e.woff"
},
{
"revision": "f39ecc1a1d2a1eff3aca8aadd818bb61",
"url": "fonts/popfont.f39ecc1a.ttf"
},
{
"revision": "6c094b6d4ae9404dbed273c41b06fae8",
"url": "fonts/shelffont.6c094b6d.ttf"
},
{
"revision": "b5c48bc1e1fe73212a31be704875b71f",
"url": "img/noCover.b5c48bc1.jpeg"
},
{
"revision": "1006935c8b91408961a7012a08445ffd",
"url": "index.html"
},
{
"revision": "80a8de284bb3fa9a4a9f",
"url": "js/about.a0534951.js"
},
{
"revision": "8e5e793e10c338503af6",
"url": "js/about~detail.47586100.js"
},
{
"revision": "9da3e990110565bfa57c",
"url": "js/app.a7aae935.js"
},
{
"revision": "c900d6091039998c94b9",
"url": "js/chunk-vendors.c98251cd.js"
},
{
"revision": "607ffe83acdcd9c9180e",
"url": "js/detail.ff471d08.js"
},
{
"revision": "b46d04eb43bc31ca0f9f95121646440d",
"url": "manifest.json"
},
{
"revision": "735ab4f94fbcd57074377afca324c813",
"url": "robots.txt"
}
]);

@ -0,0 +1,34 @@
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js"
);
workbox.core.setCacheNameDetails({prefix: "yd-web-tool"});
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

@ -5,6 +5,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
@ -12,14 +13,19 @@ import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.AppDatabase
import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ColorUtils
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefInt
@Suppress("DEPRECATION")
class App : Application() {
@ -68,28 +74,66 @@ class App : Application() {
* 更新主题
*/
fun applyTheme() {
if (AppConfig.isNightTheme) {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground(
getPrefInt("colorBottomBackgroundNight", getCompatColor(R.color.md_grey_850))
).apply()
} else {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimary", getCompatColor(R.color.md_indigo_800))
).accentColor(
getPrefInt("colorAccent", getCompatColor(R.color.md_red_600))
).backgroundColor(
getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
).bottomBackground(
getPrefInt("colorBottomBackground", getCompatColor(R.color.md_grey_200))
).apply()
when {
AppConfig.isEInkMode -> {
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(Color.WHITE)
.accentColor(Color.BLACK)
.backgroundColor(Color.WHITE)
.bottomBackground(Color.WHITE)
.apply()
}
AppConfig.isNightTheme -> {
val primary =
getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600))
val accent =
getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_deep_orange_800))
var background =
getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900))
if (ColorUtils.isColorLight(background)) {
background = getCompatColor(R.color.md_grey_900)
putPrefInt(PreferKey.cNBackground, background)
}
var bBackground =
getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_850))
if (ColorUtils.isColorLight(bBackground)) {
bBackground = getCompatColor(R.color.md_grey_850)
putPrefInt(PreferKey.cNBBackground, bBackground)
}
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(ColorUtils.withAlpha(primary, 1f))
.accentColor(ColorUtils.withAlpha(accent, 1f))
.backgroundColor(ColorUtils.withAlpha(background, 1f))
.bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
.apply()
}
else -> {
val primary =
getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_indigo_800))
val accent =
getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_red_600))
var background =
getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100))
if (!ColorUtils.isColorLight(background)) {
background = getCompatColor(R.color.md_grey_100)
putPrefInt(PreferKey.cBackground, background)
}
var bBackground =
getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200))
if (!ColorUtils.isColorLight(bBackground)) {
bBackground = getCompatColor(R.color.md_grey_200)
putPrefInt(PreferKey.cBBackground, bBackground)
}
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(ColorUtils.withAlpha(primary, 1f))
.accentColor(ColorUtils.withAlpha(accent, 1f))
.backgroundColor(ColorUtils.withAlpha(background, 1f))
.bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
.apply()
}
}
}
@ -97,6 +141,7 @@ class App : Application() {
ReadBookConfig.upBg()
applyTheme()
initNightMode()
postEvent(EventBus.RECREATE, "")
}
private fun initNightMode() {

@ -107,6 +107,11 @@ abstract class BaseActivity(
} else if (theme == Theme.Light) {
ATH.setLightStatusBar(this, true)
}
upNavigationBarColor()
}
open fun upNavigationBarColor() {
ATH.setNavigationBarColorAuto(this)
}
open fun observeLiveBus() {

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.ThemeStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -21,6 +22,7 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(ThemeStore.backgroundColor())
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}

@ -44,5 +44,20 @@ object PreferKey {
const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh"
const val textFullJustify = "textFullJustify"
const val textBottomJustify = "textBottomJustify"
const val autoReadSpeed = "autoReadSpeed"
const val barElevation = "barElevation"
const val transparentStatusBar = "transparentStatusBar"
const val defaultCover = "defaultCover"
const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent"
const val cBackground = "colorBackground"
const val cBBackground = "colorBottomBackground"
const val cNPrimary = "colorPrimaryNight"
const val cNAccent = "colorAccentNight"
const val cNBackground = "colorBackgroundNight"
const val cNBBackground = "colorBottomBackgroundNight"
}

@ -19,7 +19,7 @@ import kotlinx.coroutines.launch
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 13,
version = 14,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -31,12 +31,13 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addMigrations(migration_10_11, migration_11_12, migration_12_13)
.addMigrations(migration_10_11, migration_11_12, migration_12_13, migration_13_14)
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
}
})
.allowMainThreadQueries()
.build()
}
@ -72,6 +73,41 @@ abstract class AppDatabase : RoomDatabase() {
)
}
}
private val migration_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL,
`name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT,
`customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL,
`lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT,
`durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL,
`order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))
"""
)
database.execSQL(
"""
CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books_new` (`name`, `author`)
"""
)
database.execSQL(
"""
INSERT INTO books_new select * from books
"""
)
database.execSQL(
"""
DROP TABLE books
"""
)
database.execSQL(
"""
ALTER TABLE books_new RENAME TO books
"""
)
}
}
}
abstract fun bookDao(): BookDao

@ -21,9 +21,6 @@ interface BookDao {
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'")
fun observeLocalUri(): LiveData<List<String>>
@Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0")
fun observeDownload(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun observeByGroup(group: Int): LiveData<List<Book>>

@ -14,6 +14,12 @@ interface BookSourceDao {
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
fun liveDataEnabled(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 0 order by customOrder asc")
fun liveDataDisabled(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun liveExplore(): LiveData<List<BookSource>>

@ -9,6 +9,9 @@ interface CookieDao {
@Query("SELECT * FROM cookies Where url = :url")
fun get(url: String): Cookie?
@Query("select * from cookies where url like '%|%'")
fun getOkHttpCookies(): List<Cookie>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg cookie: Cookie)
@ -17,4 +20,7 @@ interface CookieDao {
@Query("delete from cookies where url = :url")
fun delete(url: String)
@Query("delete from cookies where url like '%|%'")
fun deleteOkHttp()
}

@ -19,7 +19,7 @@ interface SearchBookDao {
@Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder limit 1")
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources) order by originOrder limit 1")
fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query(

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

@ -4,8 +4,11 @@ import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.App
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
@ -16,11 +19,11 @@ import kotlin.math.max
@Parcelize
@Entity(
tableName = "books",
primaryKeys = ["name", "author"],
indices = [(Index(value = ["bookUrl"], unique = true))]
indices = [Index(value = ["name", "author"], unique = true)]
)
data class Book(
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 or 本地书籍文件名
@ -56,10 +59,18 @@ data class Book(
return origin == BookType.local
}
fun isTxt(): Boolean {
fun isLocalTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true)
}
fun isEpub(): Boolean {
return originName.endsWith(".epub", true)
}
fun isOnLineTxt(): Boolean {
return !isLocalBook() && type == 0
}
override fun equals(other: Any?): Boolean {
if (other is Book) {
return other.bookUrl == bookUrl
@ -71,15 +82,17 @@ data class Book(
return bookUrl.hashCode()
}
@Ignore
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null
get() {
if (field == null) {
field = GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
}
return field
}
override val variableMap by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
}
override fun putVariable(key: String, value: String) {
variableMap[key] = value
variable = GSON.toJson(variableMap)
}
@Ignore
@IgnoredOnParcel
@ -97,11 +110,6 @@ data class Book(
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 {
return charset(charset ?: "UTF-8")
}
@ -127,4 +135,23 @@ data class Book(
this.tocHtml = this@Book.tocHtml
}
}
fun changeTo(newBook: Book) {
newBook.group = group
newBook.order = order
newBook.customCoverUrl = customCoverUrl
newBook.customIntro = customIntro
newBook.customTag = customTag
newBook.canUpdate = canUpdate
newBook.useReplaceRule = useReplaceRule
delete()
App.db.bookDao().insert(newBook)
}
fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null
}
App.db.bookDao().delete(this)
}
}

@ -1,6 +1,7 @@
package io.legado.app.data.entities
import android.os.Parcelable
import android.text.TextUtils
import androidx.room.*
import io.legado.app.App
import io.legado.app.constant.AppConst
@ -8,10 +9,7 @@ import io.legado.app.constant.AppConst.userAgent
import io.legado.app.constant.BookType
import io.legado.app.data.entities.rule.*
import io.legado.app.help.JsExtensions
import io.legado.app.utils.ACache
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefString
import io.legado.app.utils.*
import kotlinx.android.parcel.Parcelize
import java.util.*
import javax.script.SimpleBindings
@ -98,13 +96,20 @@ data class BookSource(
fun addGroup(group: String) {
bookSourceGroup?.let {
if (!it.contains(group)) {
bookSourceGroup = "$it;$group"
bookSourceGroup = "$it,$group"
}
} ?: let {
bookSourceGroup = group
}
}
fun removeGroup(group: String) {
bookSourceGroup?.splitNotBlank("[,;]".toRegex())?.toHashSet()?.let {
it.remove(group)
bookSourceGroup = TextUtils.join(",", it)
}
}
fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>()
exploreUrl?.let {
@ -179,7 +184,7 @@ data class BookSource(
class Converters {
@TypeConverter
fun exploreRuleToString(exploreRule: ExploreRule?): String {
fun exploreRuleToString(exploreRule: ExploreRule?): String? {
return GSON.toJson(exploreRule)
}
@ -189,7 +194,7 @@ data class BookSource(
}
@TypeConverter
fun searchRuleToString(searchRule: SearchRule): String {
fun searchRuleToString(searchRule: SearchRule?): String? {
return GSON.toJson(searchRule)
}
@ -199,7 +204,7 @@ data class BookSource(
}
@TypeConverter
fun bookInfoRuleToString(bookInfoRule: BookInfoRule): String {
fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String? {
return GSON.toJson(bookInfoRule)
}
@ -209,7 +214,7 @@ data class BookSource(
}
@TypeConverter
fun tocRuleToString(tocRule: TocRule): String {
fun tocRuleToString(tocRule: TocRule?): String? {
return GSON.toJson(tocRule)
}
@ -219,7 +224,7 @@ data class BookSource(
}
@TypeConverter
fun contentRuleToString(contentRule: ContentRule): String {
fun contentRuleToString(contentRule: ContentRule?): String? {
return GSON.toJson(contentRule)
}

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

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

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

@ -1,8 +1,9 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class ExploreRule(
override var bookList: String? = null,
override var name: String? = null,
@ -14,46 +15,4 @@ data class ExploreRule(
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : 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)
}
}
}
) : BookListRule, Parcelable

@ -1,8 +1,9 @@
package io.legado.app.data.entities.rule
import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class SearchRule(
override var bookList: String? = null,
override var name: String? = null,
@ -14,46 +15,4 @@ data class SearchRule(
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : 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)
}
}
}
) : BookListRule, Parcelable

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

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

@ -13,6 +13,7 @@ object AppConfig {
return when (context.getPrefString(PreferKey.themeMode, "0")) {
"1" -> false
"2" -> true
"3" -> false
else -> context.sysIsDarkMode()
}
}
@ -27,10 +28,13 @@ object AppConfig {
}
}
val isEInkMode: Boolean
get() = App.INSTANCE.getPrefString(PreferKey.themeMode) == "3"
var isTransparentStatusBar: Boolean
get() = App.INSTANCE.getPrefBoolean("transparentStatusBar")
get() = App.INSTANCE.getPrefBoolean(PreferKey.transparentStatusBar)
set(value) {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
App.INSTANCE.putPrefBoolean(PreferKey.transparentStatusBar, value)
}
val requestedDirection: String?
@ -80,9 +84,6 @@ object AppConfig {
val ttsSpeechPer: String
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0"
val isEInkMode: Boolean
get() = App.INSTANCE.getPrefBoolean("isEInkMode")
val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false)
var chineseConverterType: Int
@ -124,11 +125,11 @@ object AppConfig {
var elevation: Int
@SuppressLint("PrivateResource")
get() = App.INSTANCE.getPrefInt(
"elevation",
PreferKey.barElevation,
App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
)
set(value) {
App.INSTANCE.putPrefInt("elevation", value)
App.INSTANCE.putPrefInt(PreferKey.barElevation, value)
}
val autoChangeSource: Boolean get() = App.INSTANCE.getPrefBoolean("autoChangeSource", true)

@ -6,9 +6,10 @@ import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFilesDir
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@ -20,9 +21,7 @@ import kotlin.math.min
object BookHelp {
private const val cacheFolderName = "book_cache"
private val downloadDir: File =
App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.cacheDir
private val downloadDir: File = App.INSTANCE.externalFilesDir
private fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
@ -81,7 +80,7 @@ object BookHelp {
fun getContent(book: Book, bookChapter: BookChapter): String? {
if (book.isLocalBook()) {
return AnalyzeTxtFile.getContent(book, bookChapter)
return LocalBook.getContext(book, bookChapter)
} else {
val file = FileUtils.getFile(
downloadDir,

@ -41,11 +41,11 @@ interface JsExtensions {
return EncoderUtils.base64Encode(str, flags)
}
fun md5Encode(str: String): String? {
fun md5Encode(str: String): String {
return MD5Utils.md5Encode(str)
}
fun md5Encode16(str: String): String? {
fun md5Encode16(str: String): String {
return MD5Utils.md5Encode16(str)
}

@ -26,7 +26,7 @@ object LauncherIconHelp {
fun changeIcon(icon: String?) {
if (icon.isNullOrEmpty()) return
if (Build.VERSION.SDK_INT < 26) {
App.INSTANCE.toast(R.string.chage_icon_error)
App.INSTANCE.toast(R.string.change_icon_error)
return
}
var hasEnabled = false

@ -9,7 +9,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.*
import java.io.File
@ -27,8 +27,16 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!!
}
val durConfig get() = getConfig(styleSelect)
var bg: Drawable? = null
var bgMeanColor: Int = 0
val textColor: Int
get() =
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
Color.BLACK
} else {
durConfig.textColor()
}
init {
upConfig()
@ -59,6 +67,7 @@ object ReadBookConfig {
val json = configFile.readText()
return GSON.fromJsonArray(json)
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
@ -69,13 +78,19 @@ object ReadBookConfig {
val dm = resources.displayMetrics
val width = dm.widthPixels
val height = dm.heightPixels
bg = durConfig.bgDrawable(width, height).apply {
if (this is BitmapDrawable) {
bgMeanColor = BitmapUtils.getMeanColor(bitmap)
} else if (this is ColorDrawable) {
bgMeanColor = color
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
bg = ColorDrawable(Color.WHITE)
bgMeanColor = Color.WHITE
} else {
bg = durConfig.bgDrawable(width, height).apply {
if (this is BitmapDrawable) {
bgMeanColor = BitmapUtils.getMeanColor(bitmap)
} else if (this is ColorDrawable) {
bgMeanColor = color
}
}
}
isScroll = pageAnim == 3
}
fun save() {
@ -122,17 +137,16 @@ object ReadBookConfig {
App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value)
}
}
var pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim)
var pageAnim: Int
get() = if (AppConfig.isEInkMode) -1 else App.INSTANCE.getPrefInt(PreferKey.pageAnim)
set(value) {
field = value
isScroll = value == 3
if (App.INSTANCE.getPrefInt(PreferKey.pageAnim) != value) {
App.INSTANCE.putPrefInt(PreferKey.pageAnim, value)
}
App.INSTANCE.putPrefInt(PreferKey.pageAnim, value)
isScroll = pageAnim == 3
}
var isScroll = pageAnim == 3
val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true)
val textFullJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textFullJustify, true)
val textBottomJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textBottomJustify, true)
var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
set(value) {
field = value
@ -147,7 +161,7 @@ object ReadBookConfig {
private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean
var textBold: Int
get() = config.textBold
set(value) {
config.textBold = value
@ -294,11 +308,11 @@ object ReadBookConfig {
private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
private var textColor: String = "#3E3D3B",//白天文字颜色
private var textColorNight: String = "#ADADAD",//夜间文字颜色
var textBold: Boolean = false,//是否粗体字
var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体
var textSize: Int = 20,//文字大小
var letterSpacing: Float = 0.5f,//字间距
var letterSpacing: Float = 0.1f,//字间距
var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距
var paragraphSpacing: Int = 4,//段距
var titleMode: Int = 0,//标题居中
var titleSize: Int = 0,
var titleTopSpacing: Int = 0,

@ -1,12 +1,14 @@
package io.legado.app.help.http
import android.text.TextUtils
import com.franmontiel.persistentcookiejar.persistence.CookiePersistor
import com.franmontiel.persistentcookiejar.persistence.SerializableCookie
import io.legado.app.App
import io.legado.app.data.entities.Cookie
import io.legado.app.help.coroutine.Coroutine
object CookieStore {
object CookieStore : CookiePersistor {
fun setCookie(url: String, cookie: String?) {
Coroutine.async {
@ -39,10 +41,6 @@ object CookieStore {
App.db.cookieDao().delete(url)
}
fun clearCookies() {
}
private fun cookieToMap(cookie: String): MutableMap<String, String> {
val cookieMap = mutableMapOf<String, String>()
if (cookie.isBlank()) {
@ -80,5 +78,36 @@ object CookieStore {
return builder.deleteCharAt(builder.lastIndexOf(";")).toString()
}
override fun loadAll(): MutableList<okhttp3.Cookie> {
val cookies = arrayListOf<okhttp3.Cookie>()
App.db.cookieDao().getOkHttpCookies().forEach {
val serializedCookie = it.cookie
SerializableCookie().decode(serializedCookie)?.let { ck ->
cookies.add(ck)
}
}
return cookies
}
override fun saveAll(cookies: MutableCollection<okhttp3.Cookie>?) {
val mCookies = arrayListOf<Cookie>()
cookies?.forEach {
mCookies.add(Cookie(createCookieKey(it), SerializableCookie().encode(it)))
}
App.db.cookieDao().insert(*mCookies.toTypedArray())
}
override fun removeAll(cookies: MutableCollection<okhttp3.Cookie>?) {
cookies?.forEach {
App.db.cookieDao().delete(createCookieKey(it))
}
}
override fun clear() {
App.db.cookieDao().deleteOkHttp()
}
private fun createCookieKey(cookie: okhttp3.Cookie): String {
return (if (cookie.secure()) "https" else "http") + "://" + cookie.domain() + cookie.path() + "|" + cookie.name()
}
}

@ -1,5 +1,7 @@
package io.legado.app.help.http
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import io.legado.app.constant.AppConst
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils
@ -17,6 +19,8 @@ object HttpHelper {
val client: OkHttpClient by lazy {
val cookieJar = PersistentCookieJar(SetCookieCache(), CookieStore)
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
@ -35,6 +39,7 @@ object HttpHelper {
.followSslRedirects(true)
.protocols(listOf(Protocol.HTTP_1_1))
.addInterceptor(getHeaderInterceptor())
.cookieJar(cookieJar)
builder.build()
}
@ -49,6 +54,18 @@ object HttpHelper {
return null
}
fun getBytes(url: String, refer: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val headers = mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent), Pair("refer", refer))
return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java)
.getMapByte(url, mapOf(), headers)
.execute()
.body()
}
return null
}
suspend fun simpleGetAsync(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode)
@ -58,7 +75,7 @@ object HttpHelper {
return null
}
suspend fun simpleGetByteAsync(url: String): ByteArray? {
suspend fun simpleGetBytesAsync(url: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java)

@ -39,6 +39,13 @@ interface HttpGetApi {
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
fun getMapByte(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET
suspend fun getMapByteAsync(
@Url url: String,

@ -6,113 +6,78 @@ import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast
import java.io.File
object ImportOldData {
fun import(context: Context, file: File) {
GlobalScope.launch(Dispatchers.IO) {
try {// 导入书架
val shelfFile =
FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书籍${importCount}")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
}
try {// 导入书架
val shelfFile =
FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}")
} catch (e: Exception) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
try {// Book source
val sourceFile =
FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入源失败\n${e.localizedMessage}")
}
}
try {// Book source
val sourceFile =
FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}")
} catch (e: Exception) {
context.toast("导入源失败\n${e.localizedMessage}")
}
try {// Replace rules
val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json")
if (ruleFile.exists()) {
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
context.toast("成功导入替换规则${importCount}")
}
} else {
withContext(Dispatchers.Main) {
context.toast("未找到替换规则")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
try {// Replace rules
val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json")
if (ruleFile.exists()) {
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}")
} else {
context.toast("未找到替换规则")
}
} catch (e: Exception) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
fun importUri(uri: Uri) {
Coroutine.async {
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach {
when (it.name) {
"myBookShelf.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书籍${importCount}")
}
}
} catch (e: java.lang.Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}")
}
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach {
when (it.name) {
"myBookShelf.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldBookshelf(json)
App.INSTANCE.toast("成功导入书籍${importCount}")
}
"myBookSource.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书源${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入源失败\n${e.localizedMessage}")
}
} catch (e: java.lang.Exception) {
App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}")
}
"myBookSource.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldSource(json)
App.INSTANCE.toast("成功导入书源${importCount}")
}
"myBookReplaceRule.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入替换规则${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}")
}
} catch (e: Exception) {
App.INSTANCE.toast("导入源失败\n${e.localizedMessage}")
}
"myBookReplaceRule.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldReplaceRule(json)
App.INSTANCE.toast("成功导入替换规则${importCount}")
}
}
} catch (e: Exception) {
App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}

@ -2,7 +2,6 @@ package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
@ -13,11 +12,10 @@ import io.legado.app.BuildConfig
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@ -106,19 +104,24 @@ object Restore {
} catch (e: Exception) {
e.printStackTrace()
}
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.all?.map {
val edit = App.INSTANCE.defaultSharedPreferences.edit()
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.all
?.let { map ->
val ignoreKeys = arrayOf(PreferKey.versionCode, PreferKey.defaultCover)
val edit = App.INSTANCE.defaultSharedPreferences.edit()
map.forEach {
if (!ignoreKeys.contains(it.key)) {
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
}
}
}
edit.apply()
}
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
edit.apply()
}
ReadBookConfig.apply {
styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout)
@ -132,13 +135,8 @@ object Restore {
ReadBook.loadContent(resetPageOffset = false)
}
withContext(Main) {
if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight()
} else if (!AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight()
} else {
postEvent(EventBus.RECREATE, "true")
}
App.INSTANCE.applyDayNight()
postEvent(EventBus.RECREATE, "true")
if (!BuildConfig.DEBUG) {
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
}

@ -93,18 +93,13 @@ object ATH {
}
}
fun setLightNavigationBarAuto(activity: Activity, bgColor: Int) {
setLightNavigationBar(activity, ColorUtils.isColorLight(bgColor))
}
fun setNavigationBarColorAuto(activity: Activity) {
setNavigationBarColor(activity, ThemeStore.navigationBarColor(activity))
}
fun setNavigationBarColor(activity: Activity, color: Int) {
fun setNavigationBarColorAuto(
activity: Activity,
color: Int = ThemeStore.navigationBarColor(activity)
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.navigationBarColor = color
setLightNavigationBarAuto(activity, color)
setLightNavigationBar(activity, ColorUtils.isColorLight(color))
}
}

@ -9,6 +9,7 @@ import androidx.annotation.CheckResult
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import io.legado.app.App
import io.legado.app.R
/**
@ -194,7 +195,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult
@ColorInt
fun primaryColor(context: Context): Int {
fun primaryColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
@ -212,7 +213,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult
@ColorInt
fun accentColor(context: Context): Int {
fun accentColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_ACCENT_COLOR,
ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))
@ -236,7 +237,10 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
fun navigationBarColor(context: Context): Int {
return if (!coloredNavigationBar(context)) {
Color.BLACK
} else prefs(context).getInt(ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, primaryColor(context))
} else prefs(context).getInt(
ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR,
bottomBackground(context)
)
}
@CheckResult
@ -277,7 +281,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult
@ColorInt
fun backgroundColor(context: Context): Int {
fun backgroundColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BACKGROUND_COLOR,
ATHUtils.resolveColor(context, android.R.attr.colorBackground)
@ -294,7 +298,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult
@ColorInt
fun bottomBackground(context: Context): Int {
fun bottomBackground(context: Context = App.INSTANCE): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND,
ATHUtils.resolveColor(context, android.R.attr.colorBackground)

@ -377,8 +377,9 @@ class AnalyzeByJSoup {
textS.add(element.ownText())
}
"html" -> {
elements.select("script, style").remove()
val html = elements.html()
elements.select("script").remove()
elements.select("style").remove()
val html = elements.outerHtml()
textS.add(html)
}
"all" -> textS.add(elements.outerHtml())

@ -169,7 +169,7 @@ class AnalyzeByXPath {
if (rules.size == 1) {
val jxNodes = jxNode?.sel(rule) ?: jxDocument?.selN(rule)
jxNodes?.let {
return TextUtils.join(",", jxNodes)
return TextUtils.join("\n", jxNodes)
}
return null
} else {

@ -1,6 +1,5 @@
package io.legado.app.model.localBook
import android.content.Context
import android.net.Uri
import io.legado.app.App
import io.legado.app.data.entities.Book
@ -19,8 +18,8 @@ class AnalyzeTxtFile {
private lateinit var charset: Charset
@Throws(Exception::class)
fun analyze(context: Context, book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(context, book)
fun analyze(book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(book)
book.charset = EncodingDetect.getEncode(bookFile)
charset = book.fileCharset()
val rulePattern = if (book.tocUrl.isNotEmpty()) {
@ -205,7 +204,7 @@ class AnalyzeTxtFile {
val bean = toc[i]
bean.index = i
bean.bookUrl = book.bookUrl
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title))
}
book.latestChapterTitle = toc.last().title
book.totalChapterNum = toc.size
@ -253,7 +252,7 @@ class AnalyzeTxtFile {
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
val bookFile = getBookFile(book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val content = ByteArray((bookChapter.end!! - bookChapter.start!!).toInt())
@ -262,13 +261,13 @@ class AnalyzeTxtFile {
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
private fun getBookFile(book: Book): File {
if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
DocumentUtils.readBytes(App.INSTANCE, uri)?.let {
bookFile.writeBytes(it)
}
}
@ -278,18 +277,20 @@ class AnalyzeTxtFile {
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
val rules = App.db.txtTocRule().enabled
if (rules.isEmpty()) {
return getDefaultRules()
return getDefaultEnabledRules()
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
fun getDefaultEnabledRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let { txtTocRules ->
App.db.txtTocRule().insert(*txtTocRules.toTypedArray())
return txtTocRules.filter {
it.enable
}
}
}
return emptyList()

@ -0,0 +1,185 @@
package io.legado.app.model.localBook
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.data.entities.BookChapter
import io.legado.app.utils.*
import nl.siegmann.epublib.domain.Book
import nl.siegmann.epublib.domain.TOCReference
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
class EPUBFile(val book: io.legado.app.data.entities.Book) {
companion object {
private var eFile: EPUBFile? = null
@Synchronized
private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile {
if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) {
eFile = EPUBFile(book)
return eFile!!
}
return eFile!!
}
@Synchronized
fun getChapterList(book: io.legado.app.data.entities.Book): ArrayList<BookChapter> {
return getEFile(book).getChapterList()
}
@Synchronized
fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? {
return getEFile(book).getContent(chapter)
}
@Synchronized
fun getImage(
book: io.legado.app.data.entities.Book,
href: String
): InputStream? {
return getEFile(book).getImage(href)
}
}
private var epubBook: Book? = null
private var mCharset: Charset = Charset.defaultCharset()
init {
try {
val epubReader = EpubReader()
val inputStream = if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
App.INSTANCE.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
epubBook = epubReader.readEpub(inputStream)
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
App.INSTANCE.externalFilesDir,
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg",
"covers"
)
}
if (!File(book.coverUrl!!).exists()) {
epubBook!!.coverImage?.inputStream?.use {
val cover = BitmapFactory.decodeStream(it)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getContent(chapter: BookChapter): String? {
epubBook?.let { eBook ->
val resource = eBook.resources.getByHref(chapter.url)
val doc = Jsoup.parse(String(resource.data, mCharset))
val elements = doc.body().children()
elements.select("script").remove()
elements.select("style").remove()
return elements.outerHtml().htmlFormat()
}
return null
}
private fun getImage(href: String): InputStream? {
val abHref = href.replace("../", "")
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook ->
val metadata = eBook.metadata
book.name = metadata.firstTitle
if (metadata.authors.size > 0) {
val author =
metadata.authors[0].toString().replace("^, |, $".toRegex(), "")
book.author = author
}
if (metadata.descriptions.size > 0) {
book.intro = Jsoup.parse(metadata.descriptions[0]).text()
}
val refs = eBook.tableOfContents.tocReferences
if (refs == null || refs.isEmpty()) {
val spineReferences = eBook.spine.spineReferences
var i = 0
val size = spineReferences.size
while (i < size) {
val resource =
spineReferences[i].resource
var title = resource.title
if (TextUtils.isEmpty(title)) {
try {
val doc =
Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title")
if (elements.size > 0) {
title = elements[0].text()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
val chapter = BookChapter()
chapter.index = i
chapter.bookUrl = book.bookUrl
chapter.url = resource.href
if (i == 0 && title.isEmpty()) {
chapter.title = "封面"
} else {
chapter.title = title
}
chapterList.add(chapter)
i++
}
} else {
parseMenu(chapterList, refs, 0)
for (i in chapterList.indices) {
chapterList[i].index = i
}
}
}
book.latestChapterTitle = chapterList.lastOrNull()?.title
book.totalChapterNum = chapterList.size
return chapterList
}
private fun parseMenu(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?,
level: Int
) {
if (refs == null) return
for (ref in refs) {
if (ref.resource != null) {
val chapter = BookChapter()
chapter.bookUrl = book.bookUrl
chapter.title = ref.title
chapter.url = ref.completeHref
chapterList.add(chapter)
}
if (ref.children != null && ref.children.isNotEmpty()) {
parseMenu(chapterList, ref.children, level + 1)
}
}
}
}

@ -4,14 +4,33 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFilesDir
import io.legado.app.utils.isContentPath
import java.io.File
object LocalBook {
fun getChapterList(book: Book): ArrayList<BookChapter> {
return if (book.isEpub()) {
EPUBFile.getChapterList(book)
} else {
AnalyzeTxtFile().analyze(book)
}
}
fun getContext(book: Book, chapter: BookChapter): String? {
return if (book.isEpub()) {
EPUBFile.getContent(book, chapter)
} else {
AnalyzeTxtFile.getContent(book, chapter)
}
}
fun importFile(path: String) {
val fileName = if (path.isContentPath()) {
val doc = DocumentFile.fromSingleUri(App.INSTANCE, Uri.parse(path))
@ -40,14 +59,19 @@ object LocalBook {
bookUrl = path,
name = name,
author = author,
originName = fileName
originName = fileName,
coverUrl = FileUtils.getPath(
App.INSTANCE.externalFilesDir,
"${MD5Utils.md5Encode16(path)}.jpg",
"covers"
)
)
App.db.bookDao().insert(book)
}
fun deleteBook(book: Book, deleteOriginal: Boolean) {
kotlin.runCatching {
if (book.isTxt()) {
if (book.isLocalTxt()) {
val bookFile = FileUtils.getFile(AnalyzeTxtFile.cacheFolder, book.originName)
bookFile.delete()
}

@ -2,7 +2,6 @@ package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
@ -40,11 +39,7 @@ object BookContent {
var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource
)
if (bookSource.bookSourceType == BookType.default) {
content.append(contentData.content.replace(bookChapter.title, "")).append("\n")
} else {
content.append(contentData.content).append("\n")
}
content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0]

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

@ -323,7 +323,6 @@ abstract class BaseReadAloudService : BaseService(),
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionCompat.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

@ -7,13 +7,11 @@ import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook
import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors
@ -80,24 +78,15 @@ class CheckSourceService : BaseService() {
if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl)
} else {
check(source)
CheckSource(source).check(this, searchPool) {
onNext(it)
}
}
} ?: onNext(sourceUrl)
}
}
}
private fun check(source: BookSource) {
val webBook = WebBook(source)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
}
private fun onNext(sourceUrl: String) {
synchronized(this) {
check()

@ -9,6 +9,7 @@ import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
@ -16,23 +17,33 @@ import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.service.help.Download
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.isActive
import org.jetbrains.anko.toast
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
class DownloadService : BaseService() {
private val threadCount = AppConfig.threadCount
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var tasks = CompositeCoroutine()
private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>()
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载"
private val bookMap = ConcurrentHashMap<String, Book>()
private val webBookMap = ConcurrentHashMap<String, WebBook>()
private val downloadMap = ConcurrentHashMap<String, CopyOnWriteArraySet<BookChapter>>()
private val downloadCount = ConcurrentHashMap<String, DownloadCount>()
private val finalMap = ConcurrentHashMap<String, CopyOnWriteArraySet<BookChapter>>()
private val downloadingList = CopyOnWriteArraySet<String>()
@Volatile
private var downloadingCount = 0
private var notificationContent = App.INSTANCE.getString(R.string.starting_download)
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
@ -78,20 +89,63 @@ class DownloadService : BaseService() {
postEvent(EventBus.UP_DOWNLOAD, downloadMap)
}
private fun getBook(bookUrl: String): Book? {
var book = bookMap[bookUrl]
if (book == null) {
synchronized(this) {
book = bookMap[bookUrl]
if (book == null) {
book = App.db.bookDao().getBook(bookUrl)
if (book == null) {
removeDownload(bookUrl)
}
}
}
}
return book
}
private fun getWebBook(bookUrl: String, origin: String): WebBook? {
var webBook = webBookMap[origin]
if (webBook == null) {
synchronized(this) {
webBook = webBookMap[origin]
if (webBook == null) {
App.db.bookSourceDao().getBookSource(origin)?.let {
webBook = WebBook(it)
}
if (webBook == null) {
removeDownload(bookUrl)
}
}
}
}
return webBook
}
private fun addDownloadData(bookUrl: String?, start: Int, end: Int) {
bookUrl ?: return
if (downloadMap.containsKey(bookUrl)) {
toast("该书已在下载列表")
updateNotification(getString(R.string.already_in_download))
toast(R.string.already_in_download)
return
}
downloadCount[bookUrl] = DownloadCount()
execute {
val chapterMap = downloadMap[bookUrl] ?: linkedSetOf<BookChapter>().apply {
downloadMap[bookUrl] = this
}
App.db.bookChapterDao().getChapterList(bookUrl, start, end).let {
chapterMap.addAll(it)
if (it.isNotEmpty()) {
val chapters = CopyOnWriteArraySet<BookChapter>()
chapters.addAll(it)
downloadMap[bookUrl] = chapters
} else {
Download.addLog("${getBook(bookUrl)?.name} is empty")
}
}
for (i in 0 until threadCount) {
if (downloadingCount < threadCount) {
download()
}
}
download()
}
}
@ -100,70 +154,92 @@ class DownloadService : BaseService() {
finalMap.remove(bookUrl)
}
private fun updateNotification(downloadCount:DownloadCount, totalCount: Int, content: String){
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
private fun download() {
downloadingCount += 1
val task = Coroutine.async(this, context = searchPool) {
downloadMap.forEach { entry ->
if (!isActive) return@async
if (!finalMap.containsKey(entry.key)) {
val book = App.db.bookDao().getBook(entry.key) ?: return@async
val bookSource =
App.db.bookSourceDao().getBookSource(book.origin) ?: return@async
val webBook = WebBook(bookSource)
downloadCount[entry.key] = DownloadCount()
entry.value.forEach { chapter ->
if (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) {
if (!BookHelp.hasContent(book, chapter)) {
webBook.getContent(
book,
chapter,
scope = this,
context = searchPool
).onSuccess(IO) { content ->
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
.onFinally(IO) {
synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) }
val chapterMap =
finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply {
finalMap[book.bookUrl] = this
}
chapterMap.add(chapter)
if (chapterMap.size == entry.value.size) {
downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl)
downloadCount.remove(entry.key)
}
}
if (!isActive) return@async
val bookChapter: BookChapter? = synchronized(this@DownloadService) {
downloadMap.forEach {
it.value.forEach { chapter ->
if (!downloadingList.contains(chapter.url)) {
downloadingList.add(chapter.url)
return@synchronized chapter
}
}
}
return@synchronized null
}
if (bookChapter == null) {
postDownloading(false)
} else {
val book = getBook(bookChapter.bookUrl)
if (book == null) {
postDownloading(true)
return@async
}
val webBook = getWebBook(bookChapter.bookUrl, book.origin)
if (webBook == null) {
postDownloading(true)
return@async
}
if (!BookHelp.hasContent(book, bookChapter)) {
webBook.getContent(
book,
bookChapter,
scope = this,
context = searchPool
).onError {
synchronized(this) {
downloadingList.remove(bookChapter.url)
}
Download.addLog(it.localizedMessage)
}.onSuccess(IO) { content ->
BookHelp.saveContent(book, bookChapter, content)
synchronized(this@DownloadService) {
downloadCount[book.bookUrl]?.increaseSuccess()
downloadCount[book.bookUrl]?.increaseFinished()
downloadCount[book.bookUrl]?.let {
updateNotification(
it,
downloadMap[book.bookUrl]?.size,
bookChapter.title
)
}
val chapterMap =
finalMap[book.bookUrl]
?: CopyOnWriteArraySet<BookChapter>().apply {
finalMap[book.bookUrl] = this
}
} else{
//无需下载的,设置为增加成功
downloadCount[entry.key]?.increaseSuccess()
downloadCount[entry.key]?.increaseFinished()
chapterMap.add(bookChapter)
if (chapterMap.size == downloadMap[book.bookUrl]?.size) {
downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl)
downloadCount.remove(book.bookUrl)
}
}
}.onFinally(IO) {
postDownloading(true)
}
} else {
//无需下载的,设置为增加成功
downloadCount[book.bookUrl]?.increaseSuccess()
downloadCount[book.bookUrl]?.increaseFinished()
postDownloading(true)
}
}
}.onError {
Download.addLog("ERROR:${it.localizedMessage}")
}
tasks.add(task)
task.invokeOnCompletion {
tasks.remove(task)
if (tasks.isEmpty) {
stopSelf()
}
private fun postDownloading(hasChapter: Boolean) {
downloadingCount -= 1
if (hasChapter) {
download()
} else {
if (downloadingCount < 1) {
stopDownload()
}
}
}
@ -180,6 +256,15 @@ class DownloadService : BaseService() {
handler.postDelayed(runnable, 1000)
}
private fun updateNotification(
downloadCount: DownloadCount,
totalCount: Int?,
content: String
) {
notificationContent =
"进度:" + downloadCount.downloadFinishedCount + "/" + totalCount + ",成功:" + downloadCount.successCount + "," + content
}
/**
* 更新通知
*/

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

@ -42,9 +42,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
private fun initTts() {
ttsInitFinish = false
textToSpeech = TextToSpeech(this, this).apply {
setOnUtteranceProgressListener(ttsUtteranceListener)
}
textToSpeech = TextToSpeech(this, this)
}
override fun onDestroy() {
@ -55,6 +53,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.let {
it.setOnUtteranceProgressListener(ttsUtteranceListener)
it.language = Locale.CHINA
ttsInitFinish = true
play()
@ -69,16 +68,20 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
@Synchronized
override fun play() {
if (contentList.isNotEmpty() && ttsInitFinish && requestFocus()) {
MediaHelp.playSilentSound(this)
super.play()
textToSpeech?.stop()
for (i in nowSpeak until contentList.size) {
textToSpeech?.speak(
contentList[i],
TextToSpeech.QUEUE_ADD,
null,
AppConst.APP_TAG + i
)
execute {
MediaHelp.playSilentSound(this@TTSReadAloudService)
textToSpeech?.let {
it.speak("", TextToSpeech.QUEUE_FLUSH, null, null)
for (i in nowSpeak until contentList.size) {
it.speak(
contentList[i],
TextToSpeech.QUEUE_ADD,
null,
AppConst.APP_TAG + i
)
}
}
}
}
}

@ -2,34 +2,62 @@ package io.legado.app.service.help
import android.content.Context
import android.content.Intent
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.IntentAction
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
object CheckSource {
class CheckSource(val source: BookSource) {
fun start(context: Context, sources: List<BookSource>) {
if (sources.isEmpty()) {
context.toast(R.string.non_select)
return
}
val selectedIds: ArrayList<String> = arrayListOf()
sources.map {
selectedIds.add(it.bookSourceUrl)
companion object {
var keyword = "我的"
fun start(context: Context, sources: List<BookSource>) {
if (sources.isEmpty()) {
context.toast(R.string.non_select)
return
}
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
it.putExtra("selectIds", selectedIds)
context.startService(it)
fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.stop
context.startService(it)
}
}
}
fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.stop
context.startService(it)
}
fun check(
scope: CoroutineScope,
context: CoroutineContext,
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)
}
}
}

@ -7,6 +7,18 @@ import io.legado.app.service.DownloadService
object Download {
val logs = arrayListOf<String>()
fun addLog(log: String?) {
log ?: return
synchronized(this) {
if (logs.size > 1000) {
logs.removeAt(0)
}
logs.add(log)
}
}
fun start(context: Context, bookUrl: String, start: Int, end: Int) {
Intent(context, DownloadService::class.java).let {
it.action = IntentAction.start

@ -1,18 +1,21 @@
package io.legado.app.service.help
import androidx.lifecycle.MutableLiveData
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ImageProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
@ -38,7 +41,6 @@ object ReadBook {
fun resetData(book: Book) {
this.book = book
titleDate.postValue(book.name)
durChapterIndex = book.durChapterIndex
durPageIndex = book.durChapterPos
isLocalBook = book.origin == BookType.local
@ -46,11 +48,12 @@ object ReadBook {
prevTextChapter = null
curTextChapter = null
nextTextChapter = null
titleDate.postValue(book.name)
upWebBook(book)
ImageProvider.clearAllCache()
}
fun upWebBook(book: Book?) {
book ?: return
fun upWebBook(book: Book) {
webBook = if (book.origin == BookType.local) {
null
} else {
@ -210,7 +213,7 @@ object ReadBook {
Coroutine.async {
App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter ->
BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(chapter, it, upContent, resetPageOffset)
contentLoadFinish(book, chapter, it, upContent, resetPageOffset)
removeLoading(chapter.index)
} ?: download(chapter, resetPageOffset = resetPageOffset)
} ?: removeLoading(index)
@ -246,6 +249,7 @@ object ReadBook {
?.onSuccess(Dispatchers.IO) { content ->
if (content.isEmpty()) {
contentLoadFinish(
book,
chapter,
App.INSTANCE.getString(R.string.content_empty),
resetPageOffset = resetPageOffset
@ -253,11 +257,12 @@ object ReadBook {
removeLoading(chapter.index)
} else {
BookHelp.saveContent(book, chapter, content)
contentLoadFinish(chapter, content, resetPageOffset = resetPageOffset)
contentLoadFinish(book, chapter, content, resetPageOffset = resetPageOffset)
removeLoading(chapter.index)
}
}?.onError {
contentLoadFinish(
book,
chapter,
it.localizedMessage ?: "未知错误",
resetPageOffset = resetPageOffset
@ -285,6 +290,7 @@ object ReadBook {
* 内容加载完成
*/
private fun contentLoadFinish(
book: Book,
chapter: BookChapter,
content: String,
upContent: Boolean = true,
@ -292,30 +298,36 @@ object ReadBook {
) {
Coroutine.async {
if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(chapter.title)
2 -> HanLP.convertToTraditionalChinese(chapter.title)
else -> chapter.title
}
val contents = BookHelp.disposeContent(
chapter.title,
book!!.name,
book.name,
webBook?.bookSource?.bookSourceUrl,
content,
book!!.useReplaceRule
book.useReplaceRule
)
when (chapter.index) {
durChapterIndex -> {
curTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView()
curPageChanged()
callBack?.contentLoadFinish()
ImageProvider.clearOut(durChapterIndex)
}
durChapterIndex - 1 -> {
prevTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(-1, resetPageOffset)
}
durChapterIndex + 1 -> {
nextTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize)
ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(1, resetPageOffset)
}
}

@ -24,15 +24,19 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
.replace(R.id.fl_fragment, aboutFragment, fTag)
.commit()
tv_app_summary.post {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text)
val gzh = getString(R.string.legado_gzh)
val start = spannableString.indexOf(gzh)
spannableString.setSpan(
span, start, start + gzh.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tv_app_summary.text = spannableString
try {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text)
val gzh = getString(R.string.legado_gzh)
val start = spannableString.indexOf(gzh)
spannableString.setSpan(
span, start, start + gzh.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tv_app_summary.text = spannableString
} catch (e: Exception) {
e.printStackTrace()
}
}
}

@ -27,7 +27,8 @@ class AboutFragment : PreferenceFragmentCompat() {
Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"),
Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"),
Pair("(QQ群4)256929088", "czEJPLDnT4Pd9SKQ6RoRVzKhDxLchZrO"),
Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX")
Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX"),
Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n")
)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -47,12 +48,13 @@ class AboutFragment : PreferenceFragmentCompat() {
"update_log" -> showUpdateLog()
"check_update" -> openUrl(R.string.latest_release_url)
"mail" -> sendMail()
"sourceRuleSummary" -> openUrl(R.string.source_rule_url)
"git" -> openUrl(R.string.this_github_url)
"home_page" -> openUrl(R.string.home_page_url)
"license" -> requireContext().openUrl(licenseUrl)
"disclaimer" -> requireContext().openUrl(disclaimerUrl)
"qq" -> showQqGroups()
"gzGzh" -> requireContext().sendToClip("开源阅读软件")
"gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh))
}
return super.onPreferenceTreeClick(preference)
}

@ -27,6 +27,7 @@ import io.legado.app.service.AudioPlayService
import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.book.changesource.ChangeSourceDialog
import io.legado.app.ui.book.chapterlist.ChapterListActivity
import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_audio_play.*
import org.apache.commons.lang3.time.DateFormatUtils
@ -117,8 +118,8 @@ class AudioPlayActivity :
private fun upCover(path: String?) {
ImageLoader.load(this, path)
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.placeholder(CoverImageView.defaultDrawable)
.error(CoverImageView.defaultDrawable)
.into(iv_cover)
ImageLoader.load(this, path)
.transition(DrawableTransitionOptions.withCrossFade(1500))
@ -128,7 +129,7 @@ class AudioPlayActivity :
}
private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default)
return ImageLoader.load(this, CoverImageView.defaultDrawable)
.apply(bitmapTransform(BlurTransformation(this, 25)))
}

@ -60,7 +60,6 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) :
when (it.itemId) {
R.id.menu_disable_book_source -> {
callBack.disableSource(searchBook)
removeItem(searchBook)
}
}
true

@ -2,6 +2,7 @@ package io.legado.app.ui.book.changesource
import android.app.Application
import android.os.Bundle
import android.os.Handler
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.R
@ -22,6 +23,7 @@ import java.util.concurrent.Executors
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
val handler = Handler()
val searchStateData = MutableLiveData<Boolean>()
val searchBooksLiveData = MutableLiveData<List<SearchBook>>()
var name: String = ""
@ -29,6 +31,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
private var task: Coroutine<*>? = null
private var screenKey: String = ""
private val searchBooks = hashSetOf<SearchBook>()
private var postTime = 0L
private val sendRunnable = Runnable { upAdapter() }
fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
@ -55,9 +60,17 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
@Synchronized
private fun upAdapter() {
val books = searchBooks.toList()
searchBooksLiveData.postValue(books.sortedBy { it.originOrder })
if (System.currentTimeMillis() >= postTime + 500) {
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) {
@ -172,6 +185,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
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 {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
if (oldItem.originName != newItem.originName) {
return false
}
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
return false
return when {
oldItem.originName != newItem.originName -> false
oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
else -> true
}
return true
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {

@ -12,14 +12,17 @@ import com.google.android.material.snackbar.Snackbar
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.BookHelp
import io.legado.app.service.help.Download
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.widget.dialog.TextListDialog
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_download.*
import kotlinx.coroutines.Dispatchers
@ -35,29 +38,49 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
private val exportRequestCode = 32
private val exportBookPathKey = "exportBookPath"
lateinit var adapter: DownloadAdapter
private var bookshelfLiveData: LiveData<List<Book>>? = null
private var groupLiveData: LiveData<List<BookGroup>>? = null
private var booksLiveData: LiveData<List<Book>>? = null
private var menu: Menu? = null
private var exportPosition = -1
private val groupList: ArrayList<BookGroup> = arrayListOf()
private var groupId: Int = -1
override val viewModel: DownloadViewModel
get() = getViewModel(DownloadViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
groupId = intent.getIntExtra("groupId", -1)
title_bar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all)
initRecyclerView()
initLiveData()
initGroupData()
initBookData()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.download, menu)
this.menu = menu
return super.onCompatCreateOptionsMenu(menu)
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
this.menu = menu
upMenu()
return super.onPrepareOptionsMenu(menu)
}
private fun upMenu() {
menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu ->
subMenu.removeGroup(R.id.menu_group)
groupList.forEach { bookGroup ->
subMenu.add(R.id.menu_group, bookGroup.groupId, Menu.NONE, bookGroup.groupName)
}
}
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_download -> launch(IO) {
if (adapter.downloadMap.isNullOrEmpty()) {
App.db.bookDao().webBooks.forEach { book ->
adapter.getItems().forEach { book ->
Download.start(
this@DownloadActivity,
book.bookUrl,
@ -69,6 +92,24 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
Download.stop(this@DownloadActivity)
}
}
R.id.menu_log -> {
TextListDialog.show(supportFragmentManager, getString(R.string.log), Download.logs)
}
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = AppConst.bookGroupNone.groupId
initBookData()
}
R.id.menu_all -> {
title_bar.subtitle = item.title
groupId = AppConst.bookGroupAll.groupId
initBookData()
}
else -> if (item.groupId == R.id.menu_group) {
title_bar.subtitle = item.title
groupId = item.itemId
initBookData()
}
}
return super.onCompatOptionsItemSelected(item)
}
@ -79,21 +120,39 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
recycler_view.adapter = adapter
}
private fun initLiveData() {
bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload()
bookshelfLiveData?.observe(this, Observer { list ->
private fun initBookData() {
booksLiveData?.removeObservers(this)
booksLiveData = when (groupId) {
AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll()
AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer { list ->
val booksDownload = list.filter {
it.isOnLineTxt()
}
val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
1 -> booksDownload.sortedByDescending { it.latestChapterTime }
2 -> booksDownload.sortedBy { it.name }
3 -> booksDownload.sortedBy { it.order }
else -> booksDownload.sortedByDescending { it.durChapterTime }
}
adapter.setItems(books)
initCacheSize(books)
})
}
private fun initGroupData() {
groupLiveData?.removeObservers(this)
groupLiveData = App.db.bookGroupDao().liveDataAll()
groupLiveData?.observe(this, Observer {
groupList.clear()
groupList.addAll(it)
adapter.notifyDataSetChanged()
upMenu()
})
}
private fun initCacheSize(books: List<Book>) {
launch(IO) {
books.forEach { book ->

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

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
@ -18,6 +17,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
@ -41,7 +41,7 @@ import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.*
import kotlin.collections.ArrayList
class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var viewModel: GroupViewModel
private lateinit var adapter: GroupAdapter
private val callBack: CallBack? get() = parentFragment as? CallBack
@ -62,8 +62,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
return inflater.inflate(R.layout.dialog_recycler_view, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.title = getString(R.string.group_manage)
initData()
initMenu()

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
@ -18,6 +17,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
@ -40,7 +40,7 @@ import kotlinx.android.synthetic.main.item_group_select.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.*
class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
companion object {
const val tag = "groupSelectDialog"
@ -78,8 +78,7 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
return inflater.inflate(R.layout.dialog_book_group_picker, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
callBack = activity as? CallBack
arguments?.let {
groupId = it.getInt("groupId")

@ -32,6 +32,7 @@ import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.dp
import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
@ -86,6 +87,9 @@ class BookInfoActivity :
R.id.menu_refresh -> {
upLoading(true)
viewModel.bookData.value?.let {
if (it.isLocalBook()) {
it.tocUrl = ""
}
viewModel.loadBookInfo(it)
}
}
@ -138,7 +142,7 @@ class BookInfoActivity :
}
private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default)
return ImageLoader.load(this, CoverImageView.defaultDrawable)
.apply(bitmapTransform(BlurTransformation(this, 25)))
}
@ -349,7 +353,7 @@ class BookInfoActivity :
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeSourceEdit -> if (resultCode == Activity.RESULT_OK) {
viewModel.initData(intent)
viewModel.upEditBook()
}
requestCodeChapterList -> if (resultCode == Activity.RESULT_OK) {
viewModel.bookData.value?.let {

@ -10,8 +10,8 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.model.localBook.LocalBook
import io.legado.app.service.help.ReadBook
import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) {
@ -22,13 +22,13 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun initData(intent: Intent) {
execute {
intent.getStringExtra("bookUrl")?.let {
App.db.bookDao().getBook(it)?.let { book ->
inBookshelf = true
setBook(book)
} ?: App.db.searchBookDao().getSearchBook(it)?.toBook()?.let { book ->
setBook(book)
}
val name = intent.getStringExtra("name") ?: ""
val author = intent.getStringExtra("author") ?: ""
App.db.bookDao().getBook(name, author)?.let { book ->
inBookshelf = true
setBook(book)
} ?: App.db.searchBookDao().getFirstByNameAuthor(name, author)?.toBook()?.let { book ->
setBook(book)
}
}
}
@ -81,7 +81,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
) {
execute {
if (book.isLocalBook()) {
AnalyzeTxtFile().analyze(context, book).let {
LocalBook.getChapterList(book).let {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
chapterListData.postValue(it)
@ -131,21 +131,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
}
fun changeTo(book: Book) {
fun changeTo(newBook: Book) {
execute {
if (inBookshelf) {
bookData.value?.let {
book.group = it.group
book.order = it.order
App.db.bookDao().delete(it)
}
App.db.bookDao().insert(book)
bookData.value?.changeTo(newBook)
}
bookData.postValue(book)
if (book.tocUrl.isEmpty()) {
loadBookInfo(book) { upChangeDurChapterIndex(book, it) }
bookData.postValue(newBook)
if (newBook.tocUrl.isEmpty()) {
loadBookInfo(newBook) { upChangeDurChapterIndex(newBook, it) }
} else {
loadChapter(book) { upChangeDurChapterIndex(book, it) }
loadChapter(newBook) { upChangeDurChapterIndex(newBook, it) }
}
}
}
@ -178,6 +173,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
book.durChapterTitle = it.durChapterTitle
}
App.db.bookDao().insert(book)
if (ReadBook.book?.name == book.name && ReadBook.book?.author == book.author) {
ReadBook.book = book
}
}
}.onSuccess {
success?.invoke()
@ -218,7 +216,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
execute {
bookData.value?.let {
App.db.bookDao().delete(it)
it.delete()
inBookshelf = false
if (it.isLocalBook()) {
LocalBook.deleteBook(it, deleteOriginal)
@ -228,4 +226,12 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
success?.invoke()
}
}
fun upEditBook() {
bookData.value?.let {
App.db.bookDao().getBook(it.bookUrl)?.let { book ->
bookData.postValue(book)
}
}
}
}

@ -2,17 +2,23 @@ package io.legado.app.ui.book.info.edit
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.utils.getViewModel
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_info_edit.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
import java.io.File
class BookInfoEditActivity :
VMBaseActivity<BookInfoEditViewModel>(R.layout.activity_book_info_edit),
@ -101,13 +107,48 @@ class BookInfoEditActivity :
upCover()
}
private fun coverChangeTo(uri: Uri) {
if (uri.toString().isContentPath()) {
val doc = DocumentFile.fromSingleUri(this, uri)
doc?.name?.let {
var file = this.externalFilesDir
file = FileUtils.createFileIfNotExist(file, it, "covers")
kotlin.runCatching {
DocumentUtils.readBytes(this, doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
coverChangeTo(file.absolutePath)
} ?: toast("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(this, uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = this.externalFilesDir
file = FileUtils.createFileIfNotExist(file, imgFile.name, "covers")
file.writeBytes(imgFile.readBytes())
coverChangeTo(file.absolutePath)
}
}
}
.request()
}
}
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())
coverChangeTo(uri)
}
}
}

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
import io.legado.app.service.help.ReadBook
class BookInfoEditViewModel(application: Application) : BaseViewModel(application) {
var book: Book? = null
@ -21,6 +22,9 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
fun saveBook(book: Book, success: (() -> Unit)?) {
execute {
if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.book = book
}
App.db.bookDao().insert(book)
}.onSuccess {
success?.invoke()

@ -172,7 +172,10 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
val item = docList[i]
if (item.name.startsWith(".")) {
docList.removeAt(i)
} else if (!item.isDir && !item.name.endsWith(".txt", true)) {
} else if (!item.isDir
&& !item.name.endsWith(".txt", true)
&& !item.name.endsWith(".epub", true)
) {
docList.removeAt(i)
}
}
@ -196,7 +199,9 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
Uri.parse(it.absolutePath)
)
)
} else if (it.name.endsWith(".txt", true)) {
} else if (it.name.endsWith(".txt", true)
|| it.name.endsWith(".epub", true)
) {
docList.add(
DocItem(
it.name,

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@ -30,6 +32,7 @@ import io.legado.app.help.storage.SyncBookProgress
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService
@ -132,6 +135,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onPause() {
super.onPause()
ReadBook.saveRead()
timeBatteryReceiver?.let {
unregisterReceiver(it)
timeBatteryReceiver = null
@ -143,6 +147,21 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
}
}
override fun upNavigationBarColor() {
when {
read_menu == null -> return
read_menu.isVisible -> {
ATH.setNavigationBarColorAuto(this)
}
ReadBookConfig.bg is ColorDrawable -> {
ATH.setNavigationBarColorAuto(this, ReadBookConfig.bgMeanColor)
}
else -> {
ATH.setNavigationBarColorAuto(this, Color.BLACK)
}
}
}
/**
* 初始化View
*/
@ -197,7 +216,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (item.groupId) {
R.id.menu_group_on_line -> item.isVisible = onLine
R.id.menu_group_local -> item.isVisible = !onLine
R.id.menu_group_text -> item.isVisible = book.isTxt()
R.id.menu_group_text -> item.isVisible = book.isLocalTxt()
R.id.menu_group_login ->
item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
else -> if (item.itemId == R.id.menu_enable_replace) {
@ -241,7 +260,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
onReplaceRuleSave()
}
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(
supportFragmentManager,
@ -496,8 +518,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 更新内容
*/
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
autoPageProgress = 0
launch {
page_view.upContent(relativePosition, resetPageOffset)
seek_read_page.progress = ReadBook.durPageIndex
}
}
@ -519,6 +543,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
seek_read_page.progress = ReadBook.durPageIndex
tv_pre.isEnabled = ReadBook.durChapterIndex != 0
tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1
} ?: let {
tv_chapter_name.gone()
tv_chapter_url.gone()
}
}
}
@ -649,6 +676,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/
override fun upSystemUiVisibility() {
Help.upSystemUiVisibility(this, !read_menu.isVisible)
upNavigationBarColor()
}
/**

@ -11,7 +11,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.model.localBook.LocalBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
@ -35,6 +35,10 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} ?: App.db.bookDao().lastReadBook?.let {
initBook(it)
}
}.onFinally {
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
}
}
@ -59,11 +63,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
ReadBook.loadContent(resetPageOffset = true)
}
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
}
} else {
isInitFinish = true
ReadBook.book!!.group = book.group
ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book)
if (!book.isLocalBook() && ReadBook.webBook == null) {
@ -107,13 +109,17 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
) {
execute {
if (book.isLocalBook()) {
AnalyzeTxtFile().analyze(context, book).let {
LocalBook.getChapterList(book).let {
App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = it.size
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
if (it.isEmpty()) {
ReadBook.upMsg(context.getString(R.string.error_load_toc))
} else {
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
}
}
} else {
ReadBook.webBook?.getChapterList(book, this)
@ -140,29 +146,24 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
}
fun changeTo(book1: Book) {
fun changeTo(newBook: Book) {
execute {
ReadBook.upMsg(null)
ReadBook.book?.let {
book1.group = it.group
book1.order = it.order
App.db.bookDao().delete(it)
}
ReadBook.book?.changeTo(newBook)
ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null
ReadBook.nextTextChapter = null
withContext(Main) {
ReadBook.callBack?.upContent()
}
App.db.bookDao().insert(book1)
ReadBook.book = book1
App.db.bookSourceDao().getBookSource(book1.origin)?.let {
ReadBook.book = newBook
App.db.bookSourceDao().getBookSource(newBook.origin)?.let {
ReadBook.webBook = WebBook(it)
}
if (book1.tocUrl.isEmpty()) {
loadBookInfo(book1) { upChangeDurChapterIndex(book1, it) }
if (newBook.tocUrl.isEmpty()) {
loadBookInfo(newBook) { upChangeDurChapterIndex(newBook, it) }
} else {
loadChapterList(book1) { upChangeDurChapterIndex(book1, it) }
loadChapterList(newBook) { upChangeDurChapterIndex(newBook, it) }
}
}
}
@ -221,9 +222,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun removeFromBookshelf(success: (() -> Unit)?) {
execute {
ReadBook.book?.let {
App.db.bookDao().delete(it)
}
ReadBook.book?.delete()
}.onSuccess {
success?.invoke()
}

@ -4,12 +4,12 @@ import android.content.Context
import android.util.AttributeSet
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.FrameLayout
import android.widget.SeekBar
import androidx.core.view.isVisible
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
@ -153,6 +153,7 @@ class ReadMenu : FrameLayout {
fabNightTheme.onClick {
AppConfig.isNightTheme = !AppConfig.isNightTheme
App.INSTANCE.applyDayNight()
postEvent(EventBus.RECREATE, "")
}
//上一章
@ -194,8 +195,8 @@ class ReadMenu : FrameLayout {
}
private fun initAnimation() {
menuTopIn = AnimationUtils.loadAnimation(context, R.anim.anim_readbook_top_in)
menuBottomIn = AnimationUtils.loadAnimation(context, R.anim.anim_readbook_bottom_in)
menuTopIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in)
menuBottomIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
menuTopIn.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
callBack?.upSystemUiVisibility()
@ -217,8 +218,8 @@ class ReadMenu : FrameLayout {
})
//隐藏菜单
menuTopOut = AnimationUtils.loadAnimation(context, R.anim.anim_readbook_top_out)
menuBottomOut = AnimationUtils.loadAnimation(context, R.anim.anim_readbook_bottom_out)
menuTopOut = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_out)
menuBottomOut = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
menuTopOut.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
vw_menu_bg.setOnClickListener(null)

@ -40,7 +40,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
const val BG_COLOR = 122
}
private val resultSelectBg = 123
private val requestCodeBg = 123
private lateinit var adapter: BgAdapter
override fun onStart() {
@ -136,7 +136,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, resultSelectBg)
startActivityForResult(intent, requestCodeBg)
}
class BgAdapter(context: Context) :
@ -167,11 +167,9 @@ class BgTextConfigDialog : BaseDialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
resultSelectBg -> {
if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri)
}
requestCodeBg -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri)
}
}
}

@ -111,7 +111,8 @@ class MoreConfigDialog : DialogFragment() {
Help.setOrientation(it)
}
}
PreferKey.textFullJustify -> {
PreferKey.textFullJustify,
PreferKey.textBottomJustify -> {
postEvent(EventBus.UP_CONFIG, true)
}
}

@ -103,9 +103,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
tv_title_mode.onClick {
showTitleConfig()
}
tv_text_bold.onClick {
ReadBookConfig.textBold = !ReadBookConfig.textBold
tv_text_bold.isSelected = ReadBookConfig.textBold
text_font_weight_converter.onChanged {
postEvent(EventBus.UP_CONFIG, true)
}
tv_text_font.onClick {
@ -215,7 +213,6 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun upStyle() {
ReadBookConfig.let {
tv_text_bold.isSelected = it.textBold
dsb_text_size.progress = it.textSize - 5
dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50
dsb_line_size.progress = it.lineSpacingExtra

@ -0,0 +1,55 @@
package io.legado.app.ui.book.read.config
import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import io.legado.app.R
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.text.StrokeTextView
import org.jetbrains.anko.sdk27.listeners.onClick
class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) {
private val spannableString = SpannableString("中/粗/细")
private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor)
private var onChanged: (() -> Unit)? = null
init {
text = spannableString
if (!isInEditMode) {
upUi(ReadBookConfig.textBold)
}
onClick {
selectType()
}
}
private fun upUi(type: Int) {
spannableString.removeSpan(enabledSpan)
when (type) {
0 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
1 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
2 -> spannableString.setSpan(enabledSpan, 4, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
text = spannableString
}
private fun selectType() {
context.alert(titleResource = R.string.text_font_weight_converter) {
items(context.resources.getStringArray(R.array.text_font_weight).toList()) { _, i ->
ReadBookConfig.textBold = i
upUi(i)
onChanged?.invoke()
}
}.show()
}
fun onChanged(unit: () -> Unit) {
onChanged = unit
}
}

@ -22,10 +22,8 @@ class TocRegexViewModel(application: Application) : BaseViewModel(application) {
fun importDefault() {
execute {
AnalyzeTxtFile.getDefaultRules().let {
App.db.txtTocRule().deleteDefault()
App.db.txtTocRule().insert(*it.toTypedArray())
}
App.db.txtTocRule().deleteDefault()
AnalyzeTxtFile.getDefaultEnabledRules()
}
}

@ -10,8 +10,12 @@ import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.utils.activity
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean
@ -78,18 +82,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
textPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
draw(canvas, textLine, relativeOffset)
}
if (!ReadBookConfig.isScroll) return
//滚动翻页
@ -97,37 +90,38 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val nextPage = relativePage(1)
relativeOffset = relativeOffset(1)
nextPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
draw(canvas, textLine, relativeOffset)
}
if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) {
relativePage(2).textLines.forEach { textLine ->
draw(canvas, textLine, relativeOffset)
}
}
}
private fun draw(
canvas: Canvas,
textLine: TextLine,
relativeOffset: Float
) {
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
if (textLine.isImage) {
drawImage(canvas, textLine, lineTop, lineBottom)
} else {
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
isTitle = textLine.isTitle,
isReadAloud = textLine.isReadAloud
)
}
if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) {
relativePage(2).textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
}
}
}
/**
@ -144,7 +138,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
) {
val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint
textPaint.color =
if (isReadAloud) context.accentColor else ReadBookConfig.durConfig.textColor()
if (isReadAloud) context.accentColor else ReadBookConfig.textColor
textChars.forEach {
canvas.drawText(it.charData, it.start, lineBase, textPaint)
if (it.selected) {
@ -153,6 +147,27 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
}
}
/**
* 绘制图片
*/
private fun drawImage(
canvas: Canvas,
textLine: TextLine,
lineTop: Float,
lineBottom: Float
) {
textLine.textChars.forEach { textChar ->
val rectF = RectF(textChar.start, lineTop, textChar.end, lineBottom)
ImageProvider.getImage(
ReadBook.book!!,
textPage.chapterIndex,
textChar.charData
)?.let {
canvas.drawBitmap(it, null, rectF, null)
}
}
}
/**
* 滚动事件
*/

@ -7,13 +7,12 @@ import android.view.MotionEvent
import android.widget.FrameLayout
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import com.hankcs.hanlp.HanLP
import io.legado.app.R
import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.BatteryView
import io.legado.app.utils.dp
import io.legado.app.utils.getCompatColor
@ -57,14 +56,14 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_footer_left.typeface = ChapterProvider.typeface
tv_footer_middle.typeface = ChapterProvider.typeface
tv_footer_right.typeface = ChapterProvider.typeface
bv_header_left.setColor(durConfig.textColor())
tv_header_left.setColor(durConfig.textColor())
tv_header_middle.setColor(durConfig.textColor())
tv_header_right.setColor(durConfig.textColor())
bv_footer_left.setColor(durConfig.textColor())
tv_footer_left.setColor(durConfig.textColor())
tv_footer_middle.setColor(durConfig.textColor())
tv_footer_right.setColor(durConfig.textColor())
bv_header_left.setColor(textColor)
tv_header_left.setColor(textColor)
tv_header_middle.setColor(textColor)
tv_header_right.setColor(textColor)
bv_footer_left.setColor(textColor)
tv_footer_left.setColor(textColor)
tv_footer_middle.setColor(textColor)
tv_footer_right.setColor(textColor)
//显示状态栏时隐藏header
vw_status_bar.setPadding(0, context.statusBarHeight, 0, 0)
vw_status_bar.isGone = hideStatusBar
@ -207,11 +206,7 @@ class ContentView(context: Context) : FrameLayout(context) {
@SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply {
val title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(textPage.title)
2 -> HanLP.convertToTraditionalChinese(textPage.title)
else -> textPage.title
}
val title = textPage.title
tvTitle?.text = title
tvPage?.text = "${index.plus(1)}/$pageSize"
tvTotalProgress?.text = readProgress

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

Loading…
Cancel
Save