Merge pull request #16 from gedoor/master

get update
pull/379/head
口口吕 5 years ago committed by GitHub
commit 4e8093ebd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 4
      CHANGELOG.md
  3. 7
      README.md
  4. 41
      app/build.gradle
  5. 80
      app/src/main/AndroidManifest.xml
  6. 74
      app/src/main/assets/readConfig.json
  7. 38
      app/src/main/assets/txtChapterRule.json
  8. 62
      app/src/main/assets/txtTocRule.json
  9. 105
      app/src/main/assets/updateLog.md
  10. 32
      app/src/main/java/io/legado/app/App.kt
  11. 12
      app/src/main/java/io/legado/app/README.md
  12. 7
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  13. 24
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  14. 10
      app/src/main/java/io/legado/app/base/BaseFragment.kt
  15. 12
      app/src/main/java/io/legado/app/base/BaseService.kt
  16. 66
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  17. 11
      app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
  18. 12
      app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
  19. 5
      app/src/main/java/io/legado/app/constant/EventBus.kt
  20. 3
      app/src/main/java/io/legado/app/constant/IntentAction.kt
  21. 21
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  22. 5
      app/src/main/java/io/legado/app/constant/Theme.kt
  23. 6
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  24. 18
      app/src/main/java/io/legado/app/data/README.md
  25. 3
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  26. 19
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  27. 11
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  28. 27
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  29. 13
      app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
  30. 12
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  31. 9
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  32. 3
      app/src/main/java/io/legado/app/data/dao/RssStarDao.kt
  33. 39
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  34. 28
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  35. 38
      app/src/main/java/io/legado/app/data/entities/Book.kt
  36. 13
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  37. 8
      app/src/main/java/io/legado/app/data/entities/BookGroup.kt
  38. 22
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  39. 15
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  40. 7
      app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt
  41. 11
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  42. 9
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  43. 14
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  44. 8
      app/src/main/java/io/legado/app/help/ActivityHelp.kt
  45. 5
      app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt
  46. 89
      app/src/main/java/io/legado/app/help/AppConfig.kt
  47. 202
      app/src/main/java/io/legado/app/help/BookHelp.kt
  48. 19
      app/src/main/java/io/legado/app/help/CrashHandler.kt
  49. 58
      app/src/main/java/io/legado/app/help/FileHelp.kt
  50. 34
      app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt
  51. 14
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  52. 30
      app/src/main/java/io/legado/app/help/ItemTouchCallback.kt
  53. 65
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  54. 129
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  55. 2
      app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt
  56. 102
      app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt
  57. 21
      app/src/main/java/io/legado/app/help/http/EncodeConverter.kt
  58. 28
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  59. 8
      app/src/main/java/io/legado/app/help/http/SSLHelper.kt
  60. 22
      app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt
  61. 19
      app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt
  62. 2
      app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt
  63. 2
      app/src/main/java/io/legado/app/help/permission/Request.kt
  64. 1
      app/src/main/java/io/legado/app/help/permission/RequestManager.kt
  65. 128
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  66. 149
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  67. 55
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  68. 1
      app/src/main/java/io/legado/app/help/storage/Preferences.kt
  69. 216
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  70. 96
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  71. 5
      app/src/main/java/io/legado/app/lib/README.md
  72. 2
      app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt
  73. 14
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  74. 4
      app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt
  75. 7
      app/src/main/java/io/legado/app/lib/theme/TintHelper.kt
  76. 27
      app/src/main/java/io/legado/app/lib/theme/view/ATEAccentBgTextView.kt
  77. 31
      app/src/main/java/io/legado/app/lib/theme/view/ATEStrokeTextView.kt
  78. 15
      app/src/main/java/io/legado/app/lib/theme/view/ATETextInputLayout.kt
  79. 19
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  80. 6
      app/src/main/java/io/legado/app/model/README.md
  81. 8
      app/src/main/java/io/legado/app/model/WebBook.kt
  82. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt
  83. 17
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  84. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt
  85. 44
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  86. 26
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  87. 247
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  88. 30
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  89. 234
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  90. 4
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  91. 12
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  92. 6
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  93. 2
      app/src/main/java/io/legado/app/model/webBook/ChapterData.kt
  94. 2
      app/src/main/java/io/legado/app/model/webBook/ContentData.kt
  95. 176
      app/src/main/java/io/legado/app/model/webbook/BookChapterList.kt
  96. 6
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  97. 6
      app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt
  98. 56
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  99. 55
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  100. 63
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  101. Some files were not shown because too many files have changed in this diff Show More

4
.gitignore vendored

@ -7,4 +7,6 @@
/captures
.externalNativeBuild
/release
/tmp
/tmp
node_modules/
package-lock.json

@ -0,0 +1,4 @@
# 1.0.0 (2020-02-09)

@ -1,2 +1,9 @@
# legado
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
## 阅读3.0
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg
## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html

