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. 143
      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. 40
      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. 46
      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. 3
      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. 18
      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. 43
      app/src/main/java/io/legado/app/model/WebBook.kt
  44. 7
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  45. 168
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  46. 32
      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. 8
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  52. 41
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  53. 48
      app/src/main/java/io/legado/app/service/DownloadService.kt
  54. 15
      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. 13
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  66. 7
      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. 2
      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. 40
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  73. 16
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  74. 106
      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. 69
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  79. 61
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  80. 101
      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. 13
      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. 30
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  99. 88
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  100. 17
      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
versionCode gitCommits
versionName version
flavorDimensions "versionCode"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
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 {
// Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true
@ -99,7 +108,7 @@ kapt {
dependencies {
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.espresso:espresso-core:3.2.0'
//kotlin
@ -119,7 +128,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material: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
def lifecycle_version = '2.2.0'
@ -127,12 +136,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.4'
def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//paging
implementation 'androidx.paging:paging-runtime:2.1.1'
implementation 'androidx.paging:paging-runtime:2.1.2'
//anko
def anko_version = '0.10.8'
@ -148,26 +157,26 @@ dependencies {
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 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino
implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
//
//noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer
implementation 'org.nanohttpd:nanohttpd: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'
@ -177,7 +186,7 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.8'
//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'

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

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

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

@ -23,6 +23,7 @@
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口-->
@ -78,6 +79,48 @@
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</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
android:name=".ui.main.MainActivity"
@ -91,7 +134,7 @@
<!--书籍详情页-->
<activity
android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTask" />
android:launchMode="singleTop" />
<!--书籍信息编辑-->
<activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"
@ -120,15 +163,19 @@
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--配置界面-->
<activity
android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" />
<!--搜索界面-->
<activity
android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" />
<!--关于界面-->
<activity
android:name=".ui.about.AboutActivity"
android:launchMode="singleTask" />
<!--捐赠界面-->
<activity
android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" />
@ -177,42 +224,55 @@
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--书籍管理-->
<activity
android:name=".ui.book.arrange.ArrangeBookActivity"
android:launchMode="singleTop" />
<!--书源调试-->
<activity
android:name=".ui.book.source.debug.BookSourceDebugActivity"
android:launchMode="singleTop" />
<!--目录-->
<activity
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind"
android:launchMode="singleTop" />
<!--RSS阅读-->
<activity
android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTop" />
<!--导入书籍-->
<activity
android:name="io.legado.app.ui.book.local.ImportBookActivity"
android:launchMode="singleTop" />
<!--发现界面-->
<activity
android:name="io.legado.app.ui.book.explore.ExploreShowActivity"
android:launchMode="singleTop" />
<!--订阅源调试-->
<activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" />
<!--订阅条目-->
<activity
android:name=".ui.rss.article.RssArticlesActivity"
android:launchMode="singleTop" />
<!--Rss收藏-->
<activity
android:name=".ui.rss.favorites.RssFavoritesActivity"
android:launchMode="singleTop" />
<!--下载界面-->
<activity
android:name="io.legado.app.ui.book.download.DownloadActivity"
android:launchMode="singleTop" />
<!--书源登录-->
<activity
android:name=".ui.login.SourceLogin"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!--文字处理-->
<activity
android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label">
@ -227,6 +287,19 @@
<data android:mimeType="text/plain" />
</intent-filter>
</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.DownloadService" />
@ -250,6 +323,10 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<meta-data
android:name="channel"
android:value="${APP_CHANNEL_VALUE}" />
</application>
</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,
"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
},
{
"enable": false,
"name": "目录(不匹配行前空白)",
"rule": "^(?<= |\\s)(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"name": "目录(空白)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{
"enable": false,
"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
},
{
"enable": false,
"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
},
{
"enable": false,
"name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$",
"serialNumber": 4
},
{
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$",
"serialNumber": 4
"serialNumber": 5
},
{
"enable": true,
"name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文\\s{1,4}.{0,20}$",
"serialNumber": 5
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"serialNumber": 6
},
{
"enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 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}$",
"serialNumber": 6
"serialNumber": 8
},
{
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "^[  \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$",
"serialNumber": 7
"rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$",
"serialNumber": 9
},
{
"enable": true,
"name": "特殊符号 标题",
"rule": "^[  \\t]{0,4}[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"serialNumber": 8
"enable": false,
"name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 10
},
{
"enable":false,
"name": "特殊符号 标题(不匹配空白字符)",
"rule": "^(?<= |\\s)[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"serialNumber": 9
"enable":true,
"name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 11
},
{
"enable": true,
"name": "章/卷 序号 标题",
"rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})\\s{0,4}.{0,30}$",
"serialNumber": 10
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"serialNumber": 12
},
{
"enable":false,
"name": "顶格标题",
"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)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
**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**
* 添加隐藏标题
@ -195,14 +291,7 @@
- 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996`
* 修改订阅中自动添加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>
```
订阅源的内容规则中存在`<style>``style=`
**2019/12/28**
* 添加下载界面

@ -1,13 +1,11 @@
package io.legado.app
import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import com.jeremyliao.liveeventbus.LiveEventBus
@ -56,7 +54,7 @@ class App : Application() {
.lifecycleObserverAlwaysActive(true)
.autoClear(false)
registerActivityLife()
registerActivityLifecycleCallbacks(ActivityHelp)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -116,9 +114,7 @@ class App : Application() {
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.let {
(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let {
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
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
}
fun getItem(position: Int): ITEM? =
if (position in 0 until items.size) items[position] else null
fun getItem(position: Int): ITEM? = items.getOrNull(position)
fun getItemByLayoutPosition(position: Int): ITEM? {
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

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

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

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

@ -10,16 +10,16 @@ import io.legado.app.data.entities.BookChapter
@Dao
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>>
@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>>
@Query("select * from chapters where bookUrl = :bookUrl")
@Query("select * from chapters where bookUrl = :bookUrl order by `index`")
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>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")

@ -12,24 +12,30 @@ interface BookDao {
@Query("SELECT * FROM books order by durChapterTime desc")
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>>
@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>>
@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>>
@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>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
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||'%'")
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)")
fun findByName(vararg names: String): List<Book>
@ -54,6 +60,9 @@ interface BookDao {
@get:Query("SELECT COUNT(*) FROM books")
val allBookCount: Int
@get:Query("select max(`order`) from books")
val maxOrder: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg book: Book)

@ -50,9 +50,12 @@ interface BookSourceDao {
@get:Query("select * from book_sources order by customOrder asc")
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>
@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")
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
from searchBooks as t1 inner join book_sources as t2
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
"""
)
@ -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
from searchBooks as t1 inner join book_sources as t2
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
"""
)
@ -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
from searchBooks as t1 inner join book_sources as t2
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
"""
)

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

@ -16,4 +16,16 @@ data class RssStar(
var description: String? = null,
var content: 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
import android.app.Activity
import android.app.Application
import android.os.Bundle
import java.lang.ref.WeakReference
import java.util.*
/**
* Activity管理器,管理项目中Activity的状态
*/
object ActivityHelp {
object ActivityHelp : Application.ActivityLifecycleCallbacks {
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
import android.content.Context
import android.content.pm.PackageManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
@ -32,6 +33,9 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
}
val requestedDirection: String?
get() = App.INSTANCE.getPrefString(R.string.pk_requested_direction)
var backupPath: String?
get() = App.INSTANCE.getPrefString(PreferKey.backupPath)
set(value) {
@ -49,7 +53,7 @@ object AppConfig {
}
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
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16)
@ -92,4 +96,45 @@ object AppConfig {
set(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
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.App
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule
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.withContext
import org.apache.commons.text.similarity.JaccardSimilarity
@ -20,12 +20,9 @@ import kotlin.math.min
object BookHelp {
private const val cacheFolderName = "book_cache"
val downloadPath: String
get() = App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
private val downloadUri get() = Uri.parse(downloadPath)
private val downloadDir: File =
App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.cacheDir
private fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
@ -40,107 +37,54 @@ object BookHelp {
}
fun clearCache() {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
} else {
FileUtils.deleteFile(
FileUtils.getPath(
File(downloadPath),
downloadDir,
subDirs = *arrayOf(cacheFolderName)
)
)
}
}
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFileIfNotExist(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.uri?.writeText(App.INSTANCE, content)
}
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
}
postEvent(EventBus.SAVE_CONTENT, bookChapter)
}
fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>()
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFolderIfNotExist(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.let { bookDoc ->
DocumentUtils.listFiles(App.INSTANCE, bookDoc.uri).forEach {
fileNameList.add(it.name)
}
}
}
} else {
FileUtils.createFolderIfNotExist(
File(downloadPath),
downloadDir,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.let {
fileNameList.addAll(it)
}
}
return fileNameList
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
when {
book.isLocalBook() -> {
return true
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
else -> {
return FileUtils.exists(
File(downloadPath),
return if (book.isLocalBook()) {
true
} else {
FileUtils.exists(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
return false
}
fun getContent(book: Book, bookChapter: BookChapter): String? {
when {
book.isLocalBook() -> {
if (book.isLocalBook()) {
return AnalyzeTxtFile.getContent(book, bookChapter)
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.uri?.readText(App.INSTANCE)
}
}
else -> {
} else {
val file = FileUtils.getFile(
File(downloadPath),
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
@ -148,31 +92,20 @@ object BookHelp {
return file.readText()
}
}
}
return null
}
fun delContent(book: Book, bookChapter: BookChapter) {
when {
book.isLocalBook() -> return
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.delete()
}
}
else -> {
if (book.isLocalBook()) {
return
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
}
}
private fun formatFolderName(folderName: String): String {
return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "")
@ -242,18 +175,9 @@ object BookHelp {
private var replaceRules: List<ReplaceRule> = arrayListOf()
@Synchronized
fun upReplaceRules(name: String? = null, origin: String? = null) {
if (name != null) {
if (bookName != name || bookOrigin != origin) {
replaceRules = if (origin.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(name)
} else {
App.db.replaceRuleDao().findEnabledByScope(name, origin)
}
bookName = name
bookOrigin = origin
}
} else {
suspend fun upReplaceRules() {
withContext(IO) {
synchronized(this) {
val o = bookOrigin
bookName?.let {
replaceRules = if (o.isNullOrEmpty()) {
@ -264,6 +188,7 @@ object BookHelp {
}
}
}
}
suspend fun disposeContent(
title: String,
@ -274,7 +199,17 @@ object BookHelp {
): String {
var c = content
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 ->
item.pattern.let {
if (it.isNotEmpty()) {
@ -300,6 +235,8 @@ object BookHelp {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(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) {
super.clearView(recyclerView, viewHolder)
onItemTouchCallbackListener?.clearView(recyclerView, viewHolder)
onItemTouchCallbackListener?.onClearView(recyclerView, viewHolder)
}
interface OnItemTouchCallbackListener {
@ -131,7 +131,10 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
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 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.utils.EncoderUtils
import io.legado.app.utils.MD5Utils
@ -50,6 +50,6 @@ interface JsExtensions {
}
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 io.legado.app.App
import io.legado.app.R
import io.legado.app.ui.welcome.Launcher1
import io.legado.app.ui.welcome.Launcher2
import io.legado.app.ui.welcome.Launcher3
import io.legado.app.ui.welcome.WelcomeActivity
import io.legado.app.ui.welcome.*
import org.jetbrains.anko.toast
/**
@ -20,7 +17,10 @@ object LauncherIconHelp {
private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::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?) {

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

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

@ -3,9 +3,11 @@ package io.legado.app.help.http
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import retrofit2.Retrofit
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@ -13,14 +15,12 @@ import kotlin.coroutines.resume
object HttpHelper {
val client: OkHttpClient by lazy {
val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs = ArrayList<ConnectionSpec>()
specs.add(default)
specs.add(ConnectionSpec.COMPATIBLE_TLS)
specs.add(ConnectionSpec.CLEARTEXT)
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
@ -57,6 +57,16 @@ object HttpHelper {
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 {
return getRetrofit(baseUrl, encode).create(T::class.java)
}

@ -1,8 +1,11 @@
package io.legado.app.help.permission
import android.os.Build
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.jetbrains.anko.startActivity
import java.util.ArrayList
class PermissionsCompat private constructor() {
@ -12,6 +15,17 @@ class PermissionsCompat private constructor() {
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 {
private val request: Request

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

@ -21,14 +21,6 @@ object Backup {
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 {
arrayOf(
"bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
@ -39,21 +31,18 @@ object Backup {
fun autoBack(context: Context) {
val lastBackup = context.getPrefLong(PreferKey.lastBackup)
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
return
}
Coroutine.async {
val backupPath = context.getPrefString(PreferKey.backupPath)
if (backupPath.isNullOrEmpty()) {
backup(context)
} else {
backup(context, backupPath)
context.getPrefString(PreferKey.backupPath)?.let {
backup(context, it, true)
}
}
}
}
suspend fun backup(context: Context, path: String = legadoPath) {
suspend fun backup(context: Context, path: String, isAuto: Boolean = false) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) {
synchronized(this@Backup) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
@ -80,9 +69,10 @@ object Backup {
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path))
copyBackup(context, Uri.parse(path), isAuto)
} else {
copyBackup(File(path))
copyBackup(File(path), isAuto)
}
}
}
}
@ -95,11 +85,19 @@ object Backup {
}
@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 ->
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
if (isAuto) {
treeDoc.findFile("auto")?.findFile(fileName)?.delete()
DocumentUtils.createFileIfNotExist(
treeDoc,
fileName,
subDirs = *arrayOf("auto")
)?.writeBytes(context, file.readBytes())
} else {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
@ -107,15 +105,19 @@ object Backup {
}
}
}
}
@Throws(java.lang.Exception::class)
private fun copyBackup(rootFile: File) {
private fun copyBackup(rootFile: File, isAuto: Boolean) {
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(
FileUtils.createFileIfNotExist(rootFile, fileName),
true
if (isAuto) {
FileUtils.createFileIfNotExist(rootFile, fileName, "auto")
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
)
}
}

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

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

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

@ -19,4 +19,16 @@ object ATHUtils {
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()
}
}
}

@ -108,3 +108,6 @@ val Context.isDarkTheme: Boolean
val Fragment.isDarkTheme: Boolean
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
@ColorInt
fun bottomBackground(context: Context): Int {

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

@ -56,9 +56,7 @@ object Debug {
log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null)
.onSuccess {
if (it == null) {
log(debugSource, "︽解析失败", state = -1)
} else if(it.articles.isEmpty()) {
if (it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空")
log(debugSource, "︽解析完成", state = 1000)
} else {
@ -69,7 +67,7 @@ object Debug {
if (ruleContent.isNullOrEmpty()) {
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
} else {
rssContentDebug(it.articles[0], ruleContent)
rssContentDebug(it.articles[0], ruleContent, rssSource)
}
} else {
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, "︾开始解析内容页")
Rss.getContent(rssArticle, ruleContent)
Rss.getContent(rssArticle, ruleContent, rssSource)
.onSuccess {
log(debugSource, it)
log(debugSource, "︽内容页解析完成", state = 1000)
@ -140,7 +138,6 @@ object Debug {
log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1)
.onSuccess { exploreBooks ->
exploreBooks?.let {
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
@ -149,7 +146,6 @@ object Debug {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
}
.onError {
log(
debugSource,
@ -164,7 +160,6 @@ object Debug {
log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1)
.onSuccess { searchBooks ->
searchBooks?.let {
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
@ -173,7 +168,6 @@ object Debug {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
}
.onError {
log(debugSource, it.localizedMessage, state = -1)
}
@ -197,8 +191,7 @@ object Debug {
private fun tocDebug(webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book)
.onSuccess { chapterList ->
chapterList?.let {
.onSuccess {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
@ -208,7 +201,6 @@ object Debug {
log(debugSource, "︽目录列表为空", state = -1)
}
}
}
.onError {
log(debugSource, it.localizedMessage, state = -1)
}

@ -21,7 +21,10 @@ object Rss {
context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> {
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
RssParserByRule.parseXML(body, rssSource)
}
@ -30,12 +33,15 @@ object Rss {
fun getContent(
rssArticle: RssArticle,
ruleContent: String,
rssSource: RssSource?,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
val body = AnalyzeUrl(rssArticle.link, baseUrl = rssArticle.origin)
.getResponseAwait(rssArticle.origin)
val body = AnalyzeUrl(
rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap()
).getResponseAwait(rssArticle.origin)
.body
val analyzeRule = AnalyzeRule()
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,8 +27,17 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> {
): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) {
searchBookSuspend(scope, key, page)
}
}
suspend fun searchBookSuspend(
scope: CoroutineScope,
key: String,
page: Int? = 1
): ArrayList<SearchBook> {
bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl,
@ -38,15 +47,16 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
} ?: arrayListOf()
}
return arrayListOf()
}
/**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
@ -84,8 +95,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<Book> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body =
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml
@ -111,8 +122,8 @@ class WebBook(val bookSource: BookSource) {
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
val body =
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
book.tocHtml
@ -139,9 +150,24 @@ class WebBook(val bookSource: BookSource) {
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
getContentSuspend(
book, bookChapter, nextChapterUrl, scope
)
}
}
/**
* 章节内容
*/
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@async bookChapter.url
return bookChapter.url
}
val body =
if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
@ -160,8 +186,8 @@ class WebBook(val bookSource: BookSource) {
sourceRegex = bookSource.getContentRule().sourceRegex
).body
}
BookContent.analyzeContent(
this,
return BookContent.analyzeContent(
scope,
body,
book,
bookChapter,
@ -170,5 +196,4 @@ class WebBook(val bookSource: BookSource) {
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.utils.*
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import java.net.URLEncoder
import java.util.*
@ -40,7 +39,7 @@ class AnalyzeUrl(
) : JsExtensions {
companion object {
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 = ""
@ -182,7 +181,7 @@ class AnalyzeUrl(
RequestMethod.POST -> {
bodyTxt?.let {
if (it.isJson()) {
body = it.toRequestBody(jsonType)
body = RequestBody.create(jsonType, it)
} else {
analyzeFields(it)
}

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

@ -4,30 +4,46 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentPath
import java.io.File
object LocalBook {
fun importFile(doc: DocumentFile) {
doc.name?.let { fileName ->
fun importFile(path: String) {
val fileName = if (path.isContentPath()) {
val doc = DocumentFile.fromSingleUri(App.INSTANCE, Uri.parse(path))
doc?.name ?: ""
} else {
File(path).name
}
val str = fileName.substringBeforeLast(".")
var name = str.substringBefore("作者")
val author = str.substringAfter("作者", "")
val authorIndex = str.indexOf("作者")
var name: String
var author: String
if (authorIndex == -1) {
name = str
author = ""
} else {
name = str.substring(0, authorIndex)
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 = doc.uri.toString(),
bookUrl = path,
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
}
}
fun deleteBook(book: Book, deleteOriginal: Boolean) {
kotlin.runCatching {
@ -37,8 +53,12 @@ object LocalBook {
}
if (deleteOriginal) {
if (book.bookUrl.isContentPath()) {
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.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
@ -39,7 +40,12 @@ object BookContent {
var contentData = analyzeContent(
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) {
var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
@ -64,7 +70,7 @@ object BookContent {
)
nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
content.append(contentData.content)
content.append(contentData.content).append("\n")
}
}
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
@ -91,7 +97,7 @@ object BookContent {
}
}
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.AnalyzeUrl
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
object BookList {
@Throws(Exception::class)
fun analyzeBookList(
scope: CoroutineScope,
body: String?,
bookSource: BookSource,
analyzeUrl: AnalyzeUrl,
@ -28,12 +32,13 @@ object BookList {
)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(null)
analyzeRule.setContent(body, baseUrl)
bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook ->
getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body
bookList.add(searchBook)
}
@ -59,7 +64,7 @@ object BookList {
collections = analyzeRule.getElements(ruleList)
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook ->
getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body
bookList.add(searchBook)
}
@ -74,8 +79,9 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem(
item, analyzeRule, bookSource, baseUrl, index == 0,
scope, item, analyzeRule, bookSource, baseUrl, index == 0,
ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,
ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,
ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount
@ -93,7 +99,9 @@ object BookList {
return bookList
}
@Throws(Exception::class)
private fun getInfoItem(
scope: CoroutineScope,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
baseUrl: String
@ -108,29 +116,37 @@ object BookList {
with(bookSource.getBookInfoRule()) {
init?.let {
if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it))
}
}
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = analyzeRule.getString(name)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -140,7 +156,9 @@ object BookList {
return null
}
@Throws(Exception::class)
private fun getSearchItem(
scope: CoroutineScope,
item: Any,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
@ -162,30 +180,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook
analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = analyzeRule.getString(ruleName)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
}
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) {

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

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

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

@ -7,9 +7,10 @@ import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
@ -20,7 +21,7 @@ import java.util.concurrent.Executors
class CheckSourceService : BaseService() {
private val threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>()
private var processIndex = 0
@ -42,24 +43,24 @@ class CheckSourceService : BaseService() {
override fun onDestroy() {
super.onDestroy()
task?.cancel()
tasks.clear()
searchPool.close()
}
private fun check(ids: List<String>) {
task?.cancel()
if (allIds.isNotEmpty()) {
toast("已有书源在校验,等完成后再试")
return
}
tasks.clear()
allIds.clear()
checkedIds.clear()
allIds.addAll(ids)
processIndex = 0
updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
task = execute {
for (i in 0 until threadCount) {
check()
}
}.onError {
toast("校验书源出错:${it.localizedMessage}")
}
}
@ -67,30 +68,44 @@ class CheckSourceService : BaseService() {
synchronized(this) {
processIndex++
}
execute {
if (processIndex < allIds.size) {
val sourceUrl = allIds[processIndex]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl)
} else {
check(source)
}
} ?: onNext(sourceUrl)
}
}
}
private fun check(source: BookSource) {
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
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)
)
synchronized(this) {
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.BookHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent
@ -25,12 +26,14 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf()
private var tasks = CompositeCoroutine()
private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private val downloadCount = hashMapOf<String, DownloadCount>();
private val finalMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
@ -97,6 +100,11 @@ class DownloadService : BaseService() {
finalMap.remove(bookUrl)
}
private fun updateNotification(downloadCount:DownloadCount, totalCount: Int, content: String){
notificationContent =
"进度:${downloadCount.downloadFinishedCount}/$totalCount,成功:${downloadCount.successCount},$content"
}
private fun download() {
val task = Coroutine.async(this, context = searchPool) {
downloadMap.forEach { entry ->
@ -106,6 +114,9 @@ class DownloadService : BaseService() {
val bookSource =
App.db.bookSourceDao().getBookSource(book.origin) ?: return@async
val webBook = WebBook(bookSource)
downloadCount[entry.key] = DownloadCount()
entry.value.forEach { chapter ->
if (!isActive) return@async
if (downloadMap.containsKey(book.bookUrl)) {
@ -115,17 +126,14 @@ class DownloadService : BaseService() {
chapter,
scope = this,
context = searchPool
)
.onStart {
notificationContent = chapter.title
}
.onSuccess(IO) { content ->
content?.let {
).onSuccess(IO) { content ->
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
}
.onFinally(IO) {
synchronized(this@DownloadService) {
downloadCount[entry.key]?.increaseFinished()
downloadCount[entry.key]?.let { updateNotification(it, entry.value.size, chapter.title) }
val chapterMap =
finalMap[book.bookUrl]
?: linkedSetOf<BookChapter>().apply {
@ -135,9 +143,14 @@ class DownloadService : BaseService() {
if (chapterMap.size == entry.value.size) {
downloadMap.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)
task.invokeOnCompletion {
tasks.remove(task)
if (tasks.isEmpty()) {
if (tasks.isEmpty) {
stopSelf()
}
}
@ -176,4 +189,21 @@ class DownloadService : BaseService() {
val notification = builder.build()
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() {
super.onCreate()
@ -42,22 +42,23 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
clearTTS()
}
@Synchronized
override fun onInit(status: Int) {
launch {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsIsSuccess = true
ttsInitFinish = true
play()
} else {
launch {
toast(R.string.tts_init_failed)
}
}
}
@Suppress("DEPRECATION")
@Synchronized
override fun play() {
if (contentList.isEmpty() || !ttsIsSuccess) {
if (contentList.isEmpty() || !ttsInitFinish) {
return
}
if (requestFocus()) {
@ -175,9 +176,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
}
override fun onError(s: String) {
launch {
toast(s)
}
//nothing
}
}

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

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

@ -1,13 +1,19 @@
package io.legado.app.ui.about
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.Menu
import android.view.MenuItem
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_about.*
import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -17,6 +23,16 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
supportFragmentManager.beginTransaction()
.replace(R.id.fl_fragment, aboutFragment, fTag)
.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 {

@ -2,7 +2,6 @@ package io.legado.app.ui.about
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
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.sendToClip
import org.jetbrains.anko.longToast
import java.net.URLEncoder
class DonateFragment : PreferenceFragmentCompat() {
@ -36,7 +34,6 @@ class DonateFragment : PreferenceFragmentCompat() {
"zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl)
"zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl)
"qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl)
"zfbSk" -> aliDonate(requireContext())
"zfbHbSsm" -> getZfbHb(requireContext())
"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
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
AudioPlay.titleData.observe(this, Observer { toolbar.title = it })
title_bar.transparent()
AudioPlay.titleData.observe(this, Observer { title_bar.title = it })
AudioPlay.coverData.observe(this, Observer { upCover(it) })
viewModel.initData(intent)
initView()

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

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

@ -1,19 +1,25 @@
package io.legado.app.ui.book.arrange
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
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 org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.*
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 selectedBooks: HashSet<Book> = hashSetOf()
private val selectedBooks: HashSet<Book> = hashSetOf()
var actionItem: Book? = null
fun selectAll(selectAll: Boolean) {
@ -40,14 +46,23 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
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>) {
with(holder.itemView) {
tv_name.text = if (item.author.isEmpty()) {
item.name
} else {
"${item.name}(${item.author})"
}
tv_author.text = getGroupName(item.group)
backgroundColor = context.backgroundColor
tv_name.text = item.name
tv_author.text = item.author
tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE
tv_group_s.text = getGroupName(item.group)
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>()
callBack.groupList.forEach {
if (it.groupId and groupId > 0) {
groupNames.add(it.groupName)
}
}
return groupNames
}
private fun getGroupName(groupId: Int): String {
val groupNames = getGroupList(groupId)
if (groupNames.isEmpty()) {
return context.getString(R.string.no_group)
return ""
}
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 {
val groupList: List<BookGroup>
fun upSelectCount()
fun updateBook(vararg book: Book)
fun deleteBook(book: Book)
fun selectGroup(groupId: Int, requestCode: Int)
}

@ -8,6 +8,15 @@ import io.legado.app.data.entities.Book
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) {
execute {
App.db.bookDao().update(*book)

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

@ -72,14 +72,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
fun search() {
task = execute {
searchStateData.postValue(true)
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
.onSuccess(IO) {
it?.forEach { searchBook ->
it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) {
@ -95,6 +94,10 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
}
}.onStart {
searchStateData.postValue(true)
}.onCancel {
searchStateData.postValue(false)
}
task?.invokeOnCompletion {
@ -107,7 +110,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess {
it?.let { loadChapter(it) }
loadChapter(it)
}.onError {
debug { context.getString(R.string.error_get_book_info) }
}
@ -119,14 +122,12 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
execute {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) {
it?.let { chapters ->
.onSuccess(IO) { chapters ->
if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title
val searchBook: SearchBook = book.toSearchBook()
searchFinish(searchBook)
}
}
}.onError {
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 lateinit var mLayoutManager: UpLinearLayoutManager
private var tocLiveData: LiveData<List<BookChapter>>? = null
private var scrollToDurChapter = false
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.chapterCallBack = this
@ -74,7 +75,6 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.let {
durChapterIndex = it.durChapterIndex
tv_current_chapter_info.text = it.durChapterTitle
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
initCacheFileNames(it)
}
}
@ -85,7 +85,10 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl)
tocLiveData?.observe(viewLifecycleOwner, Observer {
adapter.setItems(it)
if (!scrollToDurChapter) {
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
scrollToDurChapter = true
}
})
}
@ -103,7 +106,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) {
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.base.VMBaseActivity
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
@ -81,9 +82,15 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
private fun initLiveData() {
bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload()
bookshelfLiveData?.observe(this, Observer {
adapter.setItems(it)
initCacheSize(it)
bookshelfLiveData?.observe(this, Observer { list ->
val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
}
adapter.setItems(books)
initCacheSize(books)
})
}
@ -127,7 +134,7 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
FilePicker.selectFolder(this, exportRequestCode) {
val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey)
if (path.isNullOrEmpty()) {
toast("没有默认路径")
toast(R.string.no_default_path)
} else {
startExport(path)
}

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

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

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

@ -32,6 +32,13 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
fun delGroup(vararg bookGroup: BookGroup) {
execute {
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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
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.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.utils.dp
import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.visible
@ -52,8 +57,7 @@ class BookInfoActivity :
get() = getViewModel(BookInfoViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title_bar.transparent()
viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.chapterListData.observe(this, Observer { upLoading(false, it) })
viewModel.initData(intent)
@ -86,8 +90,13 @@ class BookInfoActivity :
}
}
R.id.menu_can_update -> {
if (viewModel.inBookshelf) {
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)
}
}
tv_toc.onClick {
tv_toc_view.onClick {
if (!viewModel.inBookshelf) {
viewModel.saveBook {
viewModel.saveChapterList {
@ -213,30 +222,41 @@ class BookInfoActivity :
openChapterList()
}
}
tv_group.onClick {
tv_change_group.onClick {
viewModel.bookData.value?.let {
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() {
viewModel.bookData.value?.let {
if (it.isLocalBook()) {
alert(
titleResource = R.string.sure,
messageResource = R.string.sure_delete_book_file
messageResource = R.string.sure_del
) {
positiveButton(R.string.yes) {
viewModel.delBook(true) {
finish()
val checkBox = CheckBox(this@BookInfoActivity).apply {
setText(R.string.delete_book_file)
}
val view = LinearLayout(this@BookInfoActivity).apply {
setPadding(16.dp, 0, 16.dp, 0)
addView(checkBox)
}
negativeButton(R.string.no) {
viewModel.delBook(false) {
customView = view
positiveButton(R.string.yes) {
viewModel.delBook(checkBox.isChecked) {
finish()
}
}
negativeButton(R.string.no)
}.show()
} else {
viewModel.delBook {

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

@ -5,8 +5,10 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
@ -16,10 +18,12 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
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.utils.DocumentUtils
import io.legado.app.utils.getViewModel
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_import_book.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@ -27,9 +31,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.sdk27.listeners.onClick
import java.io.File
import java.util.*
class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity_import_book),
FileChooserDialog.CallBack,
PopupMenu.OnMenuItemClickListener,
ImportBookAdapter.CallBack {
private val requestCodeSelectFolder = 342
@ -37,6 +43,8 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null
private var sdPath = FileUtils.getSdCardPath()
private var path = sdPath
override val viewModel: ImportBookViewModel
get() = getViewModel(ImportBookViewModel::class.java)
@ -57,7 +65,6 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
recycler_view.layoutManager = LinearLayoutManager(this)
adapter = ImportBookAdapter(this, this)
recycler_view.adapter = adapter
rotate_loading.loadingColor = accentColor
select_action_bar.setMainActionText(R.string.add_to_shelf)
select_action_bar.inflateMenu(R.menu.import_book_sel)
select_action_bar.setOnMenuItemClickListener(this)
@ -76,6 +83,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
}
}
})
}
private fun initEvent() {
@ -86,7 +94,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_select_folder -> selectImportFolder()
R.id.menu_select_folder -> FilePicker.selectFolder(this, requestCodeSelectFolder)
}
return super.onCompatOptionsItemSelected(item)
}
@ -111,11 +119,35 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private fun upRootDoc() {
AppConfig.importBookPath?.let {
if (it.isContentPath()) {
val rootUri = Uri.parse(it)
rootDoc = DocumentFile.fromTreeUri(this, rootUri)
subDocs.clear()
upPath()
} 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")
@ -131,7 +163,6 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
tv_path.text = path
adapter.selectedUris.clear()
adapter.clearItems()
rotate_loading.show()
launch(IO) {
val docList = DocumentUtils.listFiles(
this@ImportBookActivity,
@ -147,20 +178,47 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
}
docList.sortWith(compareBy({ !it.isDir }, { it.name }))
withContext(Main) {
rotate_loading.hide()
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() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, requestCodeSelectFolder)
} catch (e: Exception) {
e.printStackTrace()
override fun onFilePicked(requestCode: Int, currentPath: String) {
when (requestCode) {
requestCodeSelectFolder -> {
AppConfig.importBookPath = currentPath
upRootDoc()
}
}
}
@ -181,13 +239,27 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
}
@Synchronized
override fun nextDoc(doc: DocumentFile) {
subDocs.add(doc)
override fun nextDoc(uri: Uri) {
if (uri.toString().isContentPath()) {
subDocs.add(DocumentFile.fromSingleUri(this, uri)!!)
} else {
path = uri.toString()
}
upPath()
}
@Synchronized
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()) {
subDocs.removeAt(subDocs.lastIndex)
upPath()

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

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

@ -3,6 +3,7 @@ package io.legado.app.ui.book.read
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.os.AsyncTask
import android.os.Build
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是否存在
* 该方法需要在View完全被绘制出来之后调用否则判断不了
@ -99,7 +112,7 @@ object Help {
* 适配刘海
*/
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 {
layoutInDisplayCutoutMode =
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.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@ -13,6 +14,7 @@ import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
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.ReadBookConfig
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.lib.dialogs.alert
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 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?) {
Help.upLayoutInDisplayCutoutMode(window)
initView()
@ -106,6 +115,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ReadBook.loadContent(resetPageOffset = false)
}
override fun onResume() {
super.onResume()
upSystemUiVisibility()
@ -211,6 +225,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_copy_text ->
TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent())
R.id.menu_update_toc -> ReadBook.book?.let {
ReadBook.upMsg(getString(R.string.toc_updateing))
viewModel.loadChapterList(it)
}
R.id.menu_enable_replace -> ReadBook.book?.let {
@ -338,7 +353,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
MotionEvent.ACTION_DOWN -> textActionMenu?.dismiss()
MotionEvent.ACTION_MOVE -> {
when (v.id) {
R.id.cursor_left -> page_view.curPage.selectStartMove(
event.rawX + cursor_left.width,
@ -350,6 +366,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
)
}
}
MotionEvent.ACTION_UP -> showTextActionMenu()
}
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.y = y
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.y = y
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 = 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 ->
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) {
popup.showAtLocation(cursor_left, Gravity.BOTTOM or Gravity.START, x, y)
popup.showAtLocation(text_menu_position, Gravity.TOP or Gravity.START, x, y)
} else {
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 {
page_view.upContent(relativePosition)
page_view.upContent(relativePosition, resetPageOffset)
}
}
@ -479,6 +500,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
if (!ReadBook.isLocalBook) {
tv_chapter_url.text = it.url
tv_chapter_url.visible()
} else {
tv_chapter_url.gone()
}
seek_read_page.max = it.pageSize().minus(1)
seek_read_page.progress = ReadBook.durPageIndex
@ -558,7 +581,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onReplaceRuleSave() {
Coroutine.async {
BookHelp.upReplaceRules()
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
}
@ -587,9 +610,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 朗读按钮
*/
override fun onClickReadAloud() {
if (!BaseReadAloudService.isRun) {
SystemUtils.ignoreBatteryOptimization(this)
}
when {
!BaseReadAloudService.isRun -> ReadBook.readAloud()
BaseReadAloudService.pause -> ReadAloud.resume(this)
@ -661,7 +681,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
mHandler.removeCallbacks(keepScreenRunnable)
textActionMenu?.dismiss()
page_view.onDestroy()
ReadBook.msg = null
if (!BuildConfig.DEBUG) {
SyncBookProgress.uploadBookProgress()
Backup.autoBack(this)
}
}
override fun observeLiveBus() {
@ -684,9 +708,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upBg()
page_view.upStyle()
if (it) {
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
} else {
page_view.upContent()
page_view.upContent(resetPageOffset = false)
}
}
observeEvent<Int>(EventBus.ALOUD_STATE) {
@ -695,7 +719,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
val page = textChapter.page(ReadBook.durPageIndex)
if (page != null) {
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) {
upScreenTimeOut()
}

@ -39,7 +39,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun initBook(book: Book) {
if (ReadBook.book?.bookUrl != book.bookUrl) {
ReadBook.resetData(book)
ReadBook.resetData(book) { name, author ->
autoChangeSource(name, author)
}
isInitFinish = true
ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl)
if (ReadBook.chapterSize == 0) {
@ -52,7 +54,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
}
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
if (ReadBook.inBookshelf) {
ReadBook.saveRead()
@ -60,7 +62,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else {
isInitFinish = true
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)
if (ReadBook.chapterSize == 0) {
if (book.tocUrl.isEmpty()) {
@ -70,7 +74,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
} else {
if (ReadBook.curTextChapter != null) {
ReadBook.callBack?.upContent()
ReadBook.callBack?.upContent(resetPageOffset = false)
}
}
}
@ -98,38 +102,45 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
) {
execute {
if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let {
AnalyzeTxtFile().analyze(context, book).let {
App.db.bookChapterDao().delByBook(book.bookUrl)
App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = it.size
ReadBook.loadContent()
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
}
} else {
ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) {
if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size
ReadBook.loadContent()
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
} else {
changeDruChapterIndex(cList)
}
} else {
toast(R.string.error_load_toc)
ReadBook.upMsg(context.getString(R.string.error_load_toc))
}
}?.onError {
toast(R.string.error_load_toc)
} ?: autoChangeSource()
ReadBook.upMsg(context.getString(R.string.error_load_toc))
}
}
}.onError {
ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
}
}
fun changeTo(book1: Book) {
execute {
ReadBook.upMsg(null)
ReadBook.book?.let {
book1.group = it.group
book1.order = it.order
App.db.bookDao().delete(it)
}
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>) {
@ -167,7 +196,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*chapters.toTypedArray())
ReadBook.chapterSize = chapters.size
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
}
@ -181,7 +210,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durPageIndex = pageIndex
}
ReadBook.saveRead()
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = true)
}
fun removeFromBookshelf(success: (() -> Unit)?) {
@ -209,7 +238,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex)
?.let { 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.net.Uri
import android.os.Build
import android.speech.tts.TextToSpeech
import android.view.LayoutInflater
import android.view.Menu
import android.view.ViewGroup
@ -15,13 +16,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.core.view.isVisible
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
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.sendToClip
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.share
import org.jetbrains.anko.toast
import java.util.*
@SuppressLint("RestrictedApi")
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 {
@SuppressLint("InflateParams")
@ -44,33 +50,38 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
isFocusable = false
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) {
val adapter = Adapter(context)
recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
recycler_view.adapter = adapter
val menu = MenuBuilder(context)
recycler_view_more.adapter = adapter
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val popupMenu = PopupMenu(context, iv_menu_more)
onInitializeMenu(popupMenu.menu)
if (popupMenu.menu.size > 0) {
iv_menu_more.visible()
popupMenu.setOnMenuItemClickListener { item ->
item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
this@TextActionMenu.dismiss()
true
onInitializeMenu(moreMenu)
}
if (moreMenu.size() > 0) {
iv_menu_more.visible()
}
iv_menu_more.onClick {
popupMenu.show()
if (recycler_view.isVisible) {
iv_menu_more.setImageResource(R.drawable.ic_arrow_back)
adapter.setItems(moreMenu.visibleItems)
recycler_view.gone()
recycler_view_more.visible()
} else {
iv_menu_more.setImageResource(R.drawable.ic_more_vert)
recycler_view_more.gone()
adapter.setItems(menu.visibleItems)
recycler_view.visible()
}
}
}
@ -104,6 +115,13 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
when (item.itemId) {
R.id.menu_copy -> context.sendToClip(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 -> {
try {
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")
}
}
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)
@ -151,9 +201,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
*/
@RequiresApi(Build.VERSION_CODES.M)
private fun onInitializeMenu(menu: Menu) {
// Start with a menu Item order value that is high enough
// so that your "PROCESS_TEXT" menu items appear after the
// standard selection menu items like Cut, Copy, Paste.
try {
var menuItemOrder = 100
for (resolveInfo in getSupportedActivities()) {
menu.add(
@ -161,6 +209,9 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
menuItemOrder++, resolveInfo.loadLabel(context.packageManager)
).intent = createProcessTextIntentForResolveInfo(resolveInfo)
}
} catch (e: Exception) {
context.toast("获取文字操作菜单出错:${e.localizedMessage}")
}
}
interface CallBack {

@ -94,6 +94,7 @@ class MoreConfigDialog : DialogFragment() {
key: String?
) {
when (key) {
PreferKey.readBodyToLh -> activity?.recreate()
PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
postEvent(EventBus.UP_CONFIG, true)
@ -104,6 +105,11 @@ class MoreConfigDialog : DialogFragment() {
}
PreferKey.keepLight -> postEvent(key, true)
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
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
@ -47,8 +48,8 @@ class PaddingConfigDialog : DialogFragment() {
initView()
}
override fun onDestroy() {
super.onDestroy()
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
ReadBookConfig.save()
}
@ -75,7 +76,8 @@ class PaddingConfigDialog : DialogFragment() {
dsb_footer_padding_bottom.progress = footerPaddingBottom
dsb_footer_padding_left.progress = footerPaddingLeft
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) {
@ -130,7 +132,13 @@ class PaddingConfigDialog : DialogFragment() {
footerPaddingRight = it
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) {
showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true)

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

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

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

@ -28,6 +28,8 @@ object ChapterProvider {
var visibleBottom = 0
private var lineSpacingExtra = 0
private var paragraphSpacing = 0
private var titleTopSpacing = 0
private var titleBottomSpacing = 0
var typeface: Typeface = Typeface.SANS_SERIF
var titlePaint = TextPaint()
var contentPaint = TextPaint()
@ -73,6 +75,7 @@ object ChapterProvider {
item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize
item.title = bookChapter.title
item.upLinesPosition()
}
return TextChapter(
bookChapter.index,
@ -97,7 +100,7 @@ object ChapterProvider {
stringBuilder: StringBuilder,
isTitle: Boolean
): Float {
var durY = y
var durY = if (isTitle) y + titleTopSpacing else y
val textPaint = if (isTitle) titlePaint else contentPaint
val layout = StaticLayout(
text, textPaint, visibleWidth,
@ -142,6 +145,7 @@ object ChapterProvider {
durY += textPaint.textHeight * lineSpacingExtra / 10f
textPages.last().height = durY
}
if (isTitle) durY += titleBottomSpacing
durY += textPaint.textHeight * paragraphSpacing / 10f
return durY
}
@ -262,18 +266,19 @@ object ChapterProvider {
titlePaint.color = ReadBookConfig.durConfig.textColor()
titlePaint.letterSpacing = ReadBookConfig.letterSpacing
titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD)
titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat()
//正文
contentPaint.isAntiAlias = true
contentPaint.color = ReadBookConfig.durConfig.textColor()
contentPaint.letterSpacing = ReadBookConfig.letterSpacing
val bold = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, bold)
val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL
contentPaint.typeface = Typeface.create(typeface, style)
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra
paragraphSpacing = ReadBookConfig.paragraphSpacing
titlePaint.textSize = (ReadBookConfig.textSize + 2).sp.toFloat()
contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat()
titleTopSpacing = ReadBookConfig.titleTopSpacing.dp
titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp
upSize()
}
@ -290,5 +295,5 @@ object ChapterProvider {
}
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 selectEnd = arrayOf(0, 0, 0)
private var textPage: TextPage = TextPage()
//滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory
private val maxScrollOffset = 100f
@ -210,7 +211,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 0
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(0, lineIndex, charIndex)
return
@ -236,7 +241,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 1
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(1, lineIndex, charIndex)
return
@ -259,7 +268,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = 2
selectEnd[1] = lineIndex
selectEnd[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
select(2, lineIndex, charIndex)
return
@ -287,7 +300,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 0
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars()
}
return
@ -311,7 +328,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 1
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars()
}
return
@ -333,7 +354,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = 2
selectStart[1] = lineIndex
selectStart[2] = charIndex
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset)
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset,
textLine.lineTop + relativeOffset
)
upSelectChars()
}
return
@ -427,7 +452,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex]
val textChar = textLine.textChars[charIndex]
upSelectedStart(textChar.start, textLine.lineBottom + relativeOffset(relativePage))
upSelectedStart(
textChar.start,
textLine.lineBottom + relativeOffset(relativePage),
textLine.lineTop
)
upSelectChars()
}
@ -475,12 +504,12 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
invalidate()
}
private fun upSelectedStart(x: Float, y: Float) {
callBack.upSelectedStart(x, y + callBack.headerHeight)
private fun upSelectedStart(x: Float, y: Float, top: Float) = callBack.apply {
upSelectedStart(x, y + headerHeight, top + headerHeight)
}
private fun upSelectedEnd(x: Float, y: Float) {
callBack.upSelectedEnd(x, y + callBack.headerHeight)
private fun upSelectedEnd(x: Float, y: Float) = callBack.apply {
upSelectedEnd(x, y + headerHeight)
}
fun cancelSelect() {
@ -580,7 +609,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
}
interface CallBack {
fun upSelectedStart(x: Float, y: Float)
fun upSelectedStart(x: Float, y: Float, top: Float)
fun upSelectedEnd(x: Float, y: Float)
fun onCancelSelect()
val headerHeight: Int

@ -7,7 +7,7 @@ import android.view.MotionEvent
import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
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.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextPage
@ -18,6 +18,8 @@ import java.util.*
class ContentView(context: Context) : FrameLayout(context) {
private var battery = 100
init {
//设置背景防止切换背景时文字重叠
setBackgroundColor(context.getCompatColor(R.color.background))
@ -26,8 +28,7 @@ class ContentView(context: Context) : FrameLayout(context) {
upStyle()
upTime()
content_text_view.upView = {
tv_bottom_left.text = it.title
setPageIndex(it.index, it.pageSize)
setProgress(it)
}
}
@ -37,11 +38,9 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.typeface = ChapterProvider.typeface
tv_bottom_left.typeface = ChapterProvider.typeface
tv_bottom_right.typeface = ChapterProvider.typeface
battery_view.typeface = ChapterProvider.typeface
//显示状态栏时隐藏header
if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams.apply {
height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding(
headerPaddingLeft.dp,
headerPaddingTop.dp,
@ -60,6 +59,7 @@ class ContentView(context: Context) : FrameLayout(context) {
footerPaddingRight.dp,
footerPaddingBottom.dp
)
vw_top_divider.visible(showHeaderLine)
vw_bottom_divider.visible(showFooterLine)
content_text_view.upVisibleRect()
durConfig.textColor().let {
@ -67,6 +67,14 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_top_right.setTextColor(it)
tv_bottom_left.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() {
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) {
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) {
tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset()
fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
setProgress(textPage)
if (resetPageOffset)
resetPageOffset()
content_text_view.setContent(textPage)
}
@ -108,9 +117,19 @@ class ContentView(context: Context) : FrameLayout(context) {
}
@SuppressLint("SetTextI18n")
fun setPageIndex(pageIndex: Int?, pageSize: Int) {
pageIndex?.let {
tv_bottom_right.text = "${pageIndex.plus(1)}/${pageSize}"
fun setProgress(textPage: TextPage) = textPage.apply {
val title = when (AppConfig.chineseConverterType) {
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 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),
DataSource {
var callBack: CallBack
var pageFactory: TextPageFactory
val callBack: CallBack get() = activity as CallBack
var pageFactory: TextPageFactory = TextPageFactory(this)
var pageDelegate: PageDelegate? = null
var prevPage: ContentView
var curPage: ContentView
var nextPage: ContentView
var prevPage: ContentView = ContentView(context)
var curPage: ContentView = ContentView(context)
var nextPage: ContentView = ContentView(context)
init {
callBack = activity as CallBack
nextPage = ContentView(context)
addView(nextPage)
curPage = ContentView(context)
addView(curPage)
prevPage = ContentView(context)
addView(prevPage)
upBg()
setWillNotDraw(false)
pageFactory = TextPageFactory(this)
upPageAnim()
}
@ -43,7 +38,7 @@ class PageView(context: Context, attrs: AttributeSet) :
prevPage.x = -w.toFloat()
pageDelegate?.setViewSize(w, h)
if (oldw != 0 && oldh != 0) {
ReadBook.loadContent()
ReadBook.loadContent(resetPageOffset = false)
}
}
@ -98,9 +93,9 @@ class PageView(context: Context, attrs: AttributeSet) :
upContent()
}
override fun upContent(relativePosition: Int) {
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
if (ReadBookConfig.isScroll) {
curPage.setContent(pageFactory.currentPage)
curPage.setContent(pageFactory.currentPage, resetPageOffset)
} else {
when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage)
@ -170,5 +165,6 @@ class PageView(context: Context, attrs: AttributeSet) :
val isInitFinish: Boolean
fun clickCenter()
fun screenOffTimerStart()
fun showTextActionMenu()
}
}

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

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

@ -1,19 +1,14 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import io.legado.app.ui.book.read.page.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) {
pageView.fillPage(mDirection)
}
stopScroll()
}
}

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

@ -12,10 +12,7 @@ data class TextChapter(
val chaptersSize: Int
) {
fun page(index: Int): TextPage? {
if (index >= 0 && index < pages.size) {
return pages[index]
}
return null
return pages.getOrNull(index)
}
fun lastPage(): TextPage? {
@ -25,20 +22,6 @@ data class TextChapter(
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 {
return pages.size - 1
}
@ -70,28 +53,6 @@ data class TextChapter(
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 {
val stringBuilder = StringBuilder()
pages.forEach {

@ -5,6 +5,7 @@ import android.text.StaticLayout
import io.legado.app.App
import io.legado.app.R
import io.legado.app.ui.book.read.page.ChapterProvider
import java.text.DecimalFormat
data class TextPage(
var index: Int = 0,
@ -17,6 +18,21 @@ data class TextPage(
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")
fun format(): TextPage {
if (textLines.isEmpty() && ChapterProvider.visibleWidth > 0) {
@ -28,12 +44,11 @@ data class TextPage(
if (y < 0) y = 0f
for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine()
textLine.lineTop = (ChapterProvider.paddingTop + y -
(layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex)))
textLine.lineBase = (ChapterProvider.paddingTop + y -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex)))
textLine.lineTop = ChapterProvider.paddingTop + y + layout.getLineTop(lineIndex)
textLine.lineBase =
ChapterProvider.paddingTop + y + layout.getLineBaseline(lineIndex)
textLine.lineBottom =
textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent
ChapterProvider.paddingTop + y + layout.getLineBottom(lineIndex)
var x = ChapterProvider.paddingLeft +
(ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2
textLine.text =
@ -84,4 +99,21 @@ data class TextPage(
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? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
val payload = Bundle()
val newItem = newItems[newItemPosition]
val oldItem = oldItems[oldItemPosition]
if (oldItem.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) {
payload.putString("intro", newItem.intro)
}
if (payload.isEmpty) {
return null
}
return payload
}
}

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

@ -7,82 +7,62 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.model.SearchBookModel
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
class SearchViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
class SearchViewModel(application: Application) : BaseViewModel(application)
, SearchBookModel.CallBack {
private val searchBookModel = SearchBookModel(this, this)
var isSearchLiveData = MutableLiveData<Boolean>()
var searchBookLiveData = MutableLiveData<List<SearchBook>>()
var searchKey: String = ""
var searchPage = 1
var isLoading = false
var searchBooks = arrayListOf<SearchBook>()
private var searchID = 0L
/**
* 开始搜索
*/
fun search(key: String) {
task?.cancel()
if (key.isEmpty() && searchKey.isEmpty()) {
return
} else if (key.isEmpty()) {
isLoading = true
searchPage++
} else if (key.isNotEmpty()) {
isLoading = true
searchPage = 1
searchKey = key
if (searchKey != key && key.isNotEmpty()) {
searchBookModel.cancelSearch()
searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
}
searchBookModel.search(searchID, searchKey)
}
override fun onSearchStart() {
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 ->
}
override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
if (context.getPrefBoolean(PreferKey.precisionSearch)) {
precisionSearch(list)
precisionSearch(this, searchBooks)
} else {
App.db.searchBookDao().insert(*list.toTypedArray())
mergeItems(list)
}
}
}
App.db.searchBookDao().insert(*searchBooks.toTypedArray())
mergeItems(this, searchBooks)
}
}
task?.invokeOnCompletion {
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>()
searchBooks.forEach { searchBook ->
if (searchBook.name.contains(searchKey, true)
@ -90,14 +70,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
) books.add(searchBook)
}
App.db.searchBookDao().insert(*books.toTypedArray())
mergeItems(books)
if (scope.isActive) {
mergeItems(scope, books)
}
}
/**
* 合并搜索结果并排序
*/
@Synchronized
private fun mergeItems(newDataS: List<SearchBook>) {
private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>) {
if (newDataS.isNotEmpty()) {
val copyDataS = ArrayList(searchBooks)
val searchBooksAdd = ArrayList<SearchBook>()
@ -141,6 +123,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
}
}
}
if (!scope.isActive) return
searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) {
1
@ -166,6 +149,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
0
}
})
if (!scope.isActive) return
searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS)
}
@ -175,7 +159,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
* 停止搜索
*/
fun stop() {
task?.cancel()
searchBookModel.cancelSearch()
}
/**
@ -211,7 +195,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
override fun 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.toast
import java.io.File
import java.text.Collator
class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity_book_source),
PopupMenu.OnMenuItemClickListener,
@ -84,7 +85,12 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage")
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()
}
if (item.groupId == R.id.source_group) {
@ -98,10 +104,16 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
if (url.startsWith("http", false)){
viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else{
viewModel.importSourceFromFilePath(url){msg ->
title_bar.snackbar(msg)}
}
}
else -> {
toast("格式不对")
}
@ -197,7 +209,8 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
private fun upGroupMenu() {
groupMenu?.removeGroup(R.id.source_group)
groups.map {
groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE))
.map {
groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it)
}
}

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

Loading…
Cancel
Save