Merge pull request #6 from gedoor/master

merge
pull/179/head
Celeter 5 years ago committed by GitHub
commit 2d7dd7127a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      app/build.gradle
  2. 29
      app/google-services.json
  3. 2
      app/proguard-rules.pro
  4. 4
      app/src/debug/res/values/strings.xml
  5. 79
      app/src/main/AndroidManifest.xml
  6. 129
      app/src/main/assets/help.md
  7. 68
      app/src/main/assets/txtTocRule.json
  8. 107
      app/src/main/assets/updateLog.md
  9. 36
      app/src/main/java/io/legado/app/App.kt
  10. 5
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  11. 22
      app/src/main/java/io/legado/app/constant/AppConst.kt
  12. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  13. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  14. 8
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  15. 17
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  16. 5
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  17. 6
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  18. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  19. 14
      app/src/main/java/io/legado/app/data/entities/RssStar.kt
  20. 27
      app/src/main/java/io/legado/app/help/ActivityHelp.kt
  21. 47
      app/src/main/java/io/legado/app/help/AppConfig.kt
  22. 207
      app/src/main/java/io/legado/app/help/BookHelp.kt
  23. 7
      app/src/main/java/io/legado/app/help/ItemTouchCallback.kt
  24. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  25. 10
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  26. 24
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  27. 46
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  28. 28
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  29. 14
      app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt
  30. 2
      app/src/main/java/io/legado/app/help/permission/Request.kt
  31. 104
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  32. 1
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  33. 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  34. 3
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  35. 12
      app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt
  36. 5
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt
  37. 8
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  38. 2
      app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt
  39. 22
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  40. 56
      app/src/main/java/io/legado/app/model/Debug.kt
  41. 12
      app/src/main/java/io/legado/app/model/Rss.kt
  42. 93
      app/src/main/java/io/legado/app/model/SearchBookModel.kt
  43. 123
      app/src/main/java/io/legado/app/model/WebBook.kt
  44. 7
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  45. 206
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  46. 58
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  47. 12
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  48. 32
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  49. 19
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  50. 4
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  51. 10
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  52. 75
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  53. 50
      app/src/main/java/io/legado/app/service/DownloadService.kt
  54. 25
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  55. 82
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  56. 2
      app/src/main/java/io/legado/app/ui/README.md
  57. 16
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  58. 18
      app/src/main/java/io/legado/app/ui/about/DonateFragment.kt
  59. 5
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  60. 3
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  61. 52
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  62. 72
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt
  63. 9
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt
  64. 7
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  65. 21
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  66. 9
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt
  67. 15
      app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt
  68. 8
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  69. 45
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  70. 2
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  71. 7
      app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt
  72. 46
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  73. 44
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  74. 116
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  75. 8
      app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt
  76. 6
      app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt
  77. 15
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  78. 71
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  79. 65
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  80. 119
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  81. 6
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  82. 16
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  83. 24
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  84. 116
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  85. 4
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  86. 19
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  87. 53
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  88. 57
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  89. 2
      app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt
  90. 22
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  91. 13
      app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt
  92. 7
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  93. 11
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt
  94. 19
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  95. 41
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
  96. 42
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  97. 7
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  98. 40
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  99. 100
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  100. 25
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -37,6 +37,7 @@ android {
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName version versionName version
flavorDimensions "versionCode"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version) project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true multiDexEnabled true
@ -73,7 +74,15 @@ android {
} }
} }
} }
productFlavors{
app{
manifestPlaceholders = [APP_CHANNEL_VALUE:"app"]
}
google{
applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE:"google"]
}
}
compileOptions { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true //coreLibraryDesugaringEnabled true
@ -99,7 +108,7 @@ kapt {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//kotlin //kotlin
@ -119,7 +128,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.6'
//lifecycle //lifecycle
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
@ -127,12 +136,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.4' def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime:2.1.1' implementation 'androidx.paging:paging-runtime:2.1.2'
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'
@ -148,26 +157,26 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// //
implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino //JS rhino
implementation 'com.github.gedoor:rhino-android:1.4' implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit //
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' //noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:retrofit:2.7.2'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer //webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'org.nanohttpd:nanohttpd:2.3.1'
implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
// //
implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.6' implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'
// //
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'
@ -177,7 +186,7 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.8' implementation 'org.apache.commons:commons-text:1.8'
//MarkDown //MarkDown
implementation 'ru.noties.markwon:core:3.0.2' implementation 'ru.noties.markwon:core:3.1.0'
// //
implementation 'com.github.houbb:opencc4j:1.4.0' implementation 'com.github.houbb:opencc4j:1.4.0'

@ -92,6 +92,35 @@
] ]
} }
} }
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7",
"android_client_info": {
"package_name": "io.legado.play.release"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
} }
], ],
"configuration_version": "1" "configuration_version": "1"

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

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

@ -23,6 +23,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true" android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme.Light" android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口--> <!--主入口-->
@ -78,6 +79,48 @@
android:resource="@xml/shortcuts" android:resource="@xml/shortcuts"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
</activity> </activity>
<!--图标4-->
<activity
android:name=".ui.welcome.Launcher4"
android:icon="@mipmap/launcher4"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标5-->
<activity
android:name=".ui.welcome.Launcher5"
android:icon="@mipmap/launcher5"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--图标6-->
<activity
android:name=".ui.welcome.Launcher6"
android:icon="@mipmap/launcher6"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--主界面--> <!--主界面-->
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
@ -91,7 +134,7 @@
<!--书籍详情页--> <!--书籍详情页-->
<activity <activity
android:name=".ui.book.info.BookInfoActivity" android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTask" /> android:launchMode="singleTop" />
<!--书籍信息编辑--> <!--书籍信息编辑-->
<activity <activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"
@ -120,15 +163,19 @@
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" /> android:windowSoftInputMode="adjustResize|stateHidden" />
<!--配置界面-->
<activity <activity
android:name=".ui.config.ConfigActivity" android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!--搜索界面-->
<activity <activity
android:name="io.legado.app.ui.book.search.SearchActivity" android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!--关于界面-->
<activity <activity
android:name=".ui.about.AboutActivity" android:name=".ui.about.AboutActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!--捐赠界面-->
<activity <activity
android:name=".ui.about.DonateActivity" android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
@ -177,42 +224,55 @@
android:scheme="yuedu" /> android:scheme="yuedu" />
</intent-filter> </intent-filter>
</activity> </activity>
<!--书籍管理-->
<activity <activity
android:name=".ui.book.arrange.ArrangeBookActivity" android:name=".ui.book.arrange.ArrangeBookActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--书源调试-->
<activity <activity
android:name=".ui.book.source.debug.BookSourceDebugActivity" android:name=".ui.book.source.debug.BookSourceDebugActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--目录-->
<activity <activity
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity" android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--RSS阅读-->
<activity <activity
android:name=".ui.rss.read.ReadRssActivity" android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--导入书籍-->
<activity <activity
android:name="io.legado.app.ui.book.local.ImportBookActivity" android:name="io.legado.app.ui.book.local.ImportBookActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--发现界面-->
<activity <activity
android:name="io.legado.app.ui.book.explore.ExploreShowActivity" android:name="io.legado.app.ui.book.explore.ExploreShowActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--订阅源调试-->
<activity <activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity" android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--订阅条目-->
<activity <activity
android:name=".ui.rss.article.RssArticlesActivity" android:name=".ui.rss.article.RssArticlesActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--Rss收藏-->
<activity <activity
android:name=".ui.rss.favorites.RssFavoritesActivity" android:name=".ui.rss.favorites.RssFavoritesActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--下载界面-->
<activity <activity
android:name="io.legado.app.ui.book.download.DownloadActivity" android:name="io.legado.app.ui.book.download.DownloadActivity"
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!--书源登录-->
<activity <activity
android:name=".ui.login.SourceLogin" android:name=".ui.login.SourceLogin"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" /> android:hardwareAccelerated="true" />
<!--文字处理-->
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label"> android:label="@string/receiving_shared_label">
@ -227,6 +287,19 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.config.FileAssociationActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="text/plain" />
<data android:mimeType="application/json" />
</intent-filter>
</activity>
<service android:name=".service.CheckSourceService" /> <service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />
@ -250,6 +323,10 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<meta-data
android:name="channel"
android:value="${APP_CHANNEL_VALUE}" />
</application> </application>
</manifest> </manifest>

@ -0,0 +1,129 @@
## 常见问题
1.为什么第一次安装好之后什么东西都没有?
* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从QQ群、公众号“开源阅读软件”、酷安评论里获取由书友制作分享的书源。
2.如何导入本地书源文件?
* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入);
* 打开阅读软件;
* 我的 - 点击“书源管理”;
* 点击右上角选择“本地导入”;
* 左下角选择书源文件所在的路径;
* 点击书源文件导入;
* 导入后返回书源管理界面;
* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/
3.如何新建大佬发的单独书源?
* 复制书源代码;
* 打开阅读软件;
* 我的 - 点击“书源管理”;
* 右上角选择“新建书源”;
* 进入新建书源后点击右上角“粘贴源”;
* 粘贴书源完成后点击上方保存;
* 本次新建单独书源操作完成。
* 注:如果书源有错误或者复制不全会显示格式错误,请重新复制。
4.为什么导入2.0书源后看不了书?
* 2.0部分书源并不适用3.0,建议导入后进行筛选。
5.阅读2.0数据如何导入阅读3.0?
* 先对阅读2.0的数据进行备份,然后进入阅读3.0,点击“我的”,选择“备份与恢复”,再点击“导入旧版本数据”。
6.如何给朋友分享我的书源?
* 打开阅读软件;
* 点击备份;
* 打开手机自带的文件管理;
* 手机自带内存根目录找到YueDu3.0文件夹;
* 找到myBookSource.json长按选择分享;
* 选择微信分享或者QQ分享;
* 选择你要分享的好友点击发送;
* 好友接收后在手机自带内存根目录找到myBookSource.json文件(QQ在tencent--QQfile_recv微信在Tencent--MicroMsg--Download);
* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹);
* 打开阅读软件点击恢复。
* 注:备份路径如已修改过请在修改后的路径下查找书源文件。
7.目前阅读支持哪些格式的本地书籍?
* 目前支持TXT、EPUB格式(只支持显示EPUB里的文本内容,还不支持显示图片)。
8.如何刷新书架?
* 在书架界面下拉即可刷新。
9.书架界面书籍右上角的红色或者灰色背景小数字代表什么?
* 红色代表书籍有更新,灰色代表无更新,数字代表未读章节。
10.如何查看书籍详情?
* 长按书籍。
11.如何对书架上的书进行删除、切换书架的操作?
* 书籍详情页操作即可。
12.如何禁止或允许某本书更新?
* 书籍详情页,点击右上角 - “允许更新”。
13.如何更换小说封面、名字、作者或简介?
* 书籍详情页,点击右上角修改按钮。
14.怎么使用自定义字体?
* 阅读界面 - 字体-点击右上角选择字体文件路径。
15.目前支持哪些格式的字体文件?
* 目前支持ttf、otf格式。
16.书籍经常“正在加载中”怎么办?
* 在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。
17.书籍内容只有标题,正文内容是路径怎么办?
* 通常是缓存路径引起的,更换缓存路径即可。
18.效验书源显示失效就说明书源不能用了吗?
* 效验书源只是测试书源,可以做个参考,失效了不代表书源不能用了。
19.发现和正版书源能不能使用?
* 发现和正版书源只能用来找书,看排行榜,不能用来看书,如需看书请切换书源。
20.替换净化是什么?
* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。
21.如何自己填写净化替换规则?
* 第一行:替换规则名称 - 根据自己需求对替换净化规则进行命名;
* 第二行:分组 - 净化规则的分组组别;
* 第三行:替换规则 - 填写需要被替换的内容;
* 第四行:替换为 - 填写想替换成的内容(如不填则默认表示删除第二行里填写的内容);
* 第五行:替换范围,选填书名或者源名 - 填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。
* 注:如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第二行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行百度学习)。
22.如何听书?
* 可以使用手机自带的朗读引擎,也可使用第三方如谷歌、小米等朗读引擎。
* 【具体操作:安装-系统设置-其他高级设置-辅助功能-TTS输出-选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。】
23.如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页、自定义翻页案件?
* 阅读界面,设置(可上划,下面还有其他设置)。
24.搜索的时候感觉手机卡顿,如何解决?
* 我的 - 其他设置 - “更新和搜索线程数”调低。
25.更新前有什么注意事项?
* 要做好备份。
26.看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办?
* 一般是书源问题,切换书源即可。
27.为什么书源这么多,发现里却只有一点点?
* 书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。
28.云备份在哪?
* 我的 - 备份与恢复 - WebDav设置。
29.如何操作进行云备份?
* 侧栏设置,WebDav设置;
* 正确填写WebDAV 服务器地址、WebDAV 账号、WebDAV 密码;(要获得这三项的信息,需要注册一个坚果云账号,如果直接在手机上注册,坚果云会让你下载app,过程比较麻烦,为了一步到位,最好是在电脑上打开这个注册链接:https://www.jianguoyun.com/d/signup;注册后,进入坚果云;点击右上角账户名处选择 “账户信息”,然后选择“安全选项”;在“安全选项” 中找到“第三方应用管理”,并选择“添加应用”,输入名称如“阅读”后,会生成密码,选择完成;其中https://dav.jianguoyun.com/dav/就是填入“WebDAV 服务器地址”的内容,“使用情况”后面的邮箱地址就是你的“WebDAV 账号”,点击显示密码后得到的密码就是你的“WebDAV 密码”。)
* 无需操作,APP默认每天自动云备份一次。
30.关于云备份的相关说明
* 在正确设置好云备份的情况下,APP默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。
31.本地备份和云备份都能备份哪些东西?
* 书架、看书进度、搜索记录、书源、替换、APP设置等都会备份,基本涵盖所有内容。
32.出现某些未知bug怎么办?
* 清除软件数据试试看,不行再进行反馈。

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

@ -1,6 +1,102 @@
## 更新日志 ## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 * 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
**2020/03/29**
* 添加退出软件后是否响应耳机按键的开关
* 优化书源校验
**2020/03/26**
* 修复txt目录bug
* 最近工作比较忙,只有晚上有时间写软件,bug之类的不要催,白天不回消息
**2020/03/25**
* 修复7.1.1的网络问题,是retrofit2库最新版本的bug,暂时退回上版本
* 去除下载路径的配置,减少错误
* 添加隐藏状态栏是否扩展到刘海
**2020/03/24**
* txt文件第一章之前的文字不再放到简介里
* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别
* 修复文件关联 by wqfantexi
**2020/03/22**
* 添加文件关联 by wqfantexi
* 手动排序可以了,在书架整理里面拖动排序
* 删除分组时同时删除书籍里的分组信息,下次添加新分组时不会自动出现在分组内
* 修复换源丢失分组信息的bug
* 修复部分朗读引擎不自动朗读下一章的bug
**2020/03/21**
* 详情页点击书名搜索
**2020/03/20**
* 自动备份文件和手动备份文件分开
* 修复一些rss收藏取消不了的bug
* 修复rss请求头无效bug
**2020/03/19**
* 美化界面我的 by yangyxd
* 优化搜索
**2020/03/18**
* 尝试修复搜索时崩溃
* 解决看过书籍的移到顶部需要向上滚动才能看到的bug
* 只有再书源被删除找不到书源时才会自动换源
* 美化界面 by yangyxd
* 订阅后台播放
**2020/03/16**
* 修复滚动模式切换章节位置不归0的bug
* 修复文字选择更多菜单在部分手机上报错的bug
* 修复文字选择菜单问题
**2020/03/15**
* 加载正文无书源时自动换源
**2020/03/14**
* 修改导航栏图标
**2020/03/13**
* 更改书架控件,ViewPager2替换回2.0使用的ViewPager,解决下拉不流畅问题
* 修复点击作者搜索后,打开的详情页还是原来的书籍的bug
* 修改朗读菜单
* 优化rss朗读
**2020/03/12**
* 导入本地添加需要权限模式
**2020/03/11**
* 修复调节上边距时下边距一起动的bug
* 适配沚水的web阅读 by 六月
* 分组管理页面调整 by yangyxd
**2020/03/10**
* 优化文字选择菜单弹出位置
* 添加屏幕方向控制
* 添加点击作者搜索
**2020/03/09**
* 底部文字对齐
* 主题添加阴影调节 by yangyxd
**2020/03/08**
* 订阅长按保存图片
* 订阅全屏播放
* 书架全部分组可以隐藏了
* 内置web书架基本能用了 by 六月
* 书架整理加入未分组
* 显示总进度
* 隐藏状态栏时,标题显示在上方
**2020/03/07**
* 添加标题上下间距调整
* 添加标题大小调整
* 书籍整理添加批量启用禁用更新
* 换源禁用书源不显示
* 修复搜索界面简介最下面显示半行文字
* 搜索历史改为多行
**2020/03/06** **2020/03/06**
* 添加隐藏标题 * 添加隐藏标题
@ -195,14 +291,7 @@
- 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996` - 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996`
* 修改订阅中自动添加style的情景 * 修改订阅中自动添加style的情景
订阅源的内容规则中存在`<style>``style=`, 订阅源的内容规则中存在`<style>``style=`
```
<style>
img{max-width:100% !important; width:auto; height:auto;}
video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
</style>
```
**2019/12/28** **2019/12/28**
* 添加下载界面 * 添加下载界面

@ -1,13 +1,11 @@
package io.legado.app package io.legado.app
import android.app.Activity
import android.app.Application import android.app.Application
import android.app.NotificationChannel 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.os.Build import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
@ -56,7 +54,7 @@ class App : Application() {
.lifecycleObserverAlwaysActive(true) .lifecycleObserverAlwaysActive(true)
.autoClear(false) .autoClear(false)
registerActivityLife() registerActivityLifecycleCallbacks(ActivityHelp)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -116,9 +114,7 @@ class App : Application() {
*/ */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() { private fun createChannelId() {
val notificationManager = (getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let {
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.let {
//用唯一的ID创建渠道对象 //用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel( val downloadChannel = NotificationChannel(
channelIdDownload, channelIdDownload,
@ -157,32 +153,4 @@ class App : Application() {
} }
} }
private fun registerActivityLife() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
ActivityHelp.remove(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
ActivityHelp.add(activity)
}
})
}
} }

@ -290,12 +290,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
return footerItems?.size() ?: 0 return footerItems?.size() ?: 0
} }
fun getItem(position: Int): ITEM? = fun getItem(position: Int): ITEM? = items.getOrNull(position)
if (position in 0 until items.size) items[position] else null
fun getItemByLayoutPosition(position: Int): ITEM? { fun getItemByLayoutPosition(position: Int): ITEM? {
val pos = position - getHeaderCount() val pos = position - getHeaderCount()
return if (pos in 0 until items.size) items[pos] else null return items.getOrNull(pos)
} }
fun getItems(): List<ITEM> = items fun getItems(): List<ITEM> = items

@ -4,8 +4,6 @@ import android.annotation.SuppressLint
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.putPrefBoolean
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.script.ScriptEngine import javax.script.ScriptEngine
import javax.script.ScriptEngineManager import javax.script.ScriptEngineManager
@ -29,14 +27,18 @@ object AppConst {
ScriptEngineManager().getEngineByName("rhino") ScriptEngineManager().getEngineByName("rhino")
} }
val TIME_FORMAT: SimpleDateFormat by lazy { val timeFormat: SimpleDateFormat by lazy {
SimpleDateFormat("HH:mm") SimpleDateFormat("HH:mm")
} }
val DATE_FORMAT: SimpleDateFormat by lazy { val dateFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yyyy/MM/dd HH:mm") SimpleDateFormat("yyyy/MM/dd HH:mm")
} }
val fileNameFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yy-MM-dd-HH-mm-ss")
}
val keyboardToolChars: List<String> by lazy { val keyboardToolChars: List<String> by lazy {
arrayListOf( arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
@ -48,18 +50,6 @@ object AppConst {
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local)) val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio)) val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
var bookGroupLocalShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
}
var bookGroupAudioShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
const val notificationIdRead = 1144771 const val notificationIdRead = 1144771
const val notificationIdAudio = 1144772 const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773 const val notificationIdWeb = 1144773

