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'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders" apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
androidExtensions { androidExtensions {
@ -19,6 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android { android {
compileSdkVersion 29 compileSdkVersion 29
flavorDimensions ("version")
signingConfigs { signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig { 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 { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true //coreLibraryDesugaringEnabled true
@ -99,27 +110,27 @@ kapt {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//kotlin //kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase //fireBase
implementation 'com.google.firebase:firebase-core:17.2.3' implementation 'com.google.firebase:firebase-core:17.3.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX //androidX
implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0' implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.6'
//lifecycle //lifecycle
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
@ -127,12 +138,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.4' def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime:2.1.1' implementation 'androidx.paging:paging-runtime:2.1.2'
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'
@ -148,19 +159,19 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// //
implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino //JS rhino
implementation 'com.github.gedoor:rhino-android:1.4' implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit //
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' //noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:retrofit:2.7.2'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer //webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'org.nanohttpd:nanohttpd:2.3.1'
@ -173,14 +184,14 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'
//apache //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' implementation 'org.apache.commons:commons-text:1.8'
//MarkDown //MarkDown
implementation 'ru.noties.markwon:core:3.0.2' implementation 'ru.noties.markwon:core:3.1.0'
// //
implementation 'com.github.houbb:opencc4j:1.4.0' implementation 'com.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" "configuration_version": "1"

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

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

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

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

@ -1,6 +1,120 @@
## 更新日志 ## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(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** **2020/03/09**
* 底部文字对齐 * 底部文字对齐
@ -216,14 +330,7 @@
- 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996` - 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996`
* 修改订阅中自动添加style的情景 * 修改订阅中自动添加style的情景
订阅源的内容规则中存在`<style>``style=`, 订阅源的内容规则中存在`<style>``style=`
```
<style>
img{max-width:100% !important; width:auto; height:auto;}
video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
</style>
```
**2019/12/28** **2019/12/28**
* 添加下载界面 * 添加下载界面

@ -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> <!DOCTYPE html>
<html> <html lang=en style="padding: 0;height:100%">
<head> <head>
<meta charset="utf-8" /> <meta charset=utf-8>
<title>阅读3.0书架</title> <meta http-equiv=X-UA-Compatible content="IE=edge">
<link rel="icon" href="favicon.ico"> <meta name=viewport content="width=device-width,initial-scale=1">
<link href="bookshelf.css" rel="stylesheet" /> <!--[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> </head>
<style>
body::-webkit-scrollbar {
display: none;
}
</style>
<body> <body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without
<button id="top" class="top"></button> JavaScript enabled. Please enable it to continue.</strong></noscript>
<button id="showchapter" class="showchapter"></button> <div id=app></div>
<button id="hidebooks" class="hidebooks"></button> <script src=js/chunk-vendors.b3838a2d.js></script>
<div class="nav"> <script src=js/app.b25f3cec.js></script>
<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> </body>
</html> </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) 前缀表示忽略大小写 <br>(?i) 前缀表示忽略大小写
</div> </div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a> <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> </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( .primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600)) getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor( ).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800)) getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
).backgroundColor( ).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground( ).bottomBackground(

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

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

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

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

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

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

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

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

@ -12,17 +12,18 @@ interface RssArticleDao {
fun get(origin: String, link: String): RssArticle? fun get(origin: String, link: String): RssArticle?
@Query( @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 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) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssArticle: RssArticle) fun insert(vararg rssArticle: RssArticle)
@Query("delete from rssArticles where origin = :origin and `order` < :order") @Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order")
fun clearOld(origin: String, order: Long) fun clearOld(origin: String, sort: String, order: Long)
@Update @Update
fun update(vararg rssArticle: RssArticle) fun update(vararg rssArticle: RssArticle)

@ -4,7 +4,6 @@ import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
@ -15,9 +14,12 @@ import java.nio.charset.Charset
import kotlin.math.max import kotlin.math.max
@Parcelize @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( data class Book(
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents) var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local) var origin: String = BookType.local, // 书源URL(默认BookType.local)
@ -31,7 +33,7 @@ data class Book(
var intro: String? = null, // 简介内容(书源获取) var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改) var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType var type: Int = 0, // 0:text 1:audio
var group: Int = 0, // 自定义分组索引号 var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间 var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间

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

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

@ -23,6 +23,7 @@ data class RssSource(
var sourceIcon: String = "", var sourceIcon: String = "",
var sourceGroup: String? = null, var sourceGroup: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var sortUrl: String? = null,
//列表规则 //列表规则
var ruleArticles: String? = null, var ruleArticles: String? = null,
var ruleNextPage: String? = null, var ruleNextPage: String? = null,
@ -99,4 +100,16 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) 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( data class RssStar(
var origin: String = "", var origin: String = "",
var sort: String = "",
var title: String = "", var title: String = "",
var starTime: Long = 0, var starTime: Long = 0,
var link: String = "", var link: String = "",
@ -16,4 +17,17 @@ data class RssStar(
var description: String? = null, var description: String? = null,
var content: String? = null, var content: String? = null,
var image: String? = null var image: String? = null
) ) {
fun toRssArticle(): RssArticle {
return RssArticle(
origin = origin,
sort = sort,
title = title,
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image
)
}
}

@ -1,6 +1,7 @@
package io.legado.app.help package io.legado.app.help
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -32,6 +33,9 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value) App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
} }
val requestedDirection: String?
get() = App.INSTANCE.getPrefString(R.string.pk_requested_direction)
var backupPath: String? var backupPath: String?
get() = App.INSTANCE.getPrefString(PreferKey.backupPath) get() = App.INSTANCE.getPrefString(PreferKey.backupPath)
set(value) { set(value) {
@ -49,7 +53,7 @@ object AppConfig {
} }
val autoRefreshBook: Boolean val autoRefreshBook: Boolean
get() = App.INSTANCE.getPrefBoolean(App.INSTANCE.getString(R.string.pk_auto_refresh)) get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh)
var threadCount: Int var threadCount: Int
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16)
@ -111,9 +115,32 @@ object AppConfig {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value) App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
} }
var bookGroupNoneShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupNone", value)
}
var elevation: Int var elevation: Int
get() = App.INSTANCE.getPrefInt("elevation", -1) get() = App.INSTANCE.getPrefInt("elevation", -1)
set(value) { set(value) {
App.INSTANCE.putPrefInt("elevation", value) App.INSTANCE.putPrefInt("elevation", value)
} }
val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true)
val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google"
} }
val Context.channel: String
get() {
try {
val pm = packageManager
val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return appInfo.metaData.getString("channel") ?: ""
} catch (e: Exception) {
e.printStackTrace();
}
return ""
}

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

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

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

