Merge pull request #1 from gedoor/master

merge
pull/264/head
Celeter 4 years ago committed by GitHub
commit b9d472af24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      app/build.gradle
  2. 26
      app/proguard-rules.pro
  3. 4
      app/src/debug/res/values/strings.xml
  4. 11
      app/src/main/AndroidManifest.xml
  5. 3
      app/src/main/assets/18PlusList.txt
  6. 20
      app/src/main/assets/readConfig.json
  7. 47
      app/src/main/assets/txtTocRule.json
  8. 128
      app/src/main/assets/updateLog.md
  9. 85
      app/src/main/java/io/legado/app/App.kt
  10. 5
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  11. 13
      app/src/main/java/io/legado/app/constant/AppConst.kt
  12. 16
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  13. 39
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  14. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  15. 6
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  16. 2
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  17. 2
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  18. 25
      app/src/main/java/io/legado/app/data/entities/Book.kt
  19. 25
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  20. 2
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  21. 3
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  22. 14
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  23. 4
      app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt
  24. 35
      app/src/main/java/io/legado/app/help/AppConfig.kt
  25. 3
      app/src/main/java/io/legado/app/help/BookHelp.kt
  26. 8
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  27. 38
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  28. 54
      app/src/main/java/io/legado/app/help/SourceHelp.kt
  29. 4
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  30. 7
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  31. 39
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  32. 32
      app/src/main/java/io/legado/app/help/storage/OldReplace.kt
  33. 110
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  34. 8
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  35. 15
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  36. 14
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  37. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt
  38. 32
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  39. 23
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  40. 2
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  41. 24
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  42. 17
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  43. 35
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  44. 157
      app/src/main/java/io/legado/app/service/DownloadService.kt
  45. 6
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  46. 63
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  47. 32
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  48. 29
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  49. 9
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  50. 6
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  51. 2
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  52. 23
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  53. 4
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  54. 24
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  55. 10
      app/src/main/java/io/legado/app/ui/book/changesource/DiffCallBack.kt
  56. 79
      app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt
  57. 3
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt
  58. 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  59. 12
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  60. 31
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  61. 81
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  62. 22
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  63. 17
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  64. 100
      app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt
  65. 7
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  66. 5
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt
  67. 5
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  68. 55
      app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt
  69. 10
      app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
  70. 4
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt
  71. 48
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  72. 2
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  73. 64
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  74. 32
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  75. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  76. 13
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  77. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  78. 2
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  79. 62
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  80. 23
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  81. 24
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  82. 11
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  83. 90
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  84. 8
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  85. 46
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  86. 5
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  87. 7
      app/src/main/java/io/legado/app/ui/config/FileAssociationViewModel.kt
  88. 133
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  89. 35
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  90. 12
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  91. 22
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  92. 24
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt
  93. 26
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt
  94. 57
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt
  95. 24
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  96. 3
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  97. 26
      app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt
  98. 21
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  99. 50
      app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt
  100. 19
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -56,7 +56,7 @@ android {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
} }
applicationIdSuffix '.release' applicationIdSuffix '.release'
minifyEnabled true minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug { debug {
@ -66,7 +66,7 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
versionNameSuffix 'debug' versionNameSuffix 'debug'
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
variant.outputs.all { variant.outputs.all {
@ -87,7 +87,7 @@ android {
} }
compileOptions { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8 // Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -109,6 +109,7 @@ kapt {
} }
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
@ -117,16 +118,16 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase //fireBase
implementation 'com.google.firebase:firebase-core:17.3.0' implementation 'com.google.firebase:firebase-core:17.4.3'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX //androidX
implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0' implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
@ -143,7 +144,7 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime:2.1.2' implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'
@ -154,7 +155,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.5.7' implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
// //
def coroutines_version = '1.3.3' def coroutines_version = '1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -193,6 +194,8 @@ dependencies {
// //
implementation 'com.hankcs:hanlp:portable-1.7.7' implementation 'com.hankcs:hanlp:portable-1.7.7'
//E-Ink
//implementation 'fadeapp.widgets:scrollless-recyclerView:1.0.2'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

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

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

@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app"> package="io.legado.app">
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -237,6 +240,10 @@
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity" android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind" android:screenOrientation="behind"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--RSS条目-->
<activity
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--RSS阅读--> <!--RSS阅读-->
<activity <activity
android:name=".ui.rss.read.ReadRssActivity" android:name=".ui.rss.read.ReadRssActivity"
@ -255,10 +262,6 @@
<activity <activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity" android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--订阅条目-->
<activity
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--Rss收藏--> <!--Rss收藏-->
<activity <activity
android:name=".ui.rss.favorites.RssFavoritesActivity" android:name=".ui.rss.favorites.RssFavoritesActivity"

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

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

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

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

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

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

@ -20,7 +20,7 @@ object AppConst {
const val UA_NAME = "User-Agent" const val UA_NAME = "User-Agent"
val userAgent: String by lazy { val userAgent: String by lazy {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
} }
val SCRIPT_ENGINE: ScriptEngine by lazy { val SCRIPT_ENGINE: ScriptEngine by lazy {
@ -56,4 +56,15 @@ object AppConst {
const val notificationIdAudio = 1144772 const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773 const val notificationIdWeb = 1144773
const val notificationIdDownload = 1144774 const val notificationIdDownload = 1144774
val urlOption: String by lazy {
"""
,{
"charset": "",
"method": "POST",
"body": "",
"headers": {"User-Agent": ""}
}
""".trimIndent()
}
} }

@ -43,4 +43,20 @@ object PreferKey {
const val readStyleSelect = "readStyleSelect" const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces" const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh" const val readBodyToLh = "readBodyToLh"
const val textFullJustify = "textFullJustify"
const val textBottomJustify = "textBottomJustify"
const val autoReadSpeed = "autoReadSpeed"
const val barElevation = "barElevation"
const val transparentStatusBar = "transparentStatusBar"
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, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class], RssStar::class, TxtTocRule::class],
version = 11, version = 13,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -31,7 +31,17 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context): AppDatabase { fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addMigrations(object : Migration(10, 11) { .addMigrations(migration_10_11, migration_11_12, migration_12_13)
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
}
})
.allowMainThreadQueries()
.build()
}
private val migration_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules") database.execSQL("DROP TABLE txtTocRules")
database.execSQL( database.execSQL(
@ -42,13 +52,26 @@ abstract class AppDatabase : RoomDatabase() {
""" """
) )
} }
})
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
} }
})
.build() private val migration_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
ALTER TABLE rssSources ADD style TEXT
"""
)
}
}
private val migration_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0
"""
)
}
} }
} }

@ -21,9 +21,6 @@ interface BookDao {
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'") @Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'")
fun observeLocalUri(): LiveData<List<String>> 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") @Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun observeByGroup(group: Int): LiveData<List<Book>> 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") @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>> 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") @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun liveExplore(): LiveData<List<BookSource>> fun liveExplore(): LiveData<List<BookSource>>

@ -19,7 +19,7 @@ interface SearchBookDao {
@Query("select * from searchBooks where bookUrl = :bookUrl") @Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook? 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? fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query( @Query(

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

@ -56,10 +56,14 @@ data class Book(
return origin == BookType.local return origin == BookType.local
} }
fun isTxt(): Boolean { fun isLocalTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true) return isLocalBook() && originName.endsWith(".txt", true)
} }
fun isOnLineTxt(): Boolean {
return !isLocalBook() && type == 0
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other is Book) { if (other is Book) {
return other.bookUrl == bookUrl return other.bookUrl == bookUrl
@ -71,14 +75,16 @@ data class Book(
return bookUrl.hashCode() return bookUrl.hashCode()
} }
@Ignore @delegate:Transient
@delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null override val variableMap by lazy {
get() { GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
if (field == null) {
field = GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
} }
return field
override fun putVariable(key: String, value: String) {
variableMap[key] = value
variable = GSON.toJson(variableMap)
} }
@Ignore @Ignore
@ -97,11 +103,6 @@ data class Book(
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
override fun putVariable(key: String, value: String) {
variableMap?.put(key, value)
variable = GSON.toJson(variableMap)
}
fun fileCharset(): Charset { fun fileCharset(): Charset {
return charset(charset ?: "UTF-8") return charset(charset ?: "UTF-8")
} }

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

@ -21,7 +21,7 @@ data class RssArticle(
) { ) {
override fun hashCode(): Int { override fun hashCode(): Int {
return super.hashCode() return link.hashCode()
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -24,6 +24,7 @@ data class RssSource(
var sourceGroup: String? = null, var sourceGroup: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var sortUrl: String? = null, var sortUrl: String? = null,
var articleStyle: Int = 0,
//列表规则 //列表规则
var ruleArticles: String? = null, var ruleArticles: String? = null,
var ruleNextPage: String? = null, var ruleNextPage: String? = null,
@ -34,9 +35,11 @@ data class RssSource(
var ruleImage: String? = null, var ruleImage: String? = null,
var ruleLink: String? = null, var ruleLink: String? = null,
var ruleContent: String? = null, var ruleContent: String? = null,
var style: String? = null,
var header: String? = null, var header: String? = null,
var enableJs: Boolean = false, var enableJs: Boolean = false,
var loadWithBaseUrl: Boolean = false, var loadWithBaseUrl: Boolean = false,
var customOrder: Int = 0 var customOrder: Int = 0
) : Parcelable, JsExtensions { ) : Parcelable, JsExtensions {

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

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

@ -1,7 +1,7 @@
package io.legado.app.help package io.legado.app.help
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -13,6 +13,7 @@ object AppConfig {
return when (context.getPrefString(PreferKey.themeMode, "0")) { return when (context.getPrefString(PreferKey.themeMode, "0")) {
"1" -> false "1" -> false
"2" -> true "2" -> true
"3" -> false
else -> context.sysIsDarkMode() else -> context.sysIsDarkMode()
} }
} }
@ -27,10 +28,13 @@ object AppConfig {
} }
} }
val isEInkMode: Boolean
get() = App.INSTANCE.getPrefString(PreferKey.themeMode) == "3"
var isTransparentStatusBar: Boolean var isTransparentStatusBar: Boolean
get() = App.INSTANCE.getPrefBoolean("transparentStatusBar") get() = App.INSTANCE.getPrefBoolean(PreferKey.transparentStatusBar)
set(value) { set(value) {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value) App.INSTANCE.putPrefBoolean(PreferKey.transparentStatusBar, value)
} }
val requestedDirection: String? val requestedDirection: String?
@ -80,9 +84,6 @@ object AppConfig {
val ttsSpeechPer: String val ttsSpeechPer: String
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0" 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) val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false)
var chineseConverterType: Int var chineseConverterType: Int
@ -122,25 +123,19 @@ object AppConfig {
} }
var elevation: Int var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1) @SuppressLint("PrivateResource")
get() = App.INSTANCE.getPrefInt(
PreferKey.barElevation,
App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
)
set(value) { set(value) {
App.INSTANCE.putPrefInt("elevation", value) App.INSTANCE.putPrefInt(PreferKey.barElevation, value)
} }
val autoChangeSource: Boolean get() = App.INSTANCE.getPrefBoolean("autoChangeSource", true)
val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true) val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true)
val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google" val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google"
} }
val Context.channel: String
get() {
try {
val pm = packageManager
val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return appInfo.metaData.getString("channel") ?: ""
} catch (e: Exception) {
e.printStackTrace();
}
return ""
}