@ -11,7 +11,6 @@ object EventBus {
const val TIME_CHANGED = "timeChanged" const val TIME_CHANGED = "timeChanged"
const val UP_CONFIG = "upConfig" const val UP_CONFIG = "upConfig"
const val OPEN_CHAPTER = "openChapter" const val OPEN_CHAPTER = "openChapter"
const val REPLACE = "replace"
const val AUDIO_SUB_TITLE = "audioSubTitle" const val AUDIO_SUB_TITLE = "audioSubTitle"
const val AUDIO_STATE = "audioState" const val AUDIO_STATE = "audioState"
const val AUDIO_PROGRESS = "audioProgress" const val AUDIO_PROGRESS = "audioProgress"

@ -3,7 +3,6 @@ package io.legado.app.constant
object PreferKey { object PreferKey {
const val versionCode = "versionCode" const val versionCode = "versionCode"
const val themeMode = "themeMode" const val themeMode = "themeMode"
const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar" const val hideStatusBar = "hideStatusBar"
const val clickTurnPage = "clickTurnPage" const val clickTurnPage = "clickTurnPage"
const val clickAllNext = "clickAllNext" const val clickAllNext = "clickAllNext"
@ -43,4 +42,5 @@ object PreferKey {
const val shareLayout = "shareLayout" const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect" const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces" const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh"
} }

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

@ -12,24 +12,30 @@ interface BookDao {
@Query("SELECT * FROM books order by durChapterTime desc") @Query("SELECT * FROM books order by durChapterTime desc")
fun observeAll(): LiveData<List<Book>> fun observeAll(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE type = ${BookType.audio} order by durChapterTime desc") @Query("SELECT * FROM books WHERE type = ${BookType.audio}")
fun observeAudio(): LiveData<List<Book>> fun observeAudio(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") @Query("SELECT * FROM books WHERE origin = '${BookType.local}'")
fun observeLocal(): LiveData<List<Book>> fun observeLocal(): LiveData<List<Book>>
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") @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 order by durChapterTime desc") @Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0")
fun observeDownload(): LiveData<List<Book>> 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>>
@Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>> fun liveDataSearch(key: String): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun getBooksByGroup(group: Int): List<Book>
@Query("SELECT * FROM books WHERE `name` in (:names)") @Query("SELECT * FROM books WHERE `name` in (:names)")
fun findByName(vararg names: String): List<Book> fun findByName(vararg names: String): List<Book>
@ -54,6 +60,9 @@ interface BookDao {
@get:Query("SELECT COUNT(*) FROM books") @get:Query("SELECT COUNT(*) FROM books")
val allBookCount: Int val allBookCount: Int
@get:Query("select max(`order`) from books")
val maxOrder: Int
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg book: Book) fun insert(vararg book: Book)

@ -50,9 +50,12 @@ interface BookSourceDao {
@get:Query("select * from book_sources order by customOrder asc") @get:Query("select * from book_sources order by customOrder asc")
val all: List<BookSource> val all: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 order by customOrder asc") @get:Query("select * from book_sources where enabled = 1 order by customOrder")
val allEnabled: List<BookSource> val allEnabled: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
val allTextEnabled: List<BookSource>
@Query("select * from book_sources where bookSourceUrl = :key") @Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource? fun getBookSource(key: String): BookSource?

@ -27,7 +27,7 @@ interface SearchBookDao {
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2 from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author where t1.name = :name and t1.author = :author and t2.enabled = 1
order by t2.customOrder order by t2.customOrder
""" """
) )
@ -38,7 +38,7 @@ interface SearchBookDao {
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2 from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author and originName like '%'||:key||'%' where t1.name = :name and t1.author = :author and originName like '%'||:key||'%' and t2.enabled = 1
order by t2.customOrder order by t2.customOrder
""" """
) )
@ -49,7 +49,7 @@ interface SearchBookDao {
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2 from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> '' where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> '' and t2.enabled = 1
order by t2.customOrder order by t2.customOrder
""" """
) )

@ -31,7 +31,7 @@ data class Book(
var intro: String? = null, // 简介内容(书源获取) var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改) var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType var type: Int = 0, // 0:text 1:audio
var group: Int = 0, // 自定义分组索引号 var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间 var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间

@ -16,4 +16,16 @@ data class RssStar(
var description: String? = null, var description: String? = null,
var content: String? = null, var content: String? = null,
var image: String? = null var image: String? = null
) ) {
fun toRssArticle(): RssArticle {
return RssArticle(
origin = origin,
title = title,
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image
)
}
}

@ -1,13 +1,15 @@
package io.legado.app.help package io.legado.app.help
import android.app.Activity import android.app.Activity
import android.app.Application
import android.os.Bundle
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
/** /**
* Activity管理器,管理项目中Activity的状态 * Activity管理器,管理项目中Activity的状态
*/ */
object ActivityHelp { object ActivityHelp : Application.ActivityLifecycleCallbacks {
private val activities: MutableList<WeakReference<Activity>> = arrayListOf() private val activities: MutableList<WeakReference<Activity>> = arrayListOf()
@ -86,4 +88,27 @@ object ActivityHelp {
} }
} }
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
remove(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
add(activity)
}
} }

@ -1,6 +1,7 @@
package io.legado.app.help package io.legado.app.help
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
@ -32,6 +33,9 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value) App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
} }
val requestedDirection: String?
get() = App.INSTANCE.getPrefString(R.string.pk_requested_direction)
var backupPath: String? var backupPath: String?
get() = App.INSTANCE.getPrefString(PreferKey.backupPath) get() = App.INSTANCE.getPrefString(PreferKey.backupPath)
set(value) { set(value) {
@ -49,7 +53,7 @@ object AppConfig {
} }
val autoRefreshBook: Boolean val autoRefreshBook: Boolean
get() = App.INSTANCE.getPrefBoolean(App.INSTANCE.getString(R.string.pk_auto_refresh)) get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh)
var threadCount: Int var threadCount: Int
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16)
@ -92,4 +96,45 @@ object AppConfig {
set(value) { set(value) {
App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value) App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value)
} }
var bookGroupAllShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAll", true)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAll", value)
}
var bookGroupLocalShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
}
var bookGroupAudioShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1)
set(value) {
App.INSTANCE.putPrefInt("elevation", value)
}
val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true)
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 ""
}

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

@ -106,7 +106,7 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder) super.clearView(recyclerView, viewHolder)
onItemTouchCallbackListener?.clearView(recyclerView, viewHolder) onItemTouchCallbackListener?.onClearView(recyclerView, viewHolder)
} }
interface OnItemTouchCallbackListener { interface OnItemTouchCallbackListener {
@ -131,7 +131,10 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
return true return true
} }
fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { /**
* 手指松开
*/
fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
} }

@ -2,7 +2,7 @@ package io.legado.app.help
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.DATE_FORMAT import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.EncoderUtils import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
@ -50,6 +50,6 @@ interface JsExtensions {
} }
fun timeFormat(time: Long): String { fun timeFormat(time: Long): String {
return DATE_FORMAT.format(Date(time)) return dateFormat.format(Date(time))
} }
} }

@ -5,10 +5,7 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.welcome.Launcher1 import io.legado.app.ui.welcome.*
import io.legado.app.ui.welcome.Launcher2
import io.legado.app.ui.welcome.Launcher3
import io.legado.app.ui.welcome.WelcomeActivity
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
/** /**
@ -20,7 +17,10 @@ object LauncherIconHelp {
private val componentNames = arrayListOf( private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name), ComponentName(App.INSTANCE, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::class.java.name), ComponentName(App.INSTANCE, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name) ComponentName(App.INSTANCE, Launcher3::class.java.name),
ComponentName(App.INSTANCE, Launcher4::class.java.name),
ComponentName(App.INSTANCE, Launcher5::class.java.name),
ComponentName(App.INSTANCE, Launcher6::class.java.name)
) )
fun changeIcon(icon: String?) { fun changeIcon(icon: String?) {

@ -70,12 +70,11 @@ 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
bg = durConfig.bgDrawable(width, height) bg = durConfig.bgDrawable(width, height).apply {
bg?.let { if (this is BitmapDrawable) {
if (it is BitmapDrawable) { bgMeanColor = BitmapUtils.getMeanColor(bitmap)
bgMeanColor = BitmapUtils.getMeanColor(it.bitmap) } else if (this is ColorDrawable) {
} else if (it is ColorDrawable) { bgMeanColor = color
bgMeanColor = it.color
} }
} }
} }
@ -169,7 +168,10 @@ object ReadBookConfig {
get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode
set(value) = set(value) =
if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value
var titleSize: Int
get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize
set(value) =
if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value
var titleTopSpacing: Int var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing
set(value) = set(value) =
@ -249,6 +251,12 @@ object ReadBookConfig {
if (shareLayout) shareConfig.footerPaddingTop = value if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value else durConfig.footerPaddingTop = value
var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine
set(value) =
if (shareLayout) shareConfig.showHeaderLine = value
else durConfig.showHeaderLine = value
var showFooterLine: Boolean var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) = set(value) =
@ -271,6 +279,7 @@ object ReadBookConfig {
var lineSpacingExtra: Int = 12,//行间距 var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距 var paragraphSpacing: Int = 12,//段距
var titleMode: Int = 0,//标题居中 var titleMode: Int = 0,//标题居中
var titleSize: Int = 0,
var titleTopSpacing: Int = 0, var titleTopSpacing: Int = 0,
var titleBottomSpacing: Int = 0, var titleBottomSpacing: Int = 0,
var paddingBottom: Int = 6, var paddingBottom: Int = 6,
@ -285,6 +294,7 @@ object ReadBookConfig {
var footerPaddingLeft: Int = 16, var footerPaddingLeft: Int = 16,
var footerPaddingRight: Int = 16, var footerPaddingRight: Int = 16,
var footerPaddingTop: Int = 6, var footerPaddingTop: Int = 6,
var showHeaderLine: Boolean = false,
var showFooterLine: Boolean = true var showFooterLine: Boolean = true
) { ) {
fun setBg(bgType: Int, bg: String) { fun setBg(bgType: Int, bg: String) {

@ -6,7 +6,7 @@ import kotlin.coroutines.CoroutineContext
class Coroutine<T>( class Coroutine<T>(
scope: CoroutineScope, val scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO, context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
) { ) {
@ -28,9 +28,10 @@ class Coroutine<T>(
private val job: Job private val job: Job
private var start: VoidCallback? = null private var start: VoidCallback? = null
private var success: Callback<T?>? = null private var success: Callback<T>? = null
private var error: Callback<Throwable>? = null private var error: Callback<Throwable>? = null
private var finally: VoidCallback? = null private var finally: VoidCallback? = null
private var cancel: VoidCallback? = null
private var timeMillis: Long? = null private var timeMillis: Long? = null
private var errorReturn: Result<T>? = null private var errorReturn: Result<T>? = null
@ -45,7 +46,7 @@ class Coroutine<T>(
get() = job.isCompleted get() = job.isCompleted
init { init {
this.job = executeInternal(scope, context, block) this.job = executeInternal(context, block)
} }
fun timeout(timeMillis: () -> Long): Coroutine<T> { fun timeout(timeMillis: () -> Long): Coroutine<T> {
@ -78,7 +79,7 @@ class Coroutine<T>(
fun onSuccess( fun onSuccess(
context: CoroutineContext? = null, context: CoroutineContext? = null,
block: suspend CoroutineScope.(T?) -> Unit block: suspend CoroutineScope.(T) -> Unit
): Coroutine<T> { ): Coroutine<T> {
this.success = Callback(context, block) this.success = Callback(context, block)
return this@Coroutine return this@Coroutine
@ -100,9 +101,28 @@ class Coroutine<T>(
return this@Coroutine return this@Coroutine
} }
fun onCancel(
context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit
): Coroutine<T> {
this.cancel = VoidCallback(context, block)
return this@Coroutine
}
//取消当前任务 //取消当前任务
fun cancel(cause: CancellationException? = null) { fun cancel(cause: CancellationException? = null) {
job.cancel(cause) job.cancel(cause)
cancel?.let {
MainScope().launch {
if (null == it.context) {
it.block.invoke(scope)
} else {
withContext(scope.coroutineContext.plus(it.context)) {
it.block.invoke(this)
}
}
}
}
} }
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {
@ -110,7 +130,6 @@ class Coroutine<T>(
} }
private fun executeInternal( private fun executeInternal(
scope: CoroutineScope,
context: CoroutineContext, context: CoroutineContext,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
): Job { ): Job {
@ -118,21 +137,27 @@ class Coroutine<T>(
try { try {
start?.let { dispatchVoidCallback(this, it) } start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, context, timeMillis ?: 0L, block) val value = executeBlock(scope, context, timeMillis ?: 0L, block)
success?.let { dispatchCallback(this, value, it) } if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
val consume: Boolean = errorReturn?.value?.let { value -> val consume: Boolean = errorReturn?.value?.let { value ->
success?.let { dispatchCallback(this, value, it) } if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
true true
} ?: false } ?: false
if (!consume) { if (!consume && isActive) {
error?.let { dispatchCallback(this, e, it) } error?.let { dispatchCallback(this, e, it) }
} }
} finally { } finally {
finally?.let { dispatchVoidCallback(this, it) } if (isActive) {
finally?.let { dispatchVoidCallback(this, it) }
}
} }
} }
} }
@ -152,6 +177,7 @@ class Coroutine<T>(
value: R, value: R,
callback: Callback<R> callback: Callback<R>
) { ) {
if (!scope.isActive) return
if (null == callback.context) { if (null == callback.context) {
callback.block.invoke(scope, value) callback.block.invoke(scope, value)
} else { } else {
@ -166,7 +192,7 @@ class Coroutine<T>(
context: CoroutineContext, context: CoroutineContext,
timeMillis: Long, timeMillis: Long,
noinline block: suspend CoroutineScope.() -> T noinline block: suspend CoroutineScope.() -> T
): T? { ): T {
return withContext(scope.coroutineContext.plus(context)) { return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) { if (timeMillis > 0L) withTimeout(timeMillis) {
block() block()

@ -3,9 +3,11 @@ package io.legado.app.help.http
import io.legado.app.help.http.api.HttpGetApi import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import retrofit2.Retrofit import retrofit2.Retrofit
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -13,14 +15,12 @@ import kotlin.coroutines.resume
object HttpHelper { object HttpHelper {
val client: OkHttpClient by lazy { val client: OkHttpClient by lazy {
val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs = ArrayList<ConnectionSpec>() val specs = arrayListOf(
specs.add(default) ConnectionSpec.MODERN_TLS,
specs.add(ConnectionSpec.COMPATIBLE_TLS) ConnectionSpec.COMPATIBLE_TLS,
specs.add(ConnectionSpec.CLEARTEXT) ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
@ -57,6 +57,16 @@ object HttpHelper {
return null return null
} }
suspend fun simpleGetByteAsync(url: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java)
.getMapByteAsync(url, mapOf(), mapOf())
.body()
}
return null
}
inline fun <reified T> getApiService(baseUrl: String, encode: String? = null): T { inline fun <reified T> getApiService(baseUrl: String, encode: String? = null): T {
return getRetrofit(baseUrl, encode).create(T::class.java) return getRetrofit(baseUrl, encode).create(T::class.java)
} }

@ -1,8 +1,11 @@
package io.legado.app.help.permission package io.legado.app.help.permission
import android.os.Build
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.jetbrains.anko.startActivity
import java.util.ArrayList
class PermissionsCompat private constructor() { class PermissionsCompat private constructor() {
@ -12,6 +15,17 @@ class PermissionsCompat private constructor() {
RequestManager.pushRequest(request) RequestManager.pushRequest(request)
} }
companion object {
// 检查权限, 如果已经拥有返回 true
fun check(activity: AppCompatActivity, vararg permissions: String): Boolean {
var request: Request = Request(activity)
var pers = ArrayList<String>()
pers.addAll(listOf(*permissions))
var data = request.getDeniedPermissions(pers.toTypedArray())
return data == null;
}
}
class Builder { class Builder {
private val request: Request private val request: Request

@ -102,7 +102,7 @@ internal class Request : OnRequestPermissionsResultCallback {
deniedCallback = null deniedCallback = null
} }
private fun getDeniedPermissions(permissions: Array<String>?): Array<String>? { fun getDeniedPermissions(permissions: Array<String>?): Array<String>? {
if (permissions != null) { if (permissions != null) {
val deniedPermissionList = ArrayList<String>() val deniedPermissionList = ArrayList<String>()
for (permission in permissions) { for (permission in permissions) {

@ -21,14 +21,6 @@ object Backup {
FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath
} }
val legadoPath by lazy {
FileUtils.getSdCardPath() + File.separator + "YueDu3.0"
}
val exportPath by lazy {
legadoPath + File.separator + "Export"
}
val backupFileNames by lazy { val backupFileNames by lazy {
arrayOf( arrayOf(
"bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json", "bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
@ -39,50 +31,48 @@ object Backup {
fun autoBack(context: Context) { fun autoBack(context: Context) {
val lastBackup = context.getPrefLong(PreferKey.lastBackup) val lastBackup = context.getPrefLong(PreferKey.lastBackup)
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
return Coroutine.async {
} context.getPrefString(PreferKey.backupPath)?.let {
Coroutine.async { backup(context, it, true)
val backupPath = context.getPrefString(PreferKey.backupPath) }
if (backupPath.isNullOrEmpty()) {
backup(context)
} else {
backup(context, backupPath)
} }
} }
} }
suspend fun backup(context: Context, path: String = legadoPath) { suspend fun backup(context: Context, path: String, isAuto: Boolean = false) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath) synchronized(this@Backup) {
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath) writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath) writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath) writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath) writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath) writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let { writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName) GSON.toJson(ReadBookConfig.configList)?.let {
.writeText(it) FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
} .writeText(it)
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> }
val edit = sp.edit() Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
App.INSTANCE.defaultSharedPreferences.all.map { val edit = sp.edit()
when (val value = it.value) { App.INSTANCE.defaultSharedPreferences.all.map {
is Int -> edit.putInt(it.key, value) when (val value = it.value) {
is Boolean -> edit.putBoolean(it.key, value) is Int -> edit.putInt(it.key, value)
is Long -> edit.putLong(it.key, value) is Boolean -> edit.putBoolean(it.key, value)
is Float -> edit.putFloat(it.key, value) is Long -> edit.putLong(it.key, value)
is String -> edit.putString(it.key, value) is Float -> edit.putFloat(it.key, value)
else -> Unit is String -> edit.putString(it.key, value)
else -> Unit
}
} }
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path), isAuto)
} else {
copyBackup(File(path), isAuto)
} }
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path))
} else {
copyBackup(File(path))
} }
} }
} }
@ -95,27 +85,39 @@ object Backup {
} }
@Throws(java.lang.Exception::class) @Throws(java.lang.Exception::class)
private fun copyBackup(context: Context, uri: Uri) { private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)
if (file.exists()) { if (file.exists()) {
treeDoc.findFile(fileName)?.delete() if (isAuto) {
treeDoc.createFile("", fileName) treeDoc.findFile("auto")?.findFile(fileName)?.delete()
?.writeBytes(context, file.readBytes()) DocumentUtils.createFileIfNotExist(
treeDoc,
fileName,
subDirs = *arrayOf("auto")
)?.writeBytes(context, file.readBytes())
} else {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
}
} }
} }
} }
} }
@Throws(java.lang.Exception::class) @Throws(java.lang.Exception::class)
private fun copyBackup(rootFile: File) { private fun copyBackup(rootFile: File, isAuto: Boolean) {
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)
if (file.exists()) { if (file.exists()) {
file.copyTo( file.copyTo(
FileUtils.createFileIfNotExist(rootFile, fileName), if (isAuto) {
true FileUtils.createFileIfNotExist(rootFile, fileName, "auto")
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
) )
} }
} }

@ -40,7 +40,6 @@ object OldBook {
book.durChapterTitle = jsonItem.readString("$.durChapterName") book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce") book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName") book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0

@ -128,7 +128,7 @@ object Restore {
bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)
} }
ChapterProvider.upStyle() ChapterProvider.upStyle()
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = false)
} }
withContext(Main) { withContext(Main) {
if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) { if (AppConfig.isNightTheme && AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {

@ -199,13 +199,12 @@ object ATH {
fun applyBottomNavigationColor(bottomBar: BottomNavigationView?) { fun applyBottomNavigationColor(bottomBar: BottomNavigationView?) {
bottomBar?.apply { bottomBar?.apply {
setBackgroundColor(ThemeStore.backgroundColor(context)) setBackgroundColor(ThemeStore.bottomBackground(context))
val colorStateList = Selector.colorBuild() val colorStateList = Selector.colorBuild()
.setDefaultColor(context.getCompatColor(R.color.btn_bg_press_tp)) .setDefaultColor(context.getCompatColor(R.color.btn_bg_press_tp))
.setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create() .setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create()
itemIconTintList = colorStateList itemIconTintList = colorStateList
itemTextColor = colorStateList itemTextColor = colorStateList
setBackgroundColor(ThemeStore.bottomBackground(context))
} }
} }

@ -19,4 +19,16 @@ object ATHUtils {
a.recycle() a.recycle()
} }
} }
@JvmOverloads
fun resolveFloat(context: Context, @AttrRes attr: Int, fallback: Float = 0.0f): Float {
val a = context.theme.obtainStyledAttributes(intArrayOf(attr))
return try {
a.getFloat(0, fallback)
} catch (e: Exception) {
fallback
} finally {
a.recycle()
}
}
} }

@ -107,4 +107,7 @@ val Context.isDarkTheme: Boolean
get() = ColorUtils.isColorLight(ThemeStore.primaryColor(this)) get() = ColorUtils.isColorLight(ThemeStore.primaryColor(this))
val Fragment.isDarkTheme: Boolean val Fragment.isDarkTheme: Boolean
get() = requireContext().isDarkTheme get() = requireContext().isDarkTheme
val Context.elevation: Float
get() = ThemeStore.elevation(this)

@ -284,6 +284,14 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
) )
} }
@CheckResult
fun elevation(context: Context): Float {
return prefs(context).getFloat(
ThemeStorePrefKeys.KEY_ELEVATION,
ATHUtils.resolveFloat(context, android.R.attr.elevation, context.resources.getDimension(R.dimen.design_appbar_elevation))
)
}
@CheckResult @CheckResult
@ColorInt @ColorInt
fun bottomBackground(context: Context): Int { fun bottomBackground(context: Context): Int {

@ -27,4 +27,6 @@ object ThemeStorePrefKeys {
const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar" const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar"
const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar"
const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark"
const val KEY_ELEVATION = "elevation"
} }

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

@ -56,9 +56,7 @@ object Debug {
log(debugSource, "︾开始解析") log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null) Rss.getArticles(rssSource, null)
.onSuccess { .onSuccess {
if (it == null) { if (it.articles.isEmpty()) {
log(debugSource, "︽解析失败", state = -1)
} else if(it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空") log(debugSource, "⇒列表页解析成功,为空")
log(debugSource, "︽解析完成", state = 1000) log(debugSource, "︽解析完成", state = 1000)
} else { } else {
@ -69,7 +67,7 @@ object Debug {
if (ruleContent.isNullOrEmpty()) { if (ruleContent.isNullOrEmpty()) {
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000) log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
} else { } else {
rssContentDebug(it.articles[0], ruleContent) rssContentDebug(it.articles[0], ruleContent, rssSource)
} }
} else { } else {
log(debugSource, "⇒存在描述规则,不解析内容页") log(debugSource, "⇒存在描述规则,不解析内容页")
@ -82,9 +80,9 @@ object Debug {
} }
} }
private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String) { private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String, rssSource: RssSource) {
log(debugSource, "︾开始解析内容页") log(debugSource, "︾开始解析内容页")
Rss.getContent(rssArticle, ruleContent) Rss.getContent(rssArticle, ruleContent, rssSource)
.onSuccess { .onSuccess {
log(debugSource, it) log(debugSource, it)
log(debugSource, "︽内容页解析完成", state = 1000) log(debugSource, "︽内容页解析完成", state = 1000)
@ -140,14 +138,12 @@ object Debug {
log(debugSource, "︾开始解析发现页") log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1) val explore = webBook.exploreBook(url, 1)
.onSuccess { exploreBooks -> .onSuccess { exploreBooks ->
exploreBooks?.let { if (exploreBooks.isNotEmpty()) {
if (exploreBooks.isNotEmpty()) { log(debugSource, "︽发现页解析完成")
log(debugSource, "︽发现页解析完成") log(debugSource, showTime = false)
log(debugSource, showTime = false) infoDebug(webBook, exploreBooks[0].toBook())
infoDebug(webBook, exploreBooks[0].toBook()) } else {
} else { log(debugSource, "︽未获取到书籍", state = -1)
log(debugSource, "︽未获取到书籍", state = -1)
}
} }
} }
.onError { .onError {
@ -164,14 +160,12 @@ object Debug {
log(debugSource, "︾开始解析搜索页") log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1) val search = webBook.searchBook(key, 1)
.onSuccess { searchBooks -> .onSuccess { searchBooks ->
searchBooks?.let { if (searchBooks.isNotEmpty()) {
if (searchBooks.isNotEmpty()) { log(debugSource, "︽搜索页解析完成")
log(debugSource, "︽搜索页解析完成") log(debugSource, showTime = false)
log(debugSource, showTime = false) infoDebug(webBook, searchBooks[0].toBook())
infoDebug(webBook, searchBooks[0].toBook()) } else {
} else { log(debugSource, "︽未获取到书籍", state = -1)
log(debugSource, "︽未获取到书籍", state = -1)
}
} }
} }
.onError { .onError {
@ -197,16 +191,14 @@ object Debug {
private fun tocDebug(webBook: WebBook, book: Book) { private fun tocDebug(webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页") log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book) val chapterList = webBook.getChapterList(book)
.onSuccess { chapterList -> .onSuccess {
chapterList?.let { if (it.isNotEmpty()) {
if (it.isNotEmpty()) { log(debugSource, "︽目录页解析完成")
log(debugSource, "︽目录页解析完成") log(debugSource, showTime = false)
log(debugSource, showTime = false) val nextChapterUrl = if (it.size > 1) it[1].url else null
val nextChapterUrl = if (it.size > 1) it[1].url else null contentDebug(webBook, book, it[0], nextChapterUrl)
contentDebug(webBook, book, it[0], nextChapterUrl) } else {
} else { log(debugSource, "︽目录列表为空", state = -1)
log(debugSource, "︽目录列表为空", state = -1)
}
} }
} }
.onError { .onError {

@ -21,7 +21,10 @@ object Rss {
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> { ): Coroutine<Result> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(pageUrl ?: rssSource.sourceUrl) val analyzeUrl = AnalyzeUrl(
pageUrl ?: rssSource.sourceUrl,
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource) RssParserByRule.parseXML(body, rssSource)
} }
@ -30,12 +33,15 @@ object Rss {
fun getContent( fun getContent(
rssArticle: RssArticle, rssArticle: RssArticle,
ruleContent: String, ruleContent: String,
rssSource: RssSource?,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = AnalyzeUrl(rssArticle.link, baseUrl = rssArticle.origin) val body = AnalyzeUrl(
.getResponseAwait(rssArticle.origin) rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap()
).getResponseAwait(rssArticle.origin)
.body .body
val analyzeRule = AnalyzeRule() val analyzeRule = AnalyzeRule()
analyzeRule.setContent( analyzeRule.setContent(

@ -0,0 +1,93 @@
package io.legado.app.model
import io.legado.app.App
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var mSearchId = System.currentTimeMillis()
private var searchPage = 1
private var searchKey: String = ""
private var task: Coroutine<*>? = null
private fun initSearchPool() {
searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
}
fun search(searchId: Long, key: String) {
if (searchId != mSearchId) {
task?.cancel()
searchPool.close()
initSearchPool()
mSearchId = searchId
searchPage = 1
if (key.isEmpty()) {
return
} else {
this.searchKey = key
}
} else {
searchPage++
}
task = Coroutine.async(scope, searchPool) {
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
if (searchId == mSearchId) {
callBack.onSearchSuccess(it)
}
}
}
}.onStart {
callBack.onSearchStart()
}
task?.invokeOnCompletion {
if (searchId == mSearchId) {
callBack.onSearchFinish()
}
}
}
fun cancelSearch() {
task?.cancel()
mSearchId = 0
callBack.onSearchCancel()
}
fun close() {
task?.cancel()
mSearchId = 0
searchPool.close()
}
interface CallBack {
fun onSearchStart()
fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
fun onSearchFinish()
fun onSearchCancel()
}
}

@ -27,26 +27,36 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1, page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> { ): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
bookSource.searchUrl?.let { searchUrl -> searchBookSuspend(scope, key, page)
val analyzeUrl = AnalyzeUrl( }
ruleUrl = searchUrl, }
key = key,
page = page, suspend fun searchBookSuspend(
baseUrl = sourceUrl, scope: CoroutineScope,
headerMapF = bookSource.getHeaderMap() key: String,
) page: Int? = 1
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) ): ArrayList<SearchBook> {
BookList.analyzeBookList( bookSource.searchUrl?.let { searchUrl ->
res.body, val analyzeUrl = AnalyzeUrl(
bookSource, ruleUrl = searchUrl,
analyzeUrl, key = key,
res.url, page = page,
true baseUrl = sourceUrl,
) headerMapF = bookSource.getHeaderMap()
} ?: arrayListOf() )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
} }
return arrayListOf()
} }
/** /**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList( BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,
@ -84,8 +95,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> { ): Coroutine<Book> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body = val body =
if (!book.infoHtml.isNullOrEmpty()) { if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml book.infoHtml
@ -111,8 +122,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> { ): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body = val body =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml book.tocHtml
@ -139,36 +150,50 @@ class WebBook(val bookSource: BookSource) {
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
if (bookSource.getContentRule().content.isNullOrEmpty()) { getContentSuspend(
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") book, bookChapter, nextChapterUrl, scope
return@async bookChapter.url
}
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
val analyzeUrl =
AnalyzeUrl(
book = book,
ruleUrl = bookChapter.url,
baseUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap()
)
analyzeUrl.getResponseAwait(
bookSource.bookSourceUrl,
jsStr = bookSource.getContentRule().webJs,
sourceRegex = bookSource.getContentRule().sourceRegex
).body
}
BookContent.analyzeContent(
this,
body,
book,
bookChapter,
bookSource,
bookChapter.url,
nextChapterUrl
) )
} }
} }
/**
* 章节内容
*/
suspend fun getContentSuspend(
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT
): String {
if (bookSource.getContentRule().content.isNullOrEmpty()) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
return bookChapter.url
}
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
} else {
val analyzeUrl =
AnalyzeUrl(
book = book,
ruleUrl = bookChapter.url,
baseUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap()
)
analyzeUrl.getResponseAwait(
bookSource.bookSourceUrl,
jsStr = bookSource.getContentRule().webJs,
sourceRegex = bookSource.getContentRule().sourceRegex
).body
}
return BookContent.analyzeContent(
scope,
body,
book,
bookChapter,
bookSource,
bookChapter.url,
nextChapterUrl
)
}
} }