@ -20,13 +20,15 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android {
compileSdkVersion 29
signingConfigs {
myConfig {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
v1SigningEnabled true
v2SigningEnabled true
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
v1SigningEnabled true
v2SigningEnabled true
}
}
}
defaultConfig {
@ -49,12 +51,16 @@ android {
}
buildTypes {
release {
signingConfig signingConfigs.myConfig
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig
}
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.myConfig
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.debug'
versionNameSuffix 'debug'
minifyEnabled false
@ -96,22 +102,23 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX
implementation 'androidx.core:core-ktx:1.2.0-rc01'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
//lifecycle
def lifecycle_version = '2.1.0'
def lifecycle_version = '2.2.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
@ -137,13 +144,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS
implementation 'com.github.gedoor:rhino-android:1.3'
//JS rhino
implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
@ -169,6 +175,9 @@ dependencies {
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
//
implementation 'com.github.houbb:opencc4j:1.4.0'
}
apply plugin: 'com.google.gms.google-services'

@ -22,14 +22,53 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
android:requestLegacyExternalStorage="false"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口-->
<activity android:name=".ui.welcome.WelcomeActivity">
<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>
<!--图标1-->
<activity
android:name=".ui.welcome.Launcher1"
android:icon="@mipmap/launcher1"
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>
<!--图标2-->
<activity
android:name=".ui.welcome.Launcher2"
android:icon="@mipmap/launcher2"
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>
<!--图标3-->
<activity
android:name=".ui.welcome.WelcomeActivity"
android:label="@string/app_name">
android:name=".ui.welcome.Launcher3"
android:icon="@mipmap/launcher3"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -39,22 +78,43 @@
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<!--主界面-->
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask"
android:alwaysRetainTaskState="true" />
<!--阅读界面-->
<activity
android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" />
<!--书籍详情页-->
<activity
android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTask" />
<!--书籍信息编辑-->
<activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"
android:launchMode="singleTask" />
<!--音频播放界面-->
<activity
android:name=".ui.audio.AudioPlayActivity"
android:launchMode="singleTask" />
<!--授权界面-->
<activity
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<!--二维码扫描-->
<activity
android:name=".ui.qrcode.QrCodeActivity"
android:launchMode="singleTask" />
<!--书源编辑-->
<activity
android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--订阅源编辑-->
<activity
android:name=".ui.rss.source.edit.RssSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
@ -72,19 +132,14 @@
<activity
android:name=".ui.about.AboutActivity"
android:launchMode="singleTask" />
<activity
android:name=".ui.qrcode.QrCodeActivity"
android:launchMode="singleTask" />
<activity
android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" />
<activity android:name=".ui.book.info.BookInfoActivity" />
<activity android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" />
<activity android:name=".ui.book.arrange.ArrangeBookActivity" />
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
<activity android:name=".ui.book.source.manage.BookSourceActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -95,14 +150,12 @@
</activity>
<activity android:name=".ui.chapterlist.ChapterListActivity" />
<activity android:name=".ui.rss.read.ReadRssActivity" />
<activity
android:name=".ui.audio.AudioPlayActivity"
android:launchMode="singleTask" />
<activity android:name=".ui.importbook.ImportBookActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.rss.favorites.RssFavoritesActivity" />
<activity android:name=".ui.download.DownloadActivity" />
<activity
android:name=".receiver.SharedReceiverActivity"
@ -118,6 +171,7 @@
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".ui.login.SourceLogin" />
<service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" />

@ -1,72 +1,62 @@
[
{
"bgStr": "羊皮纸2.jpg",
"bgType": 1,
"bgStr": "#EBD9BB",
"bgStrNight": "#1E2021",
"textColor": "#63543C",
"textColorNight": "#DCDFE1",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"textColor": "#5E432E",
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
"lineSpacingExtra": 10
},
{
"bgStr": "#C6BAA1",
"bgStr": "#DDC090",
"bgStrNight": "#3C3F43",
"textColor": "#3E3422",
"textColorNight": "#DCDFE1",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"textColor": "#5E432E",
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
"lineSpacingExtra": 10
},
{
"bgStr": "#015A86",
"bgStr": "#C2D8AA",
"bgStrNight": "#3C3F43",
"textColor": "#596C44",
"textColorNight": "#88C16F",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textColor": "#FFFFFF",
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
"lineSpacingExtra": 10
},
{
"bgStr": "宁静夜色",
"bgType": 1,
"bgStr": "#DBB8E2",
"bgStrNight": "#3C3F43",
"textColor": "#68516C",
"textColorNight": "#F6AEAE",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textColor": "#adadad",
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
"lineSpacingExtra": 10
},
{
"bgStr": "#000000",
"bgStr": "#ABCEE0",
"bgStrNight": "#3C3F43",
"textColor": "#3D4C54",
"textColorNight": "#90BFF5",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textColor": "#adadad",
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
"lineSpacingExtra": 10
}
]

@ -1,38 +0,0 @@
[
{
"enable": true,
"name": "默认正则1",
"rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$",
"serialNumber": 0
},
{
"enable": true,
"name": "默认正则2",
"rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$",
"serialNumber": 1
},
{
"enable": true,
"name": "默认正则3",
"rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$",
"serialNumber": 2
},
{
"enable": true,
"name": "默认正则4",
"rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$",
"serialNumber": 3
},
{
"enable": true,
"name": "默认正则5",
"rule": "^(\\s{0,4})(正文)(.{0,20})$",
"serialNumber": 4
},
{
"enable": true,
"name": "默认正则6",
"rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$",
"serialNumber": 5
}
]

@ -0,0 +1,62 @@
[
{
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$",
"serialNumber": 0
},
{
"enable": true,
"name": "目录",
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{
"enable": false,
"name": "目录(不匹配行前空白)",
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 2
},
{
"enable": false,
"name": "目录(去简介)",
"rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 3
},
{
"enable": false,
"name": "目录(古典小说备用)",
"rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 4
},
{
"enable": true,
"name": "Chapter/Section/Part 序号 标题",
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 5
},
{
"enable": true,
"name": "正文 标题/序号",
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$",
"serialNumber": 6
},
{
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$",
"serialNumber": 7
},
{
"enable": true,
"name": "特殊符号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$",
"serialNumber": 8
},
{
"enable":false,
"name": "特殊符号 标题(不匹配空白字符)",
"rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$",
"serialNumber": 9
}
]

@ -1,14 +1,105 @@
## 更新日志
* 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/01/11
**2020/02/23**
* 修复BUG
* 本地目录正则自定义完成
* 选择文本修复框选不全的问题,增加操作按钮
**2020/02/22**
* 长按选择完成
**2020/02/21**
* 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整
* 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题
**2020/02/19**
* 导出功能完成
* 其它一些优化,仿真翻页有点问题,还没找到问题所在
**2020/02/15**
* 修复bug
* 添加一个图标
* 阅读界面文本选择开关
* 书源管理发现开启关闭标志
**2020/02/14**
* 书籍分组支持一本书籍在多个分组,既可以在追更,又可以在玄幻
* 搜索界面限制刷新频率,每秒刷新一次
* 添加一种图标,2.0的老图标
**2020/02/13**
* 修复BUG
* 优化已下载检测,解决目录卡顿
* 添加切换图标
**2020/02/12**
* 修复bug
* 优化,网页编码优先使用书源配置的编码
* 其它一些优化
* 添加简繁转换
**2020/02/10**
* 多页目录并行获取解析
* 优化详情页
* 优化换源页面,添加换源是否加载目录配置
* 换源顺序按书源顺序排列
**2020/02/09**
* 优化书源管理,备份恢复
* 主题色修改,底部操作栏更明显
**2020/02/08**
* 书架分组调整顺序后,书架及时变动
**2020/02/07**
* 优化
* 书源校验
* 书架整理
**2020/02/05**
* 修复bug
* Rss收藏功能完成
* Rss已读标记不会再丢失
**2020/02/04**
* 主界面切换时自动隐藏键盘
* 添加本地书籍完成,解析txt文件完成,本地txt可以看了
* 封面换源,书籍信息界面点击封面弹出封面换源界面
* 默认封面绘制书名和作者
* 修复在线朗读遇到单独标点,停止朗读的问题
**2020/02/02**
* merged commit e584606, rss修复BaseURL模式下部分图片无法加载, 修复可能出现的乱码
* 菜单添加网址功能完成
**2020/01/31**
* 修复搜索闪退,因为默认线程为0了
**2020/01/30**
* 优化缓存文件夹选择,不再需要存储权限
* 修复替换净化导入报错的bug
**2020/01/27**
* 添加根据系统主题切换夜间模式
* 合并Modificator提交的代码
**2020/01/26**
* 修复bug
* 未加入书架可查看目录
**2020/01/24**
* 添加线程数配置
* 记住退出时的书架
* 添加屏幕超时配置
**2020/01/11**
* RSS阅读界面添加朗读功能
* 其它一些优化
* 合并KKL369提交的代码,重写LinearLayoutManager,修复书籍目录模糊搜索后scrollToPosition在可见范围不置顶
**2020/01/10
**2020/01/10**
* 合并KKL369提交的代码
**2020/01/08**
@ -74,13 +165,13 @@
* 最近感冒了,发热咳嗽还没好,继续咸鱼
**2019/12/12**
* [fix]web服务停止问题
* web服务停止问题
* 默认显示沉浸式状态栏
**2019/12/09**
* [add]其他设置->清理缓存
* [mod]调整深色模式配色,预适配Android10
* [mod]启用web服务
* 其他设置->清理缓存
* 调整深色模式配色,预适配Android10
* 启用web服务
**2019/12/03**
* from Celeter:

@ -15,13 +15,12 @@ import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.data.AppDatabase
import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.isNightTheme
@Suppress("DEPRECATION")
class App : Application() {
@ -50,7 +49,7 @@ class App : Application() {
}
if (!ThemeStore.isConfigured(this, versionCode)) applyTheme()
initNightTheme()
initNightMode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId()
@ -67,13 +66,13 @@ class App : Application() {
* 更新主题
*/
fun applyTheme() {
if (isNightTheme) {
if (AppConfig.isNightTheme) {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color))
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
)
.accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color))
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800))
)
.backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
@ -82,31 +81,32 @@ class App : Application() {
} else {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100))
getPrefInt("colorPrimary", getCompatColor(R.color.md_indigo_800))
)
.accentColor(
getPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color))
getPrefInt("colorAccent", getCompatColor(R.color.md_red_600))
)
.backgroundColor(
getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
)
.apply()
}
ChapterProvider.upReadAloudSpan()
// ChapterProvider.upReadAloudSpan()
}
fun applyDayNight() {
ReadBookConfig.upBg()
applyTheme()
initNightTheme()
initNightMode()
}
private fun initNightTheme() {
val targetMode = if (isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
private fun initNightMode() {
val targetMode =
if (AppConfig.isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(targetMode)
}

@ -0,0 +1,12 @@
## 文件结构介绍
* base 基类
* constant 常量
* data 数据
* help 帮助
* lib 库
* model 解析
* receiver 广播侦听
* service 服务
* ui 界面
* web web服务

@ -53,8 +53,11 @@ abstract class BaseActivity(
}
override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean {
menu?.applyOpenTint(this)
return super.onMenuOpened(featureId, menu)
menu?.let {
menu.applyOpenTint(this)
return super.onMenuOpened(featureId, menu)
}
return true
}
open fun onCompatCreateOptionsMenu(menu: Menu): Boolean {

@ -0,0 +1,24 @@
package io.legado.app.base
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var job: Job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}

@ -32,6 +32,14 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun onDestroy() {
super.onDestroy()
job.cancel()
@ -52,6 +60,8 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
}
}
open fun observeLiveBus() {
}
open fun onCompatCreateOptionsMenu(menu: Menu) {
}

@ -3,17 +3,27 @@ package io.legado.app.base
import android.app.Service
import android.content.Intent
import android.os.IBinder
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
abstract class BaseService : Service(), CoroutineScope by MainScope() {
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
cancel()

@ -5,6 +5,7 @@ import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.*
@ -15,13 +16,17 @@ import java.util.*
*
* 通用的adapter 可添加headerfooter以及不同类型item
*/
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : RecyclerView.Adapter<ItemViewHolder>() {
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
RecyclerView.Adapter<ItemViewHolder>() {
constructor(context: Context, vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) : this(context) {
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) {
addItemViewDelegates(*delegates)
}
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) {
constructor(
context: Context,
vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>
) : this(context) {
addItemViewDelegates(*delegates)
}
@ -122,7 +127,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
}
fun setItems(items: List<ITEM>?, notify: Boolean = true) {
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
@ -130,9 +135,19 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
if (items != null) {
this.items.addAll(items)
}
if (notify) {
notifyDataSetChanged()
notifyDataSetChanged()
}
}
fun setItems(items: List<ITEM>?, diffResult: DiffUtil.DiffResult) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
}
if (items != null) {
this.items.addAll(items)
}
diffResult.dispatchUpdatesTo(this)
}
}
@ -236,7 +251,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
synchronized(lock) {
val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged(fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads)
notifyItemRangeChanged(
fromPosition + getHeaderCount(),
toPosition - fromPosition + 1,
payloads
)
}
}
}
@ -271,7 +290,13 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
return footerItems?.size() ?: 0
}
fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null
fun getItem(position: Int): ITEM? =
if (position in 0 until items.size) items[position] else null
fun getItemByLayoutPosition(position: Int): ITEM? {
val pos = position - getHeaderCount()
return if (pos in 0 until items.size) items[pos] else null
}
fun getItems(): List<ITEM> = items
@ -294,7 +319,9 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
return when {
isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0
else -> getItem(getActualPosition(position))?.let {
getItemViewType(it, getActualPosition(position))
} ?: 0
}
}
@ -309,7 +336,16 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
else -> {
val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutId, parent, false))
val holder = ItemViewHolder(
inflater.inflate(
itemDelegates.getValue(viewType).layoutId,
parent,
false
)
)
itemDelegates.getValue(viewType)
.registerListener(holder)
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
@ -336,7 +372,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
}
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
final override fun onBindViewHolder(
holder: ItemViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
getItem(holder.layoutPosition - getHeaderCount())?.let {
itemDelegates.getValue(getItemViewType(holder.layoutPosition))
@ -385,10 +425,6 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
private fun addAnimation(holder: ItemViewHolder) {
if (itemAnimation == null) {
itemAnimation = ItemAnimation.create().enabled(true)
}
itemAnimation?.let {
if (it.itemAnimEnabled) {
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {

@ -9,6 +9,17 @@ import android.content.Context
*/
abstract class ItemViewDelegate<ITEM>(protected val context: Context, val layoutId: Int) {
/**
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
* 使用getItem(holder.layoutPosition)来获取item,
* 或者使用registerListener(holder: ItemViewHolder, position: Int)
*/
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
/**
* 注册事件
*/
abstract fun registerListener(holder: ItemViewHolder)
}

@ -15,8 +15,20 @@ abstract class SimpleRecyclerAdapter<ITEM>(context: Context, private val layoutI
this@SimpleRecyclerAdapter.convert(holder, item, payloads)
}
override fun registerListener(holder: ItemViewHolder) {
this@SimpleRecyclerAdapter.registerListener(holder)
}
})
}
/**
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
* 使用getItem(holder.layoutPosition)来获取item
*/
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
/**
* 注册事件
*/
abstract fun registerListener(holder: ItemViewHolder)
}

@ -1,11 +1,11 @@
package io.legado.app.constant
object Bus {
object EventBus {
const val MEDIA_BUTTON = "mediaButton"
const val RECREATE = "RECREATE"
const val UP_BOOK = "sourceDebugLog"
const val ALOUD_STATE = "aloud_state"
const val TTS_START = "ttsStart"
const val TTS_PROGRESS = "ttsStart"
const val TTS_DS = "ttsDs"
const val BATTERY_CHANGED = "batteryChanged"
const val TIME_CHANGED = "timeChanged"
@ -21,4 +21,5 @@ object Bus {
const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload"
const val UP_TABS = "upTabs"
const val SAVE_CONTENT = "saveContent"
}

@ -1,6 +1,6 @@
package io.legado.app.constant
object Action {
object IntentAction {
const val start = "start"
const val play = "play"
const val stop = "stop"
@ -17,4 +17,5 @@ object Action {
const val next = "next"
const val moveTo = "moveTo"
const val init = "init"
const val remove = "remove"
}

@ -1,12 +1,17 @@
package io.legado.app.constant
object PreferKey {
const val versionCode = "versionCode"
const val themeMode = "themeMode"
const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar"
const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch"
const val readAloudOnLine = "readAloudOnLine"
const val readAloudByPage = "readAloudByPage"
const val ttsSpeechRate = "ttsSpeechRate"
const val ttsSpeechPer = "ttsSpeechPer"
const val prevKey = "prevKeyCode"
const val nextKey = "nextKeyCode"
const val showRss = "showRss"
@ -14,10 +19,22 @@ object PreferKey {
const val recordLog = "recordLog"
const val processText = "process_text"
const val cleanCache = "cleanCache"
const val lastGroup = "lastGroup"
const val saveTabPosition = "saveTabPosition"
const val pageAnim = "pageAnim"
const val readBookFont = "readBookFont"
const val fontFolder = "fontFolder"
const val backupPath = "backupUri"
const val threadCount = "threadCount"
const val keepLight = "keep_light"
const val webService = "webService"
const val webDavUrl = "web_dav_url"
const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password"
const val changeSourceLoadToc = "changeSourceLoadToc"
const val chineseConverterType = "chineseConverterType"
const val launcherIcon = "launcherIcon"
const val textSelectAble = "selectText"
const val lastBackup = "lastBackup"
const val bodyIndent = "textIndent"
const val shareLayout = "shareLayout"
}

@ -1,14 +1,13 @@
package io.legado.app.constant
import io.legado.app.App
import io.legado.app.utils.isNightTheme
import io.legado.app.help.AppConfig
enum class Theme {
Dark, Light, Auto;
companion object {
fun getTheme(): Theme {
return if (App.INSTANCE.isNightTheme) {
return if (AppConfig.isNightTheme) {
Dark
} else Light
}

@ -16,8 +16,9 @@ import kotlinx.coroutines.launch
@Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssStar::class],
version = 5,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 8,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -50,4 +51,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun rssArticleDao(): RssArticleDao
abstract fun rssStarDao(): RssStarDao
abstract fun cookieDao(): CookieDao
abstract fun txtTocRule(): TxtTocRuleDao
}

@ -1 +1,17 @@
## 存储数据用
## 存储数据用
* dao 数据操作
* entities 数据模型
* \Book 书籍信息
* \BookChapter 目录信息
* \BookGroup 书籍分组
* \Bookmark 书签
* \BookSource 书源
* \Cookie http cookie
* \ReplaceRule 替换规则
* \RssArticle rss条目
* \RssReadRecord rss阅读记录
* \RssSource rss源
* \RssStar rss收藏
* \SearchBook 搜索结果
* \SearchKeyword 搜索关键字
* \TxtTocRule txt文件目录规则

@ -19,6 +19,9 @@ interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl")
fun getChapterList(bookUrl: String): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end")
fun getChapterList(bookUrl: String, start: Int, end: Int): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
fun getChapter(bookUrl: String, index: Int): BookChapter?

@ -17,15 +17,15 @@ interface BookDao {
@Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocal(): LiveData<List<Book>>
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocalUri(): LiveData<List<String>>
@Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc")
fun observeDownload(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `group` = :group")
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun observeByGroup(group: Int): LiveData<List<Book>>
@Query("SELECT bookUrl FROM books WHERE `group` = :group")
fun observeUrlsByGroup(group: Int): LiveData<List<String>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>
@ -42,7 +42,7 @@ interface BookDao {
val hasUpdateBooks: List<Book>
@get:Query("SELECT * FROM books")
val allBooks: List<Book>
val all: List<Book>
@get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1")
val lastReadBook: Book?
@ -57,11 +57,14 @@ interface BookDao {
fun insert(vararg book: Book)
@Update
fun update(vararg books: Book)
fun update(vararg book: Book)
@Query("delete from books where bookUrl = :bookUrl")
fun delete(bookUrl: String)
@Delete
fun delete(vararg book: Book)
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
fun upProgress(bookUrl: String, pos: Int)
@Query("update books set `group` = :newGroupId where `group` = :oldGroupId")
fun upGroup(oldGroupId: Int, newGroupId: Int)
}

@ -13,11 +13,14 @@ interface BookGroupDao {
@Query("SELECT * FROM book_groups ORDER BY `order`")
fun liveDataAll(): LiveData<List<BookGroup>>
@get:Query("SELECT MAX(groupId) FROM book_groups")
val maxId: Int
@get:Query("SELECT sum(groupId) FROM book_groups")
val idsSum: Int
@Query("SELECT * FROM book_groups ORDER BY `order`")
fun all(): List<BookGroup>
@get:Query("SELECT MAX(`order`) FROM book_groups")
val maxOrder: Int
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
val all: List<BookGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookGroup: BookGroup)

@ -14,10 +14,10 @@ interface BookSourceDao {
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
@Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
fun liveExplore(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
@Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
fun liveExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
@ -26,24 +26,12 @@ interface BookSourceDao {
@Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroupEnabled(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroupExplore(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("update book_sources set enabled = 1 where bookSourceUrl = :sourceUrl")
fun enableSection(sourceUrl: String)
@Query("update book_sources set enabled = 0 where bookSourceUrl = :sourceUrl")
fun disableSection(sourceUrl: String)
@Query("update book_sources set enabledExplore = 1 where bookSourceUrl = :sourceUrl")
fun enableSectionExplore(sourceUrl: String)
@Query("update book_sources set enabledExplore = 0 where bookSourceUrl = :sourceUrl")
fun disableSectionExplore(sourceUrl: String)
@Query("delete from book_sources where bookSourceUrl = :sourceUrl")
fun delSection(sourceUrl: String)
@Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource>
@ -53,6 +41,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource>
@get:Query("select * from book_sources where bookUrlPattern is not null || bookUrlPattern <> ''")
val hasBookUrlPattern: List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
val noGroup: List<BookSource>
@ -75,7 +66,7 @@ interface BookSourceDao {
fun update(vararg bookSource: BookSource)
@Delete
fun delete(bookSource: BookSource)
fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String)

@ -32,24 +32,15 @@ interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules WHERE id in (:ids)")
fun findByIds(vararg ids: Long): List<ReplaceRule>
@Query("update replace_rules set isEnabled = 1 where id in (:ids)")
fun enableSection(vararg ids: Long)
@Query("update replace_rules set isEnabled = 0 where id in (:ids)")
fun disableSection(vararg ids: Long)
@Query("delete from replace_rules where id in (:ids)")
fun delSection(vararg ids: Long)
@Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1
AND (scope LIKE '%' || :scope || '%' or scope = null or scope = '')"""
AND (scope LIKE '%' || :scope || '%' or scope is null or scope = '')"""
)
fun findEnabledByScope(scope: String): List<ReplaceRule>
@Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope = null or scope = '')"""
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '')"""
)
fun findEnabledByScope(name: String, origin: String): List<ReplaceRule>

@ -3,6 +3,7 @@ package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssReadRecord
@Dao
interface RssArticleDao {
@ -10,7 +11,11 @@ interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin and link = :link")
fun get(origin: String, link: String): RssArticle?
@Query("select * from rssArticles where origin = :origin order by `order` desc")
@Query(
"""select t1.link, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
from rssArticles as t1 left join rssReadRecords as t2
on t1.link = t2.record where origin = :origin order by `order` desc"""
)
fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
@ -24,4 +29,9 @@ interface RssArticleDao {
@Query("delete from rssArticles where origin = :origin")
fun delete(origin: String)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertRecord(vararg rssReadRecord: RssReadRecord)
}

@ -31,21 +31,12 @@ interface RssSourceDao {
@Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)")
fun enableSection(vararg sourceUrls: String)
@Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)")
fun disableSection(vararg sourceUrls: String)
@get:Query("select min(customOrder) from rssSources")
val minOrder: Int
@get:Query("select max(customOrder) from rssSources")
val maxOrder: Int
@Query("delete from rssSources where sourceUrl in (:sourceUrls)")
fun delSection(vararg sourceUrls: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssSource: RssSource)

@ -7,6 +7,9 @@ import io.legado.app.data.entities.RssStar
@Dao
interface RssStarDao {
@get:Query("select * from rssStars order by starTime desc")
val all: List<RssStar>
@Query("select * from rssStars where origin = :origin and link = :link")
fun get(origin: String, link: String): RssStar?

@ -19,16 +19,45 @@ interface SearchBookDao {
@Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author order by originOrder limit 1")
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder limit 1")
fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author order by originOrder")
fun getByNameAuthor(name: String, author: String): List<SearchBook>
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder")
@Query(
"""
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author
order by t2.customOrder
"""
)
fun getByNameAuthorEnable(name: String, author: String): List<SearchBook>
@Query(
"""
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author and originName like '%'||:key||'%'
order by t2.customOrder
"""
)
fun getChangeSourceSearch(name: String, author: String, key: String): List<SearchBook>
@Query(
"""
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> ''
order by t2.customOrder
"""
)
fun getEnableHasCover(name: String, author: String): List<SearchBook>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long>
@Query("delete from searchBooks where time < :time")
fun clearExpired(time: Long)
}

@ -0,0 +1,28 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.TxtTocRule
@Dao
interface TxtTocRuleDao {
@Query("select * from txtTocRules order by serialNumber")
fun observeAll(): LiveData<List<TxtTocRule>>
@get:Query("select * from txtTocRules order by serialNumber")
val all: List<TxtTocRule>
@get:Query("select * from txtTocRules where enable = 1 order by serialNumber")
val enabled: List<TxtTocRule>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rule: TxtTocRule)
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg rule: TxtTocRule)
@Delete
fun delete(vararg rule: TxtTocRule)
}

@ -10,6 +10,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
import kotlin.math.max
@Parcelize
@ -19,15 +20,15 @@ data class Book(
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var originName: String = "", //书源名称 or 本地书籍文件名
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
var customCoverUrl: String? = null, // 封面Url(用户修改)
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType
var group: Int = 0, // 自定义分组索引号
@ -48,6 +49,25 @@ data class Book(
var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
) : Parcelable, BaseBook {
fun isLocalBook(): Boolean {
return origin == BookType.local
}
fun isTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true)
}
override fun equals(other: Any?): Boolean {
if (other is Book) {
return other.bookUrl == bookUrl
}
return false
}
override fun hashCode(): Int {
return bookUrl.hashCode()
}
@Ignore
@IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null
@ -66,6 +86,8 @@ data class Book(
@IgnoredOnParcel
override var tocHtml: String? = null
fun getRealAuthor() = author.replace("作者:", "")
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
@ -77,6 +99,10 @@ data class Book(
variable = GSON.toJson(variableMap)
}
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
fun toSearchBook(): SearchBook {
return SearchBook(
name = name,

@ -33,7 +33,7 @@ data class BookChapter(
var tag: String? = null, //
var start: Long? = null, // 章节起始位置
var end: Long? = null, // 章节终止位置
var variable: String? = null
var variable: String? = null //变量
) : Parcelable {
@Ignore
@ -52,5 +52,16 @@ data class BookChapter(
variable = GSON.toJson(variableMap)
}
override fun hashCode(): Int {
return url.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is BookChapter) {
return other.url == url
}
return false
}
}

@ -8,8 +8,8 @@ import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "book_groups")
data class BookGroup(
@PrimaryKey
var groupId: Int = 0,
var groupName: String,
var order: Int = 0
@PrimaryKey
val groupId: Int = 0b1,
var groupName: String,
var order: Int = 0
) : Parcelable

@ -47,6 +47,18 @@ data class BookSource(
var ruleToc: String? = null, // 目录页规则
var ruleContent: String? = null // 正文页规则
) : Parcelable, JsExtensions {
override fun hashCode(): Int {
return bookSourceUrl.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is BookSource) {
return other.bookSourceUrl == bookSourceUrl
}
return false
}
@Ignore
@IgnoredOnParcel
private var searchRuleV: SearchRule? = null
@ -126,6 +138,16 @@ data class BookSource(
return contentRuleV!!
}
fun addGroup(group: String) {
bookSourceGroup?.let {
if (!it.contains(group)) {
bookSourceGroup = "$it;$group"
}
} ?: let {
bookSourceGroup = group
}
}
fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>()
exploreUrl?.let {

@ -24,4 +24,17 @@ data class ReplaceRule(
var isRegex: Boolean = true,
@ColumnInfo(name = "sortOrder")
var order: Int = 0
) : Parcelable
) : Parcelable {
override fun equals(other: Any?): Boolean {
if (other is ReplaceRule) {
return other.id == id
}
return super.equals(other)
}
override fun hashCode(): Int {
return id.hashCode()
}
}

@ -0,0 +1,7 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "rssReadRecords")
data class RssReadRecord(@PrimaryKey val record: String, val read: Boolean = true)

@ -39,6 +39,17 @@ data class RssSource(
var customOrder: Int = 0
) : Parcelable, JsExtensions {
override fun equals(other: Any?): Boolean {
if (other is RssSource) {
return other.sourceUrl == sourceUrl
}
return false
}
override fun hashCode(): Int {
return sourceUrl.hashCode()
}
@Throws(Exception::class)
fun getHeaderMap(): Map<String, String> {
val headerMap = HashMap<String, String>()

@ -90,6 +90,15 @@ data class SearchBook(
origins?.add(origin)
}
fun getDisplayLastChapterTitle(): String {
latestChapterTitle?.let {
if (it.isNotEmpty()) {
return it
}
}
return "无最新章节"
}
fun toBook(): Book {
return Book(
name = name,

@ -0,0 +1,14 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "txtTocRules")
data class TxtTocRule(
@PrimaryKey
var name: String = "",
var rule: String = "",
var serialNumber: Int = -1,
var enable: Boolean = true
)

@ -19,7 +19,7 @@ object ActivityHelp {
* 判断指定Activity是否存在
*/
fun isExist(activityClass: Class<*>): Boolean {
for (item in activities) {
activities.forEach { item ->
if (item.get()?.javaClass == activityClass) {
return true
}
@ -63,7 +63,7 @@ object ActivityHelp {
* 关闭指定 activity
*/
fun finishActivity(vararg activities: Activity) {
for (activity in activities) {
activities.forEach { activity ->
activity.finish()
}
}
@ -81,8 +81,8 @@ object ActivityHelp {
}
}
}
for (activityWeakReference in waitFinish) {
activityWeakReference.get()?.finish()
waitFinish.forEach {
it.get()?.finish()
}
}

@ -2,7 +2,10 @@ package io.legado.app.help
import androidx.recyclerview.widget.RecyclerView
internal class AdapterDataObserverProxy(var adapterDataObserver: RecyclerView.AdapterDataObserver, var headerCount: Int) : RecyclerView.AdapterDataObserver() {
internal class AdapterDataObserverHeader(
var adapterDataObserver: RecyclerView.AdapterDataObserver,
var headerCount: Int
) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
adapterDataObserver.onChanged()
}

@ -0,0 +1,89 @@
package io.legado.app.help
import android.content.Context
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.utils.*
object AppConfig {
fun isNightTheme(context: Context): Boolean {
return when (context.getPrefString(PreferKey.themeMode, "0")) {
"1" -> false
"2" -> true
else -> context.sysIsDarkMode()
}
}
var isNightTheme: Boolean
get() = isNightTheme(App.INSTANCE)
set(value) {
if (value) {
App.INSTANCE.putPrefString(PreferKey.themeMode, "2")
} else {
App.INSTANCE.putPrefString(PreferKey.themeMode, "1")
}
}
var isTransparentStatusBar: Boolean
get() = App.INSTANCE.getPrefBoolean("transparentStatusBar")
set(value) {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
}
var backupPath: String?
get() = App.INSTANCE.getPrefString(PreferKey.backupPath)
set(value) {
if (value.isNullOrEmpty()) {
App.INSTANCE.removePref(PreferKey.backupPath)
} else {
App.INSTANCE.putPrefString(PreferKey.backupPath, value)
}
}
var isShowRSS: Boolean
get() = App.INSTANCE.getPrefBoolean(PreferKey.showRss, true)
set(value) {
App.INSTANCE.putPrefBoolean(PreferKey.showRss, value)
}
val autoRefreshBook: Boolean
get() = App.INSTANCE.getPrefBoolean(App.INSTANCE.getString(R.string.pk_auto_refresh))
var threadCount: Int
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.threadCount, value)
}
var importBookPath: String?
get() = App.INSTANCE.getPrefString("importBookPath")
set(value) {
if (value == null) {
App.INSTANCE.removePref("importBookPath")
} else {
App.INSTANCE.putPrefString("importBookPath", value)
}
}
var ttsSpeechRate: Int
get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value)
}
val ttsSpeechPer: String
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0"
val isEInkMode: Boolean
get() = App.INSTANCE.getPrefBoolean("isEInkMode")
val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false)
var chineseConverterType: Int
get() = App.INSTANCE.getPrefInt(PreferKey.chineseConverterType)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value)
}
}

@ -1,101 +1,176 @@
package io.legado.app.help
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.App
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.*
import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File
import kotlin.math.min
object BookHelp {
private var downloadPath: String =
App.INSTANCE.getPrefString(PreferKey.downloadPath)
private const val cacheFolderName = "book_cache"
val downloadPath: String
get() = App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
fun upDownloadPath() {
downloadPath =
App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
private val downloadUri get() = Uri.parse(downloadPath)
private fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
}
private fun getBookCachePath(): String {
return "$downloadPath${File.separator}book_cache"
fun formatChapterName(bookChapter: BookChapter): String {
return String.format(
"%05d-%s.nb",
bookChapter.index,
MD5Utils.md5Encode16(bookChapter.title)
)
}
fun clearCache() {
FileHelp.deleteFile(getBookCachePath())
FileHelp.getFolder(getBookCachePath())
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
} else {
FileUtils.deleteFile(
FileUtils.getPath(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName)
)
)
}
}
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
it.delete()
return@forEach
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFileIfNotExist(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.uri?.writeText(App.INSTANCE, content)
}
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
}
val filePath = getChapterPath(book, bookChapter)
val file = FileHelp.getFile(filePath)
file.writeText(content)
postEvent(EventBus.SAVE_CONTENT, bookChapter)
}
fun getChapterCount(book: Book): Int {
return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0
fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>()
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.createFolderIfNotExist(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.let { bookDoc ->
DocumentUtils.listFiles(App.INSTANCE, bookDoc.uri).forEach {
fileNameList.add(it.name)
}
}
}
} else {
FileUtils.createFolderIfNotExist(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.let {
fileNameList.addAll(it)
}
}
return fileNameList
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter)
runCatching {
val file = File(filePath)
if (file.exists()) {
when {
book.isLocalBook() -> {
return true
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists(
root,
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
else -> {
return FileUtils.exists(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
return false
}
fun getContent(book: Book, bookChapter: BookChapter): String? {
val filePath = getChapterPath(book, bookChapter)
runCatching {
val file = File(filePath)
if (file.exists()) {
return file.readText()
when {
book.isLocalBook() -> {
return AnalyzeTxtFile.getContent(book, bookChapter)
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.uri?.readText(App.INSTANCE)
}
}
else -> {
val file = FileUtils.getFile(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
}
}
}
return null
}
fun delContent(book: Book, bookChapter: BookChapter) {
val filePath = getChapterPath(book, bookChapter)
kotlin.runCatching {
val file = File(filePath)
if (file.exists()) {
file.delete()
when {
book.isLocalBook() -> return
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile(formatChapterName(bookChapter))
?.delete()
}
}
else -> {
FileUtils.createFileIfNotExist(
File(downloadPath),
formatChapterName(bookChapter),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
}
}
private fun getBookFolder(book: Book): String {
val bookFolder = formatFolderName(book.name + MD5Utils.md5Encode16(book.bookUrl))
return "${getBookCachePath()}${File.separator}$bookFolder"
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
val chapterFile =
String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title))
return "${getBookFolder(book)}${File.separator}$chapterFile.nb"
}
private fun formatFolderName(folderName: String): String {
return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "")
}
@ -124,16 +199,16 @@ object BookHelp {
}
var newIndex = 0
val jaccardSimilarity = JaccardSimilarity()
val jSimilarity = JaccardSimilarity()
var similarity = if (chapters.size > index) {
jaccardSimilarity.apply(title, chapters[index].title)
jSimilarity.apply(title, chapters[index].title)
} else 0.0
if (similarity == 1.0) {
return index
} else {
for (i in 1..50) {
if (index - i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index - i].title).let {
jSimilarity.apply(title, chapters[index - i].title).let {
if (it > similarity) {
similarity = it
newIndex = index - i
@ -144,7 +219,7 @@ object BookHelp {
}
}
if (index + i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index + i].title).let {
jSimilarity.apply(title, chapters[index + i].title).let {
if (it > similarity) {
similarity = it
newIndex = index + i
@ -159,9 +234,11 @@ object BookHelp {
return newIndex
}
var bookName: String? = null
var bookOrigin: String? = null
var replaceRules: List<ReplaceRule> = arrayListOf()
private var bookName: String? = null
private var bookOrigin: String? = null
private var replaceRules: List<ReplaceRule> = arrayListOf()
val bodyIndent
get() = " ".repeat(App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2))
fun disposeContent(
title: String,
@ -170,7 +247,6 @@ object BookHelp {
content: String,
enableReplace: Boolean
): String {
var c = content
synchronized(this) {
if (enableReplace && (bookName != name || bookOrigin != origin)) {
replaceRules = if (origin.isNullOrEmpty()) {
@ -180,9 +256,7 @@ object BookHelp {
}
}
}
if (!content.substringBefore("\n").contains(title)) {
c = title + "\n" + c
}
var c = content
for (item in replaceRules) {
item.pattern.let {
if (it.isNotEmpty()) {
@ -194,7 +268,11 @@ object BookHelp {
}
}
}
val indent = App.INSTANCE.getPrefInt("textIndent", 2)
return c.replace("\\s*\\n+\\s*".toRegex(), "\n" + " ".repeat(indent))
c = "$title\n$c"
when (AppConfig.chineseConverterType) {
1 -> c = ZhConvertBootstrap.newInstance().toSimple(c)
2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c)
}
return c.replace("\\s*\\n+\\s*".toRegex(), "\n$bodyIndent")
}
}

@ -9,12 +9,12 @@ import android.os.Looper
import android.util.Log
import android.widget.Toast
import io.legado.app.service.TTSReadAloudService
import java.io.File
import java.io.FileOutputStream
import io.legado.app.utils.FileUtils
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
/**
* 异常管理类
@ -141,14 +141,15 @@ class CrashHandler : Thread.UncaughtExceptionHandler {
val timestamp = System.currentTimeMillis()
val time = format.format(Date())
val fileName = "crash-$time-$timestamp.log"
val path = mContext?.externalCacheDir?.toString() + "/crash/"
val dir = File(path)
if (!dir.exists()) {
dir.mkdirs()
mContext?.externalCacheDir?.let { rootFile ->
FileUtils.getDirFile(rootFile, "crash").listFiles()?.forEach {
if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) {
it.delete()
}
}
FileUtils.createFileIfNotExist(rootFile, fileName, "crash")
.writeText(sb.toString())
}
val fos = FileOutputStream(path + fileName)
fos.write(sb.toString().toByteArray())
fos.close()
}
}

@ -1,58 +0,0 @@
package io.legado.app.help
import io.legado.app.App
import java.io.File
import java.io.IOException
object FileHelp {
//获取文件夹
fun getFolder(filePath: String): File {
val file = File(filePath)
//如果文件夹不存在,就创建它
if (!file.exists()) {
file.mkdirs()
}
return file
}
//获取文件
@Synchronized
fun getFile(filePath: String): File {
val file = File(filePath)
try {
if (!file.exists()) {
//创建父类文件夹
getFolder(file.parent)
//创建文件
file.createNewFile()
}
} catch (e: IOException) {
e.printStackTrace()
}
return file
}
fun getCachePath(): String {
return App.INSTANCE.externalCacheDir?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
}
//递归删除文件夹下的数据
@Synchronized
fun deleteFile(filePath: String) {
val file = File(filePath)
if (!file.exists()) return
if (file.isDirectory) {
val files = file.listFiles()
files?.forEach { subFile ->
val path = subFile.path
deleteFile(path)
}
}
//删除文件
file.delete()
}
}

@ -0,0 +1,34 @@
package io.legado.app.help
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.base.adapter.ItemViewHolder
class FirstTopListUpCallback : ListUpdateCallback {
var firstInsert = -1
lateinit var adapter: RecyclerView.Adapter<ItemViewHolder>
override fun onChanged(position: Int, count: Int, payload: Any?) {
adapter.notifyItemRangeChanged(position, count, payload)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
if (toPosition == 0) {
firstInsert = 0
}
adapter.notifyItemMoved(fromPosition, toPosition)
}
override fun onInserted(position: Int, count: Int) {
if (firstInsert == -1 || firstInsert > position) {
firstInsert = position
}
adapter.notifyItemRangeInserted(position, count)
}
override fun onRemoved(position: Int, count: Int) {
adapter.notifyItemRangeRemoved(position, count)
}
}

@ -12,13 +12,15 @@ import java.io.File
object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> {
if (path?.startsWith("http", true) == true) {
return Glide.with(context).load(path)
return when {
path.isNullOrEmpty() -> Glide.with(context).load(path)
path.startsWith("http", true) -> Glide.with(context).load(path)
else -> try {
Glide.with(context).load(File(path))
} catch (e: Exception) {
Glide.with(context).load(path)
}
}
kotlin.runCatching {
return Glide.with(context).load(File(path))
}
return Glide.with(context).load(path)
}
fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> {

@ -88,16 +88,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
srcViewHolder: RecyclerView.ViewHolder,
targetViewHolder: RecyclerView.ViewHolder
): Boolean {
onItemTouchCallbackListener?.let {
return it.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition)
}
return false
return onItemTouchCallbackListener
?.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition)
?: false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
onItemTouchCallbackListener?.let {
return it.onSwiped(viewHolder.adapterPosition)
}
onItemTouchCallbackListener?.onSwiped(viewHolder.adapterPosition)
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
@ -107,13 +104,21 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
viewPager?.requestDisallowInterceptTouchEvent(swiping)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
onItemTouchCallbackListener?.clearView(recyclerView, viewHolder)
}
interface OnItemTouchCallbackListener {
/**
* 当某个Item被滑动删除的时候
*
* @param adapterPosition item的position
*/
fun onSwiped(adapterPosition: Int)
fun onSwiped(adapterPosition: Int) {
}
/**
* 当两个Item位置互换的时候被回调
@ -122,6 +127,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
* @param targetPosition 目的地的Item的position
* @return 开发者处理了操作应该返回true开发者没有处理就返回false
*/
fun onMove(srcPosition: Int, targetPosition: Int): Boolean
fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
return true
}
fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
}
}
}

@ -0,0 +1,65 @@
package io.legado.app.help
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import io.legado.app.App
import io.legado.app.R
import io.legado.app.ui.welcome.Launcher1
import io.legado.app.ui.welcome.Launcher2
import io.legado.app.ui.welcome.Launcher3
import io.legado.app.ui.welcome.WelcomeActivity
import org.jetbrains.anko.toast
/**
* Created by GKF on 2018/2/27.
* 更换图标
*/
object LauncherIconHelp {
private val packageManager: PackageManager = App.INSTANCE.packageManager
private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name)
)
fun changeIcon(icon: String?) {
if (icon.isNullOrEmpty()) return
if (Build.VERSION.SDK_INT < 26) {
App.INSTANCE.toast(R.string.chage_icon_error)
return
}
var hasEnabled = false
componentNames.forEach {
if (icon.equals(it.className.substringAfterLast("."), true)) {
hasEnabled = true
//启用
packageManager.setComponentEnabledSetting(
it,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
} else {
//禁用
packageManager.setComponentEnabledSetting(
it,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
}
if (hasEnabled) {
packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
} else {
packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
}
}

@ -6,14 +6,10 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import io.legado.app.App
import io.legado.app.R
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException
/**
* 阅读界面配置
@ -23,6 +19,12 @@ object ReadBookConfig {
private val configFilePath =
App.INSTANCE.filesDir.absolutePath + File.separator + readConfigFileName
val configList: ArrayList<Config> = arrayListOf()
private val defaultConfigs by lazy {
val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes())
GSON.fromJsonArray<Config>(json)!!
}
val durConfig
get() = getConfig(styleSelect)
var styleSelect
get() = App.INSTANCE.getPrefInt("readStyleSelect")
@ -34,28 +36,30 @@ object ReadBookConfig {
}
@Synchronized
fun getConfig(index: Int = styleSelect): Config {
fun getConfig(index: Int): Config {
if (configList.size < 5) {
reset()
resetAll()
}
return configList[index]
}
fun upConfig() {
val configFile = File(configFilePath)
val json = if (configFile.exists()) {
configFile.readText()
} else {
String(App.INSTANCE.assets.open(readConfigFileName).readBytes())
(getConfigs() ?: defaultConfigs).let {
configList.clear()
configList.addAll(it)
}
try {
GSON.fromJsonArray<Config>(json)?.let {
configList.clear()
configList.addAll(it)
} ?: reset()
} catch (e: Exception) {
reset()
}
private fun getConfigs(): List<Config>? {
val configFile = File(configFilePath)
if (configFile.exists()) {
try {
val json = configFile.readText()
return GSON.fromJsonArray(json)
} catch (e: Exception) {
}
}
return null
}
fun upBg() {
@ -63,55 +67,63 @@ object ReadBookConfig {
val dm = resources.displayMetrics
val width = dm.widthPixels
val height = dm.heightPixels
bg = getConfig().bgDrawable(width, height)
bg = durConfig.bgDrawable(width, height)
}
fun save() {
GlobalScope.launch(IO) {
Coroutine.async {
val json = GSON.toJson(configList)
val configFile = File(configFilePath)
//获取流并存储
try {
BufferedWriter(FileWriter(configFile)).use { writer ->
writer.write(json)
writer.flush()
}
} catch (e: IOException) {
e.printStackTrace()
}
FileUtils.createFileIfNotExist(configFilePath).writeText(json)
}
}
private fun reset() {
val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes())
GSON.fromJsonArray<Config>(json)?.let {
fun resetDur() {
defaultConfigs[styleSelect].let {
durConfig.setBg(it.bgType(), it.bgStr())
durConfig.setTextColor(it.textColor())
upBg()
save()
}
}
private fun resetAll() {
defaultConfigs.let {
configList.clear()
configList.addAll(it)
save()
}
save()
}
data class Config(
var bgStr: String = "#EEEEEE",
var bgStrNight: String = "#000000",
var bgType: Int = 0,
var bgTypeNight: Int = 0,
var darkStatusIcon: Boolean = true,
var darkStatusIconNight: Boolean = false,
var letterSpacing: Float = 1f,
var lineSpacingExtra: Int = 12,
var lineSpacingMultiplier: Float = 1.2f,
var paddingBottom: Int = 0,
var bgStr: String = "#EEEEEE",//白天背景
var bgStrNight: String = "#000000",//夜间背景
var bgType: Int = 0,//白天背景类型
var bgTypeNight: Int = 0,//夜间背景类型
var darkStatusIcon: Boolean = true,//白天是否暗色状态栏
var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
var textColor: String = "#3E3D3B",//白天文字颜色
var textColorNight: String = "#adadad",//夜间文字颜色
var textBold: Boolean = false,//是否粗体字
var textSize: Int = 15,//文字大小
var letterSpacing: Float = 1f,//字间距
var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,//段距
var titleCenter: Boolean = true,//标题居中
var paddingBottom: Int = 6,
var paddingLeft: Int = 16,
var paddingRight: Int = 16,
var paddingTop: Int = 0,
var textBold: Boolean = false,
var textColor: String = "#3E3D3B",
var textColorNight: String = "#adadad",
var textSize: Int = 15
var paddingTop: Int = 6,
var headerPaddingBottom: Int = 0,
var headerPaddingLeft: Int = 16,
var headerPaddingRight: Int = 16,
var headerPaddingTop: Int = 0,
var footerPaddingBottom: Int = 6,
var footerPaddingLeft: Int = 16,
var footerPaddingRight: Int = 16,
var footerPaddingTop: Int = 6
) {
fun setBg(bgType: Int, bg: String) {
if (App.INSTANCE.isNightTheme) {
if (AppConfig.isNightTheme) {
bgTypeNight = bgType
bgStrNight = bg
} else {
@ -121,15 +133,16 @@ object ReadBookConfig {
}
fun setTextColor(color: Int) {
if (App.INSTANCE.isNightTheme) {
if (AppConfig.isNightTheme) {
textColorNight = "#${color.hexString}"
} else {
textColor = "#${color.hexString}"
}
ChapterProvider.upStyle(this)
}
fun setStatusIconDark(isDark: Boolean) {
if (App.INSTANCE.isNightTheme) {
if (AppConfig.isNightTheme) {
darkStatusIconNight = isDark
} else {
darkStatusIcon = isDark
@ -137,7 +150,7 @@ object ReadBookConfig {
}
fun statusIconDark(): Boolean {
return if (App.INSTANCE.isNightTheme) {
return if (AppConfig.isNightTheme) {
darkStatusIconNight
} else {
darkStatusIcon
@ -145,17 +158,17 @@ object ReadBookConfig {
}
fun textColor(): Int {
return if (App.INSTANCE.isNightTheme) Color.parseColor(textColorNight)
return if (AppConfig.isNightTheme) Color.parseColor(textColorNight)
else Color.parseColor(textColor)
}
fun bgStr(): String {
return if (App.INSTANCE.isNightTheme) bgStrNight
return if (AppConfig.isNightTheme) bgStrNight
else bgStr
}
fun bgType(): Int {
return if (App.INSTANCE.isNightTheme) bgTypeNight
return if (AppConfig.isNightTheme) bgTypeNight
else bgType
}

@ -76,7 +76,7 @@ class CompositeCoroutine : CoroutineContainer {
resources = null
}
set?.forEachIndexed { index, coroutine ->
set?.forEachIndexed { _, coroutine ->
coroutine.cancel()
}
}

@ -1,102 +0,0 @@
package io.legado.app.help.http
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() {
companion object {
fun create(): CoroutinesCallAdapterFactory {
return CoroutinesCallAdapterFactory()
}
}
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (Deferred::class.java != getRawType(returnType)) {
return null
}
check(returnType is ParameterizedType) { "Deferred return type must be parameterized as Deferred<Foo> or Deferred<out Foo>" }
val responseType = getParameterUpperBound(0, returnType)
val rawDeferredType = getRawType(responseType)
return if (rawDeferredType == Response::class.java) {
check(responseType is ParameterizedType) { "Response must be parameterized as Response<Foo> or Response<out Foo>" }
ResponseCallAdapter<Any>(
getParameterUpperBound(
0,
responseType
)
)
} else {
BodyCallAdapter<Any>(responseType)
}
}
private class BodyCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Deferred<T>> {
override fun responseType() = responseType
override fun adapt(call: Call<T>): Deferred<T> {
val deferred = CompletableDeferred<T>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
deferred.complete(response.body()!!)
} else {
deferred.completeExceptionally(HttpException(response))
}
}
})
return deferred
}
}
private class ResponseCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Deferred<Response<T>>> {
override fun responseType() = responseType
override fun adapt(call: Call<T>): Deferred<Response<T>> {
val deferred = CompletableDeferred<Response<T>>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
deferred.complete(response)
}
})
return deferred
}
}
}

@ -1,5 +1,6 @@
package io.legado.app.help.http
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.ResponseBody
import retrofit2.Converter
@ -16,20 +17,22 @@ class EncodeConverter(private val encode: String? = null) : Converter.Factory()
): Converter<ResponseBody, String>? {
return Converter { value ->
val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes())
encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) }
var charsetName: String? = encode
var charsetName: String? = null
val mediaType = value.contentType()
//根据http头判断
if (mediaType != null) {
val charset = mediaType.charset()
charsetName = charset?.displayName()
charsetName?.let {
try {
return@Converter String(responseBytes, Charset.forName(charsetName))
} catch (e: Exception) {
}
}
if (charsetName == null) {
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
//根据http头判断
value.contentType()?.charset()?.let {
return@Converter String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
String(responseBytes, Charset.forName(charsetName))
}
}

@ -1,5 +1,7 @@
package io.legado.app.help.http
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import retrofit2.Retrofit
@ -7,6 +9,7 @@ import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@Suppress("unused")
object HttpHelper {
val client: OkHttpClient by lazy {
@ -35,11 +38,26 @@ object HttpHelper {
builder.build()
}
inline fun <reified T> getApiService(baseUrl: String): T {
return getRetrofit(baseUrl).create(T::class.java)
fun simpleGet(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode)
.get(url, mapOf())
.execute()
return response.body()
}
return null
}
suspend fun simpleGetAsync(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode)
.getAsync(url, mapOf())
return response.body()
}
return null
}
inline fun <reified T> getApiService(baseUrl: String, encode: String): T {
inline fun <reified T> getApiService(baseUrl: String, encode: String? = null): T {
return getRetrofit(baseUrl, encode).create(T::class.java)
}
@ -47,8 +65,6 @@ object HttpHelper {
return Retrofit.Builder().baseUrl(baseUrl)
//增加返回值为字符串的支持(以实体类返回)
.addConverterFactory(EncodeConverter(encode))
//增加返回值为Observable<T>的支持
.addCallAdapterFactory(CoroutinesCallAdapterFactory.create())
.client(client)
.build()
}
@ -56,8 +72,6 @@ object HttpHelper {
fun getByteRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(ByteConverter())
//增加返回值为Observable<T>的支持
.addCallAdapterFactory(CoroutinesCallAdapterFactory.create())
.client(client)
.build()
}

@ -14,11 +14,9 @@ import javax.net.ssl.*
object SSLHelper {
val sslSocketFactory: SSLParams?
get() = getSslSocketFactoryBase(null, null, null)
/**
* 为了解决客户端不信任服务器数字证书的问题网络上大部分的解决方案都是让客户端不对证书做任何检查
* 为了解决客户端不信任服务器数字证书的问题
* 网络上大部分的解决方案都是让客户端不对证书做任何检查
* 这是一种有很大安全漏洞的办法
*/
val unsafeTrustManager: X509TrustManager = object : X509TrustManager {
@ -141,7 +139,7 @@ object SSLHelper {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null)
for ((index, certStream) in certificates.withIndex()) {
val certificateAlias = Integer.toString(index)
val certificateAlias = index.toString()
// 证书工厂根据证书文件的流生成证书 cert
val cert = certificateFactory.generateCertificate(certStream)
// 将 cert 作为可信证书放入到keyStore中

@ -1,6 +1,5 @@
package io.legado.app.data.api
package io.legado.app.help.http.api
import kotlinx.coroutines.Deferred
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
@ -12,20 +11,20 @@ import retrofit2.http.Url
* Created by GKF on 2018/1/21.
* get web content
*/
interface IHttpGetApi {
@Suppress("unused")
interface HttpGetApi {
@GET
fun getAsync(
suspend fun getAsync(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
): Response<String>
@GET
fun getMapAsync(
suspend fun getMapAsync(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
): Response<String>
@GET
fun get(
@ -39,4 +38,11 @@ interface IHttpGetApi {
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
suspend fun getMapByteAsync(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
}

@ -1,6 +1,5 @@
package io.legado.app.data.api
package io.legado.app.help.http.api
import kotlinx.coroutines.Deferred
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
@ -10,23 +9,23 @@ import retrofit2.http.*
* Created by GKF on 2018/1/29.
* post
*/
interface IHttpPostApi {
@Suppress("unused")
interface HttpPostApi {
@FormUrlEncoded
@POST
fun postMapAsync(
suspend fun postMapAsync(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
): Response<String>
@POST
fun postBodyAsync(
suspend fun postBodyAsync(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
): Response<String>
@FormUrlEncoded
@POST
@ -45,9 +44,9 @@ interface IHttpPostApi {
@FormUrlEncoded
@POST
fun postMapByteAsync(
suspend fun postMapByteAsync(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Deferred<Response<ByteArray>>
): Response<ByteArray>
}

@ -1,5 +1,7 @@
package io.legado.app.help.permission
interface OnPermissionsDeniedCallback {
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)
}

@ -43,7 +43,7 @@ internal class Request : OnRequestPermissionsResultCallback {
}
fun addPermissions(vararg permissions: String) {
this.permissions?.addAll(Arrays.asList(*permissions))
this.permissions?.addAll(listOf(*permissions))
}
fun setRequestCode(requestCode: Int) {

@ -33,6 +33,7 @@ internal object RequestManager : OnPermissionsResultCallback {
if (index >= 0) {
val to = it.size - 1
if (index != to) {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
Collections.swap(requests, index, to)
}
} else {

@ -4,23 +4,21 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.help.FileHelp
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences
import java.io.File
import java.util.concurrent.TimeUnit
object Backup {
val backupPath = App.INSTANCE.filesDir.absolutePath + File.separator + "backup"
val defaultPath by lazy {
FileUtils.getSdCardPath() + File.separator + "YueDu"
val backupPath: String by lazy {
FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath
}
val legadoPath by lazy {
@ -33,52 +31,37 @@ object Backup {
val backupFileNames by lazy {
arrayOf(
"bookshelf.json",
"bookGroup.json",
"bookSource.json",
"rssSource.json",
"replaceRule.json",
ReadBookConfig.readConfigFileName,
"config.xml"
"bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
"rssStar.json", "replaceRule.json", ReadBookConfig.readConfigFileName, "config.xml"
)
}
suspend fun backup(context: Context, uri: Uri?) {
withContext(IO) {
App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookshelf.json").writeText(json)
}
}
App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookGroup.json").writeText(json)
}
}
App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookSource.json")
.writeText(json)
}
}
App.db.rssSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "rssSource.json").writeText(json)
}
}
App.db.replaceRuleDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "replaceRule.json")
.writeText(json)
}
fun autoBack(context: Context) {
val lastBackup = context.getPrefLong(PreferKey.lastBackup)
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
return
}
Coroutine.async {
val backupPath = context.getPrefString(PreferKey.backupPath)
if (backupPath.isNullOrEmpty()) {
backup(context)
} else {
backup(context, backupPath)
}
}
}
suspend fun backup(context: Context, path: String = legadoPath) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) {
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileHelp.getFile(backupPath + File.separator + ReadBookConfig.readConfigFileName)
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
@ -96,37 +79,50 @@ object Backup {
edit.commit()
}
WebDavHelp.backUpWebDav(backupPath)
if (uri != null) {
copyBackup(context, uri)
if (path.isContentPath()) {
copyBackup(context, Uri.parse(path))
} else {
copyBackup()
copyBackup(File(path))
}
}
}
private fun writeListToJson(list: List<Any>, fileName: String, path: String) {
if (list.isNotEmpty()) {
val json = GSON.toJson(list)
FileUtils.createFileIfNotExist(path + File.separator + fileName).writeText(json)
}
}
@Throws(java.lang.Exception::class)
private fun copyBackup(context: Context, uri: Uri) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) {
val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName)
doc?.let {
DocumentUtils.writeText(
context,
FileHelp.getFile(backupPath + File.separator + fileName).readText(),
doc.uri
)
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName)
doc?.let {
DocumentUtils.writeText(
context,
file.readText(),
doc.uri
)
}
}
}
}
}
private fun copyBackup() {
try {
for (fileName in backupFileNames) {
FileHelp.getFile(backupPath + File.separator + "bookshelf.json")
.copyTo(FileHelp.getFile(legadoPath + File.separator + "bookshelf.json"), true)
@Throws(java.lang.Exception::class)
private fun copyBackup(rootFile: File) {
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(
FileUtils.createFileIfNotExist(rootFile, fileName),
true
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

@ -0,0 +1,149 @@
package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast
import java.io.File
object ImportOldData {
val yueDuPath by lazy {
FileUtils.getSdCardPath() + File.separator + "YueDu"
}
fun import(context: Context) {
GlobalScope.launch(Dispatchers.IO) {
try {// 导入书架
val shelfFile =
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书籍${importCount}")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
}
try {// Book source
val sourceFile =
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入源失败\n${e.localizedMessage}")
}
}
try {// Replace rules
val ruleFile =
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookReplaceRule.json")
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
context.toast("成功导入替换规则${importCount}")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
fun importUri(uri: Uri) {
Coroutine.async {
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach {
when (it.name) {
"myBookShelf.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldBookshelf(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书籍${importCount}")
}
}
} catch (e: java.lang.Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}")
}
}
"myBookSource.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldSource(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入书源${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入源失败\n${e.localizedMessage}")
}
}
"myBookReplaceRule.json" ->
try {
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json ->
val importCount = importOldReplaceRule(json)
withContext(Dispatchers.Main) {
App.INSTANCE.toast("成功导入替换规则${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
}
}
fun importOldBookshelf(json: String): Int {
val books = OldBook.toNewBook(json)
App.db.bookDao().insert(*books.toTypedArray())
return books.size
}
fun importOldSource(json: String): Int {
val bookSources = mutableListOf<BookSource>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToBookSource(jsonItem.jsonString())?.let {
bookSources.add(it)
}
}
App.db.bookSourceDao().insert(*bookSources.toTypedArray())
return bookSources.size
}
fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let {
replaceRules.add(it)
}
}
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray())
return replaceRules.size
}
}

@ -0,0 +1,55 @@
package io.legado.app.help.storage
import android.util.Log
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.Book
import io.legado.app.utils.readBool
import io.legado.app.utils.readInt
import io.legado.app.utils.readLong
import io.legado.app.utils.readString
object OldBook {
fun toNewBook(json: String): List<Book> {
val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao().allBookUrls.toSet()
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
val book = Book()
book.bookUrl = jsonItem.readString("$.noteUrl") ?: ""
if (book.bookUrl.isBlank()) continue
book.name = jsonItem.readString("$.bookInfoBean.name") ?: ""
if (book.bookUrl in existingBooks) {
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}")
continue
}
book.origin = jsonItem.readString("$.tag") ?: ""
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: ""
book.author = jsonItem.readString("$.bookInfoBean.author") ?: ""
book.type =
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl")
book.customCoverUrl = jsonItem.readString("$.customCoverPath")
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0
book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0
book.order = jsonItem.readInt("$.serialNumber") ?: 0
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true
book.variable = jsonItem.readString("$.variable")
books.add(book)
}
return books
}
}

@ -30,7 +30,6 @@ object Preferences {
val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir")
fieldMPreferencesDir.isAccessible = true
// 创建自定义路径
// String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Android";
val file = File(dir)
// 修改mPreferencesDir变量的值
fieldMPreferencesDir.set(objMBase, file)

@ -2,25 +2,20 @@ package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.*
import io.legado.app.help.FileHelp
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.toast
import java.io.File
object Restore {
@ -32,17 +27,35 @@ object Restore {
)
}
suspend fun restore(context: Context, uri: Uri) {
suspend fun restore(context: Context, path: String) {
withContext(IO) {
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
for (fileName in Backup.backupFileNames) {
if (doc.name == fileName) {
DocumentUtils.readText(context, doc.uri)?.let {
FileHelp.getFile(Backup.backupPath + File.separator + fileName)
.writeText(it)
if (path.isContentPath()) {
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
for (fileName in Backup.backupFileNames) {
if (doc.name == fileName) {
DocumentUtils.readText(context, doc.uri)?.let {
FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName)
.writeText(it)
}
}
}
}
} else {
try {
val file = File(path)
for (fileName in Backup.backupFileNames) {
FileUtils.getFile(file, fileName).let {
if (it.exists()) {
it.copyTo(
FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName),
true
)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
restore(Backup.backupPath)
@ -50,54 +63,27 @@ object Restore {
suspend fun restore(path: String) {
withContext(IO) {
try {
val file = FileHelp.getFile(path + File.separator + "bookshelf.json")
val json = file.readText()
GSON.fromJsonArray<Book>(json)?.let {
App.db.bookDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<Book>(path, "bookshelf.json")?.let {
App.db.bookDao().insert(*it.toTypedArray())
}
try {
val file = FileHelp.getFile(path + File.separator + "bookGroup.json")
val json = file.readText()
GSON.fromJsonArray<BookGroup>(json)?.let {
App.db.bookGroupDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<BookGroup>(path, "bookGroup.json")?.let {
App.db.bookGroupDao().insert(*it.toTypedArray())
}
try {
val file = FileHelp.getFile(path + File.separator + "bookSource.json")
val json = file.readText()
GSON.fromJsonArray<BookSource>(json)?.let {
App.db.bookSourceDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao().insert(*it.toTypedArray())
}
try {
val file = FileHelp.getFile(path + File.separator + "rssSource.json")
val json = file.readText()
GSON.fromJsonArray<RssSource>(json)?.let {
App.db.rssSourceDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<RssSource>(path, "rssSource.json")?.let {
App.db.rssSourceDao().insert(*it.toTypedArray())
}
try {
val file = FileHelp.getFile(path + File.separator + "replaceRule.json")
val json = file.readText()
GSON.fromJsonArray<ReplaceRule>(json)?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<RssStar>(path, "rssStar.json")?.let {
App.db.rssStarDao().insert(*it.toTypedArray())
}
fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray())
}
try {
val file =
FileHelp.getFile(path + File.separator + ReadBookConfig.readConfigFileName)
FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.readConfigFileName)
val configFile =
File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName)
if (file.exists()) {
@ -117,122 +103,22 @@ object Restore {
is String -> edit.putString(it.key, value)
else -> Unit
}
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
edit.commit()
}
}
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
}
fun importYueDuData(context: Context) {
GlobalScope.launch(IO) {
try {// 导入书架
val shelfFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
withContext(Main) {
context.toast("成功导入书籍${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
}
try {// Book source
val sourceFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
withContext(Main) {
context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入源失败\n${e.localizedMessage}")
}
}
try {// Replace rules
val ruleFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json")
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
withContext(Main) {
context.toast("成功导入替换规则${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
fun importOldBookshelf(json: String): Int {
val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao().allBookUrls.toSet()
for (item in items) {
val jsonItem = jsonPath.parse(item)
val book = Book()
book.bookUrl = jsonItem.readString("$.noteUrl") ?: ""
if (book.bookUrl.isBlank()) continue
book.name = jsonItem.readString("$.bookInfoBean.name") ?: ""
if (book.bookUrl in existingBooks) {
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}")
continue
}
book.origin = jsonItem.readString("$.tag") ?: ""
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: ""
book.author = jsonItem.readString("$.bookInfoBean.author") ?: ""
book.type =
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl")
book.customCoverUrl = jsonItem.readString("$.customCoverPath")
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0
book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0
book.order = jsonItem.readInt("$.serialNumber") ?: 0
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true
book.variable = jsonItem.readString("$.variable")
books.add(book)
}
App.db.bookDao().insert(*books.toTypedArray())
return books.size
}
fun importOldSource(json: String): Int {
val bookSources = mutableListOf<BookSource>()
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = jsonPath.parse(item)
OldRule.jsonToBookSource(jsonItem.jsonString())?.let {
bookSources.add(it)
}
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
val json = file.readText()
return GSON.fromJsonArray(json)
} catch (e: Exception) {
e.printStackTrace()
}
App.db.bookSourceDao().insert(*bookSources.toTypedArray())
return bookSources.size
return null
}
fun importOldReplaceRule(json: String): Int {
val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = jsonPath.parse(item)
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let {
replaceRules.add(it)
}
}
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray())
return replaceRules.size
}
}

@ -1,38 +1,42 @@
package io.legado.app.help.storage
import android.content.Context
import android.os.Handler
import android.os.Looper
import io.legado.app.App
import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.constant.PreferKey
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth
import io.legado.app.utils.FileUtils
import io.legado.app.utils.ZipUtils
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.jetbrains.anko.selector
import org.jetbrains.anko.toast
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.min
object WebDavHelp {
private val zipFilePath = FileHelp.getCachePath() + "/backup" + ".zip"
private val unzipFilesPath by lazy {
FileHelp.getCachePath()
}
private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/"
private val zipFilePath = "${FileUtils.getCachePath()}${File.separator}backup.zip"
private fun getWebDavUrl(): String? {
var url = App.INSTANCE.getPrefString("web_dav_url")
if (url.isNullOrBlank()) return null
private fun getWebDavUrl(): String {
var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl)
if (url.isNullOrEmpty()) {
url = defaultWebDavUrl
}
if (!url.endsWith("/")) url += "/"
return url
}
private fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString("web_dav_account")
val password = App.INSTANCE.getPrefString("web_dav_password")
val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password)
return true
@ -43,24 +47,30 @@ object WebDavHelp {
private fun getWebDavFileNames(): ArrayList<String> {
val url = getWebDavUrl()
val names = arrayListOf<String>()
if (!url.isNullOrBlank() && initWebDav()) {
var files = WebDav(url + "legado/").listFiles()
files = files.reversed()
for (index: Int in 0 until min(10, files.size)) {
files[index].displayName?.let {
names.add(it)
if (initWebDav()) {
try {
var files = WebDav(url + "legado/").listFiles()
files = files.reversed()
for (index: Int in 0 until min(10, files.size)) {
files[index].displayName?.let {
names.add(it)
}
}
} catch (e: Exception) {
return names
}
}
return names
}
suspend fun showRestoreDialog(context: Context): Boolean {
suspend fun showRestoreDialog(context: Context, restoreSuccess: () -> Unit): Boolean {
val names = withContext(IO) { getWebDavFileNames() }
return if (names.isNotEmpty()) {
context.selector(title = "选择恢复文件", items = names) { _, index ->
if (index in 0 until names.size) {
restoreWebDav(names[index])
withContext(Main) {
context.selector(title = "选择恢复文件", items = names) { _, index ->
if (index in 0 until names.size) {
restoreWebDav(names[index], restoreSuccess)
}
}
}
true
@ -69,35 +79,39 @@ object WebDavHelp {
}
}
private fun restoreWebDav(name: String) {
private fun restoreWebDav(name: String, success: () -> Unit) {
Coroutine.async {
getWebDavUrl()?.let {
getWebDavUrl().let {
val file = WebDav(it + "legado/" + name)
file.downloadTo(zipFilePath, true)
@Suppress("BlockingMethodInNonBlockingContext")
ZipUtils.unzipFile(zipFilePath, unzipFilesPath)
Restore.restore(unzipFilesPath)
ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
Restore.restore(Backup.backupPath)
}
}.onSuccess {
success.invoke()
}
}
fun backUpWebDav(path: String) {
if (initWebDav()) {
val paths = arrayListOf(
path + File.separator + "bookshelf.json",
path + File.separator + "bookSource.json",
path + File.separator + "rssSource.json",
path + File.separator + "replaceRule.json",
path + File.separator + "config.xml",
path + File.separator + ReadBookConfig.readConfigFileName
)
FileHelp.deleteFile(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) {
WebDav(getWebDavUrl() + "legado").makeAsDir()
val putUrl = getWebDavUrl() + "legado/backup" +
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis())) + ".zip"
WebDav(putUrl).upload(zipFilePath)
try {
if (initWebDav()) {
val paths = arrayListOf(*Backup.backupFileNames)
for (i in 0 until paths.size) {
paths[i] = path + File.separator + paths[i]
}
FileUtils.deleteFile(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) {
WebDav(getWebDavUrl() + "legado").makeAsDir()
val putUrl = getWebDavUrl() + "legado/backup" +
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis())) + ".zip"
WebDav(putUrl).upload(zipFilePath)
}
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
App.INSTANCE.toast("WebDav\n${e.localizedMessage}")
}
}
}

@ -1 +1,4 @@
## 放置一些copy过来的库
## 放置一些copy过来的库
* dialogs 弹出框
* theme 主题
* webDav 网络存储

@ -24,10 +24,8 @@ import android.content.DialogInterface
import android.graphics.drawable.Drawable
import android.view.KeyEvent
import android.view.View
import android.view.ViewManager
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.jetbrains.anko.UI
import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER
import kotlin.DeprecationLevel.ERROR

@ -16,9 +16,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.R
import io.legado.app.help.AppConfig
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.isNightTheme
import io.legado.app.utils.isTransparentStatusBar
import kotlinx.android.synthetic.main.activity_main.view.*
import org.jetbrains.anko.backgroundColor
@ -37,7 +36,7 @@ object ATH {
}
fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) {
val isTransparentStatusBar = activity.isTransparentStatusBar
val isTransparentStatusBar = AppConfig.isTransparentStatusBar
setStatusBarColor(
activity,
ThemeStore.statusBarColor(activity, isTransparentStatusBar),
@ -131,14 +130,14 @@ object ATH {
fun setTint(
view: View,
@ColorInt color: Int,
isDark: Boolean = view.context.isNightTheme
isDark: Boolean = AppConfig.isNightTheme(view.context)
) {
TintHelper.setTintAuto(view, color, false, isDark)
}
fun setBackgroundTint(
view: View, @ColorInt color: Int,
isDark: Boolean = view.context.isNightTheme
isDark: Boolean = AppConfig.isNightTheme
) {
TintHelper.setTintAuto(view, color, true, isDark)
}
@ -206,10 +205,7 @@ object ATH {
.setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create()
itemIconTintList = colorStateList
itemTextColor = colorStateList
itemBackgroundResource = when(context.isNightTheme) {
true -> R.drawable.item_bg_dark
false -> R.drawable.item_bg_light
}
itemBackgroundResource = R.color.background_menu
}
}

@ -8,10 +8,6 @@ import androidx.annotation.AttrRes
*/
object ATHUtils {
fun isWindowBackgroundDark(context: Context): Boolean {
return !ColorUtils.isColorLight(resolveColor(context, android.R.attr.windowBackground))
}
@JvmOverloads
fun resolveColor(context: Context, @AttrRes attr: Int, fallback: Int = 0): Int {
val a = context.theme.obtainStyledAttributes(intArrayOf(attr))

@ -192,6 +192,7 @@ object TintHelper {
}
}
@SuppressLint("PrivateResource")
fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList(
arrayOf(
@ -254,11 +255,10 @@ object TintHelper {
if (!skipIndeterminate)
progressBar.indeterminateTintList = sl
} else {
val mode = PorterDuff.Mode.SRC_IN
if (!skipIndeterminate && progressBar.indeterminateDrawable != null)
progressBar.indeterminateDrawable.setColorFilter(color, mode)
progressBar.indeterminateDrawable.setTint(color)
if (progressBar.progressDrawable != null)
progressBar.progressDrawable.setColorFilter(color, mode)
progressBar.progressDrawable.setTint(color)
}
}
@ -291,6 +291,7 @@ object TintHelper {
setCursorTint(editText, color)
}
@SuppressLint("PrivateResource")
fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList(
arrayOf(

@ -1,27 +0,0 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import io.legado.app.lib.theme.ColorUtils
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
class ATEAccentBgTextView(context: Context, attrs: AttributeSet) :
AppCompatTextView(context, attrs) {
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ATEAccentBgTextView)
val radios =
typedArray.getDimensionPixelOffset(R.styleable.ATEAccentBgTextView_abt_radius, 0)
typedArray.recycle()
background = Selector.shapeBuild()
.setCornerRadius(radios)
.setDefaultBgColor(ThemeStore.accentColor(context))
.setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context)))
.create()
setTextColor(Color.WHITE)
}
}

@ -1,31 +0,0 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.legado.app.R
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.dp
import io.legado.app.utils.getCompatColor
class ATEStrokeTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) {
init {
background = Selector.shapeBuild()
.setCornerRadius(1.dp)
.setStrokeWidth(1.dp)
.setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500))
.setDefaultStrokeColor(ThemeStore.textColorSecondary(context))
.setSelectedStrokeColor(ThemeStore.accentColor(context))
.setPressedBgColor(context.getCompatColor(R.color.transparent30))
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(ThemeStore.textColorSecondary(context))
.setSelectedColor(ThemeStore.accentColor(context))
.setDisabledColor(context.getCompatColor(R.color.md_grey_500))
.create()
)
}
}

@ -1,15 +0,0 @@
package io.legado.app.lib.theme.view
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputLayout
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.ThemeStore
class ATETextInputLayout(context: Context, attrs: AttributeSet?) : TextInputLayout(context, attrs) {
init {
defaultHintTextColor = Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create()
}
}

@ -23,13 +23,20 @@ class WebDav @Throws(MalformedURLException::class)
constructor(urlStr: String) {
companion object {
// 指定返回哪些属性
private const val DIR = "<?xml version=\"1.0\"?>\n" +
"<a:propfind xmlns:a=\"DAV:\">\n" +
"<a:prop>\n" +
"<a:displayname/>\n<a:resourcetype/>\n<a:getcontentlength/>\n<a:creationdate/>\n<a:getlastmodified/>\n%s" +
"</a:prop>\n" +
"</a:propfind>"
private const val DIR =
"""<?xml version="1.0"?>
<a:propfind xmlns:a="DAV:">
<a:prop>
<a:displayname/>
<a:resourcetype/>
<a:getcontentlength/>
<a:creationdate/>
<a:getlastmodified/>
%s
</a:prop>
</a:propfind>"""
}
private val url: URL = URL(null, urlStr, Handler)
private val httpUrl: String? by lazy {
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://")

@ -1,2 +1,6 @@
## 放置一些模块类
* 书源解析
* analyzeRule 书源规则解析
* localBook 本地书籍解析
* rss 订阅规则解析
* webBook 获取网络书籍

@ -6,10 +6,10 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.webbook.BookChapterList
import io.legado.app.model.webbook.BookContent
import io.legado.app.model.webbook.BookInfo
import io.legado.app.model.webbook.BookList
import io.legado.app.model.webBook.BookChapterList
import io.legado.app.model.webBook.BookContent
import io.legado.app.model.webBook.BookInfo
import io.legado.app.model.webBook.BookList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext

@ -72,7 +72,7 @@ class AnalyzeByJSonPath {
}
}
}
return TextUtils.join(",", textList)
return textList.joinToString("\n")
}
}

@ -51,7 +51,10 @@ class AnalyzeByJSoup {
val textS = getStringList(ruleStr)
return if (textS.isEmpty()) {
null
} else join(",", textS).trim { it <= ' ' }
} else {
textS.joinToString("\n")
}
}
/**
@ -356,8 +359,7 @@ class AnalyzeByJSoup {
try {
when (lastRule) {
"text" -> for (element in elements) {
val text = element.text()
textS.add(text)
textS.add(element.text())
}
"textNodes" -> for (element in elements) {
val tn = arrayListOf<String>()
@ -370,12 +372,15 @@ class AnalyzeByJSoup {
}
textS.add(join("\n", tn))
}
"ownText", "html" -> {
elements.select("script").remove()
"ownText" -> for (element in elements) {
textS.add(element.ownText())
}
"html" -> {
elements.select("script, style").remove()
val html = elements.html()
textS.add(html)
}
"all" -> textS.add(elements.html())
"all" -> textS.add(elements.outerHtml())
else -> for (element in elements) {
val url = element.attr(lastRule)
if (!isEmpty(url) && !textS.contains(url)) {

@ -181,7 +181,7 @@ class AnalyzeByXPath {
}
}
}
return TextUtils.join(",", textList)
return textList.joinToString("\n")
}
}
}

@ -8,6 +8,7 @@ import io.legado.app.data.entities.BaseBook
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.JsExtensions
import io.legado.app.utils.*
import org.jsoup.nodes.Entities
import org.mozilla.javascript.NativeObject
import java.util.*
import java.util.regex.Pattern
@ -108,18 +109,21 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/
@Throws(Exception::class)
@JvmOverloads
fun getStringList(rule: String, isUrl: Boolean = false): List<String>? {
if (TextUtils.isEmpty(rule)) return null
fun getStringList(rule: String?, isUrl: Boolean = false): List<String>? {
if (rule.isNullOrEmpty()) return null
val ruleList = splitSourceRule(rule)
return getStringList(ruleList, isUrl)
}
@Throws(Exception::class)
fun getStringList(ruleList: List<SourceRule>, isUrl: Boolean): List<String>? {
fun getStringList(ruleList: List<SourceRule>, isUrl: Boolean = false): List<String>? {
var result: Any? = null
content?.let { o ->
if (ruleList.isNotEmpty()) {
if (ruleList.isNotEmpty()) result = o
val content = this.content
if (content != null && ruleList.isNotEmpty()) {
result = content
if (content is NativeObject) {
result = content[ruleList[0].rule]?.toString()
} else {
for (sourceRule in ruleList) {
putRule(sourceRule.putMap)
sourceRule.makeUpRule(result)
@ -203,7 +207,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
else -> sourceRule.rule
}
}
if (sourceRule.replaceRegex.isNotEmpty()) {
if ((result != null) && sourceRule.replaceRegex.isNotEmpty()) {
result = replaceRegex(result.toString(), sourceRule)
}
}
@ -211,10 +215,15 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
}
}
if (result == null) result = ""
val str = try {
Entities.unescape(result.toString())
} catch (e: Exception) {
result.toString()
}
if (isUrl) {
return NetworkUtils.getAbsoluteURL(baseUrl, result.toString()) ?: ""
return NetworkUtils.getAbsoluteURL(baseUrl, str) ?: ""
}
return result.toString()
return str
}
/**
@ -582,12 +591,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/
@Throws(Exception::class)
private fun evalJS(jsStr: String, result: Any?): Any? {
val bindings = SimpleBindings()
bindings["java"] = this
bindings["book"] = book
bindings["result"] = result
bindings["baseUrl"] = baseUrl
return SCRIPT_ENGINE.eval(jsStr, bindings)
try {
val bindings = SimpleBindings()
bindings["java"] = this
bindings["book"] = book
bindings["result"] = result
bindings["baseUrl"] = baseUrl
return SCRIPT_ENGINE.eval(jsStr, bindings)
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
/**

@ -6,14 +6,14 @@ import androidx.annotation.Keep
import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.Pattern.EXP_PATTERN
import io.legado.app.constant.Pattern.JS_PATTERN
import io.legado.app.data.api.IHttpGetApi
import io.legado.app.data.api.IHttpPostApi
import io.legado.app.data.entities.BaseBook
import io.legado.app.help.JsExtensions
import io.legado.app.help.http.AjaxWebView
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.RequestMethod
import io.legado.app.help.http.Res
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.utils.*
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -162,7 +162,7 @@ class AnalyzeUrl(
}
if (urlArray.size > 1) {
val options = GSON.fromJsonObject<Map<String, String>>(urlArray[1])
options?.let {
options?.let { _ ->
options["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
options["headers"]?.let { headers ->
GSON.fromJsonObject<Map<String, String>>(headers)?.let { headerMap.putAll(it) }
@ -248,19 +248,19 @@ class AnalyzeUrl(
method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
HttpHelper
.getApiService<IHttpPostApi>(baseUrl)
.getApiService<HttpPostApi>(baseUrl, charset)
.postMap(url, fieldMap, headerMap)
} else {
HttpHelper
.getApiService<IHttpPostApi>(baseUrl)
.getApiService<HttpPostApi>(baseUrl, charset)
.postBody(url, body!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl)
.getApiService<HttpGetApi>(baseUrl, charset)
.get(url, headerMap)
else -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl)
.getApiService<HttpGetApi>(baseUrl, charset)
.getMap(url, fieldMap, headerMap)
}
}
@ -284,24 +284,20 @@ class AnalyzeUrl(
method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
HttpHelper
.getApiService<IHttpPostApi>(baseUrl)
.getApiService<HttpPostApi>(baseUrl, charset)
.postMapAsync(url, fieldMap, headerMap)
.await()
} else {
HttpHelper
.getApiService<IHttpPostApi>(baseUrl)
.getApiService<HttpPostApi>(baseUrl, charset)
.postBodyAsync(url, body!!, headerMap)
.await()
}
}
fieldMap.isEmpty() -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl)
.getApiService<HttpGetApi>(baseUrl, charset)
.getAsync(url, headerMap)
.await()
else -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl)
.getApiService<HttpGetApi>(baseUrl, charset)
.getMapAsync(url, fieldMap, headerMap)
.await()
}
return Res(NetworkUtils.getUrl(res), res.body())
}

@ -0,0 +1,247 @@
package io.legado.app.model.localBook
import android.content.Context
import android.net.Uri
import io.legado.app.App
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.utils.*
import java.io.File
import java.io.RandomAccessFile
import java.nio.charset.Charset
import java.util.regex.Matcher
import java.util.regex.Pattern
object AnalyzeTxtFile {
private const val folderName = "bookTxt"
private const val BLANK: Byte = 0x0a
//默认从文件中获取数据的长度
private const val BUFFER_SIZE = 512 * 1024
//没有标题的时候,每个章节的最大长度
private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024
private val cacheFolder: File by lazy {
val rootFile = App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.externalCacheDir
?: App.INSTANCE.cacheDir
FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName))
}
fun analyze(context: Context, book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(context, book)
book.charset = EncodingDetect.getEncode(bookFile)
val charset = book.fileCharset()
val toc = arrayListOf<BookChapter>()
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val rulePattern = getTocRule(book, bookStream, charset)
//加载章节
val buffer = ByteArray(BUFFER_SIZE)
//获取到的块起始点,在文件中的位置
var curOffset: Long = 0
//block的个数
var blockPos = 0
//读取的长度
var length: Int
var allLength = 0
//获取文件中的数据到buffer,直到没有数据为止
while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) {
++blockPos
//如果存在Chapter
if (rulePattern != null) { //将数据转换成String
var blockContent = String(buffer, 0, length, charset)
val lastN = blockContent.lastIndexOf("\n")
if (lastN != 0) {
blockContent = blockContent.substring(0, lastN)
length = blockContent.toByteArray(charset).size
allLength += length
bookStream.seek(allLength.toLong())
}
//当前Block下使过的String的指针
var seekPos = 0
//进行正则匹配
val matcher: Matcher = rulePattern.matcher(blockContent)
//如果存在相应章节
while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置
val chapterStart = matcher.start()
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容
val chapterContent = blockContent.substring(seekPos, chapterStart)
//设置指针偏移
seekPos += chapterContent.length
if (toc.size == 0) { //如果当前没有章节,那么就是序章
//加入简介
book.intro = chapterContent
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = chapterContent.toByteArray(charset).size.toLong()
toc.add(curChapter)
} else { //否则就block分割之后,上一个章节的剩余内容
//获取上一章节
val lastChapter = toc.last()
//将当前段落添加上一章去
lastChapter.end =
lastChapter.end!! + chapterContent.toByteArray(charset).size
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = lastChapter.end
toc.add(curChapter)
}
} else { //是否存在章节
if (toc.size != 0) { //获取章节内容
val chapterContent = blockContent.substring(seekPos, matcher.start())
seekPos += chapterContent.length
//获取上一章节
val lastChapter = toc.last()
lastChapter.end =
lastChapter.start!! + chapterContent.toByteArray(charset).size
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = lastChapter.end
toc.add(curChapter)
} else { //如果章节不存在则创建章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = 0L
curChapter.end = 0L
toc.add(curChapter)
}
}
}
} else { //进行本地虚拟分章
//章节在buffer的偏移量
var chapterOffset = 0
//当前剩余可分配的长度
var strLength = length
//分章的位置
var chapterPos = 0
while (strLength > 0) {
++chapterPos
//是否长度超过一章
if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { //在buffer中一章的终止点
var end = length
//寻找换行符作为终止点
for (i in chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER until length) {
if (buffer[i] == BLANK) {
end = i
break
}
}
val chapter = BookChapter()
chapter.title = "${blockPos}章($chapterPos)"
chapter.start = curOffset + chapterOffset + 1
chapter.end = curOffset + end
toc.add(chapter)
//减去已经被分配的长度
strLength -= (end - chapterOffset)
//设置偏移的位置
chapterOffset = end
} else {
val chapter = BookChapter()
chapter.title = "" + blockPos + "" + "(" + chapterPos + ")"
chapter.start = curOffset + chapterOffset + 1
chapter.end = curOffset + length
toc.add(chapter)
strLength = 0
}
}
}
//block的偏移点
curOffset += length.toLong()
if (rulePattern != null) { //设置上一章的结尾
val lastChapter = toc.last()
lastChapter.end = curOffset
}
//当添加的block太多的时候,执行GC
if (blockPos % 15 == 0) {
System.gc()
System.runFinalization()
}
}
bookStream.close()
for (i in toc.indices) {
val bean = toc[i]
bean.index = i
bean.bookUrl = book.bookUrl
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
}
book.latestChapterTitle = toc.last().title
System.gc()
System.runFinalization()
return toc
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
bookStream.seek(bookChapter.start ?: 0)
val extent = (bookChapter.end!! - bookChapter.start!!).toInt()
val content = ByteArray(extent)
bookStream.read(content, 0, extent)
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
}
return bookFile
}
private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? {
if (book.tocUrl.isNotEmpty()) {
return Pattern.compile(book.tocUrl, Pattern.MULTILINE)
}
val tocRules = getTocRules()
var rulePattern: Pattern? = null
//首先获取128k的数据
val buffer = ByteArray(BUFFER_SIZE / 4)
val length = bookStream.read(buffer, 0, buffer.size)
val content = String(buffer, 0, length, charset)
for (tocRule in tocRules) {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
book.tocUrl = tocRule.rule
rulePattern = pattern
break
}
}
bookStream.seek(0)
return rulePattern
}
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()
}
}

@ -0,0 +1,30 @@
package io.legado.app.model.localBook
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.Book
object LocalBook {
fun importFile(doc: DocumentFile) {
doc.name?.let { fileName ->
val str = fileName.substringBeforeLast(".")
var name = str.substringBefore("作者")
val author = str.substringAfter("作者", "")
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
}
val book = Book(
bookUrl = doc.uri.toString(),
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
}
}
}

@ -0,0 +1,234 @@
package io.legado.app.model.webBook
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.TocRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object BookChapterList {
suspend fun analyzeChapterList(
coroutineScope: CoroutineScope,
book: Book,
body: String?,
bookSource: BookSource,
baseUrl: String
): List<BookChapter> = suspendCancellableCoroutine { block ->
try {
val chapterList = ArrayList<BookChapter>()
body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val tocRule = bookSource.getTocRule()
val nextUrlList = arrayListOf(baseUrl)
var reverse = false
var listRule = tocRule.chapterList ?: ""
if (listRule.startsWith("-")) {
reverse = true
listRule = listRule.substring(1)
}
if (listRule.startsWith("+")) {
listRule = listRule.substring(1)
}
var chapterData =
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true)
chapterData.chapterList?.let {
chapterList.addAll(it)
}
when (chapterData.nextUrl.size) {
0 -> {
block.resume(finish(book, chapterList, reverse))
}
1 -> {
Coroutine.async(scope = coroutineScope) {
var nextUrl = chapterData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
nextUrlList.add(nextUrl)
AnalyzeUrl(
ruleUrl = nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getResponseAwait()
.body?.let { nextBody ->
chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule,
book, bookSource, log = false
)
nextUrl = if (chapterData.nextUrl.isNotEmpty()) {
chapterData.nextUrl[0]
} else ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
block.resume(finish(book, chapterList, reverse))
}.onError {
block.resumeWithException(it)
}
}
else -> {
val chapterDataList = arrayListOf<ChapterData<String>>()
for (item in chapterData.nextUrl) {
if (!nextUrlList.contains(item)) {
val data = ChapterData(nextUrl = item)
chapterDataList.add(data)
nextUrlList.add(item)
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
for (item in chapterDataList) {
Coroutine.async(scope = coroutineScope) {
val nextBody = AnalyzeUrl(
ruleUrl = item.nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getResponseAwait().body
val nextChapterData = analyzeChapterList(
nextBody, item.nextUrl, tocRule, listRule, book, bookSource
)
synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish(
chapterDataList,
item,
nextChapterData.chapterList
)
if (isFinished) {
chapterDataList.forEach { item ->
item.chapterList?.let {
chapterList.addAll(it)
}
}
block.resume(finish(book, chapterList, reverse))
}
}
}.onError {
block.resumeWithException(it)
}
}
}
}
} catch (e: Exception) {
block.resumeWithException(e)
}
}
private fun addChapterListIsFinish(
chapterDataList: ArrayList<ChapterData<String>>,
chapterData: ChapterData<String>,
chapterList: List<BookChapter>?
): Boolean {
chapterData.chapterList = chapterList
chapterDataList.forEach {
if (it.chapterList == null) {
return false
}
}
return true
}
private fun finish(
book: Book,
chapterList: ArrayList<BookChapter>,
reverse: Boolean
): ArrayList<BookChapter> {
//去重
if (!reverse) {
chapterList.reverse()
}
val lh = LinkedHashSet(chapterList)
val list = ArrayList(lh)
list.reverse()
Debug.log(book.origin, "◇目录总数:${list.size}")
for ((index, item) in list.withIndex()) {
item.index = index
}
book.latestChapterTitle = list.last().title
book.durChapterTitle =
list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle
if (book.totalChapterNum < list.size) {
book.lastCheckCount = list.size - book.totalChapterNum
}
book.totalChapterNum = list.size
return list
}
private fun analyzeChapterList(
body: String?,
baseUrl: String,
tocRule: TocRule,
listRule: String,
book: Book,
bookSource: BookSource,
getNextUrl: Boolean = true,
log: Boolean = false
): ChapterData<List<String>> {
val chapterList = arrayListOf<BookChapter>()
val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log)
analyzeRule.getStringList(nextTocRule, true)?.let {
for (item in it) {
if (item != baseUrl) {
nextUrlList.add(item)
}
}
}
Debug.log(
bookSource.bookSourceUrl,
"" + TextUtils.join("\n", nextUrlList),
log
)
}
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
if (elements.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log)
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
val update = analyzeRule.splitSourceRule(tocRule.updateTime)
var isVip: String?
for (item in elements) {
analyzeRule.setContent(item)
val bookChapter = BookChapter(bookUrl = book.bookUrl)
analyzeRule.chapter = bookChapter
bookChapter.title = analyzeRule.getString(nameRule)
bookChapter.url = analyzeRule.getString(urlRule, true)
bookChapter.tag = analyzeRule.getString(update)
isVip = analyzeRule.getString(vipRule)
if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl
if (bookChapter.title.isNotEmpty()) {
if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") {
bookChapter.title = "\uD83D\uDD12" + bookChapter.title
}
chapterList.add(bookChapter)
}
}
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log)
}
return ChapterData(chapterList, nextUrlList)
}
}

@ -1,4 +1,4 @@
package io.legado.app.model.webbook
package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
@ -37,7 +37,7 @@ object BookContent {
val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule()
var contentData = analyzeContent(body, contentRule, book, bookChapter, bookSource, baseUrl)
content.append(contentData.content)
content.append(contentData.content.replace(bookChapter.title, ""))
if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())

@ -1,4 +1,4 @@
package io.legado.app.model.webbook
package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
@ -42,9 +42,11 @@ object BookInfo {
}
Debug.log(bookSource.bookSourceUrl, "${book.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
analyzeRule.getString(infoRule.kind).let {
if (it.isNotEmpty()) book.kind = it
}
analyzeRule.getStringList(infoRule.kind)
?.joinToString(",")
?.let {
if (it.isNotEmpty()) book.kind = it
}
Debug.log(bookSource.bookSourceUrl, "${book.kind}")
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
analyzeRule.getString(infoRule.wordCount).let {
@ -68,7 +70,7 @@ object BookInfo {
}
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true) ?: baseUrl
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true)
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
if (book.tocUrl == baseUrl) {
book.tocHtml = body

@ -1,4 +1,4 @@
package io.legado.app.model.webbook
package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
@ -120,7 +120,7 @@ object BookList {
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getString(kind)
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount)
@ -170,7 +170,7 @@ object BookList {
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getString(ruleKind)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount)

@ -1,4 +1,4 @@
package io.legado.app.model.webbook
package io.legado.app.model.webBook
import io.legado.app.data.entities.BookChapter

@ -1,4 +1,4 @@
package io.legado.app.model.webbook
package io.legado.app.model.webBook
data class ContentData<T>(
var content: String = "",

@ -1,176 +0,0 @@
package io.legado.app.model.webbook
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.TocRule
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
object BookChapterList {
suspend fun analyzeChapterList(
coroutineScope: CoroutineScope,
book: Book,
body: String?,
bookSource: BookSource,
baseUrl: String
): List<BookChapter> {
var chapterList = arrayListOf<BookChapter>()
body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val tocRule = bookSource.getTocRule()
val nextUrlList = arrayListOf(baseUrl)
var reverse = false
var listRule = tocRule.chapterList ?: ""
if (listRule.startsWith("-")) {
reverse = true
listRule = listRule.substring(1)
}
if (listRule.startsWith("+")) {
listRule = listRule.substring(1)
}
var chapterData =
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true)
chapterData.chapterList?.let {
chapterList.addAll(it)
}
if (chapterData.nextUrl.size == 1) {
var nextUrl = chapterData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
nextUrlList.add(nextUrl)
AnalyzeUrl(
ruleUrl = nextUrl, book = book, headerMapF = bookSource.getHeaderMap()
).getResponseAwait()
.body?.let { nextBody ->
chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule,
book, bookSource, log = false
)
nextUrl = if (chapterData.nextUrl.isNotEmpty())
chapterData.nextUrl[0]
else ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
} else if (chapterData.nextUrl.size > 1) {
val chapterDataList = arrayListOf<ChapterData<String>>()
for (item in chapterData.nextUrl) {
val data = ChapterData(nextUrl = item)
chapterDataList.add(data)
}
for (item in chapterDataList) {
withContext(coroutineScope.coroutineContext) {
val nextBody = AnalyzeUrl(
ruleUrl = item.nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getResponseAwait().body
val nextChapterData = analyzeChapterList(
nextBody, item.nextUrl, tocRule, listRule, book, bookSource
)
item.chapterList = nextChapterData.chapterList
}
}
for (item in chapterDataList) {
item.chapterList?.let {
chapterList.addAll(it)
}
}
}
//去重
if (!reverse) {
chapterList.reverse()
}
val lh = LinkedHashSet(chapterList)
chapterList = ArrayList(lh)
chapterList.reverse()
for ((index, item) in chapterList.withIndex()) {
item.index = index
}
book.latestChapterTitle = chapterList.last().title
if (book.totalChapterNum < chapterList.size) {
book.lastCheckCount = chapterList.size - book.totalChapterNum
}
book.totalChapterNum = chapterList.size
return chapterList
}
private fun analyzeChapterList(
body: String?,
baseUrl: String,
tocRule: TocRule,
listRule: String,
book: Book,
bookSource: BookSource,
getNextUrl: Boolean = true,
log: Boolean = false
): ChapterData<List<String>> {
val chapterList = arrayListOf<BookChapter>()
val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log)
analyzeRule.getStringList(nextTocRule, true)?.let {
for (item in it) {
if (item != baseUrl) {
nextUrlList.add(item)
}
}
}
Debug.log(
bookSource.bookSourceUrl,
"" + TextUtils.join("\n", nextUrlList),
log
)
}
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
if (elements.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log)
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
val update = analyzeRule.splitSourceRule(tocRule.updateTime)
var isVip: String?
for (item in elements) {
analyzeRule.setContent(item)
val bookChapter = BookChapter(bookUrl = book.bookUrl)
analyzeRule.chapter = bookChapter
bookChapter.title = analyzeRule.getString(nameRule)
bookChapter.url = analyzeRule.getString(urlRule, true)
bookChapter.tag = analyzeRule.getString(update)
isVip = analyzeRule.getString(vipRule)
if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl
if (bookChapter.title.isNotEmpty()) {
if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") {
bookChapter.title = "\uD83D\uDD12" + bookChapter.title
}
chapterList.add(bookChapter)
}
}
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log)
}
return ChapterData(chapterList, nextUrlList)
}
}

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.view.KeyEvent
import io.legado.app.App
import io.legado.app.constant.Bus
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.help.ActivityHelp
import io.legado.app.ui.audio.AudioPlayActivity
@ -51,9 +51,9 @@ class MediaButtonReceiver : BroadcastReceiver() {
private fun readAloud(context: Context) {
when {
ActivityHelp.isExist(AudioPlayActivity::class.java) ->
postEvent(Bus.MEDIA_BUTTON, true)
postEvent(EventBus.MEDIA_BUTTON, true)
ActivityHelp.isExist(ReadBookActivity::class.java) ->
postEvent(Bus.MEDIA_BUTTON, true)
postEvent(EventBus.MEDIA_BUTTON, true)
else -> {
GlobalScope.launch(Main) {
val lastBook: Book? = withContext(IO) {

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import io.legado.app.constant.Bus
import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent
@ -28,11 +28,11 @@ class TimeElectricityReceiver : BroadcastReceiver() {
intent?.action?.let {
when (it) {
Intent.ACTION_TIME_TICK -> {
postEvent(Bus.TIME_CHANGED, "")
postEvent(EventBus.TIME_CHANGED, "")
}
Intent.ACTION_BATTERY_CHANGED -> {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
postEvent(Bus.BATTERY_CHANGED, level)
postEvent(EventBus.BATTERY_CHANGED, level)
}
}
}

@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.constant.EventBus
import io.legado.app.constant.Status
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
@ -81,21 +81,21 @@ class AudioPlayService : BaseService(),
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action ->
when (action) {
Action.play -> {
IntentAction.play -> {
AudioPlay.book?.let {
title = it.name
position = it.durChapterPos
loadContent(it.durChapterIndex)
}
}
Action.pause -> pause(true)
Action.resume -> resume()
Action.prev -> moveToPrev()
Action.next -> moveToNext()
Action.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
Action.addTimer -> addTimer()
Action.setTimer -> setTimer(intent.getIntExtra("minute", 0))
Action.adjustProgress -> adjustProgress(intent.getIntExtra("position", position))
IntentAction.pause -> pause(true)
IntentAction.resume -> resume()
IntentAction.prev -> moveToPrev()
IntentAction.next -> moveToNext()
IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
IntentAction.adjustProgress -> adjustProgress(intent.getIntExtra("position", position))
else -> stopSelf()
}
}
@ -112,7 +112,7 @@ class AudioPlayService : BaseService(),
unregisterReceiver(broadcastReceiver)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP)
postEvent(EventBus.AUDIO_STATE, Status.STOP)
}
private fun play() {
@ -120,7 +120,7 @@ class AudioPlayService : BaseService(),
if (requestFocus()) {
try {
AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY)
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
mediaPlayer.reset()
val analyzeUrl =
AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true)
@ -146,7 +146,7 @@ class AudioPlayService : BaseService(),
mediaPlayer.pause()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
AudioPlay.status = Status.PAUSE
postEvent(Bus.AUDIO_STATE, Status.PAUSE)
postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
upNotification()
}
}
@ -159,7 +159,7 @@ class AudioPlayService : BaseService(),
handler.postDelayed(mpRunnable, 1000)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY)
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification()
}
@ -178,7 +178,7 @@ class AudioPlayService : BaseService(),
if (isPlaying) {
playbackParams = playbackParams.apply { speed += adjust }
}
postEvent(Bus.AUDIO_SPEED, playbackParams.speed)
postEvent(EventBus.AUDIO_SPEED, playbackParams.speed)
}
}
}
@ -191,7 +191,7 @@ class AudioPlayService : BaseService(),
if (pause) return
mediaPlayer.start()
mediaPlayer.seekTo(position)
postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration)
postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration)
bookChapter?.let {
it.end = mediaPlayer.duration.toLong()
}
@ -205,7 +205,7 @@ class AudioPlayService : BaseService(),
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP)
postEvent(EventBus.AUDIO_STATE, Status.STOP)
launch { toast("error: $what $extra $url") }
}
return true
@ -238,7 +238,7 @@ class AudioPlayService : BaseService(),
handler.removeCallbacks(dsRunnable)
handler.postDelayed(dsRunnable, 60000)
}
postEvent(Bus.TTS_DS, timeMinute)
postEvent(EventBus.TTS_DS, timeMinute)
upNotification()
}
@ -247,7 +247,7 @@ class AudioPlayService : BaseService(),
*/
private fun upPlayProgress() {
saveProgress()
postEvent(Bus.AUDIO_PROGRESS, mediaPlayer.currentPosition)
postEvent(EventBus.AUDIO_PROGRESS, mediaPlayer.currentPosition)
handler.postDelayed(mpRunnable, 1000)
}
@ -260,9 +260,9 @@ class AudioPlayService : BaseService(),
if (index == AudioPlay.durChapterIndex) {
bookChapter = chapter
subtitle = chapter.title
postEvent(Bus.AUDIO_SUB_TITLE, subtitle)
postEvent(Bus.AUDIO_SIZE, chapter.end?.toInt() ?: 0)
postEvent(Bus.AUDIO_PROGRESS, position)
postEvent(EventBus.AUDIO_SUB_TITLE, subtitle)
postEvent(EventBus.AUDIO_SIZE, chapter.end?.toInt() ?: 0)
postEvent(EventBus.AUDIO_PROGRESS, position)
}
loadContent(chapter)
} ?: removeLoading(index)
@ -376,7 +376,7 @@ class AudioPlayService : BaseService(),
handler.postDelayed(dsRunnable, 60000)
}
}
postEvent(Bus.TTS_DS, timeMinute)
postEvent(EventBus.TTS_DS, timeMinute)
upNotification()
}
@ -485,24 +485,24 @@ class AudioPlayService : BaseService(),
builder.addAction(
R.drawable.ic_play_24dp,
getString(R.string.resume),
thisPendingIntent(Action.resume)
thisPendingIntent(IntentAction.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause_24dp,
getString(R.string.pause),
thisPendingIntent(Action.pause)
thisPendingIntent(IntentAction.pause)
)
}
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.stop),
thisPendingIntent(Action.stop)
thisPendingIntent(IntentAction.stop)
)
builder.addAction(
R.drawable.ic_time_add_24dp,
getString(R.string.set_timer),
thisPendingIntent(Action.addTimer)
thisPendingIntent(IntentAction.addTimer)
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()

@ -16,17 +16,14 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.constant.Status
import io.legado.app.constant.*
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp
import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.TextChapter
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
@ -43,18 +40,18 @@ abstract class BaseReadAloudService : BaseService(),
}
}
private val handler = Handler()
internal val handler = Handler()
private lateinit var audioManager: AudioManager
private var mFocusRequest: AudioFocusRequest? = null
private var broadcastReceiver: BroadcastReceiver? = null
private var mediaSessionCompat: MediaSessionCompat? = null
private var title: String = ""
private var subtitle: String = ""
val contentList = arrayListOf<String>()
var nowSpeak: Int = 0
var readAloudNumber: Int = 0
var textChapter: TextChapter? = null
var pageIndex = 0
internal val contentList = arrayListOf<String>()
internal var nowSpeak: Int = 0
internal var readAloudNumber: Int = 0
internal var textChapter: TextChapter? = null
internal var pageIndex = 0
private val dsRunnable: Runnable = Runnable { doDs() }
override fun onCreate() {
@ -73,7 +70,7 @@ abstract class BaseReadAloudService : BaseService(),
isRun = false
pause = true
unregisterReceiver(broadcastReceiver)
postEvent(Bus.ALOUD_STATE, Status.STOP)
postEvent(EventBus.ALOUD_STATE, Status.STOP)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
mediaSessionCompat?.release()
}
@ -81,7 +78,7 @@ abstract class BaseReadAloudService : BaseService(),
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action ->
when (action) {
Action.play -> {
IntentAction.play -> {
title = intent.getStringExtra("title") ?: ""
subtitle = intent.getStringExtra("subtitle") ?: ""
pageIndex = intent.getIntExtra("pageIndex", 0)
@ -90,13 +87,13 @@ abstract class BaseReadAloudService : BaseService(),
intent.getBooleanExtra("play", true)
)
}
Action.pause -> pauseReadAloud(true)
Action.resume -> resumeReadAloud()
Action.upTtsSpeechRate -> upSpeechRate(true)
Action.prevParagraph -> prevP()
Action.nextParagraph -> nextP()
Action.addTimer -> addTimer()
Action.setTimer -> setTimer(intent.getIntExtra("minute", 0))
IntentAction.pause -> pauseReadAloud(true)
IntentAction.resume -> resumeReadAloud()
IntentAction.upTtsSpeechRate -> upSpeechRate(true)
IntentAction.prevParagraph -> prevP()
IntentAction.nextParagraph -> nextP()
IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
else -> stopSelf()
}
}
@ -111,7 +108,7 @@ abstract class BaseReadAloudService : BaseService(),
nowSpeak = 0
readAloudNumber = textChapter.getReadLength(pageIndex)
contentList.clear()
if (getPrefBoolean("readAloudByPage")) {
if (getPrefBoolean(PreferKey.readAloudByPage)) {
for (index in pageIndex..textChapter.lastIndex()) {
textChapter.page(index)?.text?.split("\n")?.let {
contentList.addAll(it)
@ -127,13 +124,13 @@ abstract class BaseReadAloudService : BaseService(),
open fun play() {
pause = false
postEvent(Bus.ALOUD_STATE, Status.PLAY)
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
upNotification()
}
@CallSuper
open fun pauseReadAloud(pause: Boolean) {
postEvent(Bus.ALOUD_STATE, Status.PAUSE)
postEvent(EventBus.ALOUD_STATE, Status.PAUSE)
BaseReadAloudService.pause = pause
upNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
@ -170,7 +167,7 @@ abstract class BaseReadAloudService : BaseService(),
handler.removeCallbacks(dsRunnable)
handler.postDelayed(dsRunnable, 60000)
}
postEvent(Bus.TTS_DS, timeMinute)
postEvent(EventBus.TTS_DS, timeMinute)
upNotification()
}
@ -186,7 +183,7 @@ abstract class BaseReadAloudService : BaseService(),
handler.postDelayed(dsRunnable, 60000)
}
}
postEvent(Bus.TTS_DS, timeMinute)
postEvent(EventBus.TTS_DS, timeMinute)
upNotification()
}
@ -301,24 +298,24 @@ abstract class BaseReadAloudService : BaseService(),
builder.addAction(
R.drawable.ic_play_24dp,
getString(R.string.resume),
aloudServicePendingIntent(Action.resume)
aloudServicePendingIntent(IntentAction.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause_24dp,
getString(R.string.pause),
aloudServicePendingIntent(Action.pause)
aloudServicePendingIntent(IntentAction.pause)
)
}
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.stop),
aloudServicePendingIntent(Action.stop)
aloudServicePendingIntent(IntentAction.stop)
)
builder.addAction(
R.drawable.ic_time_add_24dp,
getString(R.string.set_timer),
aloudServicePendingIntent(Action.addTimer)
aloudServicePendingIntent(IntentAction.addTimer)
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()

@ -2,28 +2,77 @@ package io.legado.app.service
import android.content.Intent
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.BookSource
import io.legado.app.constant.IntentAction
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class CheckSourceService : BaseService() {
private var sourceList: List<BookSource>? = null
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var idsCount = 0
private val unCheckIds = LinkedHashSet<String>()
override fun onCreate() {
super.onCreate()
updateNotification(0, getString(R.string.start))
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentAction.start -> intent.getStringArrayListExtra("selectIds")?.let {
check(it)
}
else -> stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
task?.cancel()
searchPool.close()
}
private fun check(ids: List<String>) {
task?.cancel()
unCheckIds.clear()
idsCount = ids.size
unCheckIds.addAll(ids)
updateNotification(0, getString(R.string.progress_show, 0, idsCount))
task = execute {
unCheckIds.forEach { sourceUrl ->
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally {
unCheckIds.remove(sourceUrl)
val checkedCount = idsCount - unCheckIds.size
updateNotification(
checkedCount,
getString(R.string.progress_show, checkedCount, idsCount)
)
}
}
}
}
task?.invokeOnCompletion {
stopSelf()
}
}
/**
@ -41,11 +90,9 @@ class CheckSourceService : BaseService() {
.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, Action.stop)
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
)
sourceList?.let {
builder.setProgress(it.size, state, false)
}
builder.setProgress(idsCount, state, false)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
startForeground(112202, notification)

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

Loading…
Cancel
Save