@ -241,9 +241,10 @@ object BookHelp {
val contents = arrayListOf<String>() val contents = arrayListOf<String>()
c.split("\n").forEach { c.split("\n").forEach {
val str = it.replace("^\\s+".toRegex(), "") val str = it.replace("^\\s+".toRegex(), "")
.replace("\r", "")
if (contents.isEmpty()) { if (contents.isEmpty()) {
contents.add(title) contents.add(title)
if (it != title && it.isNotEmpty()) { if (str != title && it.isNotEmpty()) {
contents.add("${ReadBookConfig.bodyIndent}$str") contents.add("${ReadBookConfig.bodyIndent}$str")
} }
} else if (str.isNotEmpty()) { } else if (str.isNotEmpty()) {

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

@ -27,8 +27,16 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!! GSON.fromJsonArray<Config>(json)!!
} }
val durConfig get() = getConfig(styleSelect) val durConfig get() = getConfig(styleSelect)
var bg: Drawable? = null var bg: Drawable? = null
var bgMeanColor: Int = 0 var bgMeanColor: Int = 0
val textColor: Int
get() =
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
Color.BLACK
} else {
durConfig.textColor()
}
init { init {
upConfig() upConfig()
@ -59,6 +67,7 @@ object ReadBookConfig {
val json = configFile.readText() val json = configFile.readText()
return GSON.fromJsonArray(json) return GSON.fromJsonArray(json)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
} }
} }
return null return null
@ -69,6 +78,10 @@ object ReadBookConfig {
val dm = resources.displayMetrics val dm = resources.displayMetrics
val width = dm.widthPixels val width = dm.widthPixels
val height = dm.heightPixels val height = dm.heightPixels
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
bg = ColorDrawable(Color.WHITE)
bgMeanColor = Color.WHITE
} else {
bg = durConfig.bgDrawable(width, height).apply { bg = durConfig.bgDrawable(width, height).apply {
if (this is BitmapDrawable) { if (this is BitmapDrawable) {
bgMeanColor = BitmapUtils.getMeanColor(bitmap) bgMeanColor = BitmapUtils.getMeanColor(bitmap)
@ -77,6 +90,8 @@ object ReadBookConfig {
} }
} }
} }
isScroll = pageAnim == 3
}
fun save() { fun save() {
Coroutine.async { Coroutine.async {
@ -103,6 +118,11 @@ object ReadBookConfig {
} }
//配置写入读取 //配置写入读取
var autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46)
set(value) {
field = value
App.INSTANCE.putPrefInt(PreferKey.autoReadSpeed, value)
}
var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
set(value) { set(value) {
field = value field = value
@ -117,16 +137,16 @@ object ReadBookConfig {
App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value) 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) { 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 var isScroll = pageAnim == 3
val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true) val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true)
val textFullJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textFullJustify, true)
val textBottomJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textBottomJustify, true)
var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
set(value) { set(value) {
field = value field = value
@ -141,7 +161,7 @@ object ReadBookConfig {
private val config get() = if (shareLayout) getConfig(5) else durConfig private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean var textBold: Int
get() = config.textBold get() = config.textBold
set(value) { set(value) {
config.textBold = value config.textBold = value
@ -288,11 +308,11 @@ object ReadBookConfig {
private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
private var textColor: String = "#3E3D3B",//白天文字颜色 private var textColor: String = "#3E3D3B",//白天文字颜色
private var textColorNight: String = "#ADADAD",//夜间文字颜色 private var textColorNight: String = "#ADADAD",//夜间文字颜色
var textBold: Boolean = false,//是否粗体字 var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体
var textSize: Int = 20,//文字大小 var textSize: Int = 20,//文字大小
var letterSpacing: Float = 0.5f,//字间距 var letterSpacing: Float = 0.1f,//字间距
var lineSpacingExtra: Int = 12,//行间距 var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距 var paragraphSpacing: Int = 4,//段距
var titleMode: Int = 0,//标题居中 var titleMode: Int = 0,//标题居中
var titleSize: Int = 0, var titleSize: Int = 0,
var titleTopSpacing: Int = 0, var titleTopSpacing: Int = 0,

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

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

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

@ -6,116 +6,81 @@ import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils 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 org.jetbrains.anko.toast
import java.io.File import java.io.File
object ImportOldData { object ImportOldData {
fun import(context: Context, file: File) { fun import(context: Context, file: File) {
GlobalScope.launch(Dispatchers.IO) {
try {// 导入书架 try {// 导入书架
val shelfFile = val shelfFile =
FileUtils.createFileIfNotExist(file, "myBookShelf.json") FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText() val json = shelfFile.readText()
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书籍${importCount}") context.toast("成功导入书籍${importCount}")
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入书籍失败\n${e.localizedMessage}") context.toast("导入书籍失败\n${e.localizedMessage}")
} }
}
try {// Book source try {// Book source
val sourceFile = val sourceFile =
FileUtils.getFile(file, "myBookSource.json") FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText() val json = sourceFile.readText()
val importCount = importOldSource(json) val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书源${importCount}") context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入源失败\n${e.localizedMessage}") context.toast("导入源失败\n${e.localizedMessage}")
} }
}
try {// Replace rules try {// Replace rules
val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json") val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json")
if (ruleFile.exists()) { if (ruleFile.exists()) {
val json = ruleFile.readText() val json = ruleFile.readText()
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
context.toast("成功导入替换规则${importCount}") context.toast("成功导入替换规则${importCount}")
}
} else { } else {
withContext(Dispatchers.Main) {
context.toast("未找到替换规则") context.toast("未找到替换规则")
} }
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}") context.toast("导入替换规则失败\n${e.localizedMessage}")
} }
} }
}
}
fun importUri(uri: Uri) { fun importUri(uri: Uri) {
Coroutine.async {
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach {
when (it.name) { when (it.name) {
"myBookShelf.json" -> "myBookShelf.json" ->
try { try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书籍${importCount}") App.INSTANCE.toast("成功导入书籍${importCount}")
} }
}
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}") App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}")
} }
}
"myBookSource.json" -> "myBookSource.json" ->
try { try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldSource(json) val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书源${importCount}") App.INSTANCE.toast("成功导入书源${importCount}")
} }
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入源失败\n${e.localizedMessage}") App.INSTANCE.toast("导入源失败\n${e.localizedMessage}")
} }
}
"myBookReplaceRule.json" -> "myBookReplaceRule.json" ->
try { try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入替换规则${importCount}") App.INSTANCE.toast("成功导入替换规则${importCount}")
} }
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}") App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}")
} }
} }
} }
} }
}
}
private fun importOldBookshelf(json: String): Int { private fun importOldBookshelf(json: String): Int {
val books = OldBook.toNewBook(json) val books = OldBook.toNewBook(json)
@ -136,14 +101,12 @@ object ImportOldData {
return bookSources.size return bookSources.size
} }
fun importOldReplaceRule(json: String): Int { fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>() val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { OldReplace.jsonToReplaceRule(jsonItem.jsonString())?.let {
if (it.isValid()){ if (it.isValid()){
replaceRules.add(it) replaceRules.add(it)
} }

@ -0,0 +1,32 @@
package io.legado.app.help.storage
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.*
object OldReplace {
fun jsonToReplaceRule(json: String): ReplaceRule? {
var replaceRule: ReplaceRule? = null
runCatching {
replaceRule = GSON.fromJsonObject<ReplaceRule>(json.trim())
}
runCatching {
if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) {
val jsonItem = Restore.jsonPath.parse(json.trim())
val rule = ReplaceRule()
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
rule.pattern = jsonItem.readString("$.regex") ?: ""
if (rule.pattern.isEmpty()) return null
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
rule.replacement = jsonItem.readString("$.replacement") ?: ""
rule.isRegex = jsonItem.readBool("$.isRegex") == true
rule.scope = jsonItem.readString("$.useTo")
rule.isEnabled = jsonItem.readBool("$.enable") == true
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
return rule
}
}
return replaceRule
}
}

@ -1,9 +1,9 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import androidx.annotation.Keep
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.entities.rule.* import io.legado.app.data.entities.rule.*
import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.* import io.legado.app.utils.*
@ -14,13 +14,15 @@ object OldRule {
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
fun jsonToBookSource(json: String): BookSource? { fun jsonToBookSource(json: String): BookSource? {
var source: BookSource? = null val source = BookSource()
runCatching { val sourceAny = try {
source = GSON.fromJsonObject<BookSource>(json.trim()) GSON.fromJsonObject<BookSourceAny>(json.trim())
} } catch (e: Exception) {
runCatching { null
if (source == null || source?.ruleToc == null) { }
source = BookSource().apply { try {
if (sourceAny?.ruleToc == null) {
source.apply {
val jsonItem = jsonPath.parse(json.trim()) val jsonItem = jsonPath.parse(json.trim())
bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: "" bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: ""
bookSourceName = jsonItem.readString("bookSourceName") ?: "" bookSourceName = jsonItem.readString("bookSourceName") ?: ""
@ -82,11 +84,76 @@ object OldRule {
nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext")) nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext"))
) )
} }
} else {
source.bookSourceUrl = sourceAny.bookSourceUrl
source.bookSourceName = sourceAny.bookSourceName
source.bookSourceGroup = sourceAny.bookSourceGroup
source.bookSourceType = sourceAny.bookSourceType
source.bookUrlPattern = sourceAny.bookUrlPattern
source.customOrder = sourceAny.customOrder
source.enabled = sourceAny.enabled
source.enabledExplore = sourceAny.enabledExplore
source.header = sourceAny.header
source.loginUrl = sourceAny.loginUrl
source.lastUpdateTime = sourceAny.lastUpdateTime
source.weight = sourceAny.weight
source.exploreUrl = sourceAny.exploreUrl
source.ruleExplore = if (sourceAny.ruleExplore is String) {
GSON.fromJsonObject(sourceAny.ruleExplore as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleExplore))
}
source.searchUrl = sourceAny.searchUrl
source.ruleSearch = if (sourceAny.ruleSearch is String) {
GSON.fromJsonObject(sourceAny.ruleSearch as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleSearch))
}
source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) {
GSON.fromJsonObject(sourceAny.ruleBookInfo as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleBookInfo))
} }
source.ruleToc = if (sourceAny.ruleToc is String) {
GSON.fromJsonObject(sourceAny.ruleToc as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleToc))
}
source.ruleContent = if (sourceAny.ruleContent is String) {
GSON.fromJsonObject(sourceAny.ruleContent as? String)
} else {
GSON.fromJsonObject(GSON.toJson(sourceAny.ruleContent))
}
}
} catch (e: Exception) {
e.printStackTrace()
} }
return source return source
} }
@Keep
data class BookSourceAny(
var bookSourceName: String = "", // 名称
var bookSourceGroup: String? = null, // 分组
var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null, // 详情页url正则
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现
var header: String? = null, // 请求头
var loginUrl: String? = null, // 登录地址
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, // 发现url
var ruleExplore: Any? = null, // 发现规则
var searchUrl: String? = null, // 搜索url
var ruleSearch: Any? = null, // 搜索规则
var ruleBookInfo: Any? = null, // 书籍信息页规则
var ruleToc: Any? = null, // 目录页规则
var ruleContent: Any? = null // 正文页规则
)
// default规则适配 // default规则适配
// #正则#替换内容 替换成 ##正则##替换内容 // #正则#替换内容 替换成 ##正则##替换内容
// | 替换成 || // | 替换成 ||
@ -137,10 +204,10 @@ object OldRule {
} }
} }
if (allinone) { if (allinone) {
newRule = "+" + newRule newRule = "+$newRule"
} }
if (reverse) { if (reverse) {
newRule = "-" + newRule newRule = "-$newRule"
} }
return newRule return newRule
} }
@ -211,27 +278,4 @@ object OldRule {
return GSON.toJson(map) return GSON.toJson(map)
} }
fun jsonToReplaceRule(json: String): ReplaceRule? {
var replaceRule: ReplaceRule? = null
runCatching {
replaceRule = GSON.fromJsonObject<ReplaceRule>(json.trim())
}
runCatching {
if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) {
val jsonItem = jsonPath.parse(json.trim())
val rule = ReplaceRule()
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
rule.pattern = jsonItem.readString("$.regex") ?: ""
if (rule.pattern.isEmpty()) return null
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
rule.replacement = jsonItem.readString("$.replacement") ?: ""
rule.isRegex = jsonItem.readBool("$.isRegex") == true
rule.scope = jsonItem.readString("$.useTo")
rule.isEnabled = jsonItem.readBool("$.enable") == true
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
return rule
}
}
return replaceRule
}
} }

@ -2,7 +2,6 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
@ -13,7 +12,6 @@ import io.legado.app.BuildConfig
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
@ -126,18 +124,14 @@ object Restore {
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46)
} }
ChapterProvider.upStyle() ChapterProvider.upStyle()
ReadBook.loadContent(resetPageOffset = false) ReadBook.loadContent(resetPageOffset = false)
} }
withContext(Main) { withContext(Main) {
if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight() App.INSTANCE.applyDayNight()
} else if (!AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
App.INSTANCE.applyDayNight()
} else {
postEvent(EventBus.RECREATE, "true") postEvent(EventBus.RECREATE, "true")
}
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
} }

