Merge pull request #4 from gedoor/master

update
pull/198/head^2
52fisher 5 years ago committed by GitHub
commit dca078e2c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      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. 6
      app/src/google/res/values-zh-rHK/strings.xml
  6. 6
      app/src/google/res/values/strings.xml
  7. 80
      app/src/main/AndroidManifest.xml
  8. 129
      app/src/main/assets/help.md
  9. 125
      app/src/main/assets/updateLog.md
  10. 176
      app/src/main/assets/web/book.css
  11. 39
      app/src/main/assets/web/book.html
  12. 203
      app/src/main/assets/web/book.js
  13. 71
      app/src/main/assets/web/bookshelf.html
  14. 1
      app/src/main/assets/web/css/about.f23c15cb.css
  15. 1
      app/src/main/assets/web/css/app.e1c0d2e4.css
  16. 1
      app/src/main/assets/web/css/chunk-vendors.ad4ff18f.css
  17. 1
      app/src/main/assets/web/css/detail.42c41bd6.css
  18. BIN
      app/src/main/assets/web/fonts/element-icons.535877f5.woff
  19. BIN
      app/src/main/assets/web/fonts/element-icons.732389de.ttf
  20. BIN
      app/src/main/assets/web/fonts/iconfont.f9a3fb0e.woff
  21. BIN
      app/src/main/assets/web/fonts/popfont.f39ecc1a.ttf
  22. BIN
      app/src/main/assets/web/fonts/shelffont.6c094b6d.ttf
  23. BIN
      app/src/main/assets/web/img/icons/android-chrome-192x192.png
  24. BIN
      app/src/main/assets/web/img/icons/android-chrome-512x512.png
  25. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon-120x120.png
  26. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon-152x152.png
  27. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon-180x180.png
  28. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon-60x60.png
  29. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon-76x76.png
  30. BIN
      app/src/main/assets/web/img/icons/apple-touch-icon.png
  31. BIN
      app/src/main/assets/web/img/icons/favicon-16x16.png
  32. BIN
      app/src/main/assets/web/img/icons/favicon-32x32.png
  33. BIN
      app/src/main/assets/web/img/icons/msapplication-icon-144x144.png
  34. BIN
      app/src/main/assets/web/img/icons/mstile-150x150.png
  35. 149
      app/src/main/assets/web/img/icons/safari-pinned-tab.svg
  36. BIN
      app/src/main/assets/web/img/noCover.b5c48bc1.jpeg
  37. 3
      app/src/main/assets/web/index.html
  38. 1
      app/src/main/assets/web/js/about.2589b5fe.js
  39. 1
      app/src/main/assets/web/js/about~detail.08c372e6.js
  40. 1
      app/src/main/assets/web/js/app.b25f3cec.js
  41. 33
      app/src/main/assets/web/js/chunk-vendors.b3838a2d.js
  42. 1
      app/src/main/assets/web/js/detail.043d6e39.js
  43. 1
      app/src/main/assets/web/manifest.json
  44. 2
      app/src/main/java/io/legado/app/App.kt
  45. 5
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  46. 1
      app/src/main/java/io/legado/app/constant/AppConst.kt
  47. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  48. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  49. 2
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  50. 8
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  51. 23
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  52. 5
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  53. 11
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  54. 10
      app/src/main/java/io/legado/app/data/entities/Book.kt
  55. 2
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  56. 2
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  57. 13
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  58. 16
      app/src/main/java/io/legado/app/data/entities/RssStar.kt
  59. 29
      app/src/main/java/io/legado/app/help/AppConfig.kt
  60. 218
      app/src/main/java/io/legado/app/help/BookHelp.kt
  61. 7
      app/src/main/java/io/legado/app/help/ItemTouchCallback.kt
  62. 10
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  63. 179
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  64. 74
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  65. 46
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  66. 1
      app/src/main/java/io/legado/app/help/http/AjaxWebView.kt
  67. 18
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  68. 14
      app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt
  69. 2
      app/src/main/java/io/legado/app/help/permission/Request.kt
  70. 106
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  71. 1
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  72. 2
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  73. 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  74. 3
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  75. 22
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  76. 59
      app/src/main/java/io/legado/app/model/Debug.kt
  77. 16
      app/src/main/java/io/legado/app/model/Rss.kt
  78. 93
      app/src/main/java/io/legado/app/model/SearchBookModel.kt
  79. 47
      app/src/main/java/io/legado/app/model/WebBook.kt
  80. 7
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  81. 207
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  82. 58
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  83. 3
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  84. 11
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  85. 1
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  86. 9
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  87. 32
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  88. 19
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  89. 4
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  90. 10
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  91. 76
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  92. 39
      app/src/main/java/io/legado/app/service/DownloadService.kt
  93. 23
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  94. 82
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  95. 2
      app/src/main/java/io/legado/app/ui/README.md
  96. 1
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  97. 5
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  98. 7
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  99. 39
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  100. 60
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders"
apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'io.fabric'
androidExtensions {
@ -19,6 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android {
compileSdkVersion 29
flavorDimensions ("version")
signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
@ -73,7 +74,17 @@ android {
}
}
}
productFlavors {
app {
dimension "version"
manifestPlaceholders = [APP_CHANNEL_VALUE: "app"]
}
google {
dimension "version"
applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
}
}
compileOptions {
// Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true
@ -99,27 +110,27 @@ 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
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.2.3'
implementation 'com.google.firebase:firebase-core:17.3.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.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 +138,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,19 +159,19 @@ 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'
@ -173,14 +184,14 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0'
//apache
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-lang3:3.10'
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'
implementation 'com.hankcs:hanlp:portable-1.7.7'
}

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

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">阅读Pro</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,14 +224,18 @@
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
@ -192,28 +243,36 @@
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:name=".ui.rss.article.RssSortActivity"
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">
@ -228,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" />
@ -251,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怎么办?
* 清除软件数据试试看,不行再进行反馈。