@ -27,7 +27,6 @@ object ReadBookConfig {
GSON.fromJsonArray<Config>(json)!! GSON.fromJsonArray<Config>(json)!!
} }
val durConfig get() = getConfig(styleSelect) val durConfig get() = getConfig(styleSelect)
private val shareConfig get() = getConfig(5)
var bg: Drawable? = null var bg: Drawable? = null
var bgMeanColor: Int = 0 var bgMeanColor: Int = 0
@ -139,129 +138,145 @@ object ReadBookConfig {
var bodyIndent = " ".repeat(bodyIndentCount) var bodyIndent = " ".repeat(bodyIndentCount)
var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
private val config get() = if (shareLayout) getConfig(5) else durConfig
var textBold: Boolean var textBold: Boolean
get() = if (shareLayout) shareConfig.textBold else durConfig.textBold get() = config.textBold
set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value set(value) {
config.textBold = value
}
var textSize: Int var textSize: Int
get() = if (shareLayout) shareConfig.textSize else durConfig.textSize get() = config.textSize
set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value set(value) {
config.textSize = value
}
var letterSpacing: Float var letterSpacing: Float
get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing get() = config.letterSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value config.letterSpacing = value
}
var lineSpacingExtra: Int var lineSpacingExtra: Int
get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra get() = config.lineSpacingExtra
set(value) = set(value) {
if (shareLayout) shareConfig.lineSpacingExtra = value config.lineSpacingExtra = value
else durConfig.lineSpacingExtra = value }
var paragraphSpacing: Int var paragraphSpacing: Int
get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing get() = config.paragraphSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.paragraphSpacing = value config.paragraphSpacing = value
else durConfig.paragraphSpacing = value }
var titleMode: Int var titleMode: Int
get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode get() = config.titleMode
set(value) = set(value) {
if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value config.titleMode = value
}
var titleSize: Int var titleSize: Int
get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize get() = config.titleSize
set(value) = set(value) {
if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value config.titleSize = value
}
var titleTopSpacing: Int var titleTopSpacing: Int
get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing get() = config.titleTopSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.titleTopSpacing = value config.titleTopSpacing = value
else durConfig.titleTopSpacing = value }
var titleBottomSpacing: Int var titleBottomSpacing: Int
get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing get() = config.titleBottomSpacing
set(value) = set(value) {
if (shareLayout) shareConfig.titleBottomSpacing = value config.titleBottomSpacing = value
else durConfig.titleBottomSpacing = value }
var paddingBottom: Int var paddingBottom: Int
get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom get() = config.paddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value config.paddingBottom = value
}
var paddingLeft: Int var paddingLeft: Int
get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft get() = config.paddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value config.paddingLeft = value
}
var paddingRight: Int var paddingRight: Int
get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight get() = config.paddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value config.paddingRight = value
}
var paddingTop: Int var paddingTop: Int
get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop get() = config.paddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value config.paddingTop = value
}
var headerPaddingBottom: Int var headerPaddingBottom: Int
get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom get() = config.headerPaddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingBottom = value config.headerPaddingBottom = value
else durConfig.headerPaddingBottom = value }
var headerPaddingLeft: Int var headerPaddingLeft: Int
get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft get() = config.headerPaddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingLeft = value config.headerPaddingLeft = value
else durConfig.headerPaddingLeft = value }
var headerPaddingRight: Int var headerPaddingRight: Int
get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight get() = config.headerPaddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingRight = value config.headerPaddingRight = value
else durConfig.headerPaddingRight = value }
var headerPaddingTop: Int var headerPaddingTop: Int
get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop get() = config.headerPaddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.headerPaddingTop = value config.headerPaddingTop = value
else durConfig.headerPaddingTop = value }
var footerPaddingBottom: Int var footerPaddingBottom: Int
get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom get() = config.footerPaddingBottom
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingBottom = value config.footerPaddingBottom = value
else durConfig.footerPaddingBottom = value }
var footerPaddingLeft: Int var footerPaddingLeft: Int
get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft get() = config.footerPaddingLeft
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingLeft = value config.footerPaddingLeft = value
else durConfig.footerPaddingLeft = value }
var footerPaddingRight: Int var footerPaddingRight: Int
get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight get() = config.footerPaddingRight
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingRight = value config.footerPaddingRight = value
else durConfig.footerPaddingRight = value }
var footerPaddingTop: Int var footerPaddingTop: Int
get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop get() = config.footerPaddingTop
set(value) = set(value) {
if (shareLayout) shareConfig.footerPaddingTop = value config.footerPaddingTop = value
else durConfig.footerPaddingTop = value }
var showHeaderLine: Boolean var showHeaderLine: Boolean
get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine get() = config.showHeaderLine
set(value) = set(value) {
if (shareLayout) shareConfig.showHeaderLine = value config.showHeaderLine = value
else durConfig.showHeaderLine = value }
var showFooterLine: Boolean var showFooterLine: Boolean
get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine get() = config.showFooterLine
set(value) = set(value) {
if (shareLayout) shareConfig.showFooterLine = value config.showFooterLine = value
else durConfig.showFooterLine = value }
@Keep @Keep
class Config( 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>( class Coroutine<T>(
scope: CoroutineScope, val scope: CoroutineScope,
context: CoroutineContext = Dispatchers.IO, context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
) { ) {
@ -28,9 +28,10 @@ class Coroutine<T>(
private val job: Job private val job: Job
private var start: VoidCallback? = null private var start: VoidCallback? = null
private var success: Callback<T?>? = null private var success: Callback<T>? = null
private var error: Callback<Throwable>? = null private var error: Callback<Throwable>? = null
private var finally: VoidCallback? = null private var finally: VoidCallback? = null
private var cancel: VoidCallback? = null
private var timeMillis: Long? = null private var timeMillis: Long? = null
private var errorReturn: Result<T>? = null private var errorReturn: Result<T>? = null
@ -45,7 +46,7 @@ class Coroutine<T>(
get() = job.isCompleted get() = job.isCompleted
init { init {
this.job = executeInternal(scope, context, block) this.job = executeInternal(context, block)
} }
fun timeout(timeMillis: () -> Long): Coroutine<T> { fun timeout(timeMillis: () -> Long): Coroutine<T> {
@ -78,7 +79,7 @@ class Coroutine<T>(
fun onSuccess( fun onSuccess(
context: CoroutineContext? = null, context: CoroutineContext? = null,
block: suspend CoroutineScope.(T?) -> Unit block: suspend CoroutineScope.(T) -> Unit
): Coroutine<T> { ): Coroutine<T> {
this.success = Callback(context, block) this.success = Callback(context, block)
return this@Coroutine return this@Coroutine
@ -100,9 +101,28 @@ class Coroutine<T>(
return this@Coroutine return this@Coroutine
} }
fun onCancel(
context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit
): Coroutine<T> {
this.cancel = VoidCallback(context, block)
return this@Coroutine
}
//取消当前任务 //取消当前任务
fun cancel(cause: CancellationException? = null) { fun cancel(cause: CancellationException? = null) {
job.cancel(cause) job.cancel(cause)
cancel?.let {
MainScope().launch {
if (null == it.context) {
it.block.invoke(scope)
} else {
withContext(scope.coroutineContext.plus(it.context)) {
it.block.invoke(this)
}
}
}
}
} }
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {
@ -110,7 +130,6 @@ class Coroutine<T>(
} }
private fun executeInternal( private fun executeInternal(
scope: CoroutineScope,
context: CoroutineContext, context: CoroutineContext,
block: suspend CoroutineScope.() -> T block: suspend CoroutineScope.() -> T
): Job { ): Job {
@ -118,21 +137,27 @@ class Coroutine<T>(
try { try {
start?.let { dispatchVoidCallback(this, it) } start?.let { dispatchVoidCallback(this, it) }
val value = executeBlock(scope, context, timeMillis ?: 0L, block) val value = executeBlock(scope, context, timeMillis ?: 0L, block)
success?.let { dispatchCallback(this, value, it) } if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
val consume: Boolean = errorReturn?.value?.let { value -> val consume: Boolean = errorReturn?.value?.let { value ->
success?.let { dispatchCallback(this, value, it) } if (isActive) {
success?.let { dispatchCallback(this, value, it) }
}
true true
} ?: false } ?: false
if (!consume) { if (!consume && isActive) {
error?.let { dispatchCallback(this, e, it) } error?.let { dispatchCallback(this, e, it) }
} }
} finally { } finally {
finally?.let { dispatchVoidCallback(this, it) } if (isActive) {
finally?.let { dispatchVoidCallback(this, it) }
}
} }
} }
} }
@ -152,6 +177,7 @@ class Coroutine<T>(
value: R, value: R,
callback: Callback<R> callback: Callback<R>
) { ) {
if (!scope.isActive) return
if (null == callback.context) { if (null == callback.context) {
callback.block.invoke(scope, value) callback.block.invoke(scope, value)
} else { } else {
@ -166,7 +192,7 @@ class Coroutine<T>(
context: CoroutineContext, context: CoroutineContext,
timeMillis: Long, timeMillis: Long,
noinline block: suspend CoroutineScope.() -> T noinline block: suspend CoroutineScope.() -> T
): T? { ): T {
return withContext(scope.coroutineContext.plus(context)) { return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) { if (timeMillis > 0L) withTimeout(timeMillis) {
block() block()

@ -180,6 +180,7 @@ class AjaxWebView {
mWebView.get()?.evaluateJavascript(mJavaScript) { mWebView.get()?.evaluateJavascript(mJavaScript) {
if (it.isNotEmpty() && it != "null") { if (it.isNotEmpty() && it != "null") {
val content = StringEscapeUtils.unescapeJson(it) val content = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
handler.obtainMessage(MSG_SUCCESS, Res(url, content)) handler.obtainMessage(MSG_SUCCESS, Res(url, content))
.sendToTarget() .sendToTarget()
handler.removeCallbacks(this) 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.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import retrofit2.Retrofit import retrofit2.Retrofit
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -13,14 +15,12 @@ import kotlin.coroutines.resume
object HttpHelper { object HttpHelper {
val client: OkHttpClient by lazy { val client: OkHttpClient by lazy {
val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs = ArrayList<ConnectionSpec>() val specs = arrayListOf(
specs.add(default) ConnectionSpec.MODERN_TLS,
specs.add(ConnectionSpec.COMPATIBLE_TLS) ConnectionSpec.COMPATIBLE_TLS,
specs.add(ConnectionSpec.CLEARTEXT) ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)

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

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

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

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

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

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

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

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

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

@ -15,27 +15,35 @@ import kotlin.coroutines.CoroutineContext
object Rss { object Rss {
fun getArticles( fun getArticles(
sortName: String,
sortUrl: String,
rssSource: RssSource, rssSource: RssSource,
pageUrl: String? = null, pageUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<Result> { ): Coroutine<Result> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(pageUrl ?: rssSource.sourceUrl) val analyzeUrl = AnalyzeUrl(
pageUrl ?: sortUrl,
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body
RssParserByRule.parseXML(body, rssSource) RssParserByRule.parseXML(sortName, sortUrl, body, rssSource)
} }
} }
fun getContent( fun getContent(
rssArticle: RssArticle, rssArticle: RssArticle,
ruleContent: String, ruleContent: String,
rssSource: RssSource?,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = AnalyzeUrl(rssArticle.link, baseUrl = rssArticle.origin) val body = AnalyzeUrl(
.getResponseAwait(rssArticle.origin) rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap()
).getResponseAwait(rssArticle.origin)
.body .body
val analyzeRule = AnalyzeRule() val analyzeRule = AnalyzeRule()
analyzeRule.setContent( analyzeRule.setContent(

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

@ -27,26 +27,36 @@ class WebBook(val bookSource: BookSource) {
page: Int? = 1, page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT, scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<List<SearchBook>> { ): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
bookSource.searchUrl?.let { searchUrl -> searchBookSuspend(scope, key, page)
val analyzeUrl = AnalyzeUrl( }
ruleUrl = searchUrl, }
key = key,
page = page, suspend fun searchBookSuspend(
baseUrl = sourceUrl, scope: CoroutineScope,
headerMapF = bookSource.getHeaderMap() key: String,
) page: Int? = 1
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) ): ArrayList<SearchBook> {
BookList.analyzeBookList( bookSource.searchUrl?.let { searchUrl ->
res.body, val analyzeUrl = AnalyzeUrl(
bookSource, ruleUrl = searchUrl,
analyzeUrl, key = key,
res.url, page = page,
true baseUrl = sourceUrl,
) headerMapF = bookSource.getHeaderMap()
} ?: arrayListOf() )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
true
)
} }
return arrayListOf()
} }
/** /**
@ -67,6 +77,7 @@ class WebBook(val bookSource: BookSource) {
) )
val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl)
BookList.analyzeBookList( BookList.analyzeBookList(
scope,
res.body, res.body,
bookSource, bookSource,
analyzeUrl, analyzeUrl,

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

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

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

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

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

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

@ -2,6 +2,7 @@ package io.legado.app.model.webBook
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -39,7 +40,12 @@ object BookContent {
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource book, baseUrl, body, contentRule, bookChapter, bookSource
) )
content.append(contentData.content.replace(bookChapter.title, "")).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) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty()) val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
@ -95,6 +101,7 @@ object BookContent {
} }
} }
content.deleteCharAt(content.length - 1)
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") 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.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
object BookList { object BookList {
@Throws(Exception::class) @Throws(Exception::class)
fun analyzeBookList( fun analyzeBookList(
scope: CoroutineScope,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
analyzeUrl: AnalyzeUrl, analyzeUrl: AnalyzeUrl,
@ -28,12 +32,13 @@ object BookList {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(null) val analyzeRule = AnalyzeRule(null)
analyzeRule.setContent(body, baseUrl) analyzeRule.setContent(body, baseUrl)
bookSource.bookUrlPattern?.let { bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) { if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -59,7 +64,7 @@ object BookList {
collections = analyzeRule.getElements(ruleList) collections = analyzeRule.getElements(ruleList)
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(analyzeRule, bookSource, baseUrl)?.let { searchBook -> getInfoItem(scope, analyzeRule, bookSource, baseUrl)?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
} }
@ -74,8 +79,9 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) { for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem( getSearchItem(
item, analyzeRule, bookSource, baseUrl, index == 0, scope, item, analyzeRule, bookSource, baseUrl, index == 0,
ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor, ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,
ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind, ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,
ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount
@ -93,7 +99,9 @@ object BookList {
return bookList return bookList
} }
@Throws(Exception::class)
private fun getInfoItem( private fun getInfoItem(
scope: CoroutineScope,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String baseUrl: String
@ -108,29 +116,37 @@ object BookList {
with(bookSource.getBookInfoRule()) { with(bookSource.getBookInfoRule()) {
init?.let { init?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it)) analyzeRule.setContent(analyzeRule.getElement(it))
} }
} }
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名") Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = analyzeRule.getString(name) searchBook.name = analyzeRule.getString(name)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}") Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}") Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount) searchBook.wordCount = analyzeRule.getString(wordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}") Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter) searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}") Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro) searchBook.intro = analyzeRule.getString(intro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true) searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -140,7 +156,9 @@ object BookList {
return null return null
} }
@Throws(Exception::class)
private fun getSearchItem( private fun getSearchItem(
scope: CoroutineScope,
item: Any, item: Any,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
@ -162,30 +180,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook analyzeRule.book = searchBook
analyzeRule.setContent(item) analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = analyzeRule.getString(ruleName) searchBook.name = analyzeRule.getString(ruleName)
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount) searchBook.wordCount = analyzeRule.getString(ruleWordCount)
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro) searchBook.intro = analyzeRule.getString(ruleIntro)
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let { analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true) searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) { if (searchBook.bookUrl.isEmpty()) {

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

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

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

@ -7,20 +7,22 @@ import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private val threadCount = AppConfig.threadCount private val threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>() private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>() private val checkedIds = ArrayList<String>()
private var processIndex = 0 private var processIndex = 0
@ -42,23 +44,23 @@ class CheckSourceService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
task?.cancel() tasks.clear()
searchPool.close() searchPool.close()
} }
private fun check(ids: List<String>) { private fun check(ids: List<String>) {
task?.cancel() if (allIds.isNotEmpty()) {
toast("已有书源在校验,等完成后再试")
return
}
tasks.clear()
allIds.clear() allIds.clear()
checkedIds.clear() checkedIds.clear()
allIds.addAll(ids) allIds.addAll(ids)
processIndex = 0 processIndex = 0
updateNotification(0, getString(R.string.progress_show, 0, allIds.size)) updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
task = execute { for (i in 0 until min(threadCount, allIds.size)) {
for (i in 0 until threadCount) { check()
check()
}
}.onError {
toast("校验书源出错:${it.localizedMessage}")
} }
} }
@ -67,27 +69,41 @@ class CheckSourceService : BaseService() {
synchronized(this) { synchronized(this) {
processIndex++ processIndex++
} }
if (processIndex < allIds.size) { execute {
val sourceUrl = allIds[processIndex] if (processIndex < allIds.size) {
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> val sourceUrl = allIds[processIndex]
val webBook = WebBook(source) App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
webBook.searchBook("我的", scope = this, context = searchPool) if (source.searchUrl.isNullOrEmpty()) {
.onError(IO) { onNext(sourceUrl)
source.addGroup("失效") } else {
App.db.bookSourceDao().update(source) check(source)
}.onFinally(IO) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
synchronized(this) {
if (processIndex >= allIds.size + threadCount - 1) {
stopSelf()
}
}
} }
} ?: onNext(sourceUrl)
}
}
}
private fun check(source: BookSource) {
val webBook = WebBook(source)
tasks.add(webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally(IO) {
onNext(source.bookSourceUrl)
})
}
private fun onNext(sourceUrl: String) {
synchronized(this) {
check()
checkedIds.add(sourceUrl)
updateNotification(
checkedIds.size,
getString(R.string.progress_show, checkedIds.size, allIds.size)
)
if (processIndex >= allIds.size + 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.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -25,7 +26,7 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() { class DownloadService : BaseService() {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf() private var tasks = CompositeCoroutine()
private val handler = Handler() private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }
private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>() private val downloadMap = hashMapOf<String, LinkedHashSet<BookChapter>>()
@ -125,15 +126,9 @@ class DownloadService : BaseService() {
chapter, chapter,
scope = this, scope = this,
context = searchPool context = searchPool
) ).onSuccess(IO) { content ->
//.onStart { downloadCount[entry.key]?.increaseSuccess()
// notificationContent = "启动:" + chapter.title BookHelp.saveContent(book, chapter, content)
//}
.onSuccess(IO) { content ->
content?.let {
downloadCount[entry.key]?.increaseSuccess()
BookHelp.saveContent(book, chapter, content)
}
} }
.onFinally(IO) { .onFinally(IO) {
synchronized(this@DownloadService) { synchronized(this@DownloadService) {
@ -167,7 +162,7 @@ class DownloadService : BaseService() {
tasks.add(task) tasks.add(task)
task.invokeOnCompletion { task.invokeOnCompletion {
tasks.remove(task) tasks.remove(task)
if (tasks.isEmpty()) { if (tasks.isEmpty) {
stopSelf() stopSelf()
} }
} }
@ -194,17 +189,21 @@ class DownloadService : BaseService() {
val notification = builder.build() val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification) startForeground(AppConst.notificationIdDownload, notification)
} }
}
class DownloadCount{
@Volatile public var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile public var successCount = 0 //下载成功的条目数量
fun increaseSuccess(){ class DownloadCount {
++successCount; @Volatile
} var downloadFinishedCount = 0 // 下载完成的条目数量
@Volatile
var successCount = 0 //下载成功的条目数量
fun increaseFinished(){ fun increaseSuccess() {
++downloadFinishedCount; ++successCount;
}
fun increaseFinished() {
++downloadFinishedCount;
}
} }
} }

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

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

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

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

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

@ -11,7 +11,6 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AudioPlayViewModel(application: Application) : BaseViewModel(application) { class AudioPlayViewModel(application: Application) : BaseViewModel(application) {
@ -72,7 +71,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
execute { execute {
AudioPlay.webBook?.getChapterList(book, this) AudioPlay.webBook?.getChapterList(book, this)
?.onSuccess(Dispatchers.IO) { cList -> ?.onSuccess(Dispatchers.IO) { cList ->
if (!cList.isNullOrEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
AudioPlay.chapterSize = cList.size AudioPlay.chapterSize = cList.size
@ -91,10 +90,8 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
AudioPlay.book?.let { AudioPlay.book?.let {
book1.order = it.order
App.db.bookDao().delete(it) App.db.bookDao().delete(it)
}
withContext(Dispatchers.Main) {
} }
App.db.bookDao().insert(book1) App.db.bookDao().insert(book1)
AudioPlay.book = book1 AudioPlay.book = book1

@ -6,12 +6,16 @@ import android.view.MenuItem
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
@ -21,6 +25,7 @@ import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.* import kotlinx.android.synthetic.main.activity_arrange_book.*
@ -64,6 +69,10 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
recycler_view.addItemDecoration(VerticalDivider(this)) recycler_view.addItemDecoration(VerticalDivider(this))
adapter = ArrangeBookAdapter(this, this) adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = getPrefInt(PreferKey.bookshelfSort) == 3
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
select_action_bar.setMainActionText(R.string.move_to_group) select_action_bar.setMainActionText(R.string.move_to_group)
select_action_bar.inflateMenu(R.menu.arrange_book_sel) select_action_bar.inflateMenu(R.menu.arrange_book_sel)
select_action_bar.setOnMenuItemClickListener(this) select_action_bar.setOnMenuItemClickListener(this)
@ -97,14 +106,20 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
booksLiveData?.removeObservers(this) booksLiveData?.removeObservers(this)
booksLiveData = booksLiveData =
when (groupId) { when (groupId) {
-1 -> App.db.bookDao().observeAll() AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll()
-2 -> App.db.bookDao().observeLocal() AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal()
-3 -> App.db.bookDao().observeAudio() AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio()
-11 -> App.db.bookDao().observeNoGroup() AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId) else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { booksLiveData?.observe(this, Observer { list ->
adapter.setItems(it) 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() upSelectCount()
}) })
} }
@ -115,22 +130,22 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
.show(supportFragmentManager, "groupManage") .show(supportFragmentManager, "groupManage")
R.id.menu_no_group -> { R.id.menu_no_group -> {
title_bar.subtitle = getString(R.string.no_group) title_bar.subtitle = getString(R.string.no_group)
groupId = -11 groupId = AppConst.bookGroupNone.groupId
initBookData() initBookData()
} }
R.id.menu_all -> { R.id.menu_all -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -1 groupId = AppConst.bookGroupAll.groupId
initBookData() initBookData()
} }
R.id.menu_local -> { R.id.menu_local -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -2 groupId = AppConst.bookGroupLocal.groupId
initBookData() initBookData()
} }
R.id.menu_audio -> { R.id.menu_audio -> {
title_bar.subtitle = item.title title_bar.subtitle = item.title
groupId = -3 groupId = AppConst.bookGroupAudio.groupId
initBookData() initBookData()
} }
else -> if (item.groupId == R.id.menu_group) { 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) select_action_bar.upCountView(adapter.selectedBooks().size, adapter.getItems().size)
} }
override fun updateBook(vararg book: Book) {
viewModel.updateBook(*book)
}
override fun deleteBook(book: Book) { override fun deleteBook(book: Book) {
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
okButton { okButton {

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

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

Loading…
Cancel
Save