@ -93,18 +93,13 @@ object ATH {
} }
} }
fun setLightNavigationBarAuto(activity: Activity, bgColor: Int) { fun setNavigationBarColorAuto(
setLightNavigationBar(activity, ColorUtils.isColorLight(bgColor)) activity: Activity,
} color: Int = ThemeStore.navigationBarColor(activity)
) {
fun setNavigationBarColorAuto(activity: Activity) {
setNavigationBarColor(activity, ThemeStore.navigationBarColor(activity))
}
fun setNavigationBarColor(activity: Activity, color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.navigationBarColor = color 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.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
/** /**
@ -194,7 +195,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun primaryColor(context: Context): Int { fun primaryColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR, ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64")) ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
@ -212,7 +213,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun accentColor(context: Context): Int { fun accentColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_ACCENT_COLOR, ThemeStorePrefKeys.KEY_ACCENT_COLOR,
ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238")) 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 { fun navigationBarColor(context: Context): Int {
return if (!coloredNavigationBar(context)) { return if (!coloredNavigationBar(context)) {
Color.BLACK 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 @CheckResult
@ -277,7 +281,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun backgroundColor(context: Context): Int { fun backgroundColor(context: Context = App.INSTANCE): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, ThemeStorePrefKeys.KEY_BACKGROUND_COLOR,
ATHUtils.resolveColor(context, android.R.attr.colorBackground) ATHUtils.resolveColor(context, android.R.attr.colorBackground)
@ -294,7 +298,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun bottomBackground(context: Context): Int { fun bottomBackground(context: Context = App.INSTANCE): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND,
ATHUtils.resolveColor(context, android.R.attr.colorBackground) ATHUtils.resolveColor(context, android.R.attr.colorBackground)

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

@ -51,7 +51,7 @@ class AnalyzeUrl(
private var queryStr: String? = null private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>() private val fieldMap = LinkedHashMap<String, String>()
private var charset: String? = null private var charset: String? = null
private var body: Any? = null private var body: String? = null
private var requestBody: RequestBody? = null private var requestBody: RequestBody? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
@ -157,11 +157,12 @@ class AnalyzeUrl(
baseUrl = it baseUrl = it
} }
if (urlArray.size > 1) { if (urlArray.size > 1) {
val options = GSON.fromJsonObject<UrlOption>(urlArray[1]) val option = GSON.fromJsonObject<UrlOption>(urlArray[1])
options?.let { _ -> option?.let { _ ->
options.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST } option.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
options.headers?.let { headers -> option.headers?.let { headers ->
if (headers is Map<*, *>) { if (headers is Map<*, *>) {
@Suppress("unchecked_cast")
headerMap.putAll(headers as Map<out String, String>) headerMap.putAll(headers as Map<out String, String>)
} }
if (headers is String) { if (headers is String) {
@ -169,9 +170,13 @@ class AnalyzeUrl(
?.let { headerMap.putAll(it) } ?.let { headerMap.putAll(it) }
} }
} }
body = options.body charset = option.charset
charset = options.charset body = if (option.body is String) {
options.webView?.let { option.body
} else {
GSON.toJson(option.body)
}
option.webView?.let {
if (it.toString().isNotEmpty()) { if (it.toString().isNotEmpty()) {
useWebView = true useWebView = true
} }
@ -190,19 +195,18 @@ class AnalyzeUrl(
} }
RequestMethod.POST -> { RequestMethod.POST -> {
body?.let { body?.let {
if (it is String) { if (it.isJson()) {
analyzeFields(it) requestBody = RequestBody.create(jsonType, it)
} else { } else {
body = RequestBody.create(jsonType, GSON.toJson(it)) analyzeFields(it)
} }
} ?: let { } ?: let {
body = FormBody.Builder().build() requestBody = FormBody.Builder().build()
} }
} }
} }
} }
/** /**
* 解析QueryMap * 解析QueryMap
*/ */
@ -287,7 +291,7 @@ class AnalyzeUrl(
params.requestMethod = method params.requestMethod = method
params.javaScript = jsStr params.javaScript = jsStr
params.sourceRegex = sourceRegex params.sourceRegex = sourceRegex
params.postData = body?.toString()?.toByteArray() params.postData = body?.toByteArray()
params.tag = tag params.tag = tag
return HttpHelper.ajax(params) return HttpHelper.ajax(params)
} }

@ -40,6 +40,7 @@ class AnalyzeTxtFile {
book: Book, book: Book,
pattern: Pattern? pattern: Pattern?
): ArrayList<BookChapter> { ): ArrayList<BookChapter> {
bookStream.seek(0)
val toc = arrayListOf<BookChapter>() val toc = arrayListOf<BookChapter>()
var tocRule: TxtTocRule? = null var tocRule: TxtTocRule? = null
val rulePattern = pattern ?: let { val rulePattern = pattern ?: let {
@ -83,7 +84,8 @@ class AnalyzeTxtFile {
//获取章节内容 //获取章节内容
val chapterContent = blockContent.substring(seekPos, chapterStart) val chapterContent = blockContent.substring(seekPos, chapterStart)
val chapterLength = chapterContent.toByteArray(charset).size val chapterLength = chapterContent.toByteArray(charset).size
if (chapterLength > 30000 && pattern == null) { val lastStart = toc.lastOrNull()?.start ?: 0
if (curOffset + chapterLength - lastStart > 50000 && pattern == null) {
//移除不匹配的规则 //移除不匹配的规则
tocRules.remove(tocRule) tocRules.remove(tocRule)
return analyze(bookStream, book, null) return analyze(bookStream, book, null)
@ -139,6 +141,11 @@ class AnalyzeTxtFile {
//设置指针偏移 //设置指针偏移
seekPos += chapterContent.length seekPos += chapterContent.length
} }
if (seekPos == 0 && length > 50000 && pattern == null) {
//移除不匹配的规则
tocRules.remove(tocRule)
return analyze(bookStream, book, null)
}
} else { //进行本地虚拟分章 } else { //进行本地虚拟分章
//章节在buffer的偏移量 //章节在buffer的偏移量
var chapterOffset = 0 var chapterOffset = 0
@ -271,18 +278,20 @@ class AnalyzeTxtFile {
} }
private fun getTocRules(): List<TxtTocRule> { private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all val rules = App.db.txtTocRule().enabled
if (rules.isEmpty()) { if (rules.isEmpty()) {
return getDefaultRules() return getDefaultEnabledRules()
} }
return rules return rules
} }
fun getDefaultRules(): List<TxtTocRule> { fun getDefaultEnabledRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let { GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let { txtTocRules ->
App.db.txtTocRule().insert(*it.toTypedArray()) App.db.txtTocRule().insert(*txtTocRules.toTypedArray())
return it return txtTocRules.filter {
it.enable
}
} }
} }
return emptyList() return emptyList()

@ -47,7 +47,7 @@ object LocalBook {
fun deleteBook(book: Book, deleteOriginal: Boolean) { fun deleteBook(book: Book, deleteOriginal: Boolean) {
kotlin.runCatching { kotlin.runCatching {
if (book.isTxt()) { if (book.isLocalTxt()) {
val bookFile = FileUtils.getFile(AnalyzeTxtFile.cacheFolder, book.originName) val bookFile = FileUtils.getFile(AnalyzeTxtFile.cacheFolder, book.originName)
bookFile.delete() bookFile.delete()
} }

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

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

@ -7,20 +7,18 @@ import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private val threadCount = AppConfig.threadCount private var threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>() private val allIds = ArrayList<String>()
@ -58,40 +56,35 @@ class CheckSourceService : BaseService() {
checkedIds.clear() checkedIds.clear()
allIds.addAll(ids) allIds.addAll(ids)
processIndex = 0 processIndex = 0
threadCount = min(allIds.size, threadCount)
updateNotification(0, getString(R.string.progress_show, 0, allIds.size)) updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
for (i in 0 until min(threadCount, allIds.size)) { for (i in 0 until threadCount) {
check() check()
} }
} }
/**
* 检测
*/
private fun check() { private fun check() {
val index = processIndex
synchronized(this) { synchronized(this) {
processIndex++ processIndex++
} }
execute { execute {
if (processIndex < allIds.size) { if (index < allIds.size) {
val sourceUrl = allIds[processIndex] val sourceUrl = allIds[index]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) { if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl) onNext(sourceUrl)
} else { } else {
check(source) CheckSource(source).check(this, searchPool) {
onNext(it)
} }
} ?: onNext(sourceUrl)
} }
} ?: onNext(sourceUrl)
} }
} }
private fun check(source: BookSource) {
val webBook = WebBook(source)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
} }
private fun onNext(sourceUrl: String) { private fun onNext(sourceUrl: String) {
@ -102,7 +95,7 @@ class CheckSourceService : BaseService() {
checkedIds.size, checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size) getString(R.string.progress_show, checkedIds.size, allIds.size)
) )
if (processIndex >= allIds.size + min(threadCount, allIds.size) - 1) { if (processIndex >= allIds.size + threadCount - 1) {
stopSelf() stopSelf()
} }
} }

@ -9,6 +9,7 @@ import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -24,14 +25,21 @@ import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
class DownloadService : BaseService() { class DownloadService : BaseService() {
private val threadCount = AppConfig.threadCount
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val handler = Handler() private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }
private val bookMap = hashMapOf<String, Book>()
private val webBookMap = hashMapOf<String, WebBook>()
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>(); private val downloadCount = hashMapOf<String, DownloadCount>()
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadingList = arrayListOf<String>()
@Volatile
private var downloadingCount = 0
private var notificationContent = "正在启动下载" private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy { private val notificationBuilder by lazy {
@ -78,92 +86,136 @@ class DownloadService : BaseService() {
postEvent(EventBus.UP_DOWNLOAD, downloadMap) postEvent(EventBus.UP_DOWNLOAD, downloadMap)
} }
private fun getBook(bookUrl: String): Book? {
var 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) {
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) { private fun addDownloadData(bookUrl: String?, start: Int, end: Int) {
bookUrl ?: return bookUrl ?: return
if (downloadMap.containsKey(bookUrl)) { if (downloadMap.containsKey(bookUrl)) {
toast("该书已在下载列表") toast("该书已在下载列表")
return return
} }
downloadCount[bookUrl] = DownloadCount()
execute { execute {
val chapterMap = downloadMap[bookUrl] ?: linkedSetOf<BookChapter>().apply {
downloadMap[bookUrl] = this
}
App.db.bookChapterDao().getChapterList(bookUrl, start, end).let { App.db.bookChapterDao().getChapterList(bookUrl, start, end).let {
chapterMap.addAll(it) if (it.isNotEmpty()) {
val chapters = linkedSetOf<BookChapter>()
chapters.addAll(it)
downloadMap[bookUrl] = chapters
}
} }
for (i in 0 until threadCount) {
if (downloadingCount < threadCount) {
download() download()
} }
} }
}
}
private fun removeDownload(bookUrl: String?) { private fun removeDownload(bookUrl: String?) {
downloadMap.remove(bookUrl) downloadMap.remove(bookUrl)
finalMap.remove(bookUrl) finalMap.remove(bookUrl)
} }
private fun updateNotification(downloadCount:DownloadCount, totalCount: Int, content: String){
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
private fun download() { private fun download() {
val task = Coroutine.async(this, context = searchPool) { ++downloadingCount
downloadMap.forEach { entry -> tasks.add(Coroutine.async(this, context = searchPool) {
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 (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) { val bookChapter: BookChapter? = synchronized(this@DownloadService) {
if (!BookHelp.hasContent(book, chapter)) { 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( webBook.getContent(
book, book,
chapter, bookChapter,
scope = this, scope = this,
context = searchPool context = searchPool
).onSuccess(IO) { content -> ).onSuccess(IO) { content ->
downloadCount[entry.key]?.increaseSuccess() downloadCount[book.bookUrl]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content) BookHelp.saveContent(book, bookChapter, content)
} }.onFinally(IO) {
.onFinally(IO) {
synchronized(this@DownloadService) { synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished() downloadCount[book.bookUrl]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) } downloadCount[book.bookUrl]?.let {
updateNotification(
it,
downloadMap[book.bookUrl]?.size,
bookChapter.title
)
}
val chapterMap = val chapterMap =
finalMap[book.bookUrl] finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply { ?: linkedSetOf<BookChapter>().apply {
finalMap[book.bookUrl] = this finalMap[book.bookUrl] = this
} }
chapterMap.add(chapter) chapterMap.add(bookChapter)
if (chapterMap.size == entry.value.size) { if (chapterMap.size == downloadMap[book.bookUrl]?.size) {
downloadMap.remove(book.bookUrl) downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl) finalMap.remove(book.bookUrl)
downloadCount.remove(entry.key) downloadCount.remove(book.bookUrl)
} }
} }
} }
postDownloading(true)
} else { } else {
//无需下载的,设置为增加成功 //无需下载的,设置为增加成功
downloadCount[entry.key]?.increaseSuccess() downloadCount[book.bookUrl]?.increaseSuccess()
downloadCount[entry.key]?.increaseFinished() downloadCount[book.bookUrl]?.increaseFinished()
} postDownloading(true)
} }
} }
})
} }
} private fun postDownloading(hasChapter: Boolean) {
} --downloadingCount
if (hasChapter) {
tasks.add(task) download()
task.invokeOnCompletion { } else {
tasks.remove(task) if (downloadingCount < 1) {
if (tasks.isEmpty) { stopDownload()
stopSelf()
} }
} }
} }
@ -180,6 +232,15 @@ class DownloadService : BaseService() {
handler.postDelayed(runnable, 1000) handler.postDelayed(runnable, 1000)
} }
private fun updateNotification(
downloadCount: DownloadCount,
totalCount: Int?,
content: String
) {
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
/** /**
* 更新通知 * 更新通知
*/ */
@ -199,11 +260,11 @@ class DownloadService : BaseService() {
var successCount = 0 //下载成功的条目数量 var successCount = 0 //下载成功的条目数量
fun increaseSuccess() { fun increaseSuccess() {
++successCount; ++successCount
} }
fun increaseFinished() { fun increaseFinished() {
++downloadFinishedCount; ++downloadFinishedCount
} }
} }
} }

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

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

@ -2,15 +2,24 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.service.CheckSourceService import io.legado.app.service.CheckSourceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
object CheckSource { class CheckSource(val source: BookSource) {
fun start(context: Context, sources: LinkedHashSet<BookSource>) { companion object {
var keyword = "我的"
fun start(context: Context, sources: List<BookSource>) {
if (sources.isEmpty()) { if (sources.isEmpty()) {
context.toast(R.string.non_select) context.toast(R.string.non_select)
return return
@ -33,3 +42,22 @@ object CheckSource {
} }
} }
} }
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)
}
}
}