@ -1,6 +1,120 @@
## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读软件]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 新公众号[开源阅读]()已启用,[开源阅读软件]()备用
**2020/04/20**
* 优化阅读界面信息显示
**2020/04/19**
* 添加阅读界面各种信息设置
**2020/04/18**
* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, hingbong
* 修复更新时间不对的bug
**2020/04/13**
* 去除rss朗读时的引号
**2020/04/13**
* 修复调用webView返回结果多了引号的bug
**2020/04/12**
* 解决无法取消加粗的bug
* 修复换源自动加入书架的bug
**2020/04/09**
* 修复书架刷新闪烁
**2020/04/08**
* 可以隐藏书架未分组
**2020/04/07**
* 书架添加未分组,有未分组书籍时自动显示
* 其它一些优化
**2020/04/04**
* 优化备份逻辑
* 修复订阅分类太多显示不全的bug
* 修复一些分类要手动刷新的问题
**2020/04/02**
* 书架书名和作者作为唯一值
* 添加订阅分类,分类规则和发现一样,分类一::url1 && 分类2::url2
**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**
* 底部文字对齐
@ -216,14 +330,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**
* 添加下载界面

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

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>阅读3.0书架</title>
<link rel="icon" href="favicon.ico">
<link href="book.css" rel="stylesheet" />
</head>
<body>
<button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button>
<div class="nav">
<button id="back">返回</button>
<button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value="" />
<button id="refresh">重新加载</button>
</div>
<div class="allcontent" id="allcontent">
<div id="books" class="books"></div>
<div id="more" class="more">
<div id="info" class="info"></div>
<div class="clear"></div>
<div id="chapter" class="chapter"></div>
<div id="content" class="content"></div>
<div id="page" class="button">
<center><button id='up'>上一章</button><button id='down'>下一章</button></center>
</div>
</div>
</div>
<script src="book.js"></script>
</body>
</html>

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