@ -13,9 +13,8 @@ import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.help.http.api.HttpPostApi import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.utils.* import io.legado.app.utils.*
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call import retrofit2.Call
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
@ -40,7 +39,7 @@ class AnalyzeUrl(
) : JsExtensions { ) : JsExtensions {
companion object { companion object {
private val pagePattern = Pattern.compile("<(.*?)>") private val pagePattern = Pattern.compile("<(.*?)>")
private val jsonType = "application/json; charset=utf-8".toMediaTypeOrNull() private val jsonType = MediaType.parse("application/json; charset=utf-8")
} }
private var baseUrl: String = "" private var baseUrl: String = ""
@ -182,7 +181,7 @@ class AnalyzeUrl(
RequestMethod.POST -> { RequestMethod.POST -> {
bodyTxt?.let { bodyTxt?.let {
if (it.isJson()) { if (it.isJson()) {
body = it.toRequestBody(jsonType) body = RequestBody.create(jsonType, it)
} else { } else {
analyzeFields(it) analyzeFields(it)
} }

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

@ -4,29 +4,45 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentPath
import java.io.File
object LocalBook { object LocalBook {
fun importFile(doc: DocumentFile) { fun importFile(path: String) {
doc.name?.let { fileName -> val fileName = if (path.isContentPath()) {
val str = fileName.substringBeforeLast(".") val doc = DocumentFile.fromSingleUri(App.INSTANCE, Uri.parse(path))
var name = str.substringBefore("作者") doc?.name ?: ""
val author = str.substringAfter("作者", "") } else {
val smhStart = name.indexOf("") File(path).name
val smhEnd = name.indexOf("") }
if (smhStart != -1 && smhEnd != -1) { val str = fileName.substringBeforeLast(".")
name = (name.substring(smhStart + 1, smhEnd)) val authorIndex = str.indexOf("作者")
} var name: String
val book = Book( var author: String
bookUrl = doc.uri.toString(), if (authorIndex == -1) {
name = name, name = str
author = author, author = ""
originName = fileName } else {
) name = str.substring(0, authorIndex)
App.db.bookDao().insert(book) author = str.substring(authorIndex)
author = BookHelp.formatAuthor(author)
}
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
} }
val book = Book(
bookUrl = path,
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
} }
fun deleteBook(book: Book, deleteOriginal: Boolean) { fun deleteBook(book: Book, deleteOriginal: Boolean) {
@ -37,8 +53,12 @@ object LocalBook {
} }
if (deleteOriginal) { if (deleteOriginal) {
val uri = Uri.parse(book.bookUrl) if (book.bookUrl.isContentPath()) {
DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete() val uri = Uri.parse(book.bookUrl)
DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete()
} else {
FileUtils.deleteFile(book.bookUrl)
}
} }
} }
} }

@ -2,6 +2,7 @@ package io.legado.app.model.webBook
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.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.BookSource import io.legado.app.data.entities.BookSource
@ -39,7 +40,12 @@ object BookContent {
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource book, baseUrl, body, contentRule, bookChapter, bookSource
) )
content.append(contentData.content.replace(bookChapter.title, "")) if (bookSource.bookSourceType == BookType.default) {
content.append(contentData.content.replace(bookChapter.title, "")).append("\n")
} else {
content.append(contentData.content).append("\n")
}
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty()) val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
@ -64,7 +70,7 @@ object BookContent {
) )
nextUrl = nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
content.append(contentData.content) content.append(contentData.content).append("\n")
} }
} }
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
@ -91,7 +97,7 @@ object BookContent {
} }
} }
for (item in contentDataList) { for (item in contentDataList) {
content.append(item.content) content.append(item.content).append("\n")
} }
} }

@ -9,11 +9,15 @@ import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
object BookList { object BookList {
@Throws(Exception::class) @Throws(Exception::class)
fun analyzeBookList( fun analyzeBookList(
scope: CoroutineScope,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
analyzeUrl: AnalyzeUrl, analyzeUrl: AnalyzeUrl,
@ -28,12 +32,13 @@ object BookList {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(null) val analyzeRule = AnalyzeRule(null)
analyzeRule.setContent(body, baseUrl) analyzeRule.setContent(body, baseUrl)
bookSource.bookUrlPattern?.let { bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) { if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -59,7 +64,7 @@ object BookList {
collections = analyzeRule.getElements(ruleList) collections = analyzeRule.getElements(ruleList)
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -74,8 +79,9 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) { for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem( getSearchItem(
item, analyzeRule, bookSource, baseUrl, index == 0, scope, item, analyzeRule, bookSource, baseUrl, index == 0,
ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor, ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,
ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind, ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,
ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount
@ -93,7 +99,9 @@ object BookList {
return bookList return bookList
} }
@Throws(Exception::class)
private fun getInfoItem( private fun getInfoItem(
scope: CoroutineScope,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String baseUrl: String
@ -108,29 +116,37 @@ object BookList {
with(bookSource.getBookInfoRule()) { with(bookSource.getBookInfoRule()) {
init?.let { init?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it)) analyzeRule.setContent(analyzeRule.getElement(it))
} }
} }
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名") Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = analyzeRule.getString(name) searchBook.name = analyzeRule.getString(name)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}") Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}") Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount) searchBook.wordCount = analyzeRule.getString(wordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}") Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter) searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}") Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro) searchBook.intro = analyzeRule.getString(intro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true) searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -140,7 +156,9 @@ object BookList {
return null return null
} }
@Throws(Exception::class)
private fun getSearchItem( private fun getSearchItem(
scope: CoroutineScope,
item: Any, item: Any,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
@ -162,30 +180,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook analyzeRule.book = searchBook
analyzeRule.setContent(item) analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = analyzeRule.getString(ruleName) searchBook.name = analyzeRule.getString(ruleName)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount) searchBook.wordCount = analyzeRule.getString(ruleWordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro) searchBook.intro = analyzeRule.getString(ruleIntro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let { analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true) searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) { if (searchBook.bookUrl.isEmpty()) {

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

@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat
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.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.IntentAction
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.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -275,7 +275,7 @@ class AudioPlayService : BaseService(),
AudioPlay.book?.let { book -> AudioPlay.book?.let { book ->
AudioPlay.webBook?.getContent(book, chapter, scope = this) AudioPlay.webBook?.getContent(book, chapter, scope = this)
?.onSuccess(IO) { content -> ?.onSuccess(IO) { content ->
if (content.isNullOrEmpty()) { if (content.isEmpty()) {
withContext(Main) { withContext(Main) {
toast("未获取到资源链接") toast("未获取到资源链接")
} }

@ -111,11 +111,17 @@ abstract class BaseReadAloudService : BaseService(),
if (getPrefBoolean(PreferKey.readAloudByPage)) { if (getPrefBoolean(PreferKey.readAloudByPage)) {
for (index in pageIndex..textChapter.lastIndex()) { for (index in pageIndex..textChapter.lastIndex()) {
textChapter.page(index)?.text?.split("\n")?.let { textChapter.page(index)?.text?.split("\n")?.let {
contentList.addAll(it) if (it.isNotEmpty()) {
contentList.addAll(it)
}
} }
} }
} else { } else {
contentList.addAll(textChapter.getUnRead(pageIndex).split("\n")) textChapter.getUnRead(pageIndex).split("\n").forEach {
if (it.isNotEmpty()) {
contentList.add(it)
}
}
} }
if (play) play() if (play) play()
} ?: stopSelf() } ?: stopSelf()

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

@ -13,6 +13,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -25,12 +26,14 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() { class DownloadService : BaseService() {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf() private var tasks = CompositeCoroutine()
private val handler = Handler() private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>();
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载" private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy { private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download) .setSmallIcon(R.drawable.ic_download)
@ -97,6 +100,11 @@ class DownloadService : BaseService() {
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) { val task = Coroutine.async(this, context = searchPool) {
downloadMap.forEach { entry -> downloadMap.forEach { entry ->
@ -106,6 +114,9 @@ class DownloadService : BaseService() {
val bookSource = val bookSource =
App.db.bookSourceDao().getBookSource(book.origin) ?: return@async App.db.bookSourceDao().getBookSource(book.origin) ?: return@async
val webBook = WebBook(bookSource) val webBook = WebBook(bookSource)
downloadCount[entry.key] = DownloadCount()
entry.value.forEach { chapter -> entry.value.forEach { chapter ->
if (!isActive) return@async if (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) { if (downloadMap.containsKey(book.bookUrl)) {
@ -115,17 +126,14 @@ class DownloadService : BaseService() {
chapter, chapter,
scope = this, scope = this,
context = searchPool context = searchPool
) ).onSuccess(IO) { content ->
.onStart { downloadCount[entry.key]?.increaseSuccess()
notificationContent = chapter.title BookHelp.saveContent(book, chapter, content)
}
.onSuccess(IO) { content ->
content?.let {
BookHelp.saveContent(book, chapter, content)
}
} }
.onFinally(IO) { .onFinally(IO) {
synchronized(this@DownloadService) { synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) }
val chapterMap = val chapterMap =
finalMap[book.bookUrl] finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply { ?: linkedSetOf<BookChapter>().apply {
@ -135,9 +143,14 @@ class DownloadService : BaseService() {
if (chapterMap.size == entry.value.size) { if (chapterMap.size == entry.value.size) {
downloadMap.remove(book.bookUrl) downloadMap.remove(book.bookUrl)
finalMap.remove(book.bookUrl) finalMap.remove(book.bookUrl)
downloadCount.remove(entry.key)
} }
} }
} }
} else{
//无需下载的,设置为增加成功
downloadCount[entry.key]?.increaseSuccess()
downloadCount[entry.key]?.increaseFinished()
} }
} }
} }
@ -149,7 +162,7 @@ class DownloadService : BaseService() {
tasks.add(task) tasks.add(task)
task.invokeOnCompletion { task.invokeOnCompletion {
tasks.remove(task) tasks.remove(task)
if (tasks.isEmpty()) { if (tasks.isEmpty) {
stopSelf() stopSelf()
} }
} }
@ -176,4 +189,21 @@ class DownloadService : BaseService() {
val notification = builder.build() val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification) startForeground(AppConst.notificationIdDownload, notification)
} }
class DownloadCount {
@Volatile
var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile
var successCount = 0 //下载成功的条目数量
fun increaseSuccess() {
++successCount;
}
fun increaseFinished() {
++downloadFinishedCount;
}
}
} }

@ -29,7 +29,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
} }
} }
private var ttsIsSuccess: Boolean = false private var ttsInitFinish = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -42,22 +42,23 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
clearTTS() clearTTS()
} }
@Synchronized
override fun onInit(status: Int) { override fun onInit(status: Int) {
launch { if (status == TextToSpeech.SUCCESS) {
if (status == TextToSpeech.SUCCESS) { textToSpeech?.language = Locale.CHINA
textToSpeech?.language = Locale.CHINA textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener()) ttsInitFinish = true
ttsIsSuccess = true play()
play() } else {
} else { launch {
toast(R.string.tts_init_failed) toast(R.string.tts_init_failed)
} }
} }
} }
@Suppress("DEPRECATION") @Synchronized
override fun play() { override fun play() {
if (contentList.isEmpty() || !ttsIsSuccess) { if (contentList.isEmpty() || !ttsInitFinish) {
return return
} }
if (requestFocus()) { if (requestFocus()) {
@ -175,9 +176,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
} }
override fun onError(s: String) { override fun onError(s: String) {
launch { //nothing
toast(s)
}
} }
} }

@ -21,7 +21,6 @@ import org.jetbrains.anko.toast
object ReadBook { object ReadBook {
var titleDate = MutableLiveData<String>() var titleDate = MutableLiveData<String>()
var book: Book? = null var book: Book? = null
var inBookshelf = false var inBookshelf = false
@ -34,9 +33,10 @@ object ReadBook {
var curTextChapter: TextChapter? = null var curTextChapter: TextChapter? = null
var nextTextChapter: TextChapter? = null var nextTextChapter: TextChapter? = null
var webBook: WebBook? = null var webBook: WebBook? = null
var msg: String? = null
private val loadingChapters = arrayListOf<Int>() private val loadingChapters = arrayListOf<Int>()
fun resetData(book: Book) { fun resetData(book: Book, noSource: (name: String, author: String) -> Unit) {
this.book = book this.book = book
titleDate.postValue(book.name) titleDate.postValue(book.name)
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
@ -46,12 +46,27 @@ object ReadBook {
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
upWebBook(book.origin) upWebBook(book, noSource)
} }
fun upWebBook(origin: String) { fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) {
val bookSource = App.db.bookSourceDao().getBookSource(origin) book ?: return
webBook = if (bookSource != null) WebBook(bookSource) else null if (book.origin == BookType.local) {
webBook = null
} else {
val bookSource = App.db.bookSourceDao().getBookSource(book.origin)
if (bookSource != null) {
webBook = WebBook(bookSource)
} else {
webBook = null
noSource.invoke(book.name, book.author)
}
}
}
fun upMsg(msg: String?) {
this.msg = msg
callBack?.upContent()
} }
fun moveToNextPage() { fun moveToNextPage() {
@ -69,11 +84,11 @@ object ReadBook {
nextTextChapter = null nextTextChapter = null
book?.let { book?.let {
if (curTextChapter == null) { if (curTextChapter == null) {
loadContent(durChapterIndex, upContent) loadContent(durChapterIndex, upContent, false)
} else if (upContent) { } else if (upContent) {
callBack?.upContent() callBack?.upContent()
} }
loadContent(durChapterIndex.plus(1), upContent) loadContent(durChapterIndex.plus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
for (i in 2..10) { for (i in 2..10) {
delay(100) delay(100)
@ -99,11 +114,11 @@ object ReadBook {
prevTextChapter = null prevTextChapter = null
book?.let { book?.let {
if (curTextChapter == null) { if (curTextChapter == null) {
loadContent(durChapterIndex, upContent) loadContent(durChapterIndex, upContent, false)
} else if (upContent) { } else if (upContent) {
callBack?.upContent() callBack?.upContent()
} }
loadContent(durChapterIndex.minus(1), upContent) loadContent(durChapterIndex.minus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
for (i in -5..-2) { for (i in -5..-2) {
delay(100) delay(100)
@ -184,21 +199,21 @@ object ReadBook {
/** /**
* 加载章节内容 * 加载章节内容
*/ */
fun loadContent() { fun loadContent(resetPageOffset: Boolean) {
loadContent(durChapterIndex) loadContent(durChapterIndex, resetPageOffset = resetPageOffset)
loadContent(durChapterIndex + 1) loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
loadContent(durChapterIndex - 1) loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
} }
fun loadContent(index: Int, upContent: Boolean = true) { fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) {
book?.let { book -> book?.let { book ->
if (addLoading(index)) { if (addLoading(index)) {
Coroutine.async { Coroutine.async {
App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter ->
BookHelp.getContent(book, chapter)?.let { BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(chapter, it, upContent) contentLoadFinish(chapter, it, upContent, resetPageOffset)
removeLoading(chapter.index) removeLoading(chapter.index)
} ?: download(chapter) } ?: download(chapter, resetPageOffset = resetPageOffset)
} ?: removeLoading(index) } ?: removeLoading(index)
}.onError { }.onError {
removeLoading(index) removeLoading(index)
@ -216,7 +231,7 @@ object ReadBook {
if (BookHelp.hasContent(book, chapter)) { if (BookHelp.hasContent(book, chapter)) {
removeLoading(chapter.index) removeLoading(chapter.index)
} else { } else {
download(chapter) download(chapter, false)
} }
} ?: removeLoading(index) } ?: removeLoading(index)
}.onError { }.onError {
@ -226,20 +241,28 @@ object ReadBook {
} }
} }
private fun download(chapter: BookChapter) { private fun download(chapter: BookChapter, resetPageOffset: Boolean) {
book?.let { book -> book?.let { book ->
webBook?.getContent(book, chapter) webBook?.getContent(book, chapter)
?.onSuccess(Dispatchers.IO) { content -> ?.onSuccess(Dispatchers.IO) { content ->
if (content.isNullOrEmpty()) { if (content.isEmpty()) {
contentLoadFinish(chapter, App.INSTANCE.getString(R.string.content_empty)) contentLoadFinish(
chapter,
App.INSTANCE.getString(R.string.content_empty),
resetPageOffset = resetPageOffset
)
removeLoading(chapter.index) removeLoading(chapter.index)
} else { } else {
BookHelp.saveContent(book, chapter, content) BookHelp.saveContent(book, chapter, content)
contentLoadFinish(chapter, content) contentLoadFinish(chapter, content, resetPageOffset = resetPageOffset)
removeLoading(chapter.index) removeLoading(chapter.index)
} }
}?.onError { }?.onError {
contentLoadFinish(chapter, it.localizedMessage ?: "未知错误") contentLoadFinish(
chapter,
it.localizedMessage ?: "未知错误",
resetPageOffset = resetPageOffset
)
removeLoading(chapter.index) removeLoading(chapter.index)
} }
} }
@ -265,7 +288,8 @@ object ReadBook {
private fun contentLoadFinish( private fun contentLoadFinish(
chapter: BookChapter, chapter: BookChapter,
content: String, content: String,
upContent: Boolean = true upContent: Boolean = true,
resetPageOffset: Boolean
) { ) {
Coroutine.async { Coroutine.async {
if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) { if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) {
@ -279,18 +303,18 @@ object ReadBook {
when (chapter.index) { when (chapter.index) {
durChapterIndex -> { durChapterIndex -> {
curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent() if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView() callBack?.upView()
curPageChanged() curPageChanged()
callBack?.contentLoadFinish() callBack?.contentLoadFinish()
} }
durChapterIndex - 1 -> { durChapterIndex - 1 -> {
prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent(-1) if (upContent) callBack?.upContent(-1, resetPageOffset)
} }
durChapterIndex + 1 -> { durChapterIndex + 1 -> {
nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize)
if (upContent) callBack?.upContent(1) if (upContent) callBack?.upContent(1, resetPageOffset)
} }
} }
} }
@ -307,7 +331,7 @@ object ReadBook {
book.durChapterTime = System.currentTimeMillis() book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = durChapterIndex book.durChapterIndex = durChapterIndex
book.durChapterPos = durPageIndex book.durChapterPos = durPageIndex
curTextChapter?.let { App.db.bookChapterDao().getChapter(book.bookUrl, durChapterIndex)?.let {
book.durChapterTitle = it.title book.durChapterTitle = it.title
} }
App.db.bookDao().update(book) App.db.bookDao().update(book)
@ -316,7 +340,7 @@ object ReadBook {
} }
interface CallBack { interface CallBack {
fun upContent(relativePosition: Int = 0) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upView() fun upView()
fun upPageProgress() fun upPageProgress()
fun contentLoadFinish() fun contentLoadFinish()

@ -6,7 +6,7 @@
* book\info 书籍信息查看 * book\info 书籍信息查看
* book\read 书籍阅读界面 * book\read 书籍阅读界面
* book\search 搜索书籍界面 * book\search 搜索书籍界面
* book\source 搜索书源界面 * book\source 书源界面
* book\changeCover 封面换源界面 * book\changeCover 封面换源界面
* book\changeSource 换源界面 * book\changeSource 换源界面
* book\chapterList 目录界面 * book\chapterList 目录界面

@ -1,13 +1,19 @@
package io.legado.app.ui.about package io.legado.app.ui.about
import android.os.Bundle import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_about.*
import org.jetbrains.anko.share import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) { class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -17,6 +23,16 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fl_fragment, aboutFragment, fTag) .replace(R.id.fl_fragment, aboutFragment, fTag)
.commit() .commit()
tv_app_summary.post {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(tv_app_summary.text)
val start = spannableString.indexOf("开源阅读软件")
spannableString.setSpan(
span, start, start + 6,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
tv_app_summary.text = spannableString
}
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {

@ -2,7 +2,6 @@ package io.legado.app.ui.about
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
@ -12,7 +11,6 @@ import io.legado.app.utils.ACache
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import org.jetbrains.anko.longToast import org.jetbrains.anko.longToast
import java.net.URLEncoder
class DonateFragment : PreferenceFragmentCompat() { class DonateFragment : PreferenceFragmentCompat() {
@ -36,7 +34,6 @@ class DonateFragment : PreferenceFragmentCompat() {
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl) "zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl) "zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl) "qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext()) "zfbHbSsm" -> getZfbHb(requireContext())
"gzGzh" -> requireContext().sendToClip("开源阅读软件") "gzGzh" -> requireContext().sendToClip("开源阅读软件")
} }
@ -59,19 +56,4 @@ class DonateFragment : PreferenceFragmentCompat() {
} }
} }
private fun aliDonate(context: Context) {
try {
val qrCode = URLEncoder.encode(
"https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other",
"utf-8"
)
val aliPayQr =
"alipayqr://platformapi/startapp?saId=10000007&qrcode=$qrCode&_t=${System.currentTimeMillis()}"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr))
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
} }

@ -46,9 +46,8 @@ class AudioPlayActivity :
private var adjustProgress = false private var adjustProgress = false
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar) title_bar.transparent()
supportActionBar?.setDisplayHomeAsUpEnabled(true) AudioPlay.titleData.observe(this, Observer { title_bar.title = it })
AudioPlay.titleData.observe(this, Observer { toolbar.title = it })
AudioPlay.coverData.observe(this, Observer { upCover(it) }) AudioPlay.coverData.observe(this, Observer { upCover(it) })
viewModel.initData(intent) viewModel.initData(intent)
initView() initView()

@ -72,7 +72,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
execute { execute {
AudioPlay.webBook?.getChapterList(book, this) AudioPlay.webBook?.getChapterList(book, this)
?.onSuccess(Dispatchers.IO) { cList -> ?.onSuccess(Dispatchers.IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
AudioPlay.chapterSize = cList.size AudioPlay.chapterSize = cList.size
@ -91,6 +91,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
AudioPlay.book?.let { AudioPlay.book?.let {
book1.order = it.order
App.db.bookDao().delete(it) App.db.bookDao().delete(it)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

@ -6,12 +6,15 @@ import android.view.MenuItem
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
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
@ -21,6 +24,7 @@ import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.* import kotlinx.android.synthetic.main.activity_arrange_book.*
@ -32,6 +36,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
get() = getViewModel(ArrangeBookViewModel::class.java) get() = getViewModel(ArrangeBookViewModel::class.java)
override val groupList: ArrayList<BookGroup> = arrayListOf() override val groupList: ArrayList<BookGroup> = arrayListOf()
private val groupRequestCode = 22 private val groupRequestCode = 22
private val addToGroupRequestCode = 34
private lateinit var adapter: ArrangeBookAdapter private lateinit var adapter: ArrangeBookAdapter
private var groupLiveData: LiveData<List<BookGroup>>? = null private var groupLiveData: LiveData<List<BookGroup>>? = null
private var booksLiveData: LiveData<List<Book>>? = null private var booksLiveData: LiveData<List<Book>>? = null
@ -63,6 +68,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
recycler_view.addItemDecoration(VerticalDivider(this)) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this) adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = getPrefInt(PreferKey.bookshelfSort) == 3
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
select_action_bar.setMainActionText(R.string.move_to_group) select_action_bar.setMainActionText(R.string.move_to_group)
select_action_bar.inflateMenu(R.menu.arrange_book_sel) select_action_bar.inflateMenu(R.menu.arrange_book_sel)
select_action_bar.setOnMenuItemClickListener(this) select_action_bar.setOnMenuItemClickListener(this)
@ -99,11 +108,17 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
-1 -> App.db.bookDao().observeAll() -1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal() -2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio() -3 -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId) else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { booksLiveData?.observe(this, Observer { list ->
adapter.selectedBooks.clear() val books = when (getPrefInt(PreferKey.bookshelfSort)) {
adapter.setItems(it) 1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
}
adapter.setItems(books)
upSelectCount() upSelectCount()
}) })
} }
@ -112,28 +127,29 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
when (item.itemId) { when (item.itemId) {
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(supportFragmentManager, "groupManage") .show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = -11
initBookData()
}
R.id.menu_all -> { R.id.menu_all -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -1 groupId = -1
adapter.selectedBooks.clear()
initBookData() initBookData()
} }
R.id.menu_local -> { R.id.menu_local -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -2 groupId = -2
adapter.selectedBooks.clear()
initBookData() initBookData()
} }
R.id.menu_audio -> { R.id.menu_audio -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -3 groupId = -3
adapter.selectedBooks.clear()
initBookData() initBookData()
} }
else -> if (item.groupId == R.id.menu_group) { else -> if (item.groupId == R.id.menu_group) {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = item.itemId groupId = item.itemId
adapter.selectedBooks.clear()
initBookData() initBookData()
} }
} }
@ -144,9 +160,14 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
when (item?.itemId) { when (item?.itemId) {
R.id.menu_del_selection -> R.id.menu_del_selection ->
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
okButton { viewModel.deleteBook(*adapter.selectedBooks.toTypedArray()) } okButton { viewModel.deleteBook(*adapter.selectedBooks()) }
noButton { } noButton { }
}.show().applyTint() }.show().applyTint()
R.id.menu_update_enable ->
viewModel.upCanUpdate(adapter.selectedBooks(), true)
R.id.menu_update_disable ->
viewModel.upCanUpdate(adapter.selectedBooks(), false)
R.id.menu_add_to_group -> selectGroup(0, addToGroupRequestCode)
} }
return false return false
} }
@ -168,7 +189,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
when (requestCode) { when (requestCode) {
groupRequestCode -> { groupRequestCode -> {
val books = arrayListOf<Book>() val books = arrayListOf<Book>()
adapter.selectedBooks.forEach { adapter.selectedBooks().forEach {
books.add(it.copy(group = groupId)) books.add(it.copy(group = groupId))
} }
viewModel.updateBook(*books.toTypedArray()) viewModel.updateBook(*books.toTypedArray())
@ -178,11 +199,22 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
viewModel.updateBook(it.copy(group = groupId)) viewModel.updateBook(it.copy(group = groupId))
} }
} }
addToGroupRequestCode -> {
val books = arrayListOf<Book>()
adapter.selectedBooks().forEach {
books.add(it.copy(group = it.group or groupId))
}
viewModel.updateBook(*books.toTypedArray())
}
} }
} }
override fun upSelectCount() { override fun upSelectCount() {
select_action_bar.upCountView(adapter.selectedBooks.size, adapter.getItems().size) select_action_bar.upCountView(adapter.selectedBooks().size, adapter.getItems().size)
}
override fun updateBook(vararg book: Book) {
viewModel.updateBook(*book)
} }
override fun deleteBook(book: Book) { override fun deleteBook(book: Book) {

@ -1,19 +1,25 @@
package io.legado.app.ui.book.arrange package io.legado.app.ui.book.arrange
import android.content.Context import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
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
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.theme.backgroundColor
import kotlinx.android.synthetic.main.item_arrange_book.view.* import kotlinx.android.synthetic.main.item_arrange_book.view.*
import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.*
class ArrangeBookAdapter(context: Context, val callBack: CallBack) : class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book) { SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book),
ItemTouchCallback.OnItemTouchCallbackListener {
val groupRequestCode = 12 val groupRequestCode = 12
val selectedBooks: HashSet<Book> = hashSetOf() private val selectedBooks: HashSet<Book> = hashSetOf()
var actionItem: Book? = null var actionItem: Book? = null
fun selectAll(selectAll: Boolean) { fun selectAll(selectAll: Boolean) {
@ -40,14 +46,23 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
callBack.upSelectCount() callBack.upSelectCount()
} }
fun selectedBooks(): Array<Book> {
val books = arrayListOf<Book>()
selectedBooks.forEach {
if (getItems().contains(it)) {
books.add(it)
}
}
return books.toTypedArray()
}
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
tv_name.text = if (item.author.isEmpty()) { backgroundColor = context.backgroundColor
item.name tv_name.text = item.name
} else { tv_author.text = item.author
"${item.name}(${item.author})" tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE
} tv_group_s.text = getGroupName(item.group)
tv_author.text = getGroupName(item.group)
checkbox.isChecked = selectedBooks.contains(item) checkbox.isChecked = selectedBooks.contains(item)
} }
} }
@ -92,22 +107,57 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
} }
} }
private fun getGroupName(groupId: Int): String { private fun getGroupList(groupId: Int): List<String> {
val groupNames = arrayListOf<String>() val groupNames = arrayListOf<String>()
callBack.groupList.forEach { callBack.groupList.forEach {
if (it.groupId and groupId > 0) { if (it.groupId and groupId > 0) {
groupNames.add(it.groupName) groupNames.add(it.groupName)
} }
} }
return groupNames
}
private fun getGroupName(groupId: Int): String {
val groupNames = getGroupList(groupId)
if (groupNames.isEmpty()) { if (groupNames.isEmpty()) {
return context.getString(R.string.no_group) return ""
} }
return groupNames.joinToString(",") return groupNames.joinToString(",")
} }
private var isMoved = false
override fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
val srcItem = getItem(srcPosition)
val targetItem = getItem(targetPosition)
Collections.swap(getItems(), srcPosition, targetPosition)
notifyItemMoved(srcPosition, targetPosition)
if (srcItem != null && targetItem != null) {
if (srcItem.order == targetItem.order) {
for ((index, item) in getItems().withIndex()) {
item.order = index + 1
}
} else {
val pos = srcItem.order
srcItem.order = targetItem.order
targetItem.order = pos
}
}
isMoved = true
return true
}
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (isMoved) {
callBack.updateBook(*getItems().toTypedArray())
}
isMoved = false
}
interface CallBack { interface CallBack {
val groupList: List<BookGroup> val groupList: List<BookGroup>
fun upSelectCount() fun upSelectCount()
fun updateBook(vararg book: Book)
fun deleteBook(book: Book) fun deleteBook(book: Book)
fun selectGroup(groupId: Int, requestCode: Int) fun selectGroup(groupId: Int, requestCode: Int)
} }

@ -8,6 +8,15 @@ import io.legado.app.data.entities.Book
class ArrangeBookViewModel(application: Application) : BaseViewModel(application) { class ArrangeBookViewModel(application: Application) : BaseViewModel(application) {
fun upCanUpdate(books: Array<Book>, canUpdate: Boolean) {
execute {
books.forEach {
it.canUpdate = canUpdate
}
App.db.bookDao().update(*books)
}
}
fun updateBook(vararg book: Book) { fun updateBook(vararg book: Book) {
execute { execute {
App.db.bookDao().update(*book) App.db.bookDao().update(*book)

@ -51,14 +51,13 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
fun search() { fun search() {
task = execute { task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(Dispatchers.IO) { .onSuccess(Dispatchers.IO) {
if (it != null && it.isNotEmpty()) { if (it.isNotEmpty()) {
val searchBook = it[0] val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty() && !searchBook.coverUrl.isNullOrEmpty()
@ -72,6 +71,10 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
} }
} }
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {

@ -72,14 +72,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
fun search() { fun search() {
task = execute { task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) { for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute) //task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool) WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { .onSuccess(IO) {
it?.forEach { searchBook -> it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) { if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) { if (searchBook.tocUrl.isEmpty()) {
@ -95,6 +94,10 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
} }
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {
@ -107,7 +110,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess { .onSuccess {
it?.let { loadChapter(it) } loadChapter(it)
}.onError { }.onError {
debug { context.getString(R.string.error_get_book_info) } debug { context.getString(R.string.error_get_book_info) }
} }
@ -119,13 +122,11 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute { execute {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) { chapters ->
it?.let { chapters -> if (chapters.isNotEmpty()) {
if (chapters.isNotEmpty()) { book.latestChapterTitle = chapters.last().title
book.latestChapterTitle = chapters.last().title val searchBook: SearchBook = book.toSearchBook()
val searchBook: SearchBook = book.toSearchBook() searchFinish(searchBook)
searchFinish(searchBook)
}
} }
}.onError { }.onError {
debug { context.getString(R.string.error_get_chapter_list) } debug { context.getString(R.string.error_get_chapter_list) }

@ -36,6 +36,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
private var durChapterIndex = 0 private var durChapterIndex = 0
private lateinit var mLayoutManager: UpLinearLayoutManager private lateinit var mLayoutManager: UpLinearLayoutManager
private var tocLiveData: LiveData<List<BookChapter>>? = null private var tocLiveData: LiveData<List<BookChapter>>? = null
private var scrollToDurChapter = false
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.chapterCallBack = this viewModel.chapterCallBack = this
@ -74,7 +75,6 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.let { book?.let {
durChapterIndex = it.durChapterIndex durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle tv_current_chapter_info.text = it.durChapterTitle
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
initCacheFileNames(it) initCacheFileNames(it)
} }
} }
@ -85,7 +85,10 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl) tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl)
tocLiveData?.observe(viewLifecycleOwner, Observer { tocLiveData?.observe(viewLifecycleOwner, Observer {
adapter.setItems(it) adapter.setItems(it)
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) if (!scrollToDurChapter) {
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
scrollToDurChapter = true
}
}) })
} }
@ -103,7 +106,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.bookUrl?.let { bookUrl -> book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) { if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter))
adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) adapter.notifyItemChanged(chapter.index, true)
} }
} }
} }

@ -13,6 +13,7 @@ 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.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -81,9 +82,15 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
private fun initLiveData() { private fun initLiveData() {
bookshelfLiveData?.removeObservers(this) bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload() bookshelfLiveData = App.db.bookDao().observeDownload()
bookshelfLiveData?.observe(this, Observer { bookshelfLiveData?.observe(this, Observer { list ->
adapter.setItems(it) val books = when (getPrefInt(PreferKey.bookshelfSort)) {
initCacheSize(it) 1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
}
adapter.setItems(books)
initCacheSize(books)
}) })
} }
@ -127,7 +134,7 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
FilePicker.selectFolder(this, exportRequestCode) { FilePicker.selectFolder(this, exportRequestCode) {
val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey) val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey)
if (path.isNullOrEmpty()) { if (path.isNullOrEmpty()) {
toast("没有默认路径") toast(R.string.no_default_path)
} else { } else {
startExport(path) startExport(path)
} }

@ -35,11 +35,9 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
WebBook(source).exploreBook(url, page, this) WebBook(source).exploreBook(url, page, this)
.timeout(30000L) .timeout(30000L)
.onSuccess(IO) { searchBooks -> .onSuccess(IO) { searchBooks ->
searchBooks?.let { booksData.postValue(searchBooks)
booksData.postValue(searchBooks) App.db.searchBookDao().insert(*searchBooks.toTypedArray())
App.db.searchBookDao().insert(*searchBooks.toTypedArray()) page++
page++
}
} }
} }
} }

@ -20,18 +20,20 @@ import io.legado.app.App
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
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.AppConfig
import io.legado.app.help.ItemTouchCallback import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
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.requestInputMethod import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_group_manage.view.* import kotlinx.android.synthetic.main.item_group_manage.view.*
@ -42,7 +44,7 @@ import kotlin.collections.ArrayList
class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
private lateinit var viewModel: GroupViewModel private lateinit var viewModel: GroupViewModel
private lateinit var adapter: GroupAdapter private lateinit var adapter: GroupAdapter
private var callBack: CallBack? = null private val callBack: CallBack? get() = parentFragment as? CallBack
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -62,23 +64,19 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
callBack = parentFragment as? CallBack tool_bar.title = getString(R.string.group_manage)
initData() initData()
initMenu()
} }
private fun initData() { private fun initData() {
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_group_local)
.isChecked = AppConst.bookGroupLocalShow
tool_bar.menu.findItem(R.id.menu_group_audio)
.isChecked = AppConst.bookGroupAudioShow
adapter = GroupAdapter(requireContext()) adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(VerticalDivider(requireContext())) recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter recycler_view.adapter = adapter
tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() }
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult = val diffResult =
DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it)) DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it))
@ -90,17 +88,36 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
} }
private fun initMenu() {
tool_bar.setOnMenuItemClickListener(this)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.let {
it.applyTint(requireContext(), Theme.getTheme())
it.findItem(R.id.menu_group_all)
.isChecked = AppConfig.bookGroupAllShow
it.findItem(R.id.menu_group_local)
.isChecked = AppConfig.bookGroupLocalShow
it.findItem(R.id.menu_group_audio)
.isChecked = AppConfig.bookGroupAudioShow
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> addGroup()
R.id.menu_group_all -> {
item.isChecked = !item.isChecked
AppConfig.bookGroupAllShow = item.isChecked
callBack?.upGroup()
}
R.id.menu_group_local -> { R.id.menu_group_local -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
AppConst.bookGroupLocalShow = item.isChecked AppConfig.bookGroupLocalShow = item.isChecked
callBack?.upGroup() callBack?.upGroup()
} }
R.id.menu_group_audio -> { R.id.menu_group_audio -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
AppConst.bookGroupAudioShow = item.isChecked AppConfig.bookGroupAudioShow = item.isChecked
callBack?.upGroup() callBack?.upGroup()
} }
} }
@ -200,7 +217,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
return true return true
} }
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (isMoved) { if (isMoved) {
for ((index, item) in getItems().withIndex()) { for ((index, item) in getItems().withIndex()) {
item.order = index + 1 item.order = index + 1

@ -203,7 +203,7 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
return true return true
} }
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (isMoved) { if (isMoved) {
for ((index, item) in getItems().withIndex()) { for ((index, item) in getItems().withIndex()) {
item.order = index + 1 item.order = index + 1

@ -32,6 +32,13 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
fun delGroup(vararg bookGroup: BookGroup) { fun delGroup(vararg bookGroup: BookGroup) {
execute { execute {
App.db.bookGroupDao().delete(*bookGroup) App.db.bookGroupDao().delete(*bookGroup)
bookGroup.forEach { group ->
val books = App.db.bookDao().getBooksByGroup(group.groupId)
books.forEach {
it.group = it.group - group.groupId
}
App.db.bookDao().update(*books.toTypedArray())
}
} }
} }

@ -1,11 +1,14 @@
package io.legado.app.ui.book.info package io.legado.app.ui.book.info
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ -27,7 +30,9 @@ import io.legado.app.ui.book.chapterlist.ChapterListActivity
import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.utils.dp
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.visible import io.legado.app.utils.visible
@ -52,8 +57,7 @@ class BookInfoActivity :
get() = getViewModel(BookInfoViewModel::class.java) get() = getViewModel(BookInfoViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar) title_bar.transparent()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewModel.bookData.observe(this, Observer { showBook(it) }) viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.chapterListData.observe(this, Observer { upLoading(false, it) }) viewModel.chapterListData.observe(this, Observer { upLoading(false, it) })
viewModel.initData(intent) viewModel.initData(intent)
@ -86,8 +90,13 @@ class BookInfoActivity :
} }
} }
R.id.menu_can_update -> { R.id.menu_can_update -> {
viewModel.bookData.value?.let { if (viewModel.inBookshelf) {
it.canUpdate = !it.canUpdate viewModel.bookData.value?.let {
it.canUpdate = !it.canUpdate
viewModel.saveBook()
}
} else {
toast(R.string.after_add_bookshelf)
} }
} }
} }
@ -202,7 +211,7 @@ class BookInfoActivity :
ChangeSourceDialog.show(supportFragmentManager, it.name, it.author) ChangeSourceDialog.show(supportFragmentManager, it.name, it.author)
} }
} }
tv_toc.onClick { tv_toc_view.onClick {
if (!viewModel.inBookshelf) { if (!viewModel.inBookshelf) {
viewModel.saveBook { viewModel.saveBook {
viewModel.saveChapterList { viewModel.saveChapterList {
@ -213,30 +222,41 @@ class BookInfoActivity :
openChapterList() openChapterList()
} }
} }
tv_group.onClick { tv_change_group.onClick {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
GroupSelectDialog.show(supportFragmentManager, it.group) GroupSelectDialog.show(supportFragmentManager, it.group)
} }
} }
tv_author.onClick {
startActivity<SearchActivity>(Pair("key", viewModel.bookData.value?.author))
}
tv_name.onClick {
startActivity<SearchActivity>(Pair("key", viewModel.bookData.value?.name))
}
} }
@SuppressLint("InflateParams")
private fun deleteBook() { private fun deleteBook() {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
if (it.isLocalBook()) { if (it.isLocalBook()) {
alert( alert(
titleResource = R.string.sure, titleResource = R.string.sure,
messageResource = R.string.sure_delete_book_file messageResource = R.string.sure_del
) { ) {
positiveButton(R.string.yes) { val checkBox = CheckBox(this@BookInfoActivity).apply {
viewModel.delBook(true) { setText(R.string.delete_book_file)
finish() }
} val view = LinearLayout(this@BookInfoActivity).apply {
setPadding(16.dp, 0, 16.dp, 0)
addView(checkBox)
} }
negativeButton(R.string.no) { customView = view
viewModel.delBook(false) { positiveButton(R.string.yes) {
viewModel.delBook(checkBox.isChecked) {
finish() finish()
} }
} }
negativeButton(R.string.no)
}.show() }.show()
} else { } else {
viewModel.delBook { viewModel.delBook {

@ -59,13 +59,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let { bookData.postValue(book)
bookData.postValue(book) if (inBookshelf) {
if (inBookshelf) { App.db.bookDao().update(book)
App.db.bookDao().update(book)
}
loadChapter(it, changeDruChapterIndex)
} }
loadChapter(it, changeDruChapterIndex)
}.onError { }.onError {
toast(R.string.error_get_book_info) toast(R.string.error_get_book_info)
} }
@ -83,7 +81,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
) { ) {
execute { execute {
if (book.isLocalBook()) { if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let { AnalyzeTxtFile().analyze(context, book).let {
App.db.bookDao().update(book) App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray()) App.db.bookChapterDao().insert(*it.toTypedArray())
chapterListData.postValue(it) chapterListData.postValue(it)
@ -92,20 +90,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) {
it?.let { if (it.isNotEmpty()) {
if (it.isNotEmpty()) { if (inBookshelf) {
if (inBookshelf) { App.db.bookDao().update(book)
App.db.bookDao().update(book) App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookChapterDao().insert(*it.toTypedArray()) }
} if (changeDruChapterIndex == null) {
if (changeDruChapterIndex == null) { chapterListData.postValue(it)
chapterListData.postValue(it)
} else {
changeDruChapterIndex(it)
}
} else { } else {
toast(R.string.chapter_list_empty) changeDruChapterIndex(it)
} }
} else {
toast(R.string.chapter_list_empty)
} }
}.onError { }.onError {
chapterListData.postValue(null) chapterListData.postValue(null)
@ -116,6 +112,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
toast(R.string.error_no_source) toast(R.string.error_no_source)
} }
} }
}.onError {
toast("LoadTocError:${it.localizedMessage}")
} }
} }
@ -137,6 +135,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
execute { execute {
if (inBookshelf) { if (inBookshelf) {
bookData.value?.let { bookData.value?.let {
book.group = it.group
book.order = it.order
App.db.bookDao().delete(it) App.db.bookDao().delete(it)
} }
App.db.bookDao().insert(book) App.db.bookDao().insert(book)
@ -168,6 +168,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun saveBook(success: (() -> Unit)? = null) { fun saveBook(success: (() -> Unit)? = null) {
execute { execute {
bookData.value?.let { book -> bookData.value?.let { book ->
if (book.order == 0) {
book.order = App.db.bookDao().maxOrder + 1
}
App.db.bookDao().insert(book) App.db.bookDao().insert(book)
} }
}.onSuccess { }.onSuccess {
@ -188,6 +191,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun addToBookshelf(success: (() -> Unit)?) { fun addToBookshelf(success: (() -> Unit)?) {
execute { execute {
bookData.value?.let { book -> bookData.value?.let { book ->
if (book.order == 0) {
book.order = App.db.bookDao().maxOrder + 1
}
App.db.bookDao().insert(book) App.db.bookDao().insert(book)
} }
chapterListData.value?.let { chapterListData.value?.let {

@ -5,8 +5,10 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -16,10 +18,12 @@ 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.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.*
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_import_book.* import kotlinx.android.synthetic.main.activity_import_book.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -27,9 +31,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import java.io.File import java.io.File
import java.util.*
class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity_import_book), class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity_import_book),
FileChooserDialog.CallBack,
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
ImportBookAdapter.CallBack { ImportBookAdapter.CallBack {
private val requestCodeSelectFolder = 342 private val requestCodeSelectFolder = 342
@ -37,6 +43,8 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private val subDocs = arrayListOf<DocumentFile>() private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null private var localUriLiveData: LiveData<List<String>>? = null
private var sdPath = FileUtils.getSdCardPath()
private var path = sdPath
override val viewModel: ImportBookViewModel override val viewModel: ImportBookViewModel
get() = getViewModel(ImportBookViewModel::class.java) get() = getViewModel(ImportBookViewModel::class.java)
@ -57,7 +65,6 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
adapter = ImportBookAdapter(this, this) adapter = ImportBookAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
rotate_loading.loadingColor = accentColor
select_action_bar.setMainActionText(R.string.add_to_shelf) select_action_bar.setMainActionText(R.string.add_to_shelf)
select_action_bar.inflateMenu(R.menu.import_book_sel) select_action_bar.inflateMenu(R.menu.import_book_sel)
select_action_bar.setOnMenuItemClickListener(this) select_action_bar.setOnMenuItemClickListener(this)
@ -76,6 +83,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
} }
} }
}) })
} }
private fun initEvent() { private fun initEvent() {
@ -86,7 +94,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_select_folder -> selectImportFolder() R.id.menu_select_folder -> FilePicker.selectFolder(this, requestCodeSelectFolder)
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
@ -95,8 +103,8 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
when (item?.itemId) { when (item?.itemId) {
R.id.menu_del_selection -> R.id.menu_del_selection ->
viewModel.deleteDoc(adapter.selectedUris) { viewModel.deleteDoc(adapter.selectedUris) {
upPath() upPath()
} }
} }
return false return false
} }
@ -111,11 +119,35 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private fun upRootDoc() { private fun upRootDoc() {
AppConfig.importBookPath?.let { AppConfig.importBookPath?.let {
val rootUri = Uri.parse(it) if (it.isContentPath()) {
rootDoc = DocumentFile.fromTreeUri(this, rootUri) val rootUri = Uri.parse(it)
subDocs.clear() rootDoc = DocumentFile.fromTreeUri(this, rootUri)
upPath() subDocs.clear()
} else {
rootDoc = null
subDocs.clear()
path = it
}
} ?: let {
// 没有权限就显示一个授权提示和按钮
if (PermissionsCompat.check(this, *Permissions.Group.STORAGE)) {
hint_per.visibility = View.GONE
} else {
hint_per.visibility = View.VISIBLE
tv_request_per.onClick {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
hint_per.visibility = View.GONE
initData()
upRootDoc()
}
.request()
}
}
} }
upPath()
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -131,7 +163,6 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
tv_path.text = path tv_path.text = path
adapter.selectedUris.clear() adapter.selectedUris.clear()
adapter.clearItems() adapter.clearItems()
rotate_loading.show()
launch(IO) { launch(IO) {
val docList = DocumentUtils.listFiles( val docList = DocumentUtils.listFiles(
this@ImportBookActivity, this@ImportBookActivity,
@ -147,20 +178,47 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
} }
docList.sortWith(compareBy({ !it.isDir }, { it.name })) docList.sortWith(compareBy({ !it.isDir }, { it.name }))
withContext(Main) { withContext(Main) {
rotate_loading.hide()
adapter.setData(docList) adapter.setData(docList)
} }
} }
} ?: let {
tv_path.text = path.replace(sdPath, "SD")
val docList = arrayListOf<DocItem>()
File(path).listFiles()?.forEach {
if (it.isDirectory) {
if (!it.name.startsWith("."))
docList.add(
DocItem(
it.name,
DocumentsContract.Document.MIME_TYPE_DIR,
it.length(),
Date(it.lastModified()),
Uri.parse(it.absolutePath)
)
)
} else if (it.name.endsWith(".txt", true)) {
docList.add(
DocItem(
it.name,
it.extension,
it.length(),
Date(it.lastModified()),
Uri.parse(it.absolutePath)
)
)
}
}
docList.sortWith(compareBy({ !it.isDir }, { it.name }))
adapter.setData(docList)
} }
} }
private fun selectImportFolder() { override fun onFilePicked(requestCode: Int, currentPath: String) {
try { when (requestCode) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) requestCodeSelectFolder -> {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) AppConfig.importBookPath = currentPath
startActivityForResult(intent, requestCodeSelectFolder) upRootDoc()
} catch (e: Exception) { }
e.printStackTrace()
} }
} }
@ -181,13 +239,27 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
} }
@Synchronized @Synchronized
override fun nextDoc(doc: DocumentFile) { override fun nextDoc(uri: Uri) {
subDocs.add(doc) if (uri.toString().isContentPath()) {
subDocs.add(DocumentFile.fromSingleUri(this, uri)!!)
} else {
path = uri.toString()
}
upPath() upPath()
} }
@Synchronized @Synchronized
private fun goBackDir(): Boolean { private fun goBackDir(): Boolean {
if (rootDoc == null) {
if (path != sdPath) {
File(path).parent?.let {
path = it
upPath()
return true
}
}
return false
}
return if (subDocs.isNotEmpty()) { return if (subDocs.isNotEmpty()) {
subDocs.removeAt(subDocs.lastIndex) subDocs.removeAt(subDocs.lastIndex)
upPath() upPath()

@ -1,7 +1,7 @@
package io.legado.app.ui.book.local package io.legado.app.ui.book.local
import android.content.Context import android.content.Context
import androidx.documentfile.provider.DocumentFile import android.net.Uri
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
@ -87,7 +87,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
ll_brief.visible() ll_brief.visible()
tv_tag.text = item.name.substringAfterLast(".") tv_tag.text = item.name.substringAfterLast(".")
tv_size.text = StringUtils.toSize(item.size) tv_size.text = StringUtils.toSize(item.size)
tv_date.text = AppConst.DATE_FORMAT.format(item.date) tv_date.text = AppConst.dateFormat.format(item.date)
cb_select.isChecked = selectedUris.contains(item.uri.toString()) cb_select.isChecked = selectedUris.contains(item.uri.toString())
} }
tv_name.text = item.name tv_name.text = item.name
@ -101,7 +101,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
holder.itemView.onClick { holder.itemView.onClick {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
if (it.isDir) { if (it.isDir) {
callBack.nextDoc(DocumentFile.fromSingleUri(context, it.uri)!!) callBack.nextDoc(it.uri)
} else if (!bookshelf.contains(it.uri.toString())) { } else if (!bookshelf.contains(it.uri.toString())) {
if (!selectedUris.contains(it.uri.toString())) { if (!selectedUris.contains(it.uri.toString())) {
selectedUris.add(it.uri.toString()) selectedUris.add(it.uri.toString())
@ -116,7 +116,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) :
} }
interface CallBack { interface CallBack {
fun nextDoc(doc: DocumentFile) fun nextDoc(uri: Uri)
fun upCountView() fun upCountView()
} }

@ -11,10 +11,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
fun addToBookshelf(uriList: HashSet<String>, finally: () -> Unit) { fun addToBookshelf(uriList: HashSet<String>, finally: () -> Unit) {
execute { execute {
uriList.forEach { uriStr -> uriList.forEach {
DocumentFile.fromSingleUri(context, Uri.parse(uriStr))?.let { doc -> LocalBook.importFile(it)
LocalBook.importFile(doc)
}
} }
}.onFinally { }.onFinally {
finally.invoke() finally.invoke()

@ -3,6 +3,7 @@ package io.legado.app.ui.book.read
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.ActivityInfo
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.view.* import android.view.*
@ -62,6 +63,18 @@ object Help {
} }
} }
@SuppressLint("SourceLockedOrientationActivity")
fun setOrientation(activity: Activity) = activity.apply {
when (AppConfig.requestedDirection) {
"0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
"1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
"2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
"3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
}
/** /**
* 返回NavigationBar是否存在 * 返回NavigationBar是否存在
* 该方法需要在View完全被绘制出来之后调用否则判断不了 * 该方法需要在View完全被绘制出来之后调用否则判断不了
@ -99,7 +112,7 @@ object Help {
* 适配刘海 * 适配刘海
*/ */
fun upLayoutInDisplayCutoutMode(window: Window) { fun upLayoutInDisplayCutoutMode(window: Window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && AppConfig.readBodyToLh) {
window.attributes = window.attributes.apply { window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode = layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

@ -3,6 +3,7 @@ package io.legado.app.ui.book.read
import android.annotation.SuppressLint 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.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -13,6 +14,7 @@ import androidx.core.view.isVisible
import androidx.core.view.size import androidx.core.view.size
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.BuildConfig
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.EventBus import io.legado.app.constant.EventBus
@ -23,6 +25,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.SyncBookProgress 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
@ -88,6 +91,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override val pageFactory: TextPageFactory get() = page_view.pageFactory override val pageFactory: TextPageFactory get() = page_view.pageFactory
override val headerHeight: Int get() = page_view.curPage.headerHeight override val headerHeight: Int get() = page_view.curPage.headerHeight
override fun onCreate(savedInstanceState: Bundle?) {
ReadBook.msg = null
Help.setOrientation(this)
super.onCreate(savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
Help.upLayoutInDisplayCutoutMode(window) Help.upLayoutInDisplayCutoutMode(window)
initView() initView()
@ -106,6 +115,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility() upSystemUiVisibility()
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ReadBook.loadContent(resetPageOffset = false)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
upSystemUiVisibility() upSystemUiVisibility()
@ -211,6 +225,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_copy_text -> R.id.menu_copy_text ->
TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent())
R.id.menu_update_toc -> ReadBook.book?.let { R.id.menu_update_toc -> ReadBook.book?.let {
ReadBook.upMsg(getString(R.string.toc_updateing))
viewModel.loadChapterList(it) viewModel.loadChapterList(it)
} }
R.id.menu_enable_replace -> ReadBook.book?.let { R.id.menu_enable_replace -> ReadBook.book?.let {
@ -338,7 +353,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean { override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) { when (event.action) {
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> { MotionEvent.ACTION_DOWN -> textActionMenu?.dismiss()
MotionEvent.ACTION_MOVE -> {
when (v.id) { when (v.id) {
R.id.cursor_left -> page_view.curPage.selectStartMove( R.id.cursor_left -> page_view.curPage.selectStartMove(
event.rawX + cursor_left.width, event.rawX + cursor_left.width,
@ -350,6 +366,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
) )
} }
} }
MotionEvent.ACTION_UP -> showTextActionMenu()
} }
return true return true
} }
@ -357,11 +374,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/** /**
* 更新文字选择开始位置 * 更新文字选择开始位置
*/ */
override fun upSelectedStart(x: Float, y: Float) { override fun upSelectedStart(x: Float, y: Float, top: Float) {
cursor_left.x = x - cursor_left.width cursor_left.x = x - cursor_left.width
cursor_left.y = y cursor_left.y = y
cursor_left.visible(true) cursor_left.visible(true)
showTextActionMenu() text_menu_position.x = x
text_menu_position.y = top
} }
/** /**
@ -371,7 +389,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
cursor_right.x = x cursor_right.x = x
cursor_right.y = y cursor_right.y = y
cursor_right.visible(true) cursor_right.visible(true)
showTextActionMenu()
} }
/** /**
@ -386,19 +403,23 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/** /**
* 显示文本操作菜单 * 显示文本操作菜单
*/ */
private fun showTextActionMenu() { override fun showTextActionMenu() {
textActionMenu ?: let { textActionMenu ?: let {
textActionMenu = TextActionMenu(this, this) textActionMenu = TextActionMenu(this, this)
} }
val x = cursor_left.x.toInt() + cursor_left.width
val y = if (cursor_left.y - statusBarHeight > ReadBookConfig.textSize.dp * 1.5 + 20.dp) {
(page_view.height - cursor_left.y + ReadBookConfig.textSize.dp * 1.5).toInt()
} else {
(page_view.height - cursor_left.y - cursor_left.height - 40.dp).toInt()
}
textActionMenu?.let { popup -> textActionMenu?.let { popup ->
popup.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val popupHeight = popup.contentView.measuredHeight
val x = text_menu_position.x.toInt()
var y = text_menu_position.y.toInt() - popupHeight
if (y < statusBarHeight) {
y = (cursor_left.y + cursor_left.height).toInt()
}
if (cursor_right.y > y && cursor_right.y < y + popupHeight) {
y = (cursor_right.y + cursor_right.height).toInt()
}
if (!popup.isShowing) { if (!popup.isShowing) {
popup.showAtLocation(cursor_left, Gravity.BOTTOM or Gravity.START, x, y) popup.showAtLocation(text_menu_position, Gravity.TOP or Gravity.START, x, y)
} else { } else {
popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT) popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT)
} }
@ -462,9 +483,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/** /**
* 更新内容 * 更新内容
*/ */
override fun upContent(relativePosition: Int) { override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
launch { launch {
page_view.upContent(relativePosition) page_view.upContent(relativePosition, resetPageOffset)
} }
} }
@ -479,6 +500,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
if (!ReadBook.isLocalBook) { if (!ReadBook.isLocalBook) {
tv_chapter_url.text = it.url tv_chapter_url.text = it.url
tv_chapter_url.visible() tv_chapter_url.visible()
} else {
tv_chapter_url.gone()
} }
seek_read_page.max = it.pageSize().minus(1) seek_read_page.max = it.pageSize().minus(1)
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
@ -558,7 +581,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onReplaceRuleSave() { override fun onReplaceRuleSave() {
Coroutine.async { Coroutine.async {
BookHelp.upReplaceRules() BookHelp.upReplaceRules()
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = false)
} }
} }
@ -587,9 +610,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 朗读按钮 * 朗读按钮
*/ */
override fun onClickReadAloud() { override fun onClickReadAloud() {
if (!BaseReadAloudService.isRun) {
SystemUtils.ignoreBatteryOptimization(this)
}
when { when {
!BaseReadAloudService.isRun -> ReadBook.readAloud() !BaseReadAloudService.isRun -> ReadBook.readAloud()
BaseReadAloudService.pause -> ReadAloud.resume(this) BaseReadAloudService.pause -> ReadAloud.resume(this)
@ -661,7 +681,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss() textActionMenu?.dismiss()
page_view.onDestroy() page_view.onDestroy()
SyncBookProgress.uploadBookProgress() ReadBook.msg = null
if (!BuildConfig.DEBUG) {
SyncBookProgress.uploadBookProgress()
Backup.autoBack(this)
}
} }
override fun observeLiveBus() { override fun observeLiveBus() {
@ -684,9 +708,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upBg() page_view.upBg()
page_view.upStyle() page_view.upStyle()
if (it) { if (it) {
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = false)
} else { } else {
page_view.upContent() page_view.upContent(resetPageOffset = false)
} }
} }
observeEvent<Int>(EventBus.ALOUD_STATE) { observeEvent<Int>(EventBus.ALOUD_STATE) {
@ -695,7 +719,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
val page = textChapter.page(ReadBook.durPageIndex) val page = textChapter.page(ReadBook.durPageIndex)
if (page != null) { if (page != null) {
page.removePageAloudSpan() page.removePageAloudSpan()
page_view.upContent() page_view.upContent(resetPageOffset = false)
} }
} }
} }
@ -712,9 +736,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
} }
} }
observeEvent<String>(EventBus.REPLACE) {
ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog")
}
observeEvent<Boolean>(PreferKey.keepLight) { observeEvent<Boolean>(PreferKey.keepLight) {
upScreenTimeOut() upScreenTimeOut()
} }

@ -39,7 +39,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
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) ReadBook.resetData(book) { name, author ->
autoChangeSource(name, author)
}
isInitFinish = true isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) { if (ReadBook.chapterSize == 0) {
@ -52,7 +54,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) { if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1 ReadBook.durChapterIndex = ReadBook.chapterSize - 1
} }
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
if (ReadBook.inBookshelf) { if (ReadBook.inBookshelf) {
ReadBook.saveRead() ReadBook.saveRead()
@ -60,7 +62,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else { } else {
isInitFinish = true isInitFinish = true
ReadBook.titleDate.postValue(book.name) ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book.origin) ReadBook.upWebBook(book) { name, author ->
autoChangeSource(name, author)
}
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) { if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) { if (book.tocUrl.isEmpty()) {
@ -70,7 +74,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} else { } else {
if (ReadBook.curTextChapter != null) { if (ReadBook.curTextChapter != null) {
ReadBook.callBack?.upContent() ReadBook.callBack?.upContent(resetPageOffset = false)
} }
} }
} }
@ -98,38 +102,45 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
) { ) {
execute { execute {
if (book.isLocalBook()) { if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let { AnalyzeTxtFile().analyze(context, book).let {
App.db.bookChapterDao().delByBook(book.bookUrl) App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray()) App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book) App.db.bookDao().update(book)
ReadBook.chapterSize = it.size ReadBook.chapterSize = it.size
ReadBook.loadContent() ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
} }
} else { } else {
ReadBook.webBook?.getChapterList(book, this) ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList -> ?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book) App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size ReadBook.chapterSize = cList.size
ReadBook.loadContent() ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
} else { } else {
changeDruChapterIndex(cList) changeDruChapterIndex(cList)
} }
} else { } else {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} }
}?.onError { }?.onError {
toast(R.string.error_load_toc) ReadBook.upMsg(context.getString(R.string.error_load_toc))
} ?: autoChangeSource() }
} }
}.onError {
ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
} }
} }
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
ReadBook.upMsg(null)
ReadBook.book?.let { ReadBook.book?.let {
book1.group = it.group
book1.order = it.order
App.db.bookDao().delete(it) App.db.bookDao().delete(it)
} }
ReadBook.prevTextChapter = null ReadBook.prevTextChapter = null
@ -151,8 +162,26 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
private fun autoChangeSource() { private fun autoChangeSource(name: String, author: String) {
execute {
App.db.bookSourceDao().allTextEnabled.forEach { source ->
try {
val searchBooks = WebBook(source).searchBookSuspend(this, name)
searchBooks.getOrNull(0)?.let {
if (it.name == name && (it.author == author || author == "")) {
changeTo(it.toBook())
return@forEach
}
}
} catch (e: Exception) {
//nothing
}
}
}.onStart {
ReadBook.upMsg("正在自动换源")
}.onFinally {
ReadBook.upMsg(null)
}
} }
private fun upChangeDurChapterIndex(book: Book, chapters: List<BookChapter>) { private fun upChangeDurChapterIndex(book: Book, chapters: List<BookChapter>) {
@ -167,7 +196,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookDao().update(book) App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray()) App.db.bookChapterDao().insert(*chapters.toTypedArray())
ReadBook.chapterSize = chapters.size ReadBook.chapterSize = chapters.size
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
} }
@ -181,7 +210,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durPageIndex = pageIndex ReadBook.durPageIndex = pageIndex
} }
ReadBook.saveRead() ReadBook.saveRead()
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = true)
} }
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
@ -208,9 +237,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
execute { execute {
App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex) App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex)
?.let { chapter -> ?.let { chapter ->
BookHelp.delContent(book, chapter) BookHelp.delContent(book, chapter)
ReadBook.loadContent(ReadBook.durChapterIndex) ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
} }
} }
} }

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.speech.tts.TextToSpeech
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
@ -15,13 +16,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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
import io.legado.app.service.BaseReadAloudService
import io.legado.app.utils.gone
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.visible import io.legado.app.utils.visible
@ -30,10 +30,16 @@ import kotlinx.android.synthetic.main.popup_action_menu.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.share import org.jetbrains.anko.share
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.util.*
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class TextActionMenu(private val context: Context, private val callBack: CallBack) : class TextActionMenu(private val context: Context, private val callBack: CallBack) :
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT),
TextToSpeech.OnInitListener {
private val adapter = Adapter(context)
private val menu = MenuBuilder(context)
private val moreMenu = MenuBuilder(context)
init { init {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -44,33 +50,38 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
isFocusable = false isFocusable = false
initRecyclerView() initRecyclerView()
setOnDismissListener {
contentView.apply {
iv_menu_more.setImageResource(R.drawable.ic_more_vert)
recycler_view_more.gone()
adapter.setItems(menu.visibleItems)
recycler_view.visible()
}
}
} }
private fun initRecyclerView() = with(contentView) { private fun initRecyclerView() = with(contentView) {
val adapter = Adapter(context)
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val menu = MenuBuilder(context) recycler_view_more.adapter = adapter
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems) adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val popupMenu = PopupMenu(context, iv_menu_more) onInitializeMenu(moreMenu)
onInitializeMenu(popupMenu.menu) }
if (popupMenu.menu.size > 0) { if (moreMenu.size() > 0) {
iv_menu_more.visible() iv_menu_more.visible()
popupMenu.setOnMenuItemClickListener { item -> }
item.intent?.let { iv_menu_more.onClick {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (recycler_view.isVisible) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText) iv_menu_more.setImageResource(R.drawable.ic_arrow_back)
context.startActivity(it) adapter.setItems(moreMenu.visibleItems)
} recycler_view.gone()
} recycler_view_more.visible()
this@TextActionMenu.dismiss() } else {
true iv_menu_more.setImageResource(R.drawable.ic_more_vert)
} recycler_view_more.gone()
} adapter.setItems(menu.visibleItems)
iv_menu_more.onClick { recycler_view.visible()
popupMenu.show()
} }
} }
} }
@ -104,6 +115,13 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
when (item.itemId) { when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText) R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(callBack.selectedText) R.id.menu_share_str -> context.share(callBack.selectedText)
R.id.menu_aloud -> {
if (BaseReadAloudService.isRun) {
context.toast(R.string.alouding_disable)
return
}
readAloud(callBack.selectedText)
}
R.id.menu_browser -> { R.id.menu_browser -> {
try { try {
val intent = if (callBack.selectedText.isAbsUrl()) { val intent = if (callBack.selectedText.isAbsUrl()) {
@ -121,7 +139,39 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
context.toast(e.localizedMessage ?: "ERROR") context.toast(e.localizedMessage ?: "ERROR")
} }
} }
else -> item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
}
}
private var textToSpeech: TextToSpeech? = null
private var ttsInitFinish = false
private var lastText: String = ""
@SuppressLint("SetJavaScriptEnabled")
private fun readAloud(text: String) {
lastText = text
if (textToSpeech == null) {
textToSpeech = TextToSpeech(context, this)
return
} }
if (!ttsInitFinish) return
if (text == "") return
if (textToSpeech?.isSpeaking == true)
textToSpeech?.stop()
textToSpeech?.speak(text, TextToSpeech.QUEUE_ADD, null, "select_text")
lastText = ""
}
@Synchronized
override fun onInit(status: Int) {
textToSpeech?.language = Locale.CHINA
ttsInitFinish = true
readAloud(lastText)
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
@ -151,15 +201,16 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
*/ */
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun onInitializeMenu(menu: Menu) { private fun onInitializeMenu(menu: Menu) {
// Start with a menu Item order value that is high enough try {
// so that your "PROCESS_TEXT" menu items appear after the var menuItemOrder = 100
// standard selection menu items like Cut, Copy, Paste. for (resolveInfo in getSupportedActivities()) {
var menuItemOrder = 100 menu.add(
for (resolveInfo in getSupportedActivities()) { Menu.NONE, Menu.NONE,
menu.add( menuItemOrder++, resolveInfo.loadLabel(context.packageManager)
Menu.NONE, Menu.NONE, ).intent = createProcessTextIntentForResolveInfo(resolveInfo)
menuItemOrder++, resolveInfo.loadLabel(context.packageManager) }
).intent = createProcessTextIntentForResolveInfo(resolveInfo) } catch (e: Exception) {
context.toast("获取文字操作菜单出错:${e.localizedMessage}")
} }
} }

@ -94,6 +94,7 @@ class MoreConfigDialog : DialogFragment() {
key: String? key: String?
) { ) {
when (key) { when (key) {
PreferKey.readBodyToLh -> activity?.recreate()
PreferKey.hideStatusBar -> { PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar) ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
@ -104,6 +105,11 @@ class MoreConfigDialog : DialogFragment() {
} }
PreferKey.keepLight -> postEvent(key, true) PreferKey.keepLight -> postEvent(key, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
getString(R.string.pk_requested_direction) -> {
activity?.let {
Help.setOrientation(it)
}
}
} }
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -47,8 +48,8 @@ class PaddingConfigDialog : DialogFragment() {
initView() initView()
} }
override fun onDestroy() { override fun onDismiss(dialog: DialogInterface) {
super.onDestroy() super.onDismiss(dialog)
ReadBookConfig.save() ReadBookConfig.save()
} }
@ -75,7 +76,8 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_bottom.progress = footerPaddingBottom dsb_footer_padding_bottom.progress = footerPaddingBottom
dsb_footer_padding_left.progress = footerPaddingLeft dsb_footer_padding_left.progress = footerPaddingLeft
dsb_footer_padding_right.progress = footerPaddingRight dsb_footer_padding_right.progress = footerPaddingRight
cb_show_line.isChecked = showFooterLine cb_show_top_line.isChecked = showHeaderLine
cb_show_bottom_line.isChecked = showFooterLine
} }
private fun initView() = with(ReadBookConfig) { private fun initView() = with(ReadBookConfig) {
@ -130,7 +132,13 @@ class PaddingConfigDialog : DialogFragment() {
footerPaddingRight = it footerPaddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
cb_show_line.onCheckedChangeListener = { cb, isChecked -> cb_show_top_line.onCheckedChangeListener = { cb, isChecked ->
if (cb.isPressed) {
showHeaderLine = isChecked
postEvent(EventBus.UP_CONFIG, true)
}
}
cb_show_bottom_line.onCheckedChangeListener = { cb, isChecked ->
if (cb.isPressed) { if (cb.isPressed) {
showFooterLine = isChecked showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)

@ -21,7 +21,6 @@ import io.legado.app.utils.observeEvent
import io.legado.app.utils.putPrefBoolean import io.legado.app.utils.putPrefBoolean
import kotlinx.android.synthetic.main.dialog_read_aloud.* import kotlinx.android.synthetic.main.dialog_read_aloud.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadAloudDialog : BaseDialogFragment() { class ReadAloudDialog : BaseDialogFragment() {
var callBack: CallBack? = null var callBack: CallBack? = null
@ -34,7 +33,7 @@ class ReadAloudDialog : BaseDialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent) it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0) it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes val attr = it.attributes
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
@ -54,10 +53,10 @@ class ReadAloudDialog : BaseDialogFragment() {
} }
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
ll_bottom_bg.setBackgroundColor(requireContext().bottomBackground) root_view.setBackgroundColor(requireContext().bottomBackground)
initOnChange() initOnChange()
initData() initData()
initOnClick() initEvent()
} }
private fun initData() { private fun initData() {
@ -101,22 +100,19 @@ class ReadAloudDialog : BaseDialogFragment() {
}) })
} }
private fun initOnClick() { private fun initEvent() {
iv_menu.onClick { callBack?.showMenuBar(); dismiss() } ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() }
iv_other_config.onClick { ll_setting.onClick {
ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog")
} }
tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) }
tv_next.onClick { ReadBook.moveToNextChapter(true) }
iv_stop.onClick { ReadAloud.stop(requireContext()); dismiss() } iv_stop.onClick { ReadAloud.stop(requireContext()); dismiss() }
iv_play_pause.onClick { callBack?.onClickReadAloud() } iv_play_pause.onClick { callBack?.onClickReadAloud() }
iv_play_prev.onClick { ReadAloud.prevParagraph(requireContext()) } iv_play_prev.onClick { ReadAloud.prevParagraph(requireContext()) }
iv_play_prev.onLongClick {
ReadBook.moveToPrevChapter(upContent = true, toLast = false)
true
}
iv_play_next.onClick { ReadAloud.nextParagraph(requireContext()) } iv_play_next.onClick { ReadAloud.nextParagraph(requireContext()) }
iv_play_next.onLongClick { ReadBook.moveToNextChapter(true); true } ll_catalog.onClick { callBack?.openChapterList() }
fabToc.onClick { callBack?.openChapterList() } ll_to_backstage.onClick { callBack?.finish() }
fabBack.onClick { callBack?.finish() }
} }
private fun upPlayState() { private fun upPlayState() {

@ -1,5 +1,7 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.Gravity import android.view.Gravity
@ -12,6 +14,7 @@ import io.legado.app.R
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.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
@ -19,12 +22,10 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.getIndexById import io.legado.app.utils.*
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefString
import kotlinx.android.synthetic.main.activity_book_read.* import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.dialog_read_book_style.* import kotlinx.android.synthetic.main.dialog_read_book_style.*
import kotlinx.android.synthetic.main.dialog_title_config.view.*
import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onCheckedChange
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
@ -32,7 +33,6 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
val callBack get() = activity as? ReadBookActivity val callBack get() = activity as? ReadBookActivity
lateinit var titleModes: Array<String>
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -62,14 +62,13 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
titleModes = requireContext().resources.getStringArray(R.array.title_mode)
initView() initView()
initData() initData()
initViewEvent() initViewEvent()
} }
override fun onDestroy() { override fun onDismiss(dialog: DialogInterface) {
super.onDestroy() super.onDismiss(dialog)
ReadBookConfig.save() ReadBookConfig.save()
} }
@ -102,18 +101,14 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
tv_title_mode.onClick { tv_title_mode.onClick {
requireContext().selector("标题模式", titleModes.toList()) { _, index -> showTitleConfig()
ReadBookConfig.titleMode = index
tv_title_mode.text = titleModes[index]
postEvent(EventBus.UP_CONFIG, true)
}
} }
tv_text_bold.onClick { tv_text_bold.onClick {
ReadBookConfig.apply { ReadBookConfig.apply {
textBold = !textBold textBold = !textBold
tv_text_bold.isSelected = textBold tv_text_bold.isSelected = textBold
} }
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, true)
} }
tv_text_font.onClick { tv_text_font.onClick {
FontSelectDialog().show(childFragmentManager, "fontSelectDialog") FontSelectDialog().show(childFragmentManager, "fontSelectDialog")
@ -131,8 +126,19 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
dismiss() dismiss()
callBack?.showPaddingConfig() callBack?.showPaddingConfig()
} }
rg_page_anim.onCheckedChange { _, checkedId ->
ReadBookConfig.pageAnim = rg_page_anim.getIndexById(checkedId)
callBack?.page_view?.upPageAnim()
}
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
if (checkBox.isPressed) {
ReadBookConfig.shareLayout = isChecked
upStyle()
postEvent(EventBus.UP_CONFIG, true)
}
}
dsb_text_size.onChanged = { dsb_text_size.onChanged = {
ReadBookConfig.textSize = it + 10 ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
dsb_text_letter_spacing.onChanged = { dsb_text_letter_spacing.onChanged = {
@ -147,19 +153,6 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
ReadBookConfig.paragraphSpacing = it ReadBookConfig.paragraphSpacing = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
rg_page_anim.onCheckedChange { _, checkedId ->
rg_page_anim.getIndexById(checkedId).let {
ReadBookConfig.pageAnim = it
callBack?.page_view?.upPageAnim()
}
}
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
if (checkBox.isPressed) {
ReadBookConfig.shareLayout = isChecked
upStyle()
postEvent(EventBus.UP_CONFIG, true)
}
}
bg0.onClick { changeBg(0) } bg0.onClick { changeBg(0) }
bg0.onLongClick { showBgTextConfig(0) } bg0.onLongClick { showBgTextConfig(0) }
bg1.onClick { changeBg(1) } bg1.onClick { changeBg(1) }
@ -172,6 +165,36 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
bg4.onLongClick { showBgTextConfig(4) } bg4.onLongClick { showBgTextConfig(4) }
} }
@SuppressLint("InflateParams")
private fun showTitleConfig() = ReadBookConfig.apply {
requireContext().alert(R.string.title) {
val rootView = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_title_config, null).apply {
rg_title_mode.checkByIndex(titleMode)
dsb_title_size.progress = titleSize
dsb_title_top.progress = titleTopSpacing
dsb_title_bottom.progress = titleBottomSpacing
rg_title_mode.onCheckedChange { _, checkedId ->
titleMode = rg_title_mode.getIndexById(checkedId)
postEvent(EventBus.UP_CONFIG, true)
}
dsb_title_size.onChanged = {
titleSize = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_title_top.onChanged = {
titleTopSpacing = it
postEvent(EventBus.UP_CONFIG, true)
}
dsb_title_bottom.onChanged = {
titleBottomSpacing = it
postEvent(EventBus.UP_CONFIG, true)
}
}
customView = rootView
}.show()
}
private fun changeBg(index: Int) { private fun changeBg(index: Int) {
if (ReadBookConfig.styleSelect != index) { if (ReadBookConfig.styleSelect != index) {
ReadBookConfig.styleSelect = index ReadBookConfig.styleSelect = index
@ -191,7 +214,6 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun upStyle() { private fun upStyle() {
ReadBookConfig.let { ReadBookConfig.let {
tv_title_mode.text = titleModes.getOrElse(it.titleMode) { titleModes[0] }
tv_text_bold.isSelected = it.textBold 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
@ -200,12 +222,12 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
} }
private fun setBg() { private fun setBg() = ReadBookConfig.apply {
bg0.setTextColor(ReadBookConfig.getConfig(0).textColor()) bg0.setTextColor(getConfig(0).textColor())
bg1.setTextColor(ReadBookConfig.getConfig(1).textColor()) bg1.setTextColor(getConfig(1).textColor())
bg2.setTextColor(ReadBookConfig.getConfig(2).textColor()) bg2.setTextColor(getConfig(2).textColor())
bg3.setTextColor(ReadBookConfig.getConfig(3).textColor()) bg3.setTextColor(getConfig(3).textColor())
bg4.setTextColor(ReadBookConfig.getConfig(4).textColor()) bg4.setTextColor(getConfig(4).textColor())
for (i in 0..4) { for (i in 0..4) {
val iv = when (i) { val iv = when (i) {
1 -> bg1 1 -> bg1
@ -214,22 +236,22 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
4 -> bg4 4 -> bg4
else -> bg0 else -> bg0
} }
iv.setImageDrawable(ReadBookConfig.getConfig(i).bgDrawable(100, 150)) iv.setImageDrawable(getConfig(i).bgDrawable(100, 150))
} }
} }
private fun upBg() { private fun upBg() = requireContext().apply {
bg0.borderColor = requireContext().primaryColor bg0.borderColor = primaryColor
bg1.borderColor = requireContext().primaryColor bg1.borderColor = primaryColor
bg2.borderColor = requireContext().primaryColor bg2.borderColor = primaryColor
bg3.borderColor = requireContext().primaryColor bg3.borderColor = primaryColor
bg4.borderColor = requireContext().primaryColor bg4.borderColor = primaryColor
when (ReadBookConfig.styleSelect) { when (ReadBookConfig.styleSelect) {
1 -> bg1.borderColor = requireContext().accentColor 1 -> bg1.borderColor = accentColor
2 -> bg2.borderColor = requireContext().accentColor 2 -> bg2.borderColor = accentColor
3 -> bg3.borderColor = requireContext().accentColor 3 -> bg3.borderColor = accentColor
4 -> bg4.borderColor = requireContext().accentColor 4 -> bg4.borderColor = accentColor
else -> bg0.borderColor = requireContext().accentColor else -> bg0.borderColor = accentColor
} }
} }

@ -252,8 +252,8 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
return super.onMove(srcPosition, targetPosition) return super.onMove(srcPosition, targetPosition)
} }
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder) super.onClearView(recyclerView, viewHolder)
if (isMoved) { if (isMoved) {
for ((index, item) in getItems().withIndex()) { for ((index, item) in getItems().withIndex()) {
item.serialNumber = index + 1 item.serialNumber = index + 1

@ -28,6 +28,8 @@ object ChapterProvider {
var visibleBottom = 0 var visibleBottom = 0
private var lineSpacingExtra = 0 private var lineSpacingExtra = 0
private var paragraphSpacing = 0 private var paragraphSpacing = 0
private var titleTopSpacing = 0
private var titleBottomSpacing = 0
var typeface: Typeface = Typeface.SANS_SERIF var typeface: Typeface = Typeface.SANS_SERIF
var titlePaint = TextPaint() var titlePaint = TextPaint()
var contentPaint = TextPaint() var contentPaint = TextPaint()
@ -73,6 +75,7 @@ object ChapterProvider {
item.chapterIndex = bookChapter.index item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize item.chapterSize = chapterSize
item.title = bookChapter.title item.title = bookChapter.title
item.upLinesPosition()
} }
return TextChapter( return TextChapter(
bookChapter.index, bookChapter.index,
@ -97,7 +100,7 @@ object ChapterProvider {
stringBuilder: StringBuilder, stringBuilder: StringBuilder,
isTitle: Boolean isTitle: Boolean
): Float { ): Float {
var durY = y var durY = if (isTitle) y + titleTopSpacing else y
val textPaint = if (isTitle) titlePaint else contentPaint val textPaint = if (isTitle) titlePaint else contentPaint
val layout = StaticLayout( val layout = StaticLayout(
text, textPaint, visibleWidth, text, textPaint, visibleWidth,
@ -142,6 +145,7 @@ object ChapterProvider {
durY += textPaint.textHeight * lineSpacingExtra / 10f durY += textPaint.textHeight * lineSpacingExtra / 10f
textPages.last().height = durY textPages.last().height = durY
} }
if (isTitle) durY += titleBottomSpacing
durY += textPaint.textHeight * paragraphSpacing / 10f durY += textPaint.textHeight * paragraphSpacing / 10f
return durY return durY
} }
@ -262,18 +266,19 @@ object ChapterProvider {
titlePaint.color = ReadBookConfig.durConfig.textColor() titlePaint.color = ReadBookConfig.durConfig.textColor()
titlePaint.letterSpacing = ReadBookConfig.letterSpacing titlePaint.letterSpacing = ReadBookConfig.letterSpacing
titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD) titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD)
titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat()
//正文 //正文
contentPaint.isAntiAlias = true contentPaint.isAntiAlias = true
contentPaint.color = ReadBookConfig.durConfig.textColor() contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = ReadBookConfig.letterSpacing contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold) contentPaint.typeface = Typeface.create(typeface, style)
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
//间距 //间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra lineSpacingExtra = ReadBookConfig.lineSpacingExtra
paragraphSpacing = ReadBookConfig.paragraphSpacing paragraphSpacing = ReadBookConfig.paragraphSpacing
titlePaint.textSize = (ReadBookConfig.textSize + 2).sp.toFloat() titleTopSpacing = ReadBookConfig.titleTopSpacing.dp
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp
upSize() upSize()
} }
@ -290,5 +295,5 @@ object ChapterProvider {
} }
val TextPaint.textHeight: Float val TextPaint.textHeight: Float
get() = this.fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading get() = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading
} }

@ -31,6 +31,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
private val selectStart = arrayOf(0, 0, 0) private val selectStart = arrayOf(0, 0, 0)
private val selectEnd = arrayOf(0, 0, 0) private val selectEnd = arrayOf(0, 0, 0)
private var textPage: TextPage = TextPage() private var textPage: TextPage = TextPage()
//滚动参数 //滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f private val maxScrollOffset = 100f
@ -210,7 +211,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 0 selectEnd[0] = 0
selectEnd[1] = lineIndex selectEnd[1] = lineIndex
selectEnd[2] = charIndex selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(0, lineIndex, charIndex) select(0, lineIndex, charIndex)
return return
@ -236,7 +241,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 1 selectEnd[0] = 1
selectEnd[1] = lineIndex selectEnd[1] = lineIndex
selectEnd[2] = charIndex selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(1, lineIndex, charIndex) select(1, lineIndex, charIndex)
return return
@ -259,7 +268,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 2 selectEnd[0] = 2
selectEnd[1] = lineIndex selectEnd[1] = lineIndex
selectEnd[2] = charIndex selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(2, lineIndex, charIndex) select(2, lineIndex, charIndex)
return return
@ -287,7 +300,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 0 selectStart[0] = 0
selectStart[1] = lineIndex selectStart[1] = lineIndex
selectStart[2] = charIndex selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars() upSelectChars()
} }
return return
@ -311,7 +328,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 1 selectStart[0] = 1
selectStart[1] = lineIndex selectStart[1] = lineIndex
selectStart[2] = charIndex selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars() upSelectChars()
} }
return return
@ -333,7 +354,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 2 selectStart[0] = 2
selectStart[1] = lineIndex selectStart[1] = lineIndex
selectStart[2] = charIndex selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars() upSelectChars()
} }
return return
@ -427,7 +452,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[2] = charIndex selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex] val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex] val textChar = textLine.textChars[charIndex]
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage)) upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset(relativePage),
textLine.lineTop
)
upSelectChars() upSelectChars()
} }
@ -475,12 +504,12 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
invalidate() invalidate()
} }
private fun upSelectedStart(x: Float, y: Float) { private fun upSelectedStart(x: Float, y: Float, top: Float) = callBack.apply {
callBack.upSelectedStart(x, y + callBack.headerHeight) upSelectedStart(x, y + headerHeight, top + headerHeight)
} }
private fun upSelectedEnd(x: Float, y: Float) { private fun upSelectedEnd(x: Float, y: Float) = callBack.apply {
callBack.upSelectedEnd(x, y + callBack.headerHeight) upSelectedEnd(x, y + headerHeight)
} }
fun cancelSelect() { fun cancelSelect() {
@ -580,7 +609,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
interface CallBack { interface CallBack {
fun upSelectedStart(x: Float, y: Float) fun upSelectedStart(x: Float, y: Float, top: Float)
fun upSelectedEnd(x: Float, y: Float) fun upSelectedEnd(x: Float, y: Float)
fun onCancelSelect() fun onCancelSelect()
val headerHeight: Int val headerHeight: Int

@ -7,7 +7,7 @@ import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT import io.legado.app.constant.AppConst.timeFormat
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.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
@ -18,6 +18,8 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) { class ContentView(context: Context) : FrameLayout(context) {
private var battery = 100
init { init {
//设置背景防止切换背景时文字重叠 //设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background)) setBackgroundColor(context.getCompatColor(R.color.background))
@ -26,8 +28,7 @@ class ContentView(context: Context) : FrameLayout(context) {
upStyle() upStyle()
upTime() upTime()
content_text_view.upView = { content_text_view.upView = {
tv_bottom_left.text = it.title setProgress(it)
setPageIndex(it.index, it.pageSize)
} }
} }
@ -37,11 +38,9 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.typeface = ChapterProvider.typeface tv_top_right.typeface = ChapterProvider.typeface
tv_bottom_left.typeface = ChapterProvider.typeface tv_bottom_left.typeface = ChapterProvider.typeface
tv_bottom_right.typeface = ChapterProvider.typeface tv_bottom_right.typeface = ChapterProvider.typeface
battery_view.typeface = ChapterProvider.typeface
//显示状态栏时隐藏header //显示状态栏时隐藏header
if (hideStatusBar) { if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams.apply {
height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding( ll_header.setPadding(
headerPaddingLeft.dp, headerPaddingLeft.dp,
headerPaddingTop.dp, headerPaddingTop.dp,
@ -60,6 +59,7 @@ class ContentView(context: Context) : FrameLayout(context) {
footerPaddingRight.dp, footerPaddingRight.dp,
footerPaddingBottom.dp footerPaddingBottom.dp
) )
vw_top_divider.visible(showHeaderLine)
vw_bottom_divider.visible(showFooterLine) vw_bottom_divider.visible(showFooterLine)
content_text_view.upVisibleRect() content_text_view.upVisibleRect()
durConfig.textColor().let { durConfig.textColor().let {
@ -67,6 +67,14 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.setTextColor(it) tv_top_right.setTextColor(it)
tv_bottom_left.setTextColor(it) tv_bottom_left.setTextColor(it)
tv_bottom_right.setTextColor(it) tv_bottom_right.setTextColor(it)
battery_view.setColor(it)
}
if (hideStatusBar) {
tv_bottom_left.text = timeFormat.format(Date(System.currentTimeMillis()))
battery_view.visible()
battery_view.setBattery(battery)
} else {
battery_view.gone()
} }
} }
} }
@ -85,21 +93,22 @@ class ContentView(context: Context) : FrameLayout(context) {
} }
fun upTime() { fun upTime() {
tv_top_left.text = TIME_FORMAT.format(Date(System.currentTimeMillis())) if (ReadBookConfig.hideStatusBar) {
tv_bottom_right.text = timeFormat.format(Date(System.currentTimeMillis()))
}
} }
fun upBattery(battery: Int) { fun upBattery(battery: Int) {
tv_top_right.text = context.getString(R.string.battery_show, battery) this.battery = battery
if (ReadBookConfig.hideStatusBar) {
battery_view.setBattery(battery)
}
} }
fun setContent(textPage: TextPage) { fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) { setProgress(textPage)
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title) if (resetPageOffset)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title) resetPageOffset()
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
content_text_view.setContent(textPage) content_text_view.setContent(textPage)
} }
@ -108,9 +117,19 @@ class ContentView(context: Context) : FrameLayout(context) {
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?, pageSize: Int) { fun setProgress(textPage: TextPage) = textPage.apply {
pageIndex?.let { val title = when (AppConfig.chineseConverterType) {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}" 1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
if (ReadBookConfig.hideStatusBar) {
tv_top_left.text = title
tv_top_right.text = readProgress
tv_bottom_left.text = "${index.plus(1)}/$pageSize"
} else {
tv_bottom_left.text = title
tv_bottom_right.text = "${index.plus(1)}/$pageSize $readProgress"
} }
} }

@ -17,5 +17,5 @@ interface DataSource {
fun hasPrevChapter(): Boolean fun hasPrevChapter(): Boolean
fun upContent(relativePosition: Int = 0) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
} }

@ -16,25 +16,20 @@ class PageView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs), FrameLayout(context, attrs),
DataSource { DataSource {
var callBack: CallBack val callBack: CallBack get() = activity as CallBack
var pageFactory: TextPageFactory var pageFactory: TextPageFactory = TextPageFactory(this)
var pageDelegate: PageDelegate? = null var pageDelegate: PageDelegate? = null
var prevPage: ContentView var prevPage: ContentView = ContentView(context)
var curPage: ContentView var curPage: ContentView = ContentView(context)
var nextPage: ContentView var nextPage: ContentView = ContentView(context)
init { init {
callBack = activity as CallBack
nextPage = ContentView(context)
addView(nextPage) addView(nextPage)
curPage = ContentView(context)
addView(curPage) addView(curPage)
prevPage = ContentView(context)
addView(prevPage) addView(prevPage)
upBg() upBg()
setWillNotDraw(false) setWillNotDraw(false)
pageFactory = TextPageFactory(this)
upPageAnim() upPageAnim()
} }
@ -43,7 +38,7 @@ class PageView(context: Context, attrs: AttributeSet) :
prevPage.x = -w.toFloat() prevPage.x = -w.toFloat()
pageDelegate?.setViewSize(w, h) pageDelegate?.setViewSize(w, h)
if (oldw != 0 && oldh != 0) { if (oldw != 0 && oldh != 0) {
ReadBook.loadContent() ReadBook.loadContent(resetPageOffset = false)
} }
} }
@ -98,9 +93,9 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent() upContent()
} }
override fun upContent(relativePosition: Int) { override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
if (ReadBookConfig.isScroll) { if (ReadBookConfig.isScroll) {
curPage.setContent(pageFactory.currentPage) curPage.setContent(pageFactory.currentPage, resetPageOffset)
} else { } else {
when (relativePosition) { when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage) -1 -> prevPage.setContent(pageFactory.prevPage)
@ -170,5 +165,6 @@ class PageView(context: Context, attrs: AttributeSet) :
val isInitFinish: Boolean val isInitFinish: Boolean
fun clickCenter() fun clickCenter()
fun screenOffTimerStart() fun screenOffTimerStart()
fun showTextActionMenu()
} }
} }

@ -38,7 +38,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else { } else {
ReadBook.setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
if (upContent) upContent() if (upContent) upContent(resetPageOffset = false)
true true
} else } else
false false
@ -51,7 +51,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} else { } else {
ReadBook.setPageIndex(pageIndex.minus(1)) ReadBook.setPageIndex(pageIndex.minus(1))
} }
if (upContent) upContent() if (upContent) upContent(resetPageOffset = false)
true true
} else } else
false false
@ -59,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val currentPage: TextPage override val currentPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let { currentChapter?.let {
return@with it.page(pageIndex) return@with it.page(pageIndex)
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
@ -68,6 +71,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val nextPage: TextPage override val nextPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
currentChapter?.let { currentChapter?.let {
if (pageIndex < it.pageSize() - 1) { if (pageIndex < it.pageSize() - 1) {
return@with it.page(pageIndex + 1)?.removePageAloudSpan() return@with it.page(pageIndex + 1)?.removePageAloudSpan()
@ -86,6 +92,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val prevPage: TextPage override val prevPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
if (pageIndex > 0) { if (pageIndex > 0) {
currentChapter?.let { currentChapter?.let {
return@with it.page(pageIndex - 1)?.removePageAloudSpan() return@with it.page(pageIndex - 1)?.removePageAloudSpan()

@ -37,7 +37,10 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
override fun onTouch(event: MotionEvent) { override fun onTouch(event: MotionEvent) {
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
abort() if (abort()) {
onAnimStop()
stopScroll()
}
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if (isTextSelected) { if (isTextSelected) {
@ -93,7 +96,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
} }
} }
if (isMoved) { if (isMoved) {
isCancel = if (mDirection == Direction.NEXT) touchX > lastX else touchX < lastX isCancel = if (mDirection == Direction.NEXT) sumX > lastX else sumX < lastX
isRunning = true isRunning = true
//设置触摸点 //设置触摸点
setTouchPoint(sumX, sumY) setTouchPoint(sumX, sumY)

@ -1,19 +1,14 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
override fun onAnimStart() {
startScroll(touchX.toInt(), 0, 0, 0)
}
override fun onDraw(canvas: Canvas) {
}
override fun onAnimStop() { override fun onAnimStart() {
if (!isCancel) { if (!isCancel) {
pageView.fillPage(mDirection) pageView.fillPage(mDirection)
} }
stopScroll()
} }
} }

@ -126,7 +126,7 @@ abstract class PageDelegate(protected val pageView: PageView) :
pageView.invalidate() pageView.invalidate()
} }
private fun stopScroll() { protected fun stopScroll() {
isStarted = false isStarted = false
pageView.post { pageView.post {
isMoved = false isMoved = false
@ -154,10 +154,12 @@ abstract class PageDelegate(protected val pageView: PageView) :
} }
} }
fun abort() { fun abort(): Boolean {
if (!scroller.isFinished) { if (!scroller.isFinished) {
scroller.abortAnimation() scroller.abortAnimation()
return true
} }
return false
} }
open fun onAnimStart() {}//scroller start open fun onAnimStart() {}//scroller start
@ -200,11 +202,16 @@ abstract class PageDelegate(protected val pageView: PageView) :
//GestureDetector.onFling小幅移动不会触发,所以要自己判断 //GestureDetector.onFling小幅移动不会触发,所以要自己判断
when (event.action) { when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> if (isMoved) { MotionEvent.ACTION_CANCEL -> {
if (selectedOnDown) { if (isTextSelected) {
selectedOnDown = false pageView.callBack.showTextActionMenu()
}
if (isMoved) {
if (selectedOnDown) {
selectedOnDown = false
}
if (!noNext) onAnimStart()
} }
if (!noNext) onAnimStart()
} }
} }
} }

@ -12,10 +12,7 @@ data class TextChapter(
val chaptersSize: Int val chaptersSize: Int
) { ) {
fun page(index: Int): TextPage? { fun page(index: Int): TextPage? {
if (index >= 0 && index < pages.size) { return pages.getOrNull(index)
return pages[index]
}
return null
} }
fun lastPage(): TextPage? { fun lastPage(): TextPage? {
@ -25,20 +22,6 @@ data class TextChapter(
return null return null
} }
fun scrollPage(): TextPage? {
if (pages.isNotEmpty()) {
val stringBuilder = StringBuilder()
pages.forEach {
stringBuilder.append(it.text)
}
return TextPage(
index = 0, text = stringBuilder.toString(), title = title,
pageSize = pages.size, chapterSize = chaptersSize, chapterIndex = position
)
}
return null
}
fun lastIndex(): Int { fun lastIndex(): Int {
return pages.size - 1 return pages.size - 1
} }
@ -70,28 +53,6 @@ data class TextChapter(
return stringBuilder.toString() return stringBuilder.toString()
} }
fun getStartLine(pageIndex: Int): Int {
if (pageLines.size > pageIndex) {
var lines = 0
for (index: Int in 0 until pageIndex) {
lines += pageLines[index] + 1
}
return lines
}
return 0
}
fun getPageIndex(line: Int): Int {
var lines = 0
for (pageIndex in pageLines.indices) {
lines += pageLines[pageIndex] + 1
if (line < lines) {
return pageIndex
}
}
return 0
}
fun getContent(): String { fun getContent(): String {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
pages.forEach { pages.forEach {

@ -5,6 +5,7 @@ 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.ui.book.read.page.ChapterProvider import io.legado.app.ui.book.read.page.ChapterProvider
import java.text.DecimalFormat
data class TextPage( data class TextPage(
var index: Int = 0, var index: Int = 0,
@ -17,6 +18,21 @@ data class TextPage(
var height: Float = 0f var height: Float = 0f
) { ) {
fun upLinesPosition() = ChapterProvider.apply {
if (textLines.size <= 1) return@apply
if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply
val surplus = (visibleBottom - textLines.last().lineBottom)
if (surplus == 0f) return@apply
height += surplus
val tj = surplus / (textLines.size - 1)
for (i in 1 until textLines.size) {
val line = textLines[i]
line.lineTop = line.lineTop + tj * i
line.lineBase = line.lineBase + tj * i
line.lineBottom = line.lineBottom + tj * i
}
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun format(): TextPage { fun format(): TextPage {
if (textLines.isEmpty() && ChapterProvider.visibleWidth > 0) { if (textLines.isEmpty() && ChapterProvider.visibleWidth > 0) {
@ -28,12 +44,11 @@ data class TextPage(
if (y < 0) y = 0f if (y < 0) y = 0f
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine() val textLine = TextLine()
textLine.lineTop = (ChapterProvider.paddingTop + y - textLine.lineTop = ChapterProvider.paddingTop + y + layout.getLineTop(lineIndex)
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))) textLine.lineBase =
textLine.lineBase = (ChapterProvider.paddingTop + y - ChapterProvider.paddingTop + y + layout.getLineBaseline(lineIndex)
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)))
textLine.lineBottom = textLine.lineBottom =
textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent ChapterProvider.paddingTop + y + layout.getLineBottom(lineIndex)
var x = ChapterProvider.paddingLeft + var x = ChapterProvider.paddingLeft +
(ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2 (ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2
textLine.text = textLine.text =
@ -84,4 +99,21 @@ data class TextPage(
lineStart += textLine.text.length lineStart += textLine.text.length
} }
} }
val readProgress: String
get() {
val df = DecimalFormat("0.0%")
if (chapterSize == 0 || pageSize == 0 && chapterIndex == 0) {
return "0.0%"
} else if (pageSize == 0) {
return df.format((chapterIndex + 1.0f) / chapterSize.toDouble())
}
var percent =
df.format(chapterIndex * 1.0f / chapterSize + 1.0f / chapterSize * (index + 1) / pageSize.toDouble())
if (percent == "100.0%" && (chapterIndex + 1 != chapterSize || index + 1 != pageSize)) {
percent = "99.9%"
}
return percent
}
} }

@ -49,9 +49,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle() val payload = Bundle()
val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
payload.putString("name", newItem.name) payload.putString("name", newItem.name)
} }
@ -73,6 +73,9 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.intro != newItem.intro) { if (oldItem.intro != newItem.intro) {
payload.putString("intro", newItem.intro) payload.putString("intro", newItem.intro)
} }
if (payload.isEmpty) {
return null
}
return payload return payload
} }
} }

@ -11,6 +11,7 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil 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 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
@ -31,6 +32,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import java.text.Collator
class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_search), class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_search),
@ -49,7 +51,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
private var bookData: LiveData<List<Book>>? = null private var bookData: LiveData<List<Book>>? = null
private var menu: Menu? = null private var menu: Menu? = null
private var precisionSearchMenuItem: MenuItem? = null private var precisionSearchMenuItem: MenuItem? = null
private var groups = hashSetOf<String>() private var groups = linkedSetOf<String>()
private var refreshTime = System.currentTimeMillis() private var refreshTime = System.currentTimeMillis()
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -110,6 +112,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
viewModel.saveSearchKey(query) viewModel.saveSearchKey(query)
viewModel.search(it) viewModel.search(it)
} }
openOrCloseHistory(false)
return true return true
} }
@ -126,6 +129,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
openOrCloseHistory(hasFocus) openOrCloseHistory(hasFocus)
} }
} }
openOrCloseHistory(true)
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -133,11 +137,10 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
ATH.applyEdgeEffectColor(rv_bookshelf_search) ATH.applyEdgeEffectColor(rv_bookshelf_search)
ATH.applyEdgeEffectColor(rv_history_key) ATH.applyEdgeEffectColor(rv_history_key)
bookAdapter = BookAdapter(this, this) bookAdapter = BookAdapter(this, this)
rv_bookshelf_search.layoutManager = rv_bookshelf_search.layoutManager = FlexboxLayoutManager(this)
LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
rv_bookshelf_search.adapter = bookAdapter rv_bookshelf_search.adapter = bookAdapter
historyKeyAdapter = HistoryKeyAdapter(this, this) historyKeyAdapter = HistoryKeyAdapter(this, this)
rv_history_key.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) rv_history_key.layoutManager = FlexboxLayoutManager(this)
rv_history_key.adapter = historyKeyAdapter rv_history_key.adapter = historyKeyAdapter
adapter = SearchAdapter(this, this) adapter = SearchAdapter(this, this)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
@ -229,12 +232,13 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
if (selectedGroup == "") { if (selectedGroup == "") {
item?.isChecked = true item?.isChecked = true
} }
groups.map { groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE))
item = menu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it) .map {
if (it == selectedGroup) { item = menu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it)
item?.isChecked = true if (it == selectedGroup) {
item?.isChecked = true
}
} }
}
menu?.setGroupCheckable(R.id.source_group, true, true) menu?.setGroupCheckable(R.id.source_group, true, true)
} }
@ -266,7 +270,8 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
} else { } else {
App.db.searchKeywordDao().liveDataSearch(key) App.db.searchKeywordDao().liveDataSearch(key)
} }
historyData?.observe(this, Observer { historyKeyAdapter.setItems(it) historyData?.observe(this, Observer {
historyKeyAdapter.setItems(it)
if (it.isEmpty()) { if (it.isEmpty()) {
tv_clear_history.invisible() tv_clear_history.invisible()
} else { } else {
@ -280,15 +285,12 @@ 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>, isMandatoryUpdate: Boolean) {
if (!isMandatoryUpdate && System.currentTimeMillis() - refreshTime < 500) { val searchItems = ArrayList(items)
return if (isMandatoryUpdate || System.currentTimeMillis() - refreshTime > 500) {
} refreshTime = System.currentTimeMillis()
refreshTime = System.currentTimeMillis() val diffResult =
try { DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), searchItems))
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), items)) adapter.setItems(searchItems, diffResult)
adapter.setItems(items, diffResult)
} catch (e: Exception) {
e.printStackTrace()
} }
} }

@ -7,82 +7,62 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig import io.legado.app.model.SearchBookModel
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.isActive
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application)
private var searchPool = , SearchBookModel.CallBack {
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() private val searchBookModel = SearchBookModel(this, this)
private var task: Coroutine<*>? = null
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 searchPage = 1
var isLoading = false var isLoading = false
var searchBooks = arrayListOf<SearchBook>() var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L
/** /**
* 开始搜索 * 开始搜索
*/ */
fun search(key: String) { fun search(key: String) {
task?.cancel() if (searchKey != key && key.isNotEmpty()) {
if (key.isEmpty() && searchKey.isEmpty()) { searchBookModel.cancelSearch()
return
} else if (key.isEmpty()) {
isLoading = true
searchPage++
} else if (key.isNotEmpty()) {
isLoading = true
searchPage = 1
searchKey = key
searchBooks.clear() searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
} }
searchBookModel.search(searchID, searchKey)
}
override fun onSearchStart() {
isSearchLiveData.postValue(true) isSearchLiveData.postValue(true)
task = execute { }
val searchGroup = context.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this@execute,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
it?.let { list ->
if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(list)
} else {
App.db.searchBookDao().insert(*list.toTypedArray())
mergeItems(list)
}
}
}
}
}
task?.invokeOnCompletion { override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
isSearchLiveData.postValue(false) if (context.getPrefBoolean(PreferKey.precisionSearch)) {
isLoading = false precisionSearch(this, searchBooks)
} else {
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
mergeItems(this, searchBooks)
} }
} }
override fun onSearchFinish() {
isSearchLiveData.postValue(false)
isLoading = false
}
override fun onSearchCancel() {
isSearchLiveData.postValue(false)
isLoading = false
}
/** /**
* 精确搜索处理 * 精确搜索处理
*/ */
private fun precisionSearch(searchBooks: List<SearchBook>) { private fun precisionSearch(scope: CoroutineScope, searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>() val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook -> searchBooks.forEach { searchBook ->
if (searchBook.name.contains(searchKey, true) if (searchBook.name.contains(searchKey, true)
@ -90,14 +70,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
) books.add(searchBook) ) books.add(searchBook)
} }
App.db.searchBookDao().insert(*books.toTypedArray()) App.db.searchBookDao().insert(*books.toTypedArray())
mergeItems(books) if (scope.isActive) {
mergeItems(scope, books)
}
} }
/** /**
* 合并搜索结果并排序 * 合并搜索结果并排序
*/ */
@Synchronized @Synchronized
private fun mergeItems(newDataS: List<SearchBook>) { private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>) {
if (newDataS.isNotEmpty()) { if (newDataS.isNotEmpty()) {
val copyDataS = ArrayList(searchBooks) val copyDataS = ArrayList(searchBooks)
val searchBooksAdd = ArrayList<SearchBook>() val searchBooksAdd = ArrayList<SearchBook>()
@ -141,6 +123,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
if (!scope.isActive) return
searchBooks.sortWith(Comparator { o1, o2 -> searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) { if (o1.name == searchKey && o2.name != searchKey) {
1 1
@ -166,6 +149,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
0 0
} }
}) })
if (!scope.isActive) return
searchBooks = copyDataS searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS) searchBookLiveData.postValue(copyDataS)
} }
@ -175,7 +159,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
* 停止搜索 * 停止搜索
*/ */
fun stop() { fun stop() {
task?.cancel() searchBookModel.cancelSearch()
} }
/** /**
@ -211,7 +195,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchPool.close() searchBookModel.close()
} }
} }

@ -40,6 +40,7 @@ import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.io.File import java.io.File
import java.text.Collator
class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity_book_source), class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity_book_source),
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
@ -84,7 +85,12 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
R.id.menu_group_manage -> R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage") GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_import_source_local -> FilePicker R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, "text/*", arrayOf("txt", "json")) .selectFile(
this,
importRequestCode,
type = "text/*",
allowExtensions = arrayOf("txt", "json")
)
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
} }
if (item.groupId == R.id.source_group) { if (item.groupId == R.id.source_group) {
@ -98,8 +104,14 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
when (it.path) { when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url -> "/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importSource(url) { msg -> if (url.startsWith("http", false)){
title_bar.snackbar(msg) viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else{
viewModel.importSourceFromFilePath(url){msg ->
title_bar.snackbar(msg)}
} }
} }
else -> { else -> {
@ -197,9 +209,10 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private fun upGroupMenu() { private fun upGroupMenu() {
groupMenu?.removeGroup(R.id.source_group) groupMenu?.removeGroup(R.id.source_group)
groups.map { groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE))
groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it) .map {
} groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it)
}
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")

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

Loading…
Cancel
Save