@ -1,11 +1,13 @@
package io.legado.app.service.help package io.legado.app.service.help
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.hankcs.hanlp.HanLP
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
@ -36,9 +38,8 @@ object ReadBook {
var msg: String? = null var msg: String? = null
private val loadingChapters = arrayListOf<Int>() private val loadingChapters = arrayListOf<Int>()
fun resetData(book: Book, noSource: (name: String, author: String) -> Unit) { fun resetData(book: Book) {
this.book = book this.book = book
titleDate.postValue(book.name)
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
durPageIndex = book.durChapterPos durPageIndex = book.durChapterPos
isLocalBook = book.origin == BookType.local isLocalBook = book.origin == BookType.local
@ -46,20 +47,19 @@ object ReadBook {
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
upWebBook(book, noSource) titleDate.postValue(book.name)
upWebBook(book)
} }
fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) { fun upWebBook(book: Book) {
book ?: return webBook = if (book.origin == BookType.local) {
if (book.origin == BookType.local) { null
webBook = null
} else { } else {
val bookSource = App.db.bookSourceDao().getBookSource(book.origin) val bookSource = App.db.bookSourceDao().getBookSource(book.origin)
if (bookSource != null) { if (bookSource != null) {
webBook = WebBook(bookSource) WebBook(bookSource)
} else { } else {
webBook = null null
noSource.invoke(book.name, book.author)
} }
} }
} }
@ -149,7 +149,7 @@ object ReadBook {
} }
private fun curPageChanged() { private fun curPageChanged() {
callBack?.upPageProgress() callBack?.pageChanged()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause) readAloud(!BaseReadAloudService.pause)
} }
@ -293,6 +293,11 @@ object ReadBook {
) { ) {
Coroutine.async { Coroutine.async {
if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) { 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( val contents = BookHelp.disposeContent(
chapter.title, chapter.title,
book!!.name, book!!.name,
@ -345,7 +350,7 @@ object ReadBook {
interface CallBack { interface CallBack {
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upView() fun upView()
fun upPageProgress() fun pageChanged()
fun contentLoadFinish() fun contentLoadFinish()
} }
} }

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

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

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

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

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

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

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

@ -12,10 +12,12 @@ import com.google.android.material.snackbar.Snackbar
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.service.help.Download import io.legado.app.service.help.Download
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
@ -35,29 +37,49 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
private val exportRequestCode = 32 private val exportRequestCode = 32
private val exportBookPathKey = "exportBookPath" private val exportBookPathKey = "exportBookPath"
lateinit var adapter: DownloadAdapter 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 menu: Menu? = null
private var exportPosition = -1 private var exportPosition = -1
private val groupList: ArrayList<BookGroup> = arrayListOf()
private var groupId: Int = -1
override val viewModel: DownloadViewModel override val viewModel: DownloadViewModel
get() = getViewModel(DownloadViewModel::class.java) get() = getViewModel(DownloadViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
groupId = intent.getIntExtra("groupId", -1)
title_bar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all)
initRecyclerView() initRecyclerView()
initLiveData() initGroupData()
initBookData()
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.download, menu) menuInflater.inflate(R.menu.download, menu)
this.menu = menu
return super.onCompatCreateOptionsMenu(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 { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_download -> launch(IO) { R.id.menu_download -> launch(IO) {
if (adapter.downloadMap.isNullOrEmpty()) { if (adapter.downloadMap.isNullOrEmpty()) {
App.db.bookDao().webBooks.forEach { book -> adapter.getItems().forEach { book ->
Download.start( Download.start(
this@DownloadActivity, this@DownloadActivity,
book.bookUrl, book.bookUrl,
@ -69,6 +91,21 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
Download.stop(this@DownloadActivity) Download.stop(this@DownloadActivity)
} }
} }
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) return super.onCompatOptionsItemSelected(item)
} }
@ -79,21 +116,39 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
private fun initLiveData() { private fun initBookData() {
bookshelfLiveData?.removeObservers(this) booksLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload() booksLiveData = when (groupId) {
bookshelfLiveData?.observe(this, Observer { list -> 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)) { val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime } 1 -> booksDownload.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name } 2 -> booksDownload.sortedBy { it.name }
3 -> list.sortedBy { it.order } 3 -> booksDownload.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime } else -> booksDownload.sortedByDescending { it.durChapterTime }
} }
adapter.setItems(books) adapter.setItems(books)
initCacheSize(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>) { private fun initCacheSize(books: List<Book>) {
launch(IO) { launch(IO) {
books.forEach { book -> books.forEach { book ->

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

@ -86,6 +86,9 @@ class BookInfoActivity :
R.id.menu_refresh -> { R.id.menu_refresh -> {
upLoading(true) upLoading(true)
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
if (it.isLocalBook()) {
it.tocUrl = ""
}
viewModel.loadBookInfo(it) viewModel.loadBookInfo(it)
} }
} }

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

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

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler 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.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton 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.lib.theme.accentColor
import io.legado.app.receiver.TimeBatteryReceiver import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
@ -69,6 +72,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
ReadAloudDialog.CallBack, ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack, ChangeSourceDialog.CallBack,
ReadBook.CallBack, ReadBook.CallBack,
AutoReadDialog.CallBack,
TocRegexDialog.CallBack, TocRegexDialog.CallBack,
ReplaceEditDialog.CallBack, ReplaceEditDialog.CallBack,
ColorPickerDialogListener { ColorPickerDialogListener {
@ -85,7 +89,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
private val mHandler = Handler() private val mHandler = Handler()
private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) } private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) }
private val autoPageRunnable: Runnable = Runnable { autoPagePlus() }
override var autoPageProgress = 0
override var isAutoPage = false
private var screenTimeOut: Long = 0 private var screenTimeOut: Long = 0
private var timeBatteryReceiver: TimeBatteryReceiver? = null private var timeBatteryReceiver: TimeBatteryReceiver? = null
override val pageFactory: TextPageFactory get() = page_view.pageFactory override val pageFactory: TextPageFactory get() = page_view.pageFactory
@ -129,6 +135,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
ReadBook.saveRead()
timeBatteryReceiver?.let { timeBatteryReceiver?.let {
unregisterReceiver(it) unregisterReceiver(it)
timeBatteryReceiver = null timeBatteryReceiver = null
@ -140,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 * 初始化View
*/ */
@ -194,7 +216,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (item.groupId) { when (item.groupId) {
R.id.menu_group_on_line -> item.isVisible = onLine R.id.menu_group_on_line -> item.isVisible = onLine
R.id.menu_group_local -> 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 -> R.id.menu_group_login ->
item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
else -> if (item.itemId == R.id.menu_enable_replace) { else -> if (item.itemId == R.id.menu_enable_replace) {
@ -238,7 +260,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
onReplaceRuleSave() onReplaceRuleSave()
} }
R.id.menu_book_info -> ReadBook.book?.let { R.id.menu_book_info -> ReadBook.book?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl)) startActivity<BookInfoActivity>(
Pair("name", it.name),
Pair("author", it.author)
)
} }
R.id.menu_toc_regex -> TocRegexDialog.show( R.id.menu_toc_regex -> TocRegexDialog.show(
supportFragmentManager, supportFragmentManager,
@ -344,6 +369,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
toast(R.string.read_aloud_pause) toast(R.string.read_aloud_pause)
return true return true
} }
if (isAutoPage) {
autoPageStop()
return true
}
} }
} }
} }
@ -466,6 +495,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
if (getPrefBoolean("volumeKeyPageOnPlay") if (getPrefBoolean("volumeKeyPageOnPlay")
|| BaseReadAloudService.pause || BaseReadAloudService.pause
) { ) {
page_view.pageDelegate?.isCancel = false
page_view.pageDelegate?.keyTurnPage(direction) page_view.pageDelegate?.keyTurnPage(direction)
return true return true
} }
@ -511,14 +541,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
tv_pre.isEnabled = ReadBook.durChapterIndex != 0 tv_pre.isEnabled = ReadBook.durChapterIndex != 0
tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1
} ?: let {
tv_chapter_name.gone()
tv_chapter_url.gone()
} }
} }
} }
/** /**
* 更新进度条 * 页面改变
*/ */
override fun upPageProgress() { override fun pageChanged() {
autoPageProgress = 0
launch { launch {
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
} }
@ -539,12 +573,18 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
override fun clickCenter() { override fun clickCenter() {
if (BaseReadAloudService.isRun) { when {
BaseReadAloudService.isRun -> {
showReadAloudDialog() showReadAloudDialog()
} else { }
isAutoPage -> {
AutoReadDialog().show(supportFragmentManager, "autoRead")
}
else -> {
read_menu.runMenuIn() read_menu.runMenuIn()
} }
} }
}
/** /**
* 显示朗读菜单 * 显示朗读菜单
@ -557,7 +597,33 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 自动翻页 * 自动翻页
*/ */
override fun autoPage() { override fun autoPage() {
if (isAutoPage) {
autoPageStop()
} else {
isAutoPage = true
page_view.upContent()
page_view.upContent(1)
autoPagePlus()
}
read_menu.setAutoPage(isAutoPage)
}
override fun autoPageStop() {
isAutoPage = false
mHandler.removeCallbacks(autoPageRunnable)
page_view.upContent()
}
private fun autoPagePlus() {
mHandler.removeCallbacks(autoPageRunnable)
autoPageProgress++
if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 10) {
autoPageProgress = 0
page_view.fillPage(PageDelegate.Direction.NEXT)
} else {
page_view.invalidate()
}
mHandler.postDelayed(autoPageRunnable, 100)
} }
/** /**
@ -608,6 +674,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
*/ */
override fun upSystemUiVisibility() { override fun upSystemUiVisibility() {
Help.upSystemUiVisibility(this, !read_menu.isVisible) Help.upSystemUiVisibility(this, !read_menu.isVisible)
upNavigationBarColor()
} }
/** /**

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

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

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

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

@ -16,6 +16,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
@ -42,8 +43,8 @@ class ReadAloudConfigDialog : DialogFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val view = LinearLayout(context) val view = LinearLayout(requireContext())
view.setBackgroundResource(R.color.background) view.setBackgroundColor(requireContext().backgroundColor)
view.id = R.id.tag1 view.id = R.id.tag1
container?.addView(view) container?.addView(view)
return view return view

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

@ -25,12 +25,10 @@ class TipConfigDialog : BaseDialogFragment() {
Help.upSystemUiVisibility(it) Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.setLayout(
val attr = it.attributes ViewGroup.LayoutParams.MATCH_PARENT,
attr.dimAmount = 0.0f ViewGroup.LayoutParams.WRAP_CONTENT
it.attributes = attr )
it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
}
} }
override fun onCreateView( override fun onCreateView(

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

@ -78,6 +78,7 @@ object ChapterProvider {
item.title = bookChapter.title item.title = bookChapter.title
item.upLinesPosition() item.upLinesPosition()
} }
return TextChapter( return TextChapter(
bookChapter.index, bookChapter.index,
bookChapter.title, bookChapter.title,
@ -123,7 +124,7 @@ object ChapterProvider {
val x = if (isTitle && ReadBookConfig.titleMode == 1) val x = if (isTitle && ReadBookConfig.titleMode == 1)
(visibleWidth - layout.getLineWidth(lineIndex)) / 2 (visibleWidth - layout.getLineWidth(lineIndex)) / 2
else 0f else 0f
addCharsToLineLast(textLine, words, textPaint, x) addCharsToLineLast(textLine, words, textPaint, x, true)
} else { } else {
//中间行 //中间行
addCharsToLineMiddle(textLine, words, textPaint, desiredWidth, 0f) addCharsToLineMiddle(textLine, words, textPaint, desiredWidth, 0f)
@ -161,6 +162,10 @@ object ChapterProvider {
desiredWidth: Float desiredWidth: Float
) { ) {
var x = 0f var x = 0f
if (!ReadBookConfig.textFullJustify) {
addCharsToLineLast(textLine, words, textPaint, x, false)
return
}
val bodyIndent = ReadBookConfig.bodyIndent val bodyIndent = ReadBookConfig.bodyIndent
val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
bodyIndent.toStringArray().forEach { bodyIndent.toStringArray().forEach {
@ -186,6 +191,10 @@ object ChapterProvider {
desiredWidth: Float, desiredWidth: Float,
startX: Float startX: Float
) { ) {
if (!ReadBookConfig.textFullJustify) {
addCharsToLineLast(textLine, words, textPaint, startX, false)
return
}
val gapCount: Int = words.length - 1 val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount val d = (visibleWidth - desiredWidth) / gapCount
var x = startX var x = startX
@ -209,9 +218,10 @@ object ChapterProvider {
textLine: TextLine, textLine: TextLine,
words: String, words: String,
textPaint: TextPaint, textPaint: TextPaint,
startX: Float startX: Float,
isLast: Boolean
) { ) {
textLine.text = "$words\n" textLine.text = if (isLast) "$words\n" else words
var x = startX var x = startX
words.toStringArray().forEach { words.toStringArray().forEach {
val cw = StaticLayout.getDesiredWidth(it, textPaint) val cw = StaticLayout.getDesiredWidth(it, textPaint)
@ -262,23 +272,37 @@ object ChapterProvider {
App.INSTANCE.removePref(PreferKey.readBookFont) App.INSTANCE.removePref(PreferKey.readBookFont)
Typeface.SANS_SERIF Typeface.SANS_SERIF
} }
// 字体统一处理
val bold = Typeface.create(typeface, Typeface.BOLD)
val normal = Typeface.create(typeface, Typeface.NORMAL)
val (titleFont, textFont) = when (ReadBookConfig.textBold) {
1 -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
Pair(Typeface.create(typeface, 900, false), bold)
else
Pair(bold, bold)
}
2 -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
Pair(normal, Typeface.create(typeface, 300, false))
else
Pair(normal, normal)
}
else -> Pair(bold, normal)
}
//标题 //标题
titlePaint = TextPaint() titlePaint = TextPaint()
titlePaint.color = ReadBookConfig.durConfig.textColor() titlePaint.color = ReadBookConfig.textColor
titlePaint.letterSpacing = ReadBookConfig.letterSpacing titlePaint.letterSpacing = ReadBookConfig.letterSpacing
titlePaint.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { titlePaint.typeface = titleFont
Typeface.create(typeface, if (ReadBookConfig.textBold) 900 else 700, false)
} else {
Typeface.create(typeface, Typeface.BOLD)
}
titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat() titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat()
titlePaint.isAntiAlias = true titlePaint.isAntiAlias = true
//正文 //正文
contentPaint = TextPaint() contentPaint = TextPaint()
contentPaint.color = ReadBookConfig.durConfig.textColor() contentPaint.color = ReadBookConfig.textColor
contentPaint.letterSpacing = ReadBookConfig.letterSpacing contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL contentPaint.typeface = textFont
contentPaint.typeface = Typeface.create(typeface, style)
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
contentPaint.isAntiAlias = true contentPaint.isAntiAlias = true
//间距 //间距

@ -144,7 +144,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
) { ) {
val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint
textPaint.color = textPaint.color =
if (isReadAloud) context.accentColor else ReadBookConfig.durConfig.textColor() if (isReadAloud) context.accentColor else ReadBookConfig.textColor
textChars.forEach { textChars.forEach {
canvas.drawText(it.charData, it.start, lineBase, textPaint) canvas.drawText(it.charData, it.start, lineBase, textPaint)
if (it.selected) { if (it.selected) {

@ -7,10 +7,8 @@ import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import com.hankcs.hanlp.HanLP
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst.timeFormat import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig import io.legado.app.help.ReadTipConfig
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
@ -49,18 +47,22 @@ class ContentView(context: Context) : FrameLayout(context) {
fun upStyle() { fun upStyle() {
ReadBookConfig.apply { ReadBookConfig.apply {
bv_header_left.typeface = ChapterProvider.typeface
tv_header_left.typeface = ChapterProvider.typeface tv_header_left.typeface = ChapterProvider.typeface
tv_header_middle.typeface = ChapterProvider.typeface tv_header_middle.typeface = ChapterProvider.typeface
tv_header_right.typeface = ChapterProvider.typeface tv_header_right.typeface = ChapterProvider.typeface
bv_footer_left.typeface = ChapterProvider.typeface
tv_footer_left.typeface = ChapterProvider.typeface tv_footer_left.typeface = ChapterProvider.typeface
tv_footer_middle.typeface = ChapterProvider.typeface tv_footer_middle.typeface = ChapterProvider.typeface
tv_footer_right.typeface = ChapterProvider.typeface tv_footer_right.typeface = ChapterProvider.typeface
tv_header_left.setColor(durConfig.textColor()) bv_header_left.setColor(textColor)
tv_header_middle.setColor(durConfig.textColor()) tv_header_left.setColor(textColor)
tv_header_right.setColor(durConfig.textColor()) tv_header_middle.setColor(textColor)
tv_footer_left.setColor(durConfig.textColor()) tv_header_right.setColor(textColor)
tv_footer_middle.setColor(durConfig.textColor()) bv_footer_left.setColor(textColor)
tv_footer_right.setColor(durConfig.textColor()) tv_footer_left.setColor(textColor)
tv_footer_middle.setColor(textColor)
tv_footer_right.setColor(textColor)
//显示状态栏时隐藏header //显示状态栏时隐藏header
vw_status_bar.setPadding(0, context.statusBarHeight, 0, 0) vw_status_bar.setPadding(0, context.statusBarHeight, 0, 0)
vw_status_bar.isGone = hideStatusBar vw_status_bar.isGone = hideStatusBar
@ -86,18 +88,14 @@ class ContentView(context: Context) : FrameLayout(context) {
fun upTipStyle() { fun upTipStyle() {
ReadTipConfig.apply { ReadTipConfig.apply {
val tipHeaderLeftNone = tipHeaderLeft == none tv_header_left.isInvisible = tipHeaderLeft != chapterTitle
val tipHeaderRightNone = tipHeaderRight == none bv_header_left.isInvisible = tipHeaderLeft == none || !tv_header_left.isInvisible
val tipHeaderMiddleNone = tipHeaderMiddle == none tv_header_right.isGone = tipHeaderRight == none
val tipFooterLeftNone = tipFooterLeft == none tv_header_middle.isGone = tipHeaderMiddle == none
val tipFooterRightNone = tipFooterRight == none tv_footer_left.isInvisible = tipFooterLeft != chapterTitle
val tipFooterMiddleNone = tipFooterMiddle == none bv_footer_left.isInvisible = tipFooterLeft == none || !tv_footer_left.isInvisible
tv_header_left.isInvisible = tipHeaderLeftNone tv_footer_right.isGone = tipFooterRight == none
tv_header_right.isGone = tipHeaderRightNone tv_footer_middle.isGone = tipFooterMiddle == none
tv_header_middle.isGone = tipHeaderMiddleNone
tv_footer_left.isInvisible = tipFooterLeftNone
tv_footer_right.isGone = tipFooterRightNone
tv_footer_middle.isGone = tipFooterMiddleNone
ll_header.isGone = hideHeader ll_header.isGone = hideHeader
ll_footer.isGone = hideFooter ll_footer.isGone = hideFooter
} }
@ -115,10 +113,10 @@ class ContentView(context: Context) : FrameLayout(context) {
textSize = 12f textSize = 12f
} }
tvTime = when (ReadTipConfig.time) { tvTime = when (ReadTipConfig.time) {
ReadTipConfig.tipHeaderLeft -> tv_header_left ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> tv_footer_left ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null else -> null
@ -128,10 +126,10 @@ class ContentView(context: Context) : FrameLayout(context) {
textSize = 12f textSize = 12f
} }
tvBattery = when (ReadTipConfig.battery) { tvBattery = when (ReadTipConfig.battery) {
ReadTipConfig.tipHeaderLeft -> tv_header_left ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> tv_footer_left ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null else -> null
@ -141,10 +139,10 @@ class ContentView(context: Context) : FrameLayout(context) {
textSize = 10f textSize = 10f
} }
tvPage = when (ReadTipConfig.page) { tvPage = when (ReadTipConfig.page) {
ReadTipConfig.tipHeaderLeft -> tv_header_left ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> tv_footer_left ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null else -> null
@ -154,10 +152,10 @@ class ContentView(context: Context) : FrameLayout(context) {
textSize = 12f textSize = 12f
} }
tvTotalProgress = when (ReadTipConfig.totalProgress) { tvTotalProgress = when (ReadTipConfig.totalProgress) {
ReadTipConfig.tipHeaderLeft -> tv_header_left ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> tv_footer_left ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null else -> null
@ -167,10 +165,10 @@ class ContentView(context: Context) : FrameLayout(context) {
textSize = 12f textSize = 12f
} }
tvPageAndTotal = when (ReadTipConfig.pageAndTotal) { tvPageAndTotal = when (ReadTipConfig.pageAndTotal) {
ReadTipConfig.tipHeaderLeft -> tv_header_left ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> tv_footer_left ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null else -> null
@ -207,11 +205,7 @@ class ContentView(context: Context) : FrameLayout(context) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply { fun setProgress(textPage: TextPage) = textPage.apply {
val title = when (AppConfig.chineseConverterType) { val title = textPage.title
1 -> HanLP.convertToSimplifiedChinese(textPage.title)
2 -> HanLP.convertToTraditionalChinese(textPage.title)
else -> textPage.title
}
tvTitle?.text = title tvTitle?.text = title
tvPage?.text = "${index.plus(1)}/$pageSize" tvPage?.text = "${index.plus(1)}/$pageSize"
tvTotalProgress?.text = readProgress tvTotalProgress?.text = readProgress

@ -3,14 +3,19 @@ package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.delegate.* import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.screenshot
import kotlinx.android.synthetic.main.activity_book_read.view.*
class PageView(context: Context, attrs: AttributeSet) : class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs), FrameLayout(context, attrs),
@ -23,6 +28,12 @@ class PageView(context: Context, attrs: AttributeSet) :
var prevPage: ContentView = ContentView(context) var prevPage: ContentView = ContentView(context)
var curPage: ContentView = ContentView(context) var curPage: ContentView = ContentView(context)
var nextPage: ContentView = ContentView(context) var nextPage: ContentView = ContentView(context)
private val autoPageRect by lazy { Rect() }
private val autoPagePint by lazy {
Paint().apply {
color = context.accentColor
}
}
init { init {
addView(nextPage) addView(nextPage)
@ -44,8 +55,22 @@ class PageView(context: Context, attrs: AttributeSet) :
override fun dispatchDraw(canvas: Canvas) { override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
pageDelegate?.onDraw(canvas) pageDelegate?.onDraw(canvas)
if (callBack.isAutoPage) {
nextPage.screenshot()?.let {
val bottom =
page_view.height * callBack.autoPageProgress / (ReadBookConfig.autoReadSpeed * 10)
autoPageRect.set(0, 0, page_view.width, bottom)
canvas.drawBitmap(it, autoPageRect, autoPageRect, null)
canvas.drawRect(
0f,
bottom.toFloat() - 1,
page_view.width.toFloat(),
bottom.toFloat(),
autoPagePint
)
}
}
} }
override fun computeScroll() { override fun computeScroll() {
@ -94,9 +119,10 @@ class PageView(context: Context, attrs: AttributeSet) :
} }
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
if (ReadBookConfig.isScroll) { if (ReadBookConfig.isScroll && !callBack.isAutoPage) {
curPage.setContent(pageFactory.currentPage, resetPageOffset) curPage.setContent(pageFactory.currentPage, resetPageOffset)
} else { } else {
curPage.resetPageOffset()
when (relativePosition) { when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage) -1 -> prevPage.setContent(pageFactory.prevPage)
1 -> nextPage.setContent(pageFactory.nextPage) 1 -> nextPage.setContent(pageFactory.nextPage)
@ -169,6 +195,8 @@ class PageView(context: Context, attrs: AttributeSet) :
interface CallBack { interface CallBack {
val isInitFinish: Boolean val isInitFinish: Boolean
val isAutoPage: Boolean
val autoPageProgress: Int
fun clickCenter() fun clickCenter()
fun screenOffTimerStart() fun screenOffTimerStart()
fun showTextActionMenu() fun showTextActionMenu()

@ -104,7 +104,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
} }
override fun nextPageByAnim() { override fun nextPageByAnim() {
super.nextPageByAnim() abort()
if (!hasNext()) return if (!hasNext()) return
setDirection(Direction.NEXT) setDirection(Direction.NEXT)
setTouchPoint(viewWidth.toFloat(), 0f) setTouchPoint(viewWidth.toFloat(), 0f)
@ -112,7 +112,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
} }
override fun prevPageByAnim() { override fun prevPageByAnim() {
super.prevPageByAnim() abort()
if (!hasPrev()) return if (!hasPrev()) return
setDirection(Direction.PREV) setDirection(Direction.PREV)
setTouchPoint(0f, 0f) setTouchPoint(0f, 0f)

@ -15,6 +15,7 @@ import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import kotlin.math.abs import kotlin.math.abs
import io.legado.app.R
abstract class PageDelegate(protected val pageView: PageView) : abstract class PageDelegate(protected val pageView: PageView) :
GestureDetector.SimpleOnGestureListener() { GestureDetector.SimpleOnGestureListener() {
@ -170,13 +171,9 @@ abstract class PageDelegate(protected val pageView: PageView) :
open fun onScroll() {}//移动contentView, slidePage open fun onScroll() {}//移动contentView, slidePage
open fun nextPageByAnim() { abstract fun nextPageByAnim()
abort()
}
open fun prevPageByAnim() { abstract fun prevPageByAnim()
abort()
}
open fun keyTurnPage(direction: Direction) { open fun keyTurnPage(direction: Direction) {
if (isRunning) return if (isRunning) return
@ -320,7 +317,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
val hasPrev = pageView.pageFactory.hasPrev() val hasPrev = pageView.pageFactory.hasPrev()
if (!hasPrev) { if (!hasPrev) {
if (!snackBar.isShown) { if (!snackBar.isShown) {
snackBar.setText("没有上一页") snackBar.setText(R.string.no_prev_page)
snackBar.show() snackBar.show()
} }
} }
@ -334,7 +331,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
val hasNext = pageView.pageFactory.hasNext() val hasNext = pageView.pageFactory.hasNext()
if (!hasNext) { if (!hasNext) {
if (!snackBar.isShown) { if (!snackBar.isShown) {
snackBar.setText("没有下一页") snackBar.setText(R.string.no_next_page)
snackBar.show() snackBar.show()
} }
} }

@ -79,12 +79,12 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
} }
override fun nextPageByAnim() { override fun nextPageByAnim() {
super.nextPageByAnim() abort()
startScroll(0, 0, 0, -ChapterProvider.visibleHeight) startScroll(0, 0, 0, -ChapterProvider.visibleHeight)
} }
override fun prevPageByAnim() { override fun prevPageByAnim() {
super.prevPageByAnim() abort()
startScroll(0, 0, 0, ChapterProvider.visibleHeight) startScroll(0, 0, 0, ChapterProvider.visibleHeight)
} }
} }

@ -4,6 +4,7 @@ import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.ChapterProvider import io.legado.app.ui.book.read.page.ChapterProvider
import java.text.DecimalFormat import java.text.DecimalFormat
@ -19,6 +20,7 @@ data class TextPage(
) { ) {
fun upLinesPosition() = ChapterProvider.apply { fun upLinesPosition() = ChapterProvider.apply {
if (!ReadBookConfig.textBottomJustify) return@apply
if (textLines.size <= 1) return@apply if (textLines.size <= 1) return@apply
if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply
val surplus = (visibleBottom - textLines.last().lineBottom) val surplus = (visibleBottom - textLines.last().lineBottom)

@ -18,64 +18,40 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.name != newItem.name) { return when {
return false oldItem.name != newItem.name -> false
oldItem.author != newItem.author -> false
else -> true
} }
if (oldItem.author != newItem.author) {
return false
}
return true
} }
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.origins.size != newItem.origins.size) { return when {
return false oldItem.origins.size != newItem.origins.size -> false
} oldItem.coverUrl != newItem.coverUrl -> false
if (oldItem.coverUrl != newItem.coverUrl) { oldItem.kind != newItem.kind -> false
return false oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
} oldItem.intro != newItem.intro -> false
if (oldItem.kind != newItem.kind) { else -> true
return false
}
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
return false
} }
if (oldItem.intro != newItem.intro) {
return false
}
return true
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val payload = Bundle() val payload = Bundle()
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) payload.putString("name", newItem.name)
payload.putString("name", newItem.name) if (oldItem.author != newItem.author) payload.putString("author", newItem.author)
} if (oldItem.origins.size != newItem.origins.size)
if (oldItem.author != newItem.author) {
payload.putString("author", newItem.author)
}
if (oldItem.origins.size != newItem.origins.size) {
payload.putInt("origins", newItem.origins.size) payload.putInt("origins", newItem.origins.size)
} if (oldItem.coverUrl != newItem.coverUrl) payload.putString("cover", newItem.coverUrl)
if (oldItem.coverUrl != newItem.coverUrl) { if (oldItem.kind != newItem.kind) payload.putString("kind", newItem.kind)
payload.putString("cover", newItem.coverUrl) if (oldItem.latestChapterTitle != newItem.latestChapterTitle)
}
if (oldItem.kind != newItem.kind) {
payload.putString("kind", newItem.kind)
}
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
payload.putString("last", newItem.latestChapterTitle) payload.putString("last", newItem.latestChapterTitle)
} if (oldItem.intro != newItem.intro) payload.putString("intro", newItem.intro)
if (oldItem.intro != newItem.intro) { if (payload.isEmpty) return null
payload.putString("intro", newItem.intro)
}
if (payload.isEmpty) {
return null
}
return payload return payload
} }
} }

@ -8,7 +8,6 @@ import android.view.View.VISIBLE
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
@ -52,7 +51,6 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
private var menu: Menu? = null private var menu: Menu? = null
private var precisionSearchMenuItem: MenuItem? = null private var precisionSearchMenuItem: MenuItem? = null
private var groups = linkedSetOf<String>() private var groups = linkedSetOf<String>()
private var refreshTime = System.currentTimeMillis()
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView() initRecyclerView()
@ -181,14 +179,13 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
upGroupMenu() upGroupMenu()
}) })
viewModel.searchBookLiveData.observe(this, Observer { viewModel.searchBookLiveData.observe(this, Observer {
upSearchItems(it, false) upSearchItems(it)
}) })
viewModel.isSearchLiveData.observe(this, Observer { viewModel.isSearchLiveData.observe(this, Observer {
if (it) { if (it) {
startSearch() startSearch()
} else { } else {
searchFinally() searchFinally()
upSearchItems(viewModel.searchBooks, true)
} }
}) })
} }
@ -284,14 +281,8 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
* 更新搜索结果 * 更新搜索结果
*/ */
@Synchronized @Synchronized
private fun upSearchItems(items: List<SearchBook>, isMandatoryUpdate: Boolean) { private fun upSearchItems(items: List<SearchBook>) {
val searchItems = ArrayList(items) adapter.setItems(items)
if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
refreshTime = System.currentTimeMillis()
val diffResult =
DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), searchItems))
adapter.setItems(searchItems, diffResult)
}
} }
/** /**
@ -317,7 +308,10 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
override fun showBookInfo(name: String, author: String) { override fun showBookInfo(name: String, author: String) {
viewModel.getSearchBook(name, author) { searchBook -> viewModel.getSearchBook(name, author) { searchBook ->
searchBook?.let { searchBook?.let {
startActivity<BookInfoActivity>(Pair("bookUrl", it.bookUrl)) startActivity<BookInfoActivity>(
Pair("name", it.name),
Pair("author", it.author)
)
} }
} }
} }
@ -327,7 +321,8 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
*/ */
override fun showBookInfo(book: Book) { override fun showBookInfo(book: Book) {
startActivity<BookInfoActivity>( startActivity<BookInfoActivity>(
Pair("bookUrl", book.bookUrl) Pair("name", book.name),
Pair("author", book.author)
) )
} }

@ -1,6 +1,7 @@
package io.legado.app.ui.book.search package io.legado.app.ui.book.search
import android.app.Application import android.app.Application
import android.os.Handler
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
@ -12,15 +13,18 @@ import io.legado.app.utils.getPrefBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
class SearchViewModel(application: Application) : BaseViewModel(application) class SearchViewModel(application: Application) : BaseViewModel(application),
, SearchBookModel.CallBack { SearchBookModel.CallBack {
val handler = Handler()
private val searchBookModel = SearchBookModel(this, this) private val searchBookModel = SearchBookModel(this, this)
var isSearchLiveData = MutableLiveData<Boolean>() var isSearchLiveData = MutableLiveData<Boolean>()
var searchBookLiveData = MutableLiveData<List<SearchBook>>() var searchBookLiveData = MutableLiveData<List<SearchBook>>()
var searchKey: String = "" var searchKey: String = ""
var isLoading = false var isLoading = false
var searchBooks = arrayListOf<SearchBook>() private var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L private var searchID = 0L
private var postTime = 0L
private val sendRunnable = Runnable { upAdapter() }
/** /**
* 开始搜索 * 开始搜索
@ -36,6 +40,18 @@ class SearchViewModel(application: Application) : BaseViewModel(application)
searchBookModel.search(searchID, searchKey) searchBookModel.search(searchID, searchKey)
} }
@Synchronized
private fun upAdapter() {
if (System.currentTimeMillis() >= postTime + 500) {
handler.removeCallbacks(sendRunnable)
postTime = System.currentTimeMillis()
searchBookLiveData.postValue(searchBooks)
} else {
handler.removeCallbacks(sendRunnable)
handler.postDelayed(sendRunnable, 500 - System.currentTimeMillis() + postTime)
}
}
override fun onSearchStart() { override fun onSearchStart() {
isSearchLiveData.postValue(true) isSearchLiveData.postValue(true)
} }
@ -151,7 +167,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application)
}) })
if (!scope.isActive) return if (!scope.isActive) return
searchBooks = copyDataS searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS) upAdapter()
} }
} }

@ -367,16 +367,7 @@ class BookSourceEditActivity :
override fun sendText(text: String) { override fun sendText(text: String) {
if (text == AppConst.keyboardToolChars[0]) { if (text == AppConst.keyboardToolChars[0]) {
insertText( insertText(AppConst.urlOption)
"""
,{
"charset": "",
"method": "POST",
"body": "",
"headers": {"User-Agent": ""}
}
""".trimIndent()
)
} else { } else {
insertText(text) insertText(text)
} }

@ -57,6 +57,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private var bookSourceLiveDate: LiveData<List<BookSource>>? = null private var bookSourceLiveDate: LiveData<List<BookSource>>? = null
private var groups = linkedSetOf<String>() private var groups = linkedSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private var sort = 0
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme() initUriScheme()
@ -74,6 +75,8 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
groupMenu = menu?.findItem(R.id.menu_group)?.subMenu groupMenu = menu?.findItem(R.id.menu_group)?.subMenu
groupMenu?.findItem(R.id.action_sort)?.subMenu
?.setGroupCheckable(R.id.menu_group_sort, true, true)
upGroupMenu() upGroupMenu()
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
@ -92,6 +95,32 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
allowExtensions = arrayOf("txt", "json") allowExtensions = arrayOf("txt", "json")
) )
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_sort_manual -> {
item.isChecked = true
sort = 0
initLiveDataBookSource(search_view.query?.toString())
}
R.id.menu_sort_auto -> {
item.isChecked = true
sort = 2
initLiveDataBookSource(search_view.query?.toString())
}
R.id.menu_sort_pin_yin -> {
item.isChecked = true
sort = 3
initLiveDataBookSource(search_view.query?.toString())
}
R.id.menu_sort_url -> {
item.isChecked = true
sort = 4
initLiveDataBookSource(search_view.query?.toString())
}
R.id.menu_enabled_group -> {
search_view.setQuery(getString(R.string.enabled), true)
}
R.id.menu_disabled_group -> {
search_view.setQuery(getString(R.string.disabled), true)
}
} }
if (item.groupId == R.id.source_group) { if (item.groupId == R.id.source_group) {
search_view.setQuery(item.title, true) search_view.setQuery(item.title, true)
@ -108,10 +137,10 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
viewModel.importSource(url) { msg -> viewModel.importSource(url) { msg ->
title_bar.snackbar(msg) title_bar.snackbar(msg)
} }
} } else {
else{
viewModel.importSourceFromFilePath(url) { msg -> viewModel.importSourceFromFilePath(url) { msg ->
title_bar.snackbar(msg)} title_bar.snackbar(msg)
}
} }
} }
else -> { else -> {
@ -143,15 +172,30 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private fun initLiveDataBookSource(searchKey: String? = null) { private fun initLiveDataBookSource(searchKey: String? = null) {
bookSourceLiveDate?.removeObservers(this) bookSourceLiveDate?.removeObservers(this)
bookSourceLiveDate = if (searchKey.isNullOrEmpty()) { bookSourceLiveDate = when {
searchKey.isNullOrEmpty() -> {
App.db.bookSourceDao().liveDataAll() App.db.bookSourceDao().liveDataAll()
} else { }
searchKey == getString(R.string.enabled) -> {
App.db.bookSourceDao().liveDataEnabled()
}
searchKey == getString(R.string.disabled) -> {
App.db.bookSourceDao().liveDataDisabled()
}
else -> {
App.db.bookSourceDao().liveDataSearch("%$searchKey%") App.db.bookSourceDao().liveDataSearch("%$searchKey%")
} }
bookSourceLiveDate?.observe(this, Observer { }
bookSourceLiveDate?.observe(this, Observer { data ->
val sourceList = when (sort) {
1 -> data.sortedBy { it.weight }
2 -> data.sortedBy { it.bookSourceName }
3 -> data.sortedBy { it.bookSourceUrl }
else -> data
}
val diffResult = DiffUtil val diffResult = DiffUtil
.calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), it)) .calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), sourceList))
adapter.setItems(it, diffResult) adapter.setItems(sourceList, diffResult)
upCountView() upCountView()
}) })
} }
@ -202,11 +246,35 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection()) R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection())
R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection()) R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection())
R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode)
R.id.menu_check_source -> CheckSource.start(this, adapter.getSelection()) R.id.menu_check_source -> checkSource()
R.id.menu_top_sel -> viewModel.topSource(*adapter.getSelection().toTypedArray())
R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray())
} }
return true return true
} }
@SuppressLint("InflateParams")
private fun checkSource() {
alert(titleResource = R.string.search_book_key) {
var editText: AutoCompleteTextView? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view
edit_view.setText(CheckSource.keyword)
}
}
okButton {
editText?.text?.toString()?.let {
if (it.isNotEmpty()) {
CheckSource.keyword = it
}
}
CheckSource.start(this@BookSourceActivity, adapter.getSelection())
}
noButton { }
}.show().applyTint()
}
private fun upGroupMenu() { private fun upGroupMenu() {
groupMenu?.removeGroup(R.id.source_group) groupMenu?.removeGroup(R.id.source_group)
groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE)) groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE))
@ -286,6 +354,10 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
viewModel.topSource(bookSource) viewModel.topSource(bookSource)
} }
override fun toBottom(bookSource: BookSource) {
viewModel.bottomSource(bookSource)
}
override fun onFilePicked(requestCode: Int, currentPath: String) { override fun onFilePicked(requestCode: Int, currentPath: String) {
when (requestCode) { when (requestCode) {
exportRequestCode -> viewModel.exportSelection( exportRequestCode -> viewModel.exportSelection(

@ -46,14 +46,14 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
callBack.upCountView() callBack.upCountView()
} }
fun getSelection(): LinkedHashSet<BookSource> { fun getSelection(): List<BookSource> {
val selection = linkedSetOf<BookSource>() val selection = arrayListOf<BookSource>()
getItems().map { getItems().map {
if (selected.contains(it)) { if (selected.contains(it)) {
selection.add(it) selection.add(it)
} }
} }
return selection return selection.sortedBy { it.customOrder }
} }
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
@ -138,6 +138,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
popupMenu.setOnMenuItemClickListener { menuItem -> popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.menu_top -> callBack.toTop(source) R.id.menu_top -> callBack.toTop(source)
R.id.menu_bottom -> callBack.toBottom(source)
R.id.menu_del -> callBack.del(source) R.id.menu_del -> callBack.del(source)
R.id.menu_enable_explore -> { R.id.menu_enable_explore -> {
callBack.update(source.copy(enabledExplore = !source.enabledExplore)) callBack.update(source.copy(enabledExplore = !source.enabledExplore))
@ -197,6 +198,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
fun edit(bookSource: BookSource) fun edit(bookSource: BookSource)
fun update(vararg bookSource: BookSource) fun update(vararg bookSource: BookSource)
fun toTop(bookSource: BookSource) fun toTop(bookSource: BookSource)
fun toBottom(bookSource: BookSource)
fun upOrder() fun upOrder()
fun upCountView() fun upCountView()
} }

@ -19,10 +19,23 @@ import java.io.File
class BookSourceViewModel(application: Application) : BaseViewModel(application) { class BookSourceViewModel(application: Application) : BaseViewModel(application) {
fun topSource(bookSource: BookSource) { fun topSource(vararg sources: BookSource) {
execute { execute {
bookSource.customOrder = App.db.bookSourceDao().minOrder - 1 val minOrder = App.db.bookSourceDao().minOrder - 1
App.db.bookSourceDao().insert(bookSource) sources.forEachIndexed { index, bookSource ->
bookSource.customOrder = minOrder - index
}
App.db.bookSourceDao().update(*sources)
}
}
fun bottomSource(vararg sources: BookSource) {
execute {
val maxOrder = App.db.bookSourceDao().maxOrder + 1
sources.forEachIndexed { index, bookSource ->
bookSource.customOrder = maxOrder + index
}
App.db.bookSourceDao().update(*sources)
} }
} }
@ -44,7 +57,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun enableSelection(sources: LinkedHashSet<BookSource>) { fun enableSelection(sources: List<BookSource>) {
execute { execute {
val list = arrayListOf<BookSource>() val list = arrayListOf<BookSource>()
sources.forEach { sources.forEach {
@ -54,7 +67,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun disableSelection(sources: LinkedHashSet<BookSource>) { fun disableSelection(sources: List<BookSource>) {
execute { execute {
val list = arrayListOf<BookSource>() val list = arrayListOf<BookSource>()
sources.forEach { sources.forEach {
@ -64,7 +77,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun enableSelectExplore(sources: LinkedHashSet<BookSource>) { fun enableSelectExplore(sources: List<BookSource>) {
execute { execute {
val list = arrayListOf<BookSource>() val list = arrayListOf<BookSource>()
sources.forEach { sources.forEach {
@ -74,7 +87,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun disableSelectExplore(sources: LinkedHashSet<BookSource>) { fun disableSelectExplore(sources: List<BookSource>) {
execute { execute {
val list = arrayListOf<BookSource>() val list = arrayListOf<BookSource>()
sources.forEach { sources.forEach {
@ -84,13 +97,13 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun delSelection(sources: LinkedHashSet<BookSource>) { fun delSelection(sources: List<BookSource>) {
execute { execute {
App.db.bookSourceDao().delete(*sources.toTypedArray()) App.db.bookSourceDao().delete(*sources.toTypedArray())
} }
} }
fun exportSelection(sources: LinkedHashSet<BookSource>, file: File) { fun exportSelection(sources: List<BookSource>, file: File) {
execute { execute {
val json = GSON.toJson(sources) val json = GSON.toJson(sources)
FileUtils.createFileIfNotExist(file, "exportBookSource.json") FileUtils.createFileIfNotExist(file, "exportBookSource.json")
@ -102,7 +115,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun exportSelection(sources: LinkedHashSet<BookSource>, doc: DocumentFile) { fun exportSelection(sources: List<BookSource>, doc: DocumentFile) {
execute { execute {
val json = GSON.toJson(sources) val json = GSON.toJson(sources)
doc.findFile("exportBookSource.json")?.delete() doc.findFile("exportBookSource.json")?.delete()
@ -145,10 +158,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
execute { execute {
val sources = App.db.bookSourceDao().getByGroup(group) val sources = App.db.bookSourceDao().getByGroup(group)
sources.map { source -> sources.map { source ->
source.bookSourceGroup?.splitNotBlank(",")?.toHashSet()?.let { source.removeGroup(group)
it.remove(group)
source.bookSourceGroup = TextUtils.join(",", it)
}
} }
App.db.bookSourceDao().update(*sources.toTypedArray()) App.db.bookSourceDao().update(*sources.toTypedArray())
} }
@ -220,6 +230,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
else -> "格式不对" else -> "格式不对"
} }
}.onError { }.onError {
it.printStackTrace()
finally(it.localizedMessage ?: "") finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it) finally(it)
@ -227,7 +238,11 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
private fun importSourceUrl(url: String): Int { private fun importSourceUrl(url: String): Int {
HttpHelper.simpleGet(url, "UTF-8")?.let { body -> HttpHelper.simpleGet(url, "UTF-8").let { body ->
if (body == null) {
toast("访问网站失败")
return 0
}
val bookSources = mutableListOf<BookSource>() val bookSources = mutableListOf<BookSource>()
val items: List<Map<String, Any>> = jsonPath.parse(body).read("$") val items: List<Map<String, Any>> = jsonPath.parse(body).read("$")
for (item in items) { for (item in items) {
@ -239,6 +254,5 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
App.db.bookSourceDao().insert(*bookSources.toTypedArray()) App.db.bookSourceDao().insert(*bookSources.toTypedArray())
return bookSources.size return bookSources.size
} }
return 0
} }
} }

@ -190,11 +190,12 @@ object BackupRestoreUi {
AppConfig.backupPath = uri.toString() AppConfig.backupPath = uri.toString()
} }
} }
oldDataRequestCode -> oldDataRequestCode -> if (resultCode == RESULT_OK) {
if (resultCode == RESULT_OK) data?.data?.let { uri -> data?.data?.let { uri ->
ImportOldData.importUri(uri) ImportOldData.importUri(uri)
} }
} }
} }
}
} }

@ -13,7 +13,9 @@ import io.legado.app.utils.readText
import java.io.File import java.io.File
class FileAssociationViewModel(application: Application) : BaseViewModel(application) { class FileAssociationViewModel(application: Application) : BaseViewModel(application) {
fun dispatchIndent(uri: Uri): Intent? { fun dispatchIndent(uri: Uri): Intent? {
try {
val url: String val url: String
//如果是普通的url,需要根据返回的内容判断是什么 //如果是普通的url,需要根据返回的内容判断是什么
if (uri.scheme == "file" || uri.scheme == "content") { if (uri.scheme == "file" || uri.scheme == "content") {
@ -74,5 +76,10 @@ class FileAssociationViewModel(application: Application) : BaseViewModel(applica
val newIndent = Intent(Intent.ACTION_VIEW) val newIndent = Intent(Intent.ACTION_VIEW)
newIndent.data = data newIndent.data = data
return newIndent return newIndent
} catch (e: Exception) {
e.printStackTrace()
toast(e.localizedMessage)
return null
}
} }
} }

@ -1,8 +1,12 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.annotation.SuppressLint
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -22,7 +26,8 @@ import io.legado.app.ui.widget.prefs.IconListPreference
import io.legado.app.utils.* import io.legado.app.utils.*
class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { class ThemeConfigFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
val items = arrayListOf("极简", "曜夜", "经典", "黑白", "A屏黑") val items = arrayListOf("极简", "曜夜", "经典", "黑白", "A屏黑")
@ -33,12 +38,13 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
preferenceScreen.removePreference(it) preferenceScreen.removePreference(it)
} }
} }
upPreferenceSummary("barElevation", AppConfig.elevation.toString()) upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
ATH.applyEdgeEffectColor(listView) ATH.applyEdgeEffectColor(listView)
setHasOptionsMenu(true)
} }
override fun onResume() { override fun onResume() {
@ -51,22 +57,37 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
super.onPause() super.onPause()
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.theme_config, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_theme_mode -> {
AppConfig.isNightTheme = !AppConfig.isNightTheme
App.INSTANCE.applyDayNight()
}
}
return super.onOptionsItemSelected(item)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences ?: return sharedPreferences ?: return
when (key) { when (key) {
PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key)) PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key))
"transparentStatusBar" -> recreateActivities() PreferKey.transparentStatusBar -> recreateActivities()
"colorPrimary", PreferKey.cPrimary,
"colorAccent", PreferKey.cAccent,
"colorBackground", PreferKey.cBackground,
"colorBottomBackground" -> { PreferKey.cBBackground -> {
if (backgroundIsDark(sharedPreferences)) { if (backgroundIsDark(sharedPreferences)) {
alert { alert {
title = "白天背景太暗" title = "白天背景太暗"
message = "将会恢复默认背景?" message = "将会恢复默认背景?"
yesButton { yesButton {
putPrefInt( putPrefInt(
"colorBackground", PreferKey.cBackground,
getCompatColor(R.color.md_grey_100) getCompatColor(R.color.md_grey_100)
) )
upTheme(false) upTheme(false)
@ -80,17 +101,17 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
upTheme(false) upTheme(false)
} }
} }
"colorPrimaryNight", PreferKey.cNPrimary,
"colorAccentNight", PreferKey.cNAccent,
"colorBackgroundNight", PreferKey.cNBackground,
"colorBottomBackgroundNight" -> { PreferKey.cNBBackground -> {
if (backgroundIsLight(sharedPreferences)) { if (backgroundIsLight(sharedPreferences)) {
alert { alert {
title = "夜间背景太亮" title = "夜间背景太亮"
message = "将会恢复默认背景?" message = "将会恢复默认背景?"
yesButton { yesButton {
putPrefInt( putPrefInt(
"colorBackgroundNight", PreferKey.cNBackground,
getCompatColor(R.color.md_grey_800) getCompatColor(R.color.md_grey_800)
) )
upTheme(true) upTheme(true)
@ -108,45 +129,65 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
} }
@SuppressLint("PrivateResource")
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) { when (preference?.key) {
"defaultTheme" -> alert(title = "切换默认主题") { "defaultTheme" -> changeTheme()
PreferKey.barElevation -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.bar_elevation))
.setMaxValue(32)
.setMinValue(0)
.setValue(AppConfig.elevation)
.setCustomButton((R.string.btn_default_s)) {
AppConfig.elevation =
App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
recreateActivities()
}
.show {
AppConfig.elevation = it
recreateActivities()
}
}
return super.onPreferenceTreeClick(preference)
}
private fun changeTheme() {
alert(title = "切换默认主题") {
items(items) { _, which -> items(items) { _, which ->
when (which) { when (which) {
0 -> { 0 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100)) putPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_grey_100))
putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color)) putPrefInt(PreferKey.cAccent, getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) putPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100))
putPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200))
AppConfig.isNightTheme = false AppConfig.isNightTheme = false
} }
1 -> { 1 -> {
putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) putPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_grey_900))
putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color)) putPrefInt(PreferKey.cNAccent, getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) putPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900))
putPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_900))
AppConfig.isNightTheme = true AppConfig.isNightTheme = true
} }
2 -> { 2 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)) putPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_light_blue_500))
putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)) putPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_pink_800))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) putPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100))
putPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200))
AppConfig.isNightTheme = false AppConfig.isNightTheme = false
} }
3 -> { 3 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.white)) putPrefInt(PreferKey.cPrimary, getCompatColor(R.color.white))
putPrefInt("colorAccent", getCompatColor(R.color.black)) putPrefInt(PreferKey.cAccent, getCompatColor(R.color.black))
putPrefInt("colorBackground", getCompatColor(R.color.white)) putPrefInt(PreferKey.cBackground, getCompatColor(R.color.white))
putPrefInt(PreferKey.cBBackground, getCompatColor(R.color.white))
AppConfig.isNightTheme = false AppConfig.isNightTheme = false
} }
4 -> { 4 -> {
putPrefInt("colorPrimaryNight", getCompatColor(R.color.black)) putPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.black))
putPrefInt( putPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_grey_500))
"colorAccentNight", putPrefInt(PreferKey.cNBackground, getCompatColor(R.color.black))
getCompatColor(R.color.md_grey_600) putPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.black))
)
putPrefInt(
"colorBackgroundNight",
getCompatColor(R.color.black)
)
AppConfig.isNightTheme = true AppConfig.isNightTheme = true
} }
} }
@ -154,27 +195,12 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
recreateActivities() recreateActivities()
} }
}.show().applyTint() }.show().applyTint()
"barElevation" -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.bar_elevation))
.setMaxValue(32)
.setMinValue(0)
.setValue(AppConfig.elevation)
.setCustomButton((R.string.btn_default_s)) {
AppConfig.elevation = App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
recreateActivities()
}
.show {
AppConfig.elevation = it
recreateActivities()
}
}
return super.onPreferenceTreeClick(preference)
} }
private fun backgroundIsDark(sharedPreferences: SharedPreferences): Boolean { private fun backgroundIsDark(sharedPreferences: SharedPreferences): Boolean {
return !ColorUtils.isColorLight( return !ColorUtils.isColorLight(
sharedPreferences.getInt( sharedPreferences.getInt(
"colorBackground", PreferKey.cBackground,
getCompatColor(R.color.md_grey_100) getCompatColor(R.color.md_grey_100)
) )
) )
@ -183,7 +209,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
private fun backgroundIsLight(sharedPreferences: SharedPreferences): Boolean { private fun backgroundIsLight(sharedPreferences: SharedPreferences): Boolean {
return ColorUtils.isColorLight( return ColorUtils.isColorLight(
sharedPreferences.getInt( sharedPreferences.getInt(
"colorBackgroundNight", PreferKey.cNBackground,
getCompatColor(R.color.md_grey_800) getCompatColor(R.color.md_grey_800)
) )
) )
@ -205,7 +231,8 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
private fun upPreferenceSummary(preferenceKey: String, value: String?) { private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) { when (preferenceKey) {
"barElevation" -> preference.summary = getString(R.string.bar_elevation_s, value) PreferKey.barElevation -> preference.summary =
getString(R.string.bar_elevation_s, value)
} }
} }
} }

@ -18,7 +18,6 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.main.bookshelf.BookshelfFragment import io.legado.app.ui.main.bookshelf.BookshelfFragment
import io.legado.app.ui.main.explore.ExploreFragment import io.legado.app.ui.main.explore.ExploreFragment
import io.legado.app.ui.main.my.MyFragment import io.legado.app.ui.main.my.MyFragment
@ -26,13 +25,16 @@ import io.legado.app.ui.main.rss.RssFragment
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.toast
class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main), class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
BottomNavigationView.OnNavigationItemSelectedListener, BottomNavigationView.OnNavigationItemSelectedListener,
BottomNavigationView.OnNavigationItemReselectedListener,
ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() { ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() {
override val viewModel: MainViewModel override val viewModel: MainViewModel
get() = getViewModel(MainViewModel::class.java) get() = getViewModel(MainViewModel::class.java)
private var exitTime: Long = 0
private var bookshelfReselected: Long = 0
private var pagePosition = 0 private var pagePosition = 0
private val fragmentId = arrayOf(0, 1, 2, 3) private val fragmentId = arrayOf(0, 1, 2, 3)
private val fragmentMap = mapOf<Int, Fragment>( private val fragmentMap = mapOf<Int, Fragment>(
@ -49,6 +51,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager)
view_pager_main.addOnPageChangeListener(this) view_pager_main.addOnPageChangeListener(this)
bottom_navigation_view.setOnNavigationItemSelectedListener(this) bottom_navigation_view.setOnNavigationItemSelectedListener(this)
bottom_navigation_view.setOnNavigationItemReselectedListener(this)
bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
} }
@ -73,6 +76,18 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
return false return false
} }
override fun onNavigationItemReselected(item: MenuItem) {
when (item.itemId) {
R.id.menu_bookshelf -> {
if (System.currentTimeMillis() - bookshelfReselected > 300) {
bookshelfReselected = System.currentTimeMillis()
} else {
(fragmentMap[0] as? BookshelfFragment)?.gotoTop()
}
}
}
}
private fun upVersion() { private fun upVersion() {
if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) { if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) {
putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode) putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
@ -104,11 +119,18 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
view_pager_main.currentItem = 0 view_pager_main.currentItem = 0
return true return true
} }
if (!BaseReadAloudService.pause) { if (System.currentTimeMillis() - exitTime > 2000) {
toast(R.string.double_click_exit)
exitTime = System.currentTimeMillis()
} else {
if (BaseReadAloudService.pause) {
finish()
} else {
moveTaskToBack(true) moveTaskToBack(true)
return true
} }
} }
return true
}
} }
} }
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
@ -121,11 +143,6 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
} }
override fun onDestroy() {
super.onDestroy()
ReadAloud.stop(this)
}
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<String>(EventBus.RECREATE) { observeEvent<String>(EventBus.RECREATE) {
recreate() recreate()

@ -3,7 +3,9 @@ package io.legado.app.ui.main
import android.app.Application import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
@ -19,7 +21,15 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
fun upChapterList() { fun upChapterList() {
execute { execute {
App.db.bookDao().hasUpdateBooks.forEach { book -> upChapterList(App.db.bookDao().hasUpdateBooks)
}
}
fun upChapterList(books: List<Book>) {
execute {
books.filter {
it.origin != BookType.local && it.canUpdate
}.forEach { book ->
if (!updateList.contains(book.bookUrl)) { if (!updateList.contains(book.bookUrl)) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
synchronized(this) { synchronized(this) {

@ -31,6 +31,7 @@ import io.legado.app.ui.book.download.DownloadActivity
import io.legado.app.ui.book.group.GroupManageDialog import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
@ -52,11 +53,12 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
override val viewModel: BookshelfViewModel override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java) get() = getViewModel(BookshelfViewModel::class.java)
private val activityViewModel: MainViewModel
get() = getViewModelOfActivity(MainViewModel::class.java)
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private var noGroupLiveData: LiveData<Int>? = null private var noGroupLiveData: LiveData<Int>? = null
private val bookGroups = mutableListOf<BookGroup>() private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Int, Fragment>() private val fragmentMap = hashMapOf<Int, BooksFragment>()
private var showGroupNone = false private var showGroupNone = false
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
@ -73,6 +75,13 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
super.onCompatOptionsItemSelected(item) super.onCompatOptionsItemSelected(item)
when (item.itemId) { when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>() R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_update_toc -> {
val group = bookGroups[tab_layout.selectedTabPosition]
val fragment = fragmentMap[group.groupId]
fragment?.getBooks()?.let {
activityViewModel.upChapterList(it)
}
}
R.id.menu_bookshelf_layout -> configBookshelf() R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog") .show(childFragmentManager, "groupManageDialog")
@ -82,7 +91,10 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
Pair("groupId", selectedGroup.groupId), Pair("groupId", selectedGroup.groupId),
Pair("groupName", selectedGroup.groupName) Pair("groupName", selectedGroup.groupName)
) )
R.id.menu_download -> startActivity<DownloadActivity>() R.id.menu_download -> startActivity<DownloadActivity>(
Pair("groupId", selectedGroup.groupId),
Pair("groupName", selectedGroup.groupName)
)
} }
} }
@ -254,6 +266,10 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
} }
fun gotoTop() {
fragmentMap[selectedGroup.groupId]?.gotoTop()
}
private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf.books
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
@ -22,20 +23,21 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
ATH.applyBackgroundTint(this) ATH.applyBackgroundTint(this)
tv_name.text = item.name tv_name.text = item.name
iv_cover.load(item.getDisplayCover(), item.name, item.author) iv_cover.load(item.getDisplayCover(), item.name, item.author)
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { upRefresh(this, item)
bv_unread.invisible()
rl_loading.show()
} else {
rl_loading.hide()
bv_unread.setBadgeCount(item.getUnreadChapterNum())
bv_unread.setHighlight(item.lastCheckCount > 0)
}
} else { } else {
bundle.keySet().map { bundle.keySet().map {
when (it) { when (it) {
"name" -> tv_name.text = item.name "name" -> tv_name.text = item.name
"cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author)
"refresh" -> if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { "refresh" -> upRefresh(this, item)
}
}
}
}
}
private fun upRefresh(itemView: View, item: Book) = with(itemView) {
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
bv_unread.invisible() bv_unread.invisible()
rl_loading.show() rl_loading.show()
} else { } else {
@ -44,10 +46,6 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
bv_unread.setHighlight(item.lastCheckCount > 0) bv_unread.setHighlight(item.lastCheckCount > 0)
} }
} }
}
}
}
}
override fun registerListener(holder: ItemViewHolder) { override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply { holder.itemView.apply {

@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf.books
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
@ -25,14 +26,7 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) :
tv_read.text = item.durChapterTitle tv_read.text = item.durChapterTitle
tv_last.text = item.latestChapterTitle tv_last.text = item.latestChapterTitle
iv_cover.load(item.getDisplayCover(), item.name, item.author) iv_cover.load(item.getDisplayCover(), item.name, item.author)
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { upRefresh(this, item)
bv_unread.invisible()
rl_loading.show()
} else {
rl_loading.hide()
bv_unread.setBadgeCount(item.getUnreadChapterNum())
bv_unread.setHighlight(item.lastCheckCount > 0)
}
} else { } else {
bundle.keySet().map { bundle.keySet().map {
when (it) { when (it) {
@ -41,17 +35,21 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) :
"dur" -> tv_read.text = item.durChapterTitle "dur" -> tv_read.text = item.durChapterTitle
"last" -> tv_last.text = item.latestChapterTitle "last" -> tv_last.text = item.latestChapterTitle
"cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author)
"refresh" -> if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { "refresh" -> upRefresh(this, item)
}
}
}
}
}
private fun upRefresh(itemView: View, item: Book) = with(itemView) {
if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) {
bv_unread.invisible() bv_unread.invisible()
rl_loading.show() rl_loading.show()
} else { } else {
rl_loading.hide() rl_loading.hide()
bv_unread.setBadgeCount(item.getUnreadChapterNum())
bv_unread.setHighlight(item.lastCheckCount > 0) bv_unread.setHighlight(item.lastCheckCount > 0)
} bv_unread.setBadgeCount(item.getUnreadChapterNum())
}
}
}
} }
} }

@ -16,54 +16,45 @@ class BooksDiffCallBack(private val oldItems: List<Book>, private val newItems:
} }
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition].bookUrl == newItems[newItemPosition].bookUrl val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return oldItem.name == newItem.name
&& oldItem.author == newItem.author
} }
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.name != newItem.name) return when {
return false oldItem.durChapterTime != newItem.durChapterTime -> false
if (oldItem.author != newItem.author) oldItem.name != newItem.name -> false
return false oldItem.author != newItem.author -> false
if (oldItem.durChapterTitle != newItem.durChapterTitle) oldItem.durChapterTitle != newItem.durChapterTitle -> false
return false oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) oldItem.lastCheckCount != newItem.lastCheckCount -> false
return false oldItem.getDisplayCover() != newItem.getDisplayCover() -> false
if (oldItem.lastCheckCount != newItem.lastCheckCount) oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false
return false else -> true
if (oldItem.getDisplayCover() != newItem.getDisplayCover()) }
return false
if (oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum())
return false
return true
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
val bundle = bundleOf() val bundle = bundleOf()
if (oldItem.name != newItem.name) if (oldItem.name != newItem.name) bundle.putString("name", null)
bundle.putString("name", null) if (oldItem.author != newItem.author) bundle.putString("author", null)
if (oldItem.author != newItem.author) if (oldItem.durChapterTitle != newItem.durChapterTitle) bundle.putString("dur", null)
bundle.putString("author", null) if (oldItem.latestChapterTitle != newItem.latestChapterTitle) bundle.putString("last", null)
if (oldItem.durChapterTitle != newItem.durChapterTitle) if (oldItem.getDisplayCover() != newItem.getDisplayCover()) bundle.putString("cover", null)
bundle.putString("dur", null) if (oldItem.lastCheckCount != newItem.lastCheckCount
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) || oldItem.durChapterTime != newItem.durChapterTime
bundle.putString("last", null) || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()
if (oldItem.getDisplayCover() != newItem.getDisplayCover())
bundle.putString("cover", null)
if (oldItem.lastCheckCount != newItem.lastCheckCount)
bundle.putString("refresh", null)
if (oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()
|| oldItem.lastCheckCount != newItem.lastCheckCount || oldItem.lastCheckCount != newItem.lastCheckCount
) { ) {
bundle.putString("refresh", null) bundle.putString("refresh", null)
} }
if (bundle.isEmpty) return null
if (bundle.isEmpty) {
return null
}
return bundle return bundle
} }

@ -16,6 +16,7 @@ import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
@ -45,14 +46,14 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
} }
} }
private lateinit var activityViewModel: MainViewModel private val activityViewModel: MainViewModel
get() = getViewModelOfActivity(MainViewModel::class.java)
private lateinit var booksAdapter: BaseBooksAdapter private lateinit var booksAdapter: BaseBooksAdapter
private var bookshelfLiveData: LiveData<List<Book>>? = null private var bookshelfLiveData: LiveData<List<Book>>? = null
private var position = 0 private var position = 0
private var groupId = -1 private var groupId = -1
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
activityViewModel = getViewModelOfActivity(MainViewModel::class.java)
arguments?.let { arguments?.let {
position = it.getInt("position", 0) position = it.getInt("position", 0)
groupId = it.getInt("groupId", -1) groupId = it.getInt("groupId", -1)
@ -66,7 +67,7 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
refresh_layout.setColorSchemeColors(accentColor) refresh_layout.setColorSchemeColors(accentColor)
refresh_layout.setOnRefreshListener { refresh_layout.setOnRefreshListener {
refresh_layout.isRefreshing = false refresh_layout.isRefreshing = false
activityViewModel.upChapterList() activityViewModel.upChapterList(booksAdapter.getItems())
} }
val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
if (bookshelfLayout == 0) { if (bookshelfLayout == 0) {
@ -118,6 +119,18 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
}) })
} }
fun getBooks(): List<Book> {
return booksAdapter.getItems()
}
fun gotoTop() {
if (AppConfig.isEInkMode) {
rv_bookshelf.scrollToPosition(0)
} else {
rv_bookshelf.smoothScrollToPosition(0)
}
}
override fun open(book: Book) { override fun open(book: Book) {
when (book.type) { when (book.type) {
BookType.audio -> BookType.audio ->
@ -130,7 +143,10 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
} }
override fun openBookInfo(book: Book) { override fun openBookInfo(book: Book) {
context?.startActivity<BookInfoActivity>(Pair("bookUrl", book.bookUrl)) context?.startActivity<BookInfoActivity>(
Pair("name", book.name),
Pair("author", book.author)
)
} }
override fun isUpdate(bookUrl: String): Boolean { override fun isUpdate(bookUrl: String): Boolean {

@ -65,6 +65,9 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.
BackupRestoreUi.onActivityResult(requestCode, resultCode, data) BackupRestoreUi.onActivityResult(requestCode, resultCode, data)
} }
/**
* 配置
*/
class PreferenceFragment : PreferenceFragmentCompat(), class PreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {

@ -1,6 +1,8 @@
package io.legado.app.ui.main.rss package io.legado.app.ui.main.rss
import android.content.Context import android.content.Context
import android.view.View
import androidx.appcompat.widget.PopupMenu
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
@ -8,6 +10,7 @@ import io.legado.app.data.entities.RssSource
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import kotlinx.android.synthetic.main.item_rss.view.* import kotlinx.android.synthetic.main.item_rss.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class RssAdapter(context: Context, val callBack: CallBack) : class RssAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss) { SimpleRecyclerAdapter<RssSource>(context, R.layout.item_rss) {
@ -29,9 +32,32 @@ class RssAdapter(context: Context, val callBack: CallBack) :
callBack.openRss(it) callBack.openRss(it)
} }
} }
holder.itemView.onLongClick {
getItem(holder.layoutPosition)?.let {
showMenu(holder.itemView.iv_icon, it)
}
true
}
}
private fun showMenu(view: View, rssSource: RssSource) {
val popupMenu = PopupMenu(context, view)
popupMenu.inflate(R.menu.rss_main_item)
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_top -> callBack.toTop(rssSource)
R.id.menu_edit -> callBack.edit(rssSource)
R.id.menu_del -> callBack.del(rssSource)
}
true
}
popupMenu.show()
} }
interface CallBack { interface CallBack {
fun openRss(rssSource: RssSource) fun openRss(rssSource: RssSource)
fun toTop(rssSource: RssSource)
fun edit(rssSource: RssSource)
fun del(rssSource: RssSource)
} }
} }