@ -1,39 +1,46 @@
<!DOCTYPE html>
<html>
<!DOCTYPE html>
<html lang=en style="padding: 0;height:100%">
<head>
<meta charset="utf-8" />
<title>阅读3.0书架</title>
<link rel="icon" href="favicon.ico">
<link href="bookshelf.css" rel="stylesheet" />
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<!--[if IE]><link rel="icon" href="favicon.ico" /><![endif]-->
<title>yd-web-tool</title>
<link href=css/about.f23c15cb.css rel=prefetch>
<link href=css/detail.42c41bd6.css rel=prefetch>
<link href=js/about.2589b5fe.js rel=prefetch>
<link href=js/about~detail.08c372e6.js rel=prefetch>
<link href=js/detail.043d6e39.js rel=prefetch>
<link href=css/app.e1c0d2e4.css rel=preload as=style>
<link href=css/chunk-vendors.ad4ff18f.css rel=preload as=style>
<link href=js/app.b25f3cec.js rel=preload as=script>
<link href=js/chunk-vendors.b3838a2d.js rel=preload as=script>
<link href=css/chunk-vendors.ad4ff18f.css rel=stylesheet>
<link href=css/app.e1c0d2e4.css rel=stylesheet>
<link rel=icon type=image/png sizes=32x32 href=img/icons/favicon-32x32.png>
<link rel=icon type=image/png sizes=16x16 href=img/icons/favicon-16x16.png>
<link rel=manifest href=manifest.json>
<meta name=theme-color content=#4DBA87>
<meta name=apple-mobile-web-app-capable content=no>
<meta name=apple-mobile-web-app-status-bar-style content=default>
<meta name=apple-mobile-web-app-title content=yd-web-tool>
<link rel=apple-touch-icon href=img/icons/apple-touch-icon-152x152.png>
<link rel=mask-icon href=img/icons/safari-pinned-tab.svg color=#4DBA87>
<meta name=msapplication-TileImage content=img/icons/msapplication-icon-144x144.png>
<meta name=msapplication-TileColor content=#000000>
</head>
<style>
body::-webkit-scrollbar {
display: none;
}
</style>
<body>
<button id="top" class="top"></button>
<button id="showchapter" class="showchapter"></button>
<button id="hidebooks" class="hidebooks"></button>
<div class="nav">
<button id="back">返回</button>
<button id="type">所有书籍 ▼</button>
<button id="sort">手动排序 ▼</button>
<button id="setting">阅读设置</button>
<input type="text" class="address" id="address" title="阅读APP地址或IP" value="" />
<button id="refresh">重新加载</button>
</div>
<div class="allcontent" id="allcontent">
<div id="books" class="books"></div>
<div id="more" class="more">
<div id="info" class="info"></div>
<div class="clear"></div>
<div id="chapter" class="chapter"></div>
<div id="content" class="content"></div>
<div id="page" class="button">
<center><button id='up'>上一章</button><button id='down'>下一章</button></center>
</div>
</div>
</div>
<script src="bookshelf.js"></script>
<body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without
JavaScript enabled. Please enable it to continue.</strong></noscript>
<div id=app></div>
<script src=js/chunk-vendors.b3838a2d.js></script>
<script src=js/app.b25f3cec.js></script>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;margin:0;height:100%}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,149 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
fill="#000000" stroke="none">
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
-9615 0 20 -32z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -359,7 +359,8 @@
<br>(?i) 前缀表示忽略大小写
</div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a>
<a target="_blank" href="bookshelf.html">阅读书架(测试)</a>
<a target="_blank" href="book.html">阅读书架(经典)</a>
<a target="_blank" href="bookshelf.html">阅读书架(新潮)</a>
</div>
</div>
</div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
{"name":"yd-web-tool","short_name":"yd-web-tool","theme_color":"#4DBA87","icons":[{"src":"./img/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"./img/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"},{"src":"./img/icons/android-chrome-maskable-192x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},{"src":"./img/icons/android-chrome-maskable-512x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"}],"start_url":".","display":"standalone","background_color":"#000000"}

@ -73,7 +73,7 @@ class App : Application() {
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800))
getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground(

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

@ -49,6 +49,7 @@ object AppConst {
val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
val bookGroupNone = BookGroup(-4, App.INSTANCE.getString(R.string.no_group))
const val notificationIdRead = 1144771
const val notificationIdAudio = 1144772

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

@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 8,
version = 10,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {

@ -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,16 +12,16 @@ 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")
@ -30,15 +30,27 @@ interface BookDao {
@Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>>
@Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroupSize(): LiveData<Int>
@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>
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
fun getBook(bookUrl: String): Book?
@Query("SELECT * FROM books WHERE name = :name and author = :author")
fun getBook(name: String, author: String): Book?
@get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
val noGroupSize: Int
@get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0")
val webBooks: List<Book>
@ -57,6 +69,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?

@ -12,17 +12,18 @@ interface RssArticleDao {
fun get(origin: String, link: String): RssArticle?
@Query(
"""select t1.link, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
from rssArticles as t1 left join rssReadRecords as t2
on t1.link = t2.record where origin = :origin order by `order` desc"""
on t1.link = t2.record where origin = :origin and sort = :sort
order by `order` desc"""
)
fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
fun liveByOriginSort(origin: String, sort: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssArticle: RssArticle)
@Query("delete from rssArticles where origin = :origin and `order` < :order")
fun clearOld(origin: String, order: Long)
@Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order")
fun clearOld(origin: String, sort: String, order: Long)
@Update
fun update(vararg rssArticle: RssArticle)

@ -4,7 +4,6 @@ import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.utils.GSON
@ -15,9 +14,12 @@ import java.nio.charset.Charset
import kotlin.math.max
@Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
@Entity(
tableName = "books",
primaryKeys = ["name", "author"],
indices = [(Index(value = ["bookUrl"], unique = true))]
)
data class Book(
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
@ -31,7 +33,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(), // 最新章节标题更新时间

@ -169,7 +169,7 @@ data class BookSource(
}
}
val b = a.split("(&&|\n)+".toRegex())
b.map { c ->
b.forEach { c ->
val d = c.split("::")
if (d.size > 1)
exploreKinds.add(ExploreKind(d[0], d[1]))

@ -9,6 +9,7 @@ import androidx.room.Entity
)
data class RssArticle(
var origin: String = "",
var sort: String = "",
var title: String = "",
var order: Long = 0,
var link: String = "",
@ -36,6 +37,7 @@ data class RssArticle(
fun toStar(): RssStar {
return RssStar(
origin = origin,
sort = sort,
title = title,
starTime = System.currentTimeMillis(),
link = link,

@ -23,6 +23,7 @@ data class RssSource(
var sourceIcon: String = "",
var sourceGroup: String? = null,
var enabled: Boolean = true,
var sortUrl: String? = null,
//列表规则
var ruleArticles: String? = null,
var ruleNextPage: String? = null,
@ -99,4 +100,16 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
}
fun sortUrls(): LinkedHashMap<String, String> {
val sortMap = linkedMapOf<String, String>()
sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c ->
val d = c.split("::")
if (d.size > 1)
sortMap[d[0]] = d[1]
}
if (sortMap.isEmpty()) {
sortMap[""] = sourceUrl
}
return sortMap
}
}

@ -9,6 +9,7 @@ import androidx.room.Entity
)
data class RssStar(
var origin: String = "",
var sort: String = "",
var title: String = "",
var starTime: Long = 0,
var link: String = "",
@ -16,4 +17,17 @@ data class RssStar(
var description: String? = null,
var content: String? = null,
var image: String? = null
)
) {
fun toRssArticle(): RssArticle {
return RssArticle(
origin = origin,
sort = sort,
title = title,
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image
)
}
}

@ -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)
@ -111,9 +115,32 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
var bookGroupNoneShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupNone", 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 com.hankcs.hanlp.HanLP
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,137 +37,73 @@ object BookHelp {
}
fun clearCache() {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
} else {
FileUtils.deleteFile(
FileUtils.getPath(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName)
)
FileUtils.deleteFile(
FileUtils.getPath(
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),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
}
FileUtils.createFileIfNotExist(
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),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.let {
fileNameList.addAll(it)
}
FileUtils.createFolderIfNotExist(
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),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
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() -> {
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 -> {
val file = FileUtils.getFile(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
}
if (book.isLocalBook()) {
return AnalyzeTxtFile.getContent(book, bookChapter)
} else {
val file = FileUtils.getFile(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
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 -> {
FileUtils.createFileIfNotExist(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
if (book.isLocalBook()) {
return
} else {
FileUtils.createFileIfNotExist(
downloadDir,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
}
@ -242,24 +175,16 @@ 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 {
val o = bookOrigin
bookName?.let {
replaceRules = if (o.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(it)
} else {
App.db.replaceRuleDao().findEnabledByScope(it, o)
suspend fun upReplaceRules() {
withContext(IO) {
synchronized(this) {
val o = bookOrigin
bookName?.let {
replaceRules = if (o.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(it)
} else {
App.db.replaceRuleDao().findEnabledByScope(it, o)
}
}
}
}
@ -274,7 +199,17 @@ object BookHelp {
): String {
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()) {
@ -296,12 +231,17 @@ object BookHelp {
if (!c.substringBefore("\n").contains(title)) {
c = "$title\n$c"
}
when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(c)
2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c)
try {
when (AppConfig.chineseConverterType) {
1 -> c = HanLP.convertToSimplifiedChinese(c)
2 -> c = HanLP.convertToTraditionalChinese(c)
}
} catch (e: Exception) {
withContext(Main) {
App.INSTANCE.toast("简繁转换出错")
}
}
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) {
}

@ -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?) {

@ -27,7 +27,6 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!!
}
val durConfig get() = getConfig(styleSelect)
private val shareConfig get() = getConfig(5)
var bg: Drawable? = null
var bgMeanColor: Int = 0
@ -139,129 +138,145 @@ object ReadBookConfig {
var bodyIndent = " ".repeat(bodyIndentCount)
var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean
get() = if (shareLayout) shareConfig.textBold else durConfig.textBold
set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value
get() = config.textBold
set(value) {
config.textBold = value
}
var textSize: Int
get() = if (shareLayout) shareConfig.textSize else durConfig.textSize
set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value
get() = config.textSize
set(value) {
config.textSize = value
}
var letterSpacing: Float
get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing
set(value) =
if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value
get() = config.letterSpacing
set(value) {
config.letterSpacing = value
}
var lineSpacingExtra: Int
get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra
set(value) =
if (shareLayout) shareConfig.lineSpacingExtra = value
else durConfig.lineSpacingExtra = value
get() = config.lineSpacingExtra
set(value) {
config.lineSpacingExtra = value
}
var paragraphSpacing: Int
get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing
set(value) =
if (shareLayout) shareConfig.paragraphSpacing = value
else durConfig.paragraphSpacing = value
get() = config.paragraphSpacing
set(value) {
config.paragraphSpacing = value
}
var titleMode: Int
get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode
set(value) =
if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value
get() = config.titleMode
set(value) {
config.titleMode = value
}
var titleSize: Int
get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize
set(value) =
if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value
get() = config.titleSize
set(value) {
config.titleSize = value
}
var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing
set(value) =
if (shareLayout) shareConfig.titleTopSpacing = value
else durConfig.titleTopSpacing = value
get() = config.titleTopSpacing
set(value) {
config.titleTopSpacing = value
}
var titleBottomSpacing: Int
get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing
set(value) =
if (shareLayout) shareConfig.titleBottomSpacing = value
else durConfig.titleBottomSpacing = value
get() = config.titleBottomSpacing
set(value) {
config.titleBottomSpacing = value
}
var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom
set(value) =
if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value
get() = config.paddingBottom
set(value) {
config.paddingBottom = value
}
var paddingLeft: Int
get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft
set(value) =
if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value
get() = config.paddingLeft
set(value) {
config.paddingLeft = value
}
var paddingRight: Int
get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight
set(value) =
if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value
get() = config.paddingRight
set(value) {
config.paddingRight = value
}
var paddingTop: Int
get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop
set(value) =
if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value
get() = config.paddingTop
set(value) {
config.paddingTop = value
}
var headerPaddingBottom: Int
get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom
set(value) =
if (shareLayout) shareConfig.headerPaddingBottom = value
else durConfig.headerPaddingBottom = value
get() = config.headerPaddingBottom
set(value) {
config.headerPaddingBottom = value
}
var headerPaddingLeft: Int
get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft
set(value) =
if (shareLayout) shareConfig.headerPaddingLeft = value
else durConfig.headerPaddingLeft = value
get() = config.headerPaddingLeft
set(value) {
config.headerPaddingLeft = value
}
var headerPaddingRight: Int
get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight
set(value) =
if (shareLayout) shareConfig.headerPaddingRight = value
else durConfig.headerPaddingRight = value
get() = config.headerPaddingRight
set(value) {
config.headerPaddingRight = value
}
var headerPaddingTop: Int
get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop
set(value) =
if (shareLayout) shareConfig.headerPaddingTop = value
else durConfig.headerPaddingTop = value
get() = config.headerPaddingTop
set(value) {
config.headerPaddingTop = value
}
var footerPaddingBottom: Int
get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom
set(value) =
if (shareLayout) shareConfig.footerPaddingBottom = value
else durConfig.footerPaddingBottom = value
get() = config.footerPaddingBottom
set(value) {
config.footerPaddingBottom = value
}
var footerPaddingLeft: Int
get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft
set(value) =
if (shareLayout) shareConfig.footerPaddingLeft = value
else durConfig.footerPaddingLeft = value
get() = config.footerPaddingLeft
set(value) {
config.footerPaddingLeft = value
}
var footerPaddingRight: Int
get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight
set(value) =
if (shareLayout) shareConfig.footerPaddingRight = value
else durConfig.footerPaddingRight = value
get() = config.footerPaddingRight
set(value) {
config.footerPaddingRight = value
}
var footerPaddingTop: Int
get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop
set(value) =
if (shareLayout) shareConfig.footerPaddingTop = value
else durConfig.footerPaddingTop = value
get() = config.footerPaddingTop
set(value) {
config.footerPaddingTop = value
}
var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine
set(value) =
if (shareLayout) shareConfig.showHeaderLine = value
else durConfig.showHeaderLine = value
get() = config.showHeaderLine
set(value) {
config.showHeaderLine = value
}
var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine
set(value) =
if (shareLayout) shareConfig.showFooterLine = value
else durConfig.showFooterLine = value
get() = config.showFooterLine
set(value) {
config.showFooterLine = value
}
@Keep
class Config(

@ -0,0 +1,74 @@
package io.legado.app.help
import io.legado.app.App
import io.legado.app.R
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefInt
object ReadTipConfig {
val tipArray: Array<String> = App.INSTANCE.resources.getStringArray(R.array.read_tip)
const val none = 0
const val chapterTitle = 1
const val time = 2
const val battery = 3
const val page = 4
const val totalProgress = 5
const val pageAndTotal = 6
val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] }
val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] }
val tipHeaderRightStr: String get() = tipArray.getOrElse(tipHeaderRight) { tipArray[none] }
val tipFooterLeftStr: String get() = tipArray.getOrElse(tipFooterLeft) { tipArray[none] }
val tipFooterMiddleStr: String get() = tipArray.getOrElse(tipFooterMiddle) { tipArray[none] }
val tipFooterRightStr: String get() = tipArray.getOrElse(tipFooterRight) { tipArray[none] }
var tipHeaderLeft: Int
get() = App.INSTANCE.getPrefInt("tipHeaderLeft", time)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderLeft", value)
}
var tipHeaderMiddle: Int
get() = App.INSTANCE.getPrefInt("tipHeaderMiddle", none)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderMiddle", value)
}
var tipHeaderRight: Int
get() = App.INSTANCE.getPrefInt("tipHeaderRight", battery)
set(value) {
App.INSTANCE.putPrefInt("tipHeaderRight", value)
}
var tipFooterLeft: Int
get() = App.INSTANCE.getPrefInt("tipFooterLeft", chapterTitle)
set(value) {
App.INSTANCE.putPrefInt("tipFooterLeft", value)
}
var tipFooterMiddle: Int
get() = App.INSTANCE.getPrefInt("tipFooterMiddle", none)
set(value) {
App.INSTANCE.putPrefInt("tipFooterMiddle", value)
}
var tipFooterRight: Int
get() = App.INSTANCE.getPrefInt("tipFooterRight", pageAndTotal)
set(value) {
App.INSTANCE.putPrefInt("tipFooterRight", value)
}
var hideHeader: Boolean
get() = App.INSTANCE.getPrefBoolean("hideHeader", true)
set(value) {
App.INSTANCE.putPrefBoolean("hideHeader", value)
}
var hideFooter: Boolean
get() = App.INSTANCE.getPrefBoolean("hideFooter", false)
set(value) {
App.INSTANCE.putPrefBoolean("hideFooter", value)
}
}

@ -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,21 +137,27 @@ class Coroutine<T>(
try {
start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, context, timeMillis ?: 0L, block)
success?.let { dispatchCallback(this, value, it) }
if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
val consume: Boolean = errorReturn?.value?.let { value ->
success?.let { dispatchCallback(this, value, it) }
if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
true
} ?: false
if (!consume) {
if (!consume && isActive) {
error?.let { dispatchCallback(this, e, it) }
}
} finally {
finally?.let { dispatchVoidCallback(this, it) }
if (isActive) {
finally?.let { dispatchVoidCallback(this, it) }
}
}
}
}
@ -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()

@ -180,6 +180,7 @@ class AjaxWebView {
mWebView.get()?.evaluateJavascript(mJavaScript) {
if (it.isNotEmpty() && it != "null") {
val content = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
handler.obtainMessage(MSG_SUCCESS, Res(url, content))
.sendToTarget()
handler.removeCallbacks(this)

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

@ -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,50 +31,50 @@ 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)
Coroutine.async {
backup(context, context.getPrefString(PreferKey.backupPath) ?: "", 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) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map {
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
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)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map {
when (val value = it.value) {
is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value)
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
}
}
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path), isAuto)
} else {
if (path.isEmpty()) {
copyBackup(context.getExternalFilesDir(null)!!, false)
} else {
copyBackup(File(path), isAuto)
}
}
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path))
} else {
copyBackup(File(path))
}
}
}
@ -95,27 +87,39 @@ 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()) {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
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())
}
}
}
}
}
@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

@ -19,7 +19,7 @@ object OldRule {
source = GSON.fromJsonObject<BookSource>(json.trim())
}
runCatching {
if (source == null || source?.searchUrl.isNullOrBlank()) {
if (source == null || source?.ruleToc.isNullOrBlank()) {
source = BookSource().apply {
val jsonItem = jsonPath.parse(json.trim())
bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: ""

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

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

@ -54,11 +54,10 @@ object Debug {
cancelDebug()
debugSource = rssSource.sourceUrl
log(debugSource, "︾开始解析")
Rss.getArticles(rssSource, null)
val sort = rssSource.sortUrls().entries.first()
Rss.getArticles(sort.key, sort.value, 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 +68,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 +81,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,14 +139,12 @@ object Debug {
log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1)
.onSuccess { exploreBooks ->
exploreBooks?.let {
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
.onError {
@ -164,14 +161,12 @@ object Debug {
log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1)
.onSuccess { searchBooks ->
searchBooks?.let {
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
}
.onError {
@ -197,16 +192,14 @@ object Debug {
private fun tocDebug(webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book)
.onSuccess { chapterList ->
chapterList?.let {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(webBook, book, it[0], nextChapterUrl)
} else {
log(debugSource, "︽目录列表为空", state = -1)
}
.onSuccess {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(webBook, book, it[0], nextChapterUrl)
} else {
log(debugSource, "︽目录列表为空", state = -1)
}
}
.onError {

@ -15,27 +15,35 @@ import kotlin.coroutines.CoroutineContext
object Rss {
fun getArticles(
sortName: String,
sortUrl: String,
rssSource: RssSource,
pageUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> {
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(pageUrl ?: rssSource.sourceUrl)
val analyzeUrl = AnalyzeUrl(
pageUrl ?: sortUrl,
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource)
RssParserByRule.parseXML(sortName, sortUrl, body, rssSource)
}
}
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,26 +27,36 @@ 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) {
bookSource.searchUrl?.let { searchUrl ->
val analyzeUrl = AnalyzeUrl(
ruleUrl = searchUrl,
key = key,
page = page,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
} ?: arrayListOf()
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,
key = key,
page = page,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
}
return arrayListOf()
}
/**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
)
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,

@ -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
}
@ -175,41 +201,18 @@ object AnalyzeTxtFile {
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
}
book.latestChapterTitle = toc.last().title
book.totalChapterNum = toc.size
System.gc()
System.runFinalization()
return toc
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
bookStream.seek(bookChapter.start ?: 0)
val extent = (bookChapter.end!! - bookChapter.start!!).toInt()
val content = ByteArray(extent)
bookStream.read(content, 0, extent)
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
tocRule?.let {
book.tocUrl = it.rule
}
return bookFile
return toc
}
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
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)
@ -218,30 +221,72 @@ object AnalyzeTxtFile {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
book.tocUrl = tocRule.rule
rulePattern = pattern
txtTocRule = tocRule
break
}
}
bookStream.seek(0)
return rulePattern
return txtTocRule
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
return getDefaultRules()
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))
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val content = ByteArray((bookChapter.end!! - bookChapter.start!!).toInt())
bookStream.seek(bookChapter.start!!)
bookStream.read(content)
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
}
return bookFile
}
return File(book.bookUrl)
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
return getDefaultRules()
}
return rules
}
fun getDefaultRules(): List<TxtTocRule> {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
}
}
return emptyList()
}
return emptyList()
}
}

@ -4,29 +4,45 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import 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 ->
val str = fileName.substringBeforeLast(".")
var name = str.substringBefore("作者")
val author = str.substringAfter("作者", "")
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(),
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
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(".")
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 = path,
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
}
fun deleteBook(book: Book, deleteOriginal: Boolean) {
@ -37,8 +53,12 @@ object LocalBook {
}
if (deleteOriginal) {
val uri = Uri.parse(book.bookUrl)
DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete()
if (book.bookUrl.isContentPath()) {
val uri = Uri.parse(book.bookUrl)
DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete()
} else {
FileUtils.deleteFile(book.bookUrl)
}
}
}
}

@ -11,7 +11,7 @@ import java.io.StringReader
object RssParser {
@Throws(XmlPullParserException::class, IOException::class)
fun parseXML(xml: String, sourceUrl: String): Result {
fun parseXML(sortName: String, xml: String, sourceUrl: String): Result {
val articleList = mutableListOf<RssArticle>()
var currentArticle = RssArticle()
@ -87,6 +87,7 @@ object RssParser {
// The item is correctly parsed
insideItem = false
currentArticle.origin = sourceUrl
currentArticle.sort = sortName
articleList.add(currentArticle)
currentArticle = RssArticle()
}

@ -13,7 +13,7 @@ import io.legado.app.utils.NetworkUtils
object RssParserByRule {
@Throws(Exception::class)
fun parseXML(body: String?, rssSource: RssSource): Result {
fun parseXML(sortName: String, sortUrl: String, body: String?, rssSource: RssSource): Result {
val sourceUrl = rssSource.sourceUrl
var nextUrl: String? = null
if (body.isNullOrBlank()) {
@ -28,11 +28,11 @@ object RssParserByRule {
var ruleArticles = rssSource.ruleArticles
if (ruleArticles.isNullOrBlank()) {
Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析")
return RssParser.parseXML(body, sourceUrl)
return RssParser.parseXML(sortName, body, sourceUrl)
} else {
val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(body, rssSource.sourceUrl)
analyzeRule.setContent(body, sortUrl)
var reverse = false
if (ruleArticles.startsWith("-")) {
reverse = true
@ -45,7 +45,7 @@ object RssParserByRule {
Debug.log(sourceUrl, "┌获取下一页链接")
nextUrl = analyzeRule.getString(rssSource.ruleNextPage)
if (nextUrl.isNotEmpty()) {
nextUrl = NetworkUtils.getAbsoluteURL(sourceUrl, nextUrl)
nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl)
}
Debug.log(sourceUrl, "$nextUrl")
}
@ -59,7 +59,8 @@ object RssParserByRule {
sourceUrl, item, analyzeRule, index == 0,
ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink
)?.let {
it.origin = rssSource.sourceUrl
it.sort = sortName
it.origin = sourceUrl
articleList.add(it)
}
}

@ -193,6 +193,7 @@ object BookChapterList {
list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle
if (book.totalChapterNum < list.size) {
book.lastCheckCount = list.size - book.totalChapterNum
book.latestChapterTime = System.currentTimeMillis()
}
book.totalChapterNum = list.size
return list

@ -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, "")).append("\n")
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())
@ -95,6 +101,7 @@ object BookContent {
}
}
content.deleteCharAt(content.length - 1)
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")

@ -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 {
contentList.addAll(it)
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,20 +7,22 @@ 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
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors
import kotlin.math.min
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,23 +44,23 @@ 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}")
for (i in 0 until min(threadCount, allIds.size)) {
check()
}
}
@ -67,27 +69,41 @@ class CheckSourceService : BaseService() {
synchronized(this) {
processIndex++
}
if (processIndex < allIds.size) {
val sourceUrl = allIds[processIndex]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
synchronized(this) {
if (processIndex >= allIds.size + threadCount - 1) {
stopSelf()
}
}
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)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
}
private fun onNext(sourceUrl: String) {
synchronized(this) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
if (processIndex >= allIds.size + min(threadCount, allIds.size) - 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,7 +26,7 @@ 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>>()
@ -125,15 +126,9 @@ class DownloadService : BaseService() {
chapter,
scope = this,
context = searchPool
)
//.onStart {
// notificationContent = "启动:" + chapter.title
//}
.onSuccess(IO) { content ->
content?.let {
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
).onSuccess(IO) { content ->
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
.onFinally(IO) {
synchronized(this@DownloadService) {
@ -167,7 +162,7 @@ class DownloadService : BaseService() {
tasks.add(task)
task.invokeOnCompletion {
tasks.remove(task)
if (tasks.isEmpty()) {
if (tasks.isEmpty) {
stopSelf()
}
}
@ -194,17 +189,21 @@ class DownloadService : BaseService() {
val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification)
}
}
class DownloadCount{
@Volatile public var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile public var successCount = 0 //下载成功的条目数量
fun increaseSuccess(){
++successCount;
}
class DownloadCount {
@Volatile
var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile
var successCount = 0 //下载成功的条目数量
fun increaseFinished(){
++downloadFinishedCount;
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
play()
} else {
if (status == TextToSpeech.SUCCESS) {
textToSpeech?.language = Locale.CHINA
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
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,7 +176,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
}
override fun onError(s: String) {
pauseReadAloud(true)
//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 目录界面

@ -22,6 +22,7 @@ class AboutFragment : PreferenceFragmentCompat() {
private val qqGroups = linkedMapOf(
Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"),
Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"),
Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"),
Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"),
Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"),
Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"),

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

@ -11,7 +11,6 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.service.help.AudioPlay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AudioPlayViewModel(application: Application) : BaseViewModel(application) {
@ -72,7 +71,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,10 +90,8 @@ 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) {
}
App.db.bookDao().insert(book1)
AudioPlay.book = book1

@ -6,12 +6,16 @@ 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.AppConst
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 +25,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.*
@ -64,6 +69,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)
@ -97,14 +106,20 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
booksLiveData?.removeObservers(this)
booksLiveData =
when (groupId) {
-1 -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup()
AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll()
AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal()
AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio()
AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer {
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()
})
}
@ -115,22 +130,22 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
.show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group)
groupId = -11
groupId = AppConst.bookGroupNone.groupId
initBookData()
}
R.id.menu_all -> {
title_bar.subtitle = item.title
groupId = -1
groupId = AppConst.bookGroupAll.groupId
initBookData()
}
R.id.menu_local -> {
title_bar.subtitle = item.title
groupId = -2
groupId = AppConst.bookGroupLocal.groupId
initBookData()
}
R.id.menu_audio -> {
title_bar.subtitle = item.title
groupId = -3
groupId = AppConst.bookGroupAudio.groupId
initBookData()
}
else -> if (item.groupId == R.id.menu_group) {
@ -199,6 +214,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
select_action_bar.upCountView(adapter.selectedBooks().size, adapter.getItems().size)
}
override fun updateBook(vararg book: Book) {
viewModel.updateBook(*book)
}
override fun deleteBook(book: Book) {
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
okButton {

@ -1,17 +1,23 @@
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
private val selectedBooks: HashSet<Book> = hashSetOf()
var actionItem: Book? = null
@ -52,12 +58,11 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
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)
}
}
@ -102,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)
}

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

Loading…
Cancel
Save