@ -8,22 +8,27 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.main.MainViewModel import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.rss.article.RssSortActivity import io.legado.app.ui.rss.article.RssSortActivity
import io.legado.app.ui.rss.favorites.RssFavoritesActivity import io.legado.app.ui.rss.favorites.RssFavoritesActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.ui.rss.source.manage.RssSourceViewModel
import io.legado.app.utils.getViewModel
import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.fragment_rss.* import kotlinx.android.synthetic.main.fragment_rss.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
class RssFragment : BaseFragment(R.layout.fragment_rss), class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
RssAdapter.CallBack { RssAdapter.CallBack {
private lateinit var adapter: RssAdapter private lateinit var adapter: RssAdapter
override val viewModel: RssSourceViewModel
get() = getViewModel(RssSourceViewModel::class.java)
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
setSupportToolbar(toolbar) setSupportToolbar(toolbar)
@ -62,4 +67,16 @@ class RssFragment : BaseFragment(R.layout.fragment_rss),
override fun openRss(rssSource: RssSource) { override fun openRss(rssSource: RssSource) {
startActivity<RssSortActivity>(Pair("url", rssSource.sourceUrl)) startActivity<RssSortActivity>(Pair("url", rssSource.sourceUrl))
} }
override fun toTop(rssSource: RssSource) {
viewModel.topSource(rssSource)
}
override fun edit(rssSource: RssSource) {
startActivity<RssSourceEditActivity>(Pair("data", rssSource.sourceUrl))
}
override fun del(rssSource: RssSource) {
viewModel.del(rssSource)
}
} }

@ -1,26 +1,32 @@
package io.legado.app.ui.replacerule.edit package io.legado.app.ui.replacerule.edit
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.EditText
import android.widget.PopupWindow
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.toast import io.legado.app.utils.toast
import kotlinx.android.synthetic.main.dialog_replace_edit.* import kotlinx.android.synthetic.main.dialog_replace_edit.*
import org.jetbrains.anko.sdk27.listeners.onFocusChange
class ReplaceEditDialog : DialogFragment(), class ReplaceEditDialog : DialogFragment(),
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener,
KeyboardToolPop.CallBack {
companion object { companion object {
@ -41,12 +47,11 @@ class ReplaceEditDialog : DialogFragment(),
} }
private lateinit var viewModel: ReplaceEditViewModel private lateinit var viewModel: ReplaceEditViewModel
private lateinit var mSoftKeyboardTool: PopupWindow
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = DisplayMetrics() dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
activity?.windowManager?.defaultDisplay?.getMetrics(dm)
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), WRAP_CONTENT)
} }
override fun onCreateView( override fun onCreateView(
@ -60,6 +65,7 @@ class ReplaceEditDialog : DialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mSoftKeyboardTool = KeyboardToolPop(requireContext(), AppConst.keyboardToolChars, this)
tool_bar.inflateMenu(R.menu.replace_edit) tool_bar.inflateMenu(R.menu.replace_edit)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)
@ -69,12 +75,20 @@ class ReplaceEditDialog : DialogFragment(),
arguments?.let { arguments?.let {
viewModel.initData(it) viewModel.initData(it)
} }
et_replace_rule.onFocusChange { v, hasFocus ->
if (hasFocus) {
mSoftKeyboardTool.width = v.width
mSoftKeyboardTool.showAsDropDown(v)
} else {
mSoftKeyboardTool.dismiss()
}
}
} }
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_save -> { R.id.menu_save -> {
val rule = getReplaceRule(); val rule = getReplaceRule()
if (!rule.isValid()){ if (!rule.isValid()){
toast(R.string.replace_rule_invalid) toast(R.string.replace_rule_invalid)
} }
@ -111,6 +125,30 @@ class ReplaceEditDialog : DialogFragment(),
val callBack get() = activity as? CallBack val callBack get() = activity as? CallBack
private fun insertText(text: String) {
if (text.isBlank()) return
val view = dialog?.window?.decorView?.findFocus()
if (view is EditText) {
val start = view.selectionStart
val end = view.selectionEnd
val edit = view.editableText//获取EditText的文字
if (start < 0 || start >= edit.length) {
edit.append(text)
} else {
edit.replace(start, end, text)//光标所在位置插入文字
}
}
}
override fun sendText(text: String) {
if (text == AppConst.keyboardToolChars[0]) {
val view = dialog?.window?.decorView?.findFocus()
view?.clearFocus()
} else {
insertText(text)
}
}
interface CallBack { interface CallBack {
fun onReplaceRuleSave() fun onReplaceRuleSave()
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.rss.article package io.legado.app.ui.rss.article
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
@ -18,18 +19,22 @@ import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.textColorResource import org.jetbrains.anko.textColorResource
class RssArticlesAdapter(context: Context, val callBack: CallBack) : class RssArticlesAdapter(context: Context, layoutId: Int, val callBack: CallBack) :
SimpleRecyclerAdapter<RssArticle>(context, R.layout.item_rss_article) { SimpleRecyclerAdapter<RssArticle>(context, layoutId) {
@SuppressLint("CheckResult")
override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
tv_title.text = item.title tv_title.text = item.title
tv_pub_date.text = item.pubDate tv_pub_date.text = item.pubDate
if (item.image.isNullOrBlank()) { if (item.image.isNullOrBlank() && !callBack.isGridLayout) {
image_view.gone() image_view.gone()
} else { } else {
ImageLoader.load(context, item.image) ImageLoader.load(context, item.image).apply {
.addListener(object : RequestListener<Drawable> { if (callBack.isGridLayout) {
placeholder(R.drawable.image_rss_article)
} else {
addListener(object : RequestListener<Drawable> {
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
@ -52,7 +57,8 @@ class RssArticlesAdapter(context: Context, val callBack: CallBack) :
} }
}) })
.into(image_view) }
}.into(image_view)
} }
if (item.read) { if (item.read) {
tv_title.textColorResource = R.color.tv_text_summary tv_title.textColorResource = R.color.tv_text_summary
@ -71,6 +77,7 @@ class RssArticlesAdapter(context: Context, val callBack: CallBack) :
} }
interface CallBack { interface CallBack {
val isGridLayout: Boolean
fun readRss(rssArticle: RssArticle) fun readRss(rssArticle: RssArticle)
} }
} }

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

Loading…
Cancel
Save