Merge pull request #1 from gedoor/master

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

2
.gitignore vendored

@ -8,3 +8,5 @@
.externalNativeBuild .externalNativeBuild
/release /release
/tmp /tmp
node_modules/
package-lock.json

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

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

@ -20,6 +20,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android { android {
compileSdkVersion 29 compileSdkVersion 29
signingConfigs { signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig { myConfig {
storeFile file(RELEASE_STORE_FILE) storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD storePassword RELEASE_STORE_PASSWORD
@ -29,6 +30,7 @@ android {
v2SigningEnabled true v2SigningEnabled true
} }
} }
}
defaultConfig { defaultConfig {
applicationId "io.legado.app" applicationId "io.legado.app"
minSdkVersion 21 minSdkVersion 21
@ -38,15 +40,27 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version) project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"room.incremental" : "true",
"room.expandProjection": "true"
]
}
}
} }
buildTypes { buildTypes {
release { release {
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
}
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug { debug {
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
versionNameSuffix 'debug' versionNameSuffix 'debug'
minifyEnabled false minifyEnabled false
@ -88,22 +102,23 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase //fireBase
implementation 'com.google.firebase:firebase-core:17.2.1' implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX //androidX
implementation 'androidx.core:core-ktx:1.2.0-rc01' implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0' implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha03' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
//lifecycle //lifecycle
def lifecycle_version = '2.1.0' def lifecycle_version = '2.2.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
@ -129,13 +144,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" 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 'org.jsoup:jsoup:1.12.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS //JS rhino
implementation 'com.github.gedoor:rhino-android:1.3' implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit //Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
@ -161,6 +175,9 @@ dependencies {
//MarkDown //MarkDown
implementation 'ru.noties.markwon:core:3.0.2' implementation 'ru.noties.markwon:core:3.0.2'
//
implementation 'com.github.houbb:opencc4j:1.4.0'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

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

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

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

@ -1,8 +1,114 @@
## 更新日志 ## 更新日志
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】,提示存储权限,选择允许即可导入成功。 * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 注意:由于安卓10更改了权限策略,还需要给「允许安装其他应用」的权限才能导入源。MIUI11也需要此权限。
**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**
* 合并KKL369提交的代码
**2020/01/08**
* 导入本地源不再需要存储权限
**2020/01/07**
* 修复备份问题
* 设置背景不再需要存储权限
**2020/01/06**
* 适配Android 10 权限
* 备份恢复不再需要存储权限
**2020/01/03**
* 适配Android 10 权限
* 导入旧版本配置不在需要存储权限
* 选择字体不在需要存储权限
**2019/12/30** **2019/12/30**
* 修改书源调试 * 修改书源调试
@ -51,13 +157,13 @@
* 最近感冒了,发热咳嗽还没好,继续咸鱼 * 最近感冒了,发热咳嗽还没好,继续咸鱼
**2019/12/12** **2019/12/12**
* [fix]web服务停止问题 * web服务停止问题
* 默认显示沉浸式状态栏 * 默认显示沉浸式状态栏
**2019/12/09** **2019/12/09**
* [add]其他设置->清理缓存 * 其他设置->清理缓存
* [mod]调整深色模式配色,预适配Android10 * 调整深色模式配色,预适配Android10
* [mod]启用web服务 * 启用web服务
**2019/12/03** **2019/12/03**
* from Celeter: * from Celeter:

@ -45,15 +45,15 @@ var init = () => {
books.forEach(book => { books.forEach(book => {
let bookDiv = $c("div"); let bookDiv = $c("div");
let img = $c("img"); let img = $c("img");
img.src = book.bookInfoBean.coverUrl || randomImg; img.src = book.coverUrl || randomImg;
img.setAttribute("data-series-num", book.serialNumber); img.setAttribute("data-series-num", book.serialNumber);
bookDiv.appendChild(img); bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody> bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.bookInfoBean.name}</td></tr> <tr><td>书名</td><td>${book.name}</td></tr>
<tr><td>作者</td><td>${book.bookInfoBean.author}</td></tr> <tr><td>作者</td><td>${book.author}</td></tr>
<tr><td>阅读</td><td>${book.durChapterName}<br>${formatTime(book.finalDate)}</td></tr> <tr><td>阅读</td><td>${book.durChapterTitle}<br>${formatTime(book.durChapterTime)}</td></tr>
<tr><td>更新</td><td>${book.lastChapterName}<br>${formatTime(book.finalRefreshData)}</td></tr> <tr><td>更新</td><td>${book.latestChapterTitle}<br>${formatTime(book.latestChapterTime)}</td></tr>
<tr><td>来源</td><td>${book.bookInfoBean.origin}</td></tr> <tr><td>来源</td><td>${book.origin}</td></tr>
</tbody></table>`; </tbody></table>`;
$('#books').appendChild(bookDiv); $('#books').appendChild(bookDiv);
}); });
@ -62,19 +62,19 @@ var init = () => {
$('#allcontent').classList.add("read"); $('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")]; var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}"> $("#info").innerHTML = `<img src="${bookImg.src}">
<p>  来源${book.bookInfoBean.origin}</p> <p>  来源${book.origin}</p>
<p>  书名${book.bookInfoBean.name}</p> <p>  书名${book.name}</p>
<p>  作者${book.bookInfoBean.author}</p> <p>  作者${book.author}</p>
<p>阅读章节${book.durChapterName}</p> <p>阅读章节${book.durChapterName}</p>
<p>阅读时间${formatTime(book.finalDate)}</p> <p>阅读时间${formatTime(book.durChapterTime)}</p>
<p>最新章节${book.lastChapterName}</p> <p>最新章节${book.latestChapterTitle}</p>
<p>检查时间${formatTime(book.finalRefreshData)}</p> <p>检查时间${formatTime(book.lastCheckTime)}</p>
<p>  简介${book.bookInfoBean.introduce.trim().replace(/\n/g, "<br>")}</p>`; <p>  简介${book.intro.trim().replace(/\n/g, "<br>")}</p>`;
window.location.hash = ""; window.location.hash = "";
window.location.hash = "#info"; window.location.hash = "#info";
$("#content").innerHTML = "章节列表加载中..."; $("#content").innerHTML = "章节列表加载中...";
$("#chapter").innerHTML = ""; $("#chapter").innerHTML = "";
fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" }) fetch(apiAddress("getChapterList", book.bookUrl), { mode: "cors" })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (!data.isSuccess) { if (!data.isSuccess) {

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

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

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

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

@ -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) 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
@ -52,6 +60,8 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
} }
} }
open fun observeLiveBus() {
}
open fun onCompatCreateOptionsMenu(menu: Menu) { open fun onCompatCreateOptionsMenu(menu: Menu) {
} }

@ -3,17 +3,27 @@ package io.legado.app.base
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
abstract class BaseService : Service(), CoroutineScope by MainScope() { 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? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
cancel() cancel()

@ -5,6 +5,7 @@ import android.util.SparseArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.* import java.util.*
@ -15,13 +16,17 @@ import java.util.*
* *
* 通用的adapter 可添加headerfooter以及不同类型item * 通用的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) addItemViewDelegates(*delegates)
} }
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) { constructor(
context: Context,
vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>
) : this(context) {
addItemViewDelegates(*delegates) 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) { synchronized(lock) {
if (this.items.isNotEmpty()) { if (this.items.isNotEmpty()) {
this.items.clear() this.items.clear()
@ -130,10 +135,20 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
if (items != null) { if (items != null) {
this.items.addAll(items) 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)
}
} }
fun setItem(position: Int, item: ITEM) { fun setItem(position: Int, item: ITEM) {
@ -236,7 +251,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
synchronized(lock) { synchronized(lock) {
val size = getActualItemCount() val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) { 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,8 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
return footerItems?.size() ?: 0 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 getItems(): List<ITEM> = items fun getItems(): List<ITEM> = items
@ -294,7 +314,9 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
return when { return when {
isHeader(position) -> TYPE_HEADER_VIEW + position isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() 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 +331,16 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
} }
else -> { 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) { if (itemClickListener != null) {
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
@ -336,7 +367,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) {
} }
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)) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
getItem(holder.layoutPosition - getHeaderCount())?.let { getItem(holder.layoutPosition - getHeaderCount())?.let {
itemDelegates.getValue(getItemViewType(holder.layoutPosition)) itemDelegates.getValue(getItemViewType(holder.layoutPosition))
@ -385,10 +420,6 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
} }
private fun addAnimation(holder: ItemViewHolder) { private fun addAnimation(holder: ItemViewHolder) {
if (itemAnimation == null) {
itemAnimation = ItemAnimation.create().enabled(true)
}
itemAnimation?.let { itemAnimation?.let {
if (it.itemAnimEnabled) { if (it.itemAnimEnabled) {
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { 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) { 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 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) 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 convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
/**
* 注册事件
*/
abstract fun registerListener(holder: ItemViewHolder)
} }

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

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

@ -1,11 +1,17 @@
package io.legado.app.constant package io.legado.app.constant
object PreferKey { object PreferKey {
const val versionCode = "versionCode"
const val themeMode = "themeMode"
const val downloadPath = "downloadPath" const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar" const val hideStatusBar = "hideStatusBar"
const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar" const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch" 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 prevKey = "prevKeyCode"
const val nextKey = "nextKeyCode" const val nextKey = "nextKeyCode"
const val showRss = "showRss" const val showRss = "showRss"
@ -13,7 +19,21 @@ object PreferKey {
const val recordLog = "recordLog" const val recordLog = "recordLog"
const val processText = "process_text" const val processText = "process_text"
const val cleanCache = "cleanCache" const val cleanCache = "cleanCache"
const val lastGroup = "lastGroup" const val saveTabPosition = "saveTabPosition"
const val pageAnim = "pageAnim" const val pageAnim = "pageAnim"
const val readBookFont = "readBookFont" 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"
} }

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

@ -7,14 +7,18 @@ import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database( @Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssStar::class], RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
version = 5, RssStar::class, TxtTocRule::class],
version = 8,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -28,7 +32,7 @@ abstract class AppDatabase : RoomDatabase() {
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addCallback(object : Callback() { .addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) { override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
Restore.restore() GlobalScope.launch { Restore.restore(Backup.backupPath) }
} }
}) })
.build() .build()
@ -47,4 +51,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun rssArticleDao(): RssArticleDao abstract fun rssArticleDao(): RssArticleDao
abstract fun rssStarDao(): RssStarDao abstract fun rssStarDao(): RssStarDao
abstract fun cookieDao(): CookieDao 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文件目录规则

@ -13,9 +13,15 @@ interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl") @Query("select * from chapters where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): LiveData<List<BookChapter>> fun observeByBook(bookUrl: String): LiveData<List<BookChapter>>
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%'")
fun liveDataSearch(bookUrl: String, key: String): LiveData<List<BookChapter>>
@Query("select * from chapters where bookUrl = :bookUrl") @Query("select * from chapters where bookUrl = :bookUrl")
fun getChapterList(bookUrl: String): List<BookChapter> fun getChapterList(bookUrl: String): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end")
fun getChapterList(bookUrl: String, start: Int, end: Int): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
fun getChapter(bookUrl: String, index: Int): BookChapter? 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") @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocal(): LiveData<List<Book>> 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") @Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc")
fun observeDownload(): LiveData<List<Book>> 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>> 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||'%'") @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>> fun liveDataSearch(key: String): LiveData<List<Book>>
@ -42,7 +42,7 @@ interface BookDao {
val hasUpdateBooks: List<Book> val hasUpdateBooks: List<Book>
@get:Query("SELECT * FROM books") @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") @get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1")
val lastReadBook: Book? val lastReadBook: Book?
@ -57,11 +57,14 @@ interface BookDao {
fun insert(vararg book: Book) fun insert(vararg book: Book)
@Update @Update
fun update(vararg books: Book) fun update(vararg book: Book)
@Query("delete from books where bookUrl = :bookUrl") @Delete
fun delete(bookUrl: String) fun delete(vararg book: Book)
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl") @Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
fun upProgress(bookUrl: String, pos: Int) 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`") @Query("SELECT * FROM book_groups ORDER BY `order`")
fun liveDataAll(): LiveData<List<BookGroup>> fun liveDataAll(): LiveData<List<BookGroup>>
@get:Query("SELECT MAX(groupId) FROM book_groups") @get:Query("SELECT sum(groupId) FROM book_groups")
val maxId: Int val idsSum: Int
@Query("SELECT * FROM book_groups ORDER BY `order`") @get:Query("SELECT MAX(`order`) FROM book_groups")
fun all(): List<BookGroup> val maxOrder: Int
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
val all: List<BookGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookGroup: BookGroup) 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") @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>> 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>> 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>> fun liveExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''") @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 <> ''") @Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroupEnabled(): LiveData<List<String>> 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") @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> 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") @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource> fun observeFind(): DataSource.Factory<Int, BookSource>
@ -53,6 +41,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'") @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource> 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 = ''") @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
val noGroup: List<BookSource> val noGroup: List<BookSource>
@ -75,7 +66,7 @@ interface BookSourceDao {
fun update(vararg bookSource: BookSource) fun update(vararg bookSource: BookSource)
@Delete @Delete
fun delete(bookSource: BookSource) fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key") @Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String) fun delete(key: String)

@ -2,6 +2,8 @@ package io.legado.app.data.dao
import androidx.paging.DataSource import androidx.paging.DataSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
@ -15,4 +17,13 @@ interface BookmarkDao {
@Query("select * from bookmarks where bookUrl = :bookUrl") @Query("select * from bookmarks where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): DataSource.Factory<Int, Bookmark> fun observeByBook(bookUrl: String): DataSource.Factory<Int, Bookmark>
@Query("SELECT * FROM bookmarks where bookUrl = :bookUrl and chapterName like '%'||:key||'%' or content like '%'||:key||'%'")
fun liveDataSearch(bookUrl: String, key: String): DataSource.Factory<Int, Bookmark>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookmark: Bookmark)
@Query("delete from bookmarks where bookUrl = :bookUrl and chapterName like '%'||:chapterName||'%'")
fun delByBookmark(bookUrl: String, chapterName: String)
} }

@ -32,24 +32,15 @@ interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules WHERE id in (:ids)") @Query("SELECT * FROM replace_rules WHERE id in (:ids)")
fun findByIds(vararg ids: Long): List<ReplaceRule> 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( @Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1 """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> fun findEnabledByScope(scope: String): List<ReplaceRule>
@Query( @Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1 """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> fun findEnabledByScope(name: String, origin: String): List<ReplaceRule>

@ -3,6 +3,7 @@ package io.legado.app.data.dao
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssReadRecord
@Dao @Dao
interface RssArticleDao { interface RssArticleDao {
@ -10,7 +11,11 @@ interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin and link = :link") @Query("select * from rssArticles where origin = :origin and link = :link")
fun get(origin: String, link: String): RssArticle? 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>> fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
@ -24,4 +29,9 @@ interface RssArticleDao {
@Query("delete from rssArticles where origin = :origin") @Query("delete from rssArticles where origin = :origin")
fun delete(origin: String) 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 <> ''") @Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
fun liveGroup(): LiveData<List<String>> 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") @get:Query("select min(customOrder) from rssSources")
val minOrder: Int val minOrder: Int
@get:Query("select max(customOrder) from rssSources") @get:Query("select max(customOrder) from rssSources")
val maxOrder: Int val maxOrder: Int
@Query("delete from rssSources where sourceUrl in (:sourceUrls)")
fun delSection(vararg sourceUrls: String)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssSource: RssSource) fun insert(vararg rssSource: RssSource)

@ -7,6 +7,9 @@ import io.legado.app.data.entities.RssStar
@Dao @Dao
interface RssStarDao { 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") @Query("select * from rssStars where origin = :origin and link = :link")
fun get(origin: String, link: String): RssStar? fun get(origin: String, link: String): RssStar?

@ -19,16 +19,45 @@ interface SearchBookDao {
@Query("select * from searchBooks where bookUrl = :bookUrl") @Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook? 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? fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author order by originOrder") @Query(
fun getByNameAuthor(name: String, author: String): List<SearchBook> """
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
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by 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> 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) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long> fun insert(vararg searchBook: SearchBook): List<Long>
@Query("delete from searchBooks where time < :time")
fun clearExpired(time: Long)
} }

@ -0,0 +1,20 @@
package io.legado.app.data.dao
import androidx.room.*
import io.legado.app.data.entities.TxtTocRule
@Dao
interface TxtTocRuleDao {
@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)
@Delete
fun delete(vararg rule: TxtTocRule)
}

@ -10,6 +10,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
import kotlin.math.max import kotlin.math.max
@Parcelize @Parcelize
@ -19,7 +20,7 @@ data class Book(
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents) var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local) var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 var originName: String = "", //书源名称 or 本地书籍文件名
var name: String = "", // 书籍名称(书源获取) var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取) var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取) override var kind: String? = null, // 分类信息(书源获取)
@ -48,6 +49,25 @@ data class Book(
var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息) var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
) : Parcelable, BaseBook { ) : 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 @Ignore
@IgnoredOnParcel @IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null override var variableMap: HashMap<String, String>? = null
@ -66,6 +86,8 @@ data class Book(
@IgnoredOnParcel @IgnoredOnParcel
override var tocHtml: String? = null override var tocHtml: String? = null
fun getRealAuthor() = author.replace("作者:", "")
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
@ -77,6 +99,10 @@ data class Book(
variable = GSON.toJson(variableMap) variable = GSON.toJson(variableMap)
} }
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
fun toSearchBook(): SearchBook { fun toSearchBook(): SearchBook {
return SearchBook( return SearchBook(
name = name, name = name,

@ -33,7 +33,7 @@ data class BookChapter(
var tag: String? = null, // var tag: String? = null, //
var start: Long? = null, // 章节起始位置 var start: Long? = null, // 章节起始位置
var end: Long? = null, // 章节终止位置 var end: Long? = null, // 章节终止位置
var variable: String? = null var variable: String? = null //变量
) : Parcelable { ) : Parcelable {
@Ignore @Ignore
@ -52,5 +52,16 @@ data class BookChapter(
variable = GSON.toJson(variableMap) 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
}
} }

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

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

@ -7,15 +7,15 @@ import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "bookmarks", indices = [(Index(value = ["bookUrl"], unique = true))]) @Entity(tableName = "bookmarks", indices = [(Index(value = ["time"], unique = true))])
data class Bookmark( data class Bookmark(
@PrimaryKey @PrimaryKey
var time: Long = System.currentTimeMillis(), var time: Long = System.currentTimeMillis(),
var bookUrl: String = "", var bookUrl: String = "",
var bookName: String = "", var bookName: String = "",
var chapterName: String = "",
var chapterIndex: Int = 0, var chapterIndex: Int = 0,
var pageIndex: Int = 0, var pageIndex: Int = 0,
var chapterName: String = "",
var content: String = "" var content: String = ""
) : Parcelable ) : Parcelable

@ -24,4 +24,17 @@ data class ReplaceRule(
var isRegex: Boolean = true, var isRegex: Boolean = true,
@ColumnInfo(name = "sortOrder") @ColumnInfo(name = "sortOrder")
var order: Int = 0 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 var customOrder: Int = 0
) : Parcelable, JsExtensions { ) : 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) @Throws(Exception::class)
fun getHeaderMap(): Map<String, String> { fun getHeaderMap(): Map<String, String> {
val headerMap = HashMap<String, String>() val headerMap = HashMap<String, String>()

@ -90,6 +90,15 @@ data class SearchBook(
origins?.add(origin) origins?.add(origin)
} }
fun getDisplayLastChapterTitle(): String {
latestChapterTitle?.let {
if (it.isNotEmpty()) {
return it
}
}
return "无最新章节"
}
fun toBook(): Book { fun toBook(): Book {
return Book( return Book(
name = name, 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,
var enable: Boolean = true
)

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

@ -2,7 +2,10 @@ package io.legado.app.help
import androidx.recyclerview.widget.RecyclerView 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() { override fun onChanged() {
adapterDataObserver.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,99 +1,174 @@
package io.legado.app.help package io.legado.app.help
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.MD5Utils import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.getPrefInt import io.legado.app.utils.*
import io.legado.app.utils.getPrefString
import org.apache.commons.text.similarity.JaccardSimilarity import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File import java.io.File
import kotlin.math.min import kotlin.math.min
object BookHelp { object BookHelp {
private const val cacheFolderName = "book_cache"
private var downloadPath: String = val downloadPath: String
App.INSTANCE.getPrefString(PreferKey.downloadPath) get() = App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath ?: App.INSTANCE.cacheDir.absolutePath
fun upDownloadPath() { private val downloadUri get() = Uri.parse(downloadPath)
downloadPath =
App.INSTANCE.getPrefString(PreferKey.downloadPath) private fun bookFolderName(book: Book): String {
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
?: App.INSTANCE.cacheDir.absolutePath
} }
private fun getBookCachePath(): String { fun formatChapterName(bookChapter: BookChapter): String {
return "$downloadPath${File.separator}book_cache" return String.format(
"%05d-%s.nb",
bookChapter.index,
MD5Utils.md5Encode16(bookChapter.title)
)
} }
fun clearCache() { fun clearCache() {
FileHelp.deleteFile(getBookCachePath()) if (downloadPath.isContentPath()) {
FileHelp.getFolder(getBookCachePath()) DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
} else {
FileUtils.deleteFile(
FileUtils.getPath(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName)
)
)
}
} }
@Synchronized @Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) { fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return if (content.isEmpty()) return
FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach { if (downloadPath.isContentPath()) {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) { DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
it.delete() DocumentUtils.createFileIfNotExist(
return@forEach 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) postEvent(EventBus.SAVE_CONTENT, bookChapter)
val file = FileHelp.getFile(filePath)
file.writeText(content)
} }
fun getChapterCount(book: Book): Int { fun getChapterFiles(book: Book): List<String> {
return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0 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 { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter) when {
runCatching { book.isLocalBook() -> {
val file = File(filePath)
if (file.exists()) {
return true 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 return false
} }
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
val filePath = getChapterPath(book, bookChapter) when {
runCatching { book.isLocalBook() -> {
val file = File(filePath) 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()) { if (file.exists()) {
return file.readText() return file.readText()
} }
} }
}
return null return null
} }
fun delContent(book: Book, bookChapter: BookChapter) { fun delContent(book: Book, bookChapter: BookChapter) {
val filePath = getChapterPath(book, bookChapter) when {
kotlin.runCatching { book.isLocalBook() -> return
val file = File(filePath) downloadPath.isContentPath() -> {
if (file.exists()) { DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
file.delete() 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 + 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 { private fun formatFolderName(folderName: String): String {
@ -108,6 +183,9 @@ object BookHelp {
?: "" ?: ""
} }
/**
* 找到相似度最高的章节
*/
fun getDurChapterIndexByChapterTitle( fun getDurChapterIndexByChapterTitle(
title: String?, title: String?,
index: Int, index: Int,
@ -121,16 +199,16 @@ object BookHelp {
} }
var newIndex = 0 var newIndex = 0
val jaccardSimilarity = JaccardSimilarity() val jSimilarity = JaccardSimilarity()
var similarity = if (chapters.size > index) { var similarity = if (chapters.size > index) {
jaccardSimilarity.apply(title, chapters[index].title) jSimilarity.apply(title, chapters[index].title)
} else 0.0 } else 0.0
if (similarity == 1.0) { if (similarity == 1.0) {
return index return index
} else { } else {
for (i in 1..50) { for (i in 1..50) {
if (index - i in chapters.indices) { 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) { if (it > similarity) {
similarity = it similarity = it
newIndex = index - i newIndex = index - i
@ -141,7 +219,7 @@ object BookHelp {
} }
} }
if (index + i in chapters.indices) { 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) { if (it > similarity) {
similarity = it similarity = it
newIndex = index + i newIndex = index + i
@ -156,9 +234,11 @@ object BookHelp {
return newIndex return newIndex
} }
var bookName: String? = null private var bookName: String? = null
var bookOrigin: String? = null private var bookOrigin: String? = null
var replaceRules: List<ReplaceRule> = arrayListOf() private var replaceRules: List<ReplaceRule> = arrayListOf()
val bodyIndent
get() = " ".repeat(App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2))
fun disposeContent( fun disposeContent(
title: String, title: String,
@ -177,9 +257,6 @@ object BookHelp {
} }
} }
} }
if (!content.substringBefore("\n").contains(title)) {
c = title + "\n" + c
}
for (item in replaceRules) { for (item in replaceRules) {
item.pattern.let { item.pattern.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
@ -191,7 +268,13 @@ object BookHelp {
} }
} }
} }
val indent = App.INSTANCE.getPrefInt("textIndent", 2) if (!content.substringBefore("\n").contains(title)) {
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.util.Log
import android.widget.Toast import android.widget.Toast
import io.legado.app.service.TTSReadAloudService import io.legado.app.service.TTSReadAloudService
import java.io.File import io.legado.app.utils.FileUtils
import java.io.FileOutputStream
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* 异常管理类 * 异常管理类
@ -141,14 +141,15 @@ class CrashHandler : Thread.UncaughtExceptionHandler {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
val time = format.format(Date()) val time = format.format(Date())
val fileName = "crash-$time-$timestamp.log" val fileName = "crash-$time-$timestamp.log"
val path = mContext?.externalCacheDir?.toString() + "/crash/" mContext?.externalCacheDir?.let { rootFile ->
val dir = File(path) FileUtils.getDirFile(rootFile, "crash").listFiles()?.forEach {
if (!dir.exists()) { if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) {
dir.mkdirs() it.delete()
} }
val fos = FileOutputStream(path + fileName) }
fos.write(sb.toString().toByteArray()) FileUtils.createFileIfNotExist(rootFile, fileName, "crash")
fos.close() .writeText(sb.toString())
}
} }
} }

@ -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 { object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> { fun load(context: Context, path: String?): RequestBuilder<Drawable> {
if (path?.startsWith("http", true) == true) { return when {
return Glide.with(context).load(path) 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> { fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> {

@ -88,16 +88,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
srcViewHolder: RecyclerView.ViewHolder, srcViewHolder: RecyclerView.ViewHolder,
targetViewHolder: RecyclerView.ViewHolder targetViewHolder: RecyclerView.ViewHolder
): Boolean { ): Boolean {
onItemTouchCallbackListener?.let { return onItemTouchCallbackListener
return it.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition) ?.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition)
} ?: false
return false
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
onItemTouchCallbackListener?.let { onItemTouchCallbackListener?.onSwiped(viewHolder.adapterPosition)
return it.onSwiped(viewHolder.adapterPosition)
}
} }
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
@ -107,13 +104,21 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
viewPager?.requestDisallowInterceptTouchEvent(swiping) viewPager?.requestDisallowInterceptTouchEvent(swiping)
} }
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
onItemTouchCallbackListener?.clearView(recyclerView, viewHolder)
}
interface OnItemTouchCallbackListener { interface OnItemTouchCallbackListener {
/** /**
* 当某个Item被滑动删除的时候 * 当某个Item被滑动删除的时候
* *
* @param adapterPosition item的position * @param adapterPosition item的position
*/ */
fun onSwiped(adapterPosition: Int) fun onSwiped(adapterPosition: Int) {
}
/** /**
* 当两个Item位置互换的时候被回调 * 当两个Item位置互换的时候被回调
@ -122,6 +127,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() {
* @param targetPosition 目的地的Item的position * @param targetPosition 目的地的Item的position
* @return 开发者处理了操作应该返回true开发者没有处理就返回false * @return 开发者处理了操作应该返回true开发者没有处理就返回false
*/ */
fun onMove(srcPosition: Int, targetPosition: Int): Boolean fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
return false
}
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 android.graphics.drawable.Drawable
import io.legado.app.App import io.legado.app.App
import io.legado.app.R 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 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.File
import java.io.FileWriter
import java.io.IOException
/** /**
* 阅读界面配置 * 阅读界面配置
@ -23,6 +19,12 @@ object ReadBookConfig {
private val configFilePath = private val configFilePath =
App.INSTANCE.filesDir.absolutePath + File.separator + readConfigFileName App.INSTANCE.filesDir.absolutePath + File.separator + readConfigFileName
val configList: ArrayList<Config> = arrayListOf() 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 var styleSelect
get() = App.INSTANCE.getPrefInt("readStyleSelect") get() = App.INSTANCE.getPrefInt("readStyleSelect")
@ -34,84 +36,93 @@ object ReadBookConfig {
} }
@Synchronized @Synchronized
fun getConfig(index: Int = styleSelect): Config { fun getConfig(index: Int): Config {
if (configList.size < 5) { if (configList.size < 5) {
reset() resetAll()
} }
return configList[index] return configList[index]
} }
fun upConfig() { fun upConfig() {
val configFile = File(configFilePath) (getConfigs() ?: defaultConfigs).let {
val json = if (configFile.exists()) {
configFile.readText()
} else {
String(App.INSTANCE.assets.open(readConfigFileName).readBytes())
}
try {
GSON.fromJsonArray<Config>(json)?.let {
configList.clear() configList.clear()
configList.addAll(it) configList.addAll(it)
} ?: 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) { } catch (e: Exception) {
reset()
} }
} }
return null
}
fun upBg() { fun upBg() {
val resources = App.INSTANCE.resources val resources = App.INSTANCE.resources
val dm = resources.displayMetrics val dm = resources.displayMetrics
val width = dm.widthPixels val width = dm.widthPixels
val height = dm.heightPixels val height = dm.heightPixels
bg = getConfig().bgDrawable(width, height) bg = durConfig.bgDrawable(width, height)
} }
fun save() { fun save() {
GlobalScope.launch(IO) { Coroutine.async {
val json = GSON.toJson(configList) val json = GSON.toJson(configList)
val configFile = File(configFilePath) FileUtils.createFileIfNotExist(configFilePath).writeText(json)
//获取流并存储
try {
BufferedWriter(FileWriter(configFile)).use { writer ->
writer.write(json)
writer.flush()
} }
} catch (e: IOException) {
e.printStackTrace()
} }
fun resetDur() {
defaultConfigs[styleSelect].let {
durConfig.setBg(it.bgType(), it.bgStr())
durConfig.setTextColor(it.textColor())
upBg()
save()
} }
} }
private fun reset() { private fun resetAll() {
val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes()) defaultConfigs.let {
GSON.fromJsonArray<Config>(json)?.let {
configList.clear() configList.clear()
configList.addAll(it) configList.addAll(it)
}
save() save()
} }
}
data class Config( data class Config(
var bgStr: String = "#EEEEEE", var bgStr: String = "#EEEEEE",//白天背景
var bgStrNight: String = "#000000", var bgStrNight: String = "#000000",//夜间背景
var bgType: Int = 0, var bgType: Int = 0,//白天背景类型
var bgTypeNight: Int = 0, var bgTypeNight: Int = 0,//夜间背景类型
var darkStatusIcon: Boolean = true, var darkStatusIcon: Boolean = true,//白天是否暗色状态栏
var darkStatusIconNight: Boolean = false, var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
var letterSpacing: Float = 1f, var textColor: String = "#3E3D3B",//白天文字颜色
var lineSpacingExtra: Int = 12, var textColorNight: String = "#adadad",//夜间文字颜色
var lineSpacingMultiplier: Float = 1.2f, var textBold: Boolean = false,//是否粗体字
var paddingBottom: Int = 0, var textSize: Int = 15,//文字大小
var letterSpacing: Float = 1f,//字间距
var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 12,
var paddingBottom: Int = 6,
var paddingLeft: Int = 16, var paddingLeft: Int = 16,
var paddingRight: Int = 16, var paddingRight: Int = 16,
var paddingTop: Int = 0, var paddingTop: Int = 6,
var textBold: Boolean = false, var headerPaddingBottom: Int = 0,
var textColor: String = "#3E3D3B", var headerPaddingLeft: Int = 16,
var textColorNight: String = "#adadad", var headerPaddingRight: Int = 16,
var textSize: Int = 15 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) { fun setBg(bgType: Int, bg: String) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
bgTypeNight = bgType bgTypeNight = bgType
bgStrNight = bg bgStrNight = bg
} else { } else {
@ -121,15 +132,16 @@ object ReadBookConfig {
} }
fun setTextColor(color: Int) { fun setTextColor(color: Int) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
textColorNight = "#${color.hexString}" textColorNight = "#${color.hexString}"
} else { } else {
textColor = "#${color.hexString}" textColor = "#${color.hexString}"
} }
ChapterProvider.upStyle(this)
} }
fun setStatusIconDark(isDark: Boolean) { fun setStatusIconDark(isDark: Boolean) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
darkStatusIconNight = isDark darkStatusIconNight = isDark
} else { } else {
darkStatusIcon = isDark darkStatusIcon = isDark
@ -137,7 +149,7 @@ object ReadBookConfig {
} }
fun statusIconDark(): Boolean { fun statusIconDark(): Boolean {
return if (App.INSTANCE.isNightTheme) { return if (AppConfig.isNightTheme) {
darkStatusIconNight darkStatusIconNight
} else { } else {
darkStatusIcon darkStatusIcon
@ -145,17 +157,17 @@ object ReadBookConfig {
} }
fun textColor(): Int { fun textColor(): Int {
return if (App.INSTANCE.isNightTheme) Color.parseColor(textColorNight) return if (AppConfig.isNightTheme) Color.parseColor(textColorNight)
else Color.parseColor(textColor) else Color.parseColor(textColor)
} }
fun bgStr(): String { fun bgStr(): String {
return if (App.INSTANCE.isNightTheme) bgStrNight return if (AppConfig.isNightTheme) bgStrNight
else bgStr else bgStr
} }
fun bgType(): Int { fun bgType(): Int {
return if (App.INSTANCE.isNightTheme) bgTypeNight return if (AppConfig.isNightTheme) bgTypeNight
else bgType else bgType
} }

@ -76,7 +76,7 @@ class CompositeCoroutine : CoroutineContainer {
resources = null resources = null
} }
set?.forEachIndexed { index, coroutine -> set?.forEachIndexed { _, coroutine ->
coroutine.cancel() 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 package io.legado.app.help.http
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter import io.legado.app.utils.UTF8BOMFighter
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Converter import retrofit2.Converter
@ -16,20 +17,22 @@ class EncodeConverter(private val encode: String? = null) : Converter.Factory()
): Converter<ResponseBody, String>? { ): Converter<ResponseBody, String>? {
return Converter { value -> return Converter { value ->
val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()) val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes())
encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) } var charsetName: String? = encode
var charsetName: String? = null charsetName?.let {
val mediaType = value.contentType() try {
//根据http头判断 return@Converter String(responseBytes, Charset.forName(charsetName))
if (mediaType != null) { } catch (e: Exception) {
val charset = mediaType.charset() }
charsetName = charset?.displayName()
} }
if (charsetName == null) { //根据http头判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes) value.contentType()?.charset()?.let {
return@Converter String(responseBytes, it)
} }
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
String(responseBytes, Charset.forName(charsetName)) String(responseBytes, Charset.forName(charsetName))
} }
} }

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

@ -14,11 +14,9 @@ import javax.net.ssl.*
object SSLHelper { object SSLHelper {
val sslSocketFactory: SSLParams?
get() = getSslSocketFactoryBase(null, null, null)
/** /**
* 为了解决客户端不信任服务器数字证书的问题网络上大部分的解决方案都是让客户端不对证书做任何检查 * 为了解决客户端不信任服务器数字证书的问题
* 网络上大部分的解决方案都是让客户端不对证书做任何检查
* 这是一种有很大安全漏洞的办法 * 这是一种有很大安全漏洞的办法
*/ */
val unsafeTrustManager: X509TrustManager = object : X509TrustManager { val unsafeTrustManager: X509TrustManager = object : X509TrustManager {
@ -141,7 +139,7 @@ object SSLHelper {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null) keyStore.load(null)
for ((index, certStream) in certificates.withIndex()) { for ((index, certStream) in certificates.withIndex()) {
val certificateAlias = Integer.toString(index) val certificateAlias = index.toString()
// 证书工厂根据证书文件的流生成证书 cert // 证书工厂根据证书文件的流生成证书 cert
val cert = certificateFactory.generateCertificate(certStream) val cert = certificateFactory.generateCertificate(certStream)
// 将 cert 作为可信证书放入到keyStore中 // 将 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.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
@ -12,20 +11,20 @@ import retrofit2.http.Url
* Created by GKF on 2018/1/21. * Created by GKF on 2018/1/21.
* get web content * get web content
*/ */
@Suppress("unused")
interface IHttpGetApi { interface HttpGetApi {
@GET @GET
fun getAsync( suspend fun getAsync(
@Url url: String, @Url url: String,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Deferred<Response<String>> ): Response<String>
@GET @GET
fun getMapAsync( suspend fun getMapAsync(
@Url url: String, @Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>, @QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Deferred<Response<String>> ): Response<String>
@GET @GET
fun get( fun get(
@ -39,4 +38,11 @@ interface IHttpGetApi {
@QueryMap(encoded = true) queryMap: Map<String, String>, @QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Call<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 okhttp3.RequestBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
@ -10,23 +9,23 @@ import retrofit2.http.*
* Created by GKF on 2018/1/29. * Created by GKF on 2018/1/29.
* post * post
*/ */
@Suppress("unused")
interface IHttpPostApi { interface HttpPostApi {
@FormUrlEncoded @FormUrlEncoded
@POST @POST
fun postMapAsync( suspend fun postMapAsync(
@Url url: String, @Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>, @FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Deferred<Response<String>> ): Response<String>
@POST @POST
fun postBodyAsync( suspend fun postBodyAsync(
@Url url: String, @Url url: String,
@Body body: RequestBody, @Body body: RequestBody,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Deferred<Response<String>> ): Response<String>
@FormUrlEncoded @FormUrlEncoded
@POST @POST
@ -45,9 +44,9 @@ interface IHttpPostApi {
@FormUrlEncoded @FormUrlEncoded
@POST @POST
fun postMapByteAsync( suspend fun postMapByteAsync(
@Url url: String, @Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>, @FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Deferred<Response<ByteArray>> ): Response<ByteArray>
} }

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

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

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

@ -1,22 +1,24 @@
package io.legado.app.help.storage 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.App
import io.legado.app.R import io.legado.app.constant.PreferKey
import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.FileUtils import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.GSON import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
object Backup { object Backup {
val defaultPath by lazy { val backupPath: String by lazy {
FileUtils.getSdCardPath() + File.separator + "YueDu" FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath
} }
val legadoPath by lazy { val legadoPath by lazy {
@ -27,104 +29,100 @@ object Backup {
legadoPath + File.separator + "Export" legadoPath + File.separator + "Export"
} }
private fun pBackup(path: String = legadoPath) { val backupFileNames by lazy {
backupBookshelf(path) arrayOf(
backupBookGroup(path) "bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json",
backupBookSource(path) "rssStar.json", "replaceRule.json", ReadBookConfig.readConfigFileName, "config.xml"
backupRssSource(path) )
backupReplaceRule(path)
backupReadConfig(path)
backupPreference(path)
WebDavHelp.backUpWebDav(path)
} }
fun backup() { fun autoBack(context: Context) {
doAsync { val lastBackup = context.getPrefLong(PreferKey.lastBackup)
pBackup() if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
uiThread { return
App.INSTANCE.toast(R.string.backup_success)
} }
Coroutine.async {
val backupPath = context.getPrefString(PreferKey.backupPath)
if (backupPath.isNullOrEmpty()) {
backup(context)
} else {
backup(context, backupPath)
} }
} }
fun autoBackup() {
doAsync {
pBackup()
}
} }
private fun backupBookshelf(path: String) { suspend fun backup(context: Context, path: String = legadoPath) {
App.db.bookDao().allBooks.let { context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
if (it.isNotEmpty()) { withContext(IO) {
val json = GSON.toJson(it) writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
val file = FileHelp.getFile(path + File.separator + "bookshelf.json") writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
file.writeText(json) writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
} writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
} writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
} }
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
private fun backupBookGroup(path: String) { val edit = sp.edit()
App.db.bookGroupDao().all().let { App.INSTANCE.defaultSharedPreferences.all.map {
if (it.isNotEmpty()) { when (val value = it.value) {
val json = GSON.toJson(it) is Int -> edit.putInt(it.key, value)
val file = FileHelp.getFile(path + File.separator + "bookGroup.json") is Boolean -> edit.putBoolean(it.key, value)
file.writeText(json) is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
} }
} }
edit.commit()
} }
WebDavHelp.backUpWebDav(backupPath)
private fun backupBookSource(path: String) { if (path.isContentPath()) {
App.db.bookSourceDao().all.let { copyBackup(context, Uri.parse(path))
if (it.isNotEmpty()) { } else {
val json = GSON.toJson(it) copyBackup(File(path))
val file = FileHelp.getFile(path + File.separator + "bookSource.json")
file.writeText(json)
} }
} }
} }
private fun backupRssSource(path: String) { private fun writeListToJson(list: List<Any>, fileName: String, path: String) {
App.db.rssSourceDao().all.let { if (list.isNotEmpty()) {
if (it.isNotEmpty()) { val json = GSON.toJson(list)
val json = GSON.toJson(it) FileUtils.createFileIfNotExist(path + File.separator + fileName).writeText(json)
val file = FileHelp.getFile(path + File.separator + "rssSource.json")
file.writeText(json)
}
} }
} }
private fun backupReplaceRule(path: String) { @Throws(java.lang.Exception::class)
App.db.replaceRuleDao().all.let { private fun copyBackup(context: Context, uri: Uri) {
if (it.isNotEmpty()) { DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
val json = GSON.toJson(it) for (fileName in backupFileNames) {
val file = FileHelp.getFile(path + File.separator + "replaceRule.json") val file = File(backupPath + File.separator + fileName)
file.writeText(json) if (file.exists()) {
val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName)
doc?.let {
DocumentUtils.writeText(
context,
file.readText(),
doc.uri
)
} }
} }
} }
private fun backupReadConfig(path: String) {
GSON.toJson(ReadBookConfig.configList)?.let {
FileHelp.getFile(path + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)
} }
} }
private fun backupPreference(path: String) { @Throws(java.lang.Exception::class)
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.let { sp -> private fun copyBackup(rootFile: File) {
val edit = sp.edit() for (fileName in backupFileNames) {
App.INSTANCE.defaultSharedPreferences.all.map { val file = File(backupPath + File.separator + fileName)
when (val value = it.value) { if (file.exists()) {
is Int -> edit.putInt(it.key, value) file.copyTo(
is Boolean -> edit.putBoolean(it.key, value) FileUtils.createFileIfNotExist(rootFile, fileName),
is Long -> edit.putLong(it.key, value) true
is Float -> edit.putFloat(it.key, value) )
is String -> edit.putString(it.key, value)
else -> Unit
}
} }
edit.commit()
} }
} }
} }

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

@ -3,6 +3,7 @@ package io.legado.app.help.storage
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.entities.rule.* import io.legado.app.data.entities.rule.*
import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.help.storage.Restore.jsonPath
import io.legado.app.utils.* import io.legado.app.utils.*
@ -205,4 +206,27 @@ object OldRule {
return GSON.toJson(map) return GSON.toJson(map)
} }
fun jsonToReplaceRule(json: String): ReplaceRule? {
var replaceRule: ReplaceRule? = null
runCatching {
replaceRule = GSON.fromJsonObject<ReplaceRule>(json.trim())
}
runCatching {
if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) {
val jsonItem = jsonPath.parse(json.trim())
val rule = ReplaceRule()
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
rule.pattern = jsonItem.readString("$.regex") ?: ""
if (rule.pattern.isEmpty()) return null
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
rule.replacement = jsonItem.readString("$.replacement") ?: ""
rule.isRegex = jsonItem.readBool("$.isRegex") == true
rule.scope = jsonItem.readString("$.useTo")
rule.isEnabled = jsonItem.readBool("$.enable") == true
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
return rule
}
}
return replaceRule
}
} }

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

@ -1,27 +1,21 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.util.Log import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext import com.jayway.jsonpath.ParseContext
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.constant.PreferKey
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.* 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.help.ReadBookConfig
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.File import java.io.File
object Restore { object Restore {
@ -33,56 +27,63 @@ object Restore {
) )
} }
fun restore(path: String = Backup.legadoPath) { suspend fun restore(context: Context, path: String) {
doAsync { withContext(IO) {
try { if (path.isContentPath()) {
val file = FileHelp.getFile(path + File.separator + "bookshelf.json") DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
val json = file.readText() for (fileName in Backup.backupFileNames) {
GSON.fromJsonArray<Book>(json)?.let { if (doc.name == fileName) {
App.db.bookDao().insert(*it.toTypedArray()) DocumentUtils.readText(context, doc.uri)?.let {
FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName)
.writeText(it)
} }
} catch (e: Exception) {
e.printStackTrace()
} }
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()
} }
} else {
try { try {
val file = FileHelp.getFile(path + File.separator + "bookSource.json") val file = File(path)
val json = file.readText() for (fileName in Backup.backupFileNames) {
GSON.fromJsonArray<BookSource>(json)?.let { FileUtils.getFile(file, fileName).let {
App.db.bookSourceDao().insert(*it.toTypedArray()) if (it.exists()) {
it.copyTo(
FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName),
true
)
}
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
try { }
val file = FileHelp.getFile(path + File.separator + "rssSource.json") }
val json = file.readText() restore(Backup.backupPath)
GSON.fromJsonArray<RssSource>(json)?.let { }
suspend fun restore(path: String) {
withContext(IO) {
fileToListT<Book>(path, "bookshelf.json")?.let {
App.db.bookDao().insert(*it.toTypedArray())
}
fileToListT<BookGroup>(path, "bookGroup.json")?.let {
App.db.bookGroupDao().insert(*it.toTypedArray())
}
fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao().insert(*it.toTypedArray())
}
fileToListT<RssSource>(path, "rssSource.json")?.let {
App.db.rssSourceDao().insert(*it.toTypedArray()) App.db.rssSourceDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) { fileToListT<RssStar>(path, "rssStar.json")?.let {
e.printStackTrace() App.db.rssStarDao().insert(*it.toTypedArray())
} }
try { fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
val file = FileHelp.getFile(path + File.separator + "replaceRule.json")
val json = file.readText()
GSON.fromJsonArray<ReplaceRule>(json)?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray()) App.db.replaceRuleDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) {
e.printStackTrace()
}
try { try {
val file = val file =
FileHelp.getFile(path + File.separator + ReadBookConfig.readConfigFileName) FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.readConfigFileName)
val configFile = val configFile =
File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName) File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName)
if (file.exists()) { if (file.exists()) {
@ -102,113 +103,22 @@ object Restore {
is String -> edit.putString(it.key, value) is String -> edit.putString(it.key, value)
else -> Unit else -> Unit
} }
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
edit.commit() edit.commit()
} }
uiThread { App.INSTANCE.toast(R.string.restore_success) }
}
}
fun importYueDuData(context: Context) {
GlobalScope.launch(IO) {
try {// 导入书架
val shelfFile = FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json")
val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = jsonPath.parse(shelfFile.readText()).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())
withContext(Main) {
context.toast("成功导入书籍${books.size}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
} }
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
} }
try {// Book source private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
val sourceFile = try {
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json") val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
val bookSources = mutableListOf<BookSource>() val json = file.readText()
val items: List<Map<String, Any>> = jsonPath.parse(sourceFile.readText()).read("$") return GSON.fromJsonArray(json)
for (item in items) {
val jsonItem = jsonPath.parse(item)
OldRule.jsonToBookSource(jsonItem.jsonString())?.let {
bookSources.add(it)
}
}
App.db.bookSourceDao().insert(*bookSources.toTypedArray())
withContext(Main) {
context.toast("成功导入书源${bookSources.size}")
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Main) { e.printStackTrace()
context.toast("导入源失败\n${e.localizedMessage}")
} }
return null
} }
try {// Replace rules
val ruleFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json")
val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = jsonPath.parse(ruleFile.readText()).read("$")
val existingRules = App.db.replaceRuleDao().all.map { it.pattern }.toSet()
for ((index: Int, item: Map<String, Any>) in items.withIndex()) {
val jsonItem = jsonPath.parse(item)
val rule = ReplaceRule()
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis().plus(index)
rule.pattern = jsonItem.readString("$.regex") ?: ""
if (rule.pattern.isEmpty() || rule.pattern in existingRules) continue
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
rule.replacement = jsonItem.readString("$.replacement") ?: ""
rule.isRegex = jsonItem.readBool("$.isRegex") == true
rule.scope = jsonItem.readString("$.useTo")
rule.isEnabled = jsonItem.readBool("$.enable") == true
rule.order = jsonItem.readInt("$.serialNumber") ?: index
replaceRules.add(rule)
}
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray())
withContext(Main) {
context.toast("成功导入替换规则${replaceRules.size}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
} }

@ -1,37 +1,42 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import io.legado.app.App 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.help.coroutine.Coroutine
import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth 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.ZipUtils
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import org.jetbrains.anko.doAsync import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.jetbrains.anko.selector import org.jetbrains.anko.selector
import org.jetbrains.anko.uiThread import org.jetbrains.anko.toast
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
object WebDavHelp { object WebDavHelp {
private val zipFilePath = FileHelp.getCachePath() + "/backup" + ".zip" private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/"
private val unzipFilesPath by lazy { private val zipFilePath = "${FileUtils.getCachePath()}${File.separator}backup.zip"
FileHelp.getCachePath()
}
private fun getWebDavUrl(): String? { private fun getWebDavUrl(): String {
var url = App.INSTANCE.getPrefString("web_dav_url") var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl)
if (url.isNullOrBlank()) return null if (url.isNullOrEmpty()) {
url = defaultWebDavUrl
}
if (!url.endsWith("/")) url += "/" if (!url.endsWith("/")) url += "/"
return url return url
} }
private fun initWebDav(): Boolean { private fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString("web_dav_account") val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString("web_dav_password") val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password) HttpAuth.auth = HttpAuth.Auth(account, password)
return true return true
@ -42,7 +47,8 @@ object WebDavHelp {
private fun getWebDavFileNames(): ArrayList<String> { private fun getWebDavFileNames(): ArrayList<String> {
val url = getWebDavUrl() val url = getWebDavUrl()
val names = arrayListOf<String>() val names = arrayListOf<String>()
if (!url.isNullOrBlank() && initWebDav()) { if (initWebDav()) {
try {
var files = WebDav(url + "legado/").listFiles() var files = WebDav(url + "legado/").listFiles()
files = files.reversed() files = files.reversed()
for (index: Int in 0 until min(10, files.size)) { for (index: Int in 0 until min(10, files.size)) {
@ -50,49 +56,51 @@ object WebDavHelp {
names.add(it) names.add(it)
} }
} }
} catch (e: Exception) {
return names
}
} }
return names return names
} }
fun showRestoreDialog(context: Context) { suspend fun showRestoreDialog(context: Context, restoreSuccess: () -> Unit): Boolean {
doAsync { val names = withContext(IO) { getWebDavFileNames() }
val names = getWebDavFileNames() return if (names.isNotEmpty()) {
if (names.isNotEmpty()) { withContext(Main) {
uiThread {
context.selector(title = "选择恢复文件", items = names) { _, index -> context.selector(title = "选择恢复文件", items = names) { _, index ->
if (index in 0 until names.size) { if (index in 0 until names.size) {
restoreWebDav(names[index]) restoreWebDav(names[index], restoreSuccess)
} }
} }
} }
true
} else { } else {
Restore.restore() false
}
} }
} }
private fun restoreWebDav(name: String) { private fun restoreWebDav(name: String, success: () -> Unit) {
doAsync { Coroutine.async {
getWebDavUrl()?.let { getWebDavUrl().let {
val file = WebDav(it + "legado/" + name) val file = WebDav(it + "legado/" + name)
file.downloadTo(zipFilePath, true) file.downloadTo(zipFilePath, true)
ZipUtils.unzipFile(zipFilePath, unzipFilesPath) @Suppress("BlockingMethodInNonBlockingContext")
Restore.restore(unzipFilesPath) ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
Restore.restore(Backup.backupPath)
} }
}.onSuccess {
success.invoke()
} }
} }
fun backUpWebDav(path: String) { fun backUpWebDav(path: String) {
try {
if (initWebDav()) { if (initWebDav()) {
val paths = arrayListOf( val paths = arrayListOf(*Backup.backupFileNames)
path + File.separator + "bookshelf.json", for (i in 0 until paths.size) {
path + File.separator + "bookSource.json", paths[i] = path + File.separator + paths[i]
path + File.separator + "rssSource.json", }
path + File.separator + "replaceRule.json", FileUtils.deleteFile(zipFilePath)
path + File.separator + "config.xml",
path + File.separator + ReadBookConfig.readConfigFileName
)
FileHelp.deleteFile(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) { if (ZipUtils.zipFiles(paths, zipFilePath)) {
WebDav(getWebDavUrl() + "legado").makeAsDir() WebDav(getWebDavUrl() + "legado").makeAsDir()
val putUrl = getWebDavUrl() + "legado/backup" + val putUrl = getWebDavUrl() + "legado/backup" +
@ -101,5 +109,10 @@ object WebDavHelp {
WebDav(putUrl).upload(zipFilePath) 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.graphics.drawable.Drawable
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewManager
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.jetbrains.anko.UI
import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER
import kotlin.DeprecationLevel.ERROR import kotlin.DeprecationLevel.ERROR

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

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

@ -15,7 +15,7 @@ import io.legado.app.R
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/ */
class ThemeStore @SuppressLint("CommitPrefEdits") class ThemeStore @SuppressLint("CommitPrefEdits")
private constructor(private val mContext: Context) : ThemeStorePrefKeys, ThemeStoreInterface { private constructor(private val mContext: Context) : ThemeStoreInterface {
private val mEditor: SharedPreferences.Editor private val mEditor: SharedPreferences.Editor
init { init {

@ -3,8 +3,7 @@ package io.legado.app.lib.theme
/** /**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
*/ */
internal interface ThemeStorePrefKeys { object ThemeStorePrefKeys {
companion object {
const val CONFIG_PREFS_KEY_DEFAULT = "app_themes" const val CONFIG_PREFS_KEY_DEFAULT = "app_themes"
const val IS_CONFIGURED_KEY = "is_configured" const val IS_CONFIGURED_KEY = "is_configured"
@ -28,4 +27,3 @@ internal interface ThemeStorePrefKeys {
const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar"
const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark"
} }
}

@ -192,6 +192,7 @@ object TintHelper {
} }
} }
@SuppressLint("PrivateResource")
fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) { fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList( val sl = ColorStateList(
arrayOf( arrayOf(
@ -254,11 +255,10 @@ object TintHelper {
if (!skipIndeterminate) if (!skipIndeterminate)
progressBar.indeterminateTintList = sl progressBar.indeterminateTintList = sl
} else { } else {
val mode = PorterDuff.Mode.SRC_IN
if (!skipIndeterminate && progressBar.indeterminateDrawable != null) if (!skipIndeterminate && progressBar.indeterminateDrawable != null)
progressBar.indeterminateDrawable.setColorFilter(color, mode) progressBar.indeterminateDrawable.setTint(color)
if (progressBar.progressDrawable != null) if (progressBar.progressDrawable != null)
progressBar.progressDrawable.setColorFilter(color, mode) progressBar.progressDrawable.setTint(color)
} }
} }
@ -291,6 +291,7 @@ object TintHelper {
setCursorTint(editText, color) setCursorTint(editText, color)
} }
@SuppressLint("PrivateResource")
fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) { fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) {
val sl = ColorStateList( val sl = ColorStateList(
arrayOf( 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) { constructor(urlStr: String) {
companion object { companion object {
// 指定返回哪些属性 // 指定返回哪些属性
private const val DIR = "<?xml version=\"1.0\"?>\n" + private const val DIR =
"<a:propfind xmlns:a=\"DAV:\">\n" + """<?xml version="1.0"?>
"<a:prop>\n" + <a:propfind xmlns:a="DAV:">
"<a:displayname/>\n<a:resourcetype/>\n<a:getcontentlength/>\n<a:creationdate/>\n<a:getlastmodified/>\n%s" + <a:prop>
"</a:prop>\n" + <a:displayname/>
"</a:propfind>" <a:resourcetype/>
<a:getcontentlength/>
<a:creationdate/>
<a:getlastmodified/>
%s
</a:prop>
</a:propfind>"""
} }
private val url: URL = URL(null, urlStr, Handler) private val url: URL = URL(null, urlStr, Handler)
private val httpUrl: String? by lazy { private val httpUrl: String? by lazy {
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") 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.data.entities.SearchBook
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.webbook.BookChapterList import io.legado.app.model.webBook.BookChapterList
import io.legado.app.model.webbook.BookContent import io.legado.app.model.webBook.BookContent
import io.legado.app.model.webbook.BookInfo import io.legado.app.model.webBook.BookInfo
import io.legado.app.model.webbook.BookList import io.legado.app.model.webBook.BookList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext 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) val textS = getStringList(ruleStr)
return if (textS.isEmpty()) { return if (textS.isEmpty()) {
null null
} else join(",", textS).trim { it <= ' ' } } else {
textS.joinToString("\n")
}
} }
/** /**

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

@ -108,18 +108,21 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/ */
@Throws(Exception::class) @Throws(Exception::class)
@JvmOverloads @JvmOverloads
fun getStringList(rule: String, isUrl: Boolean = false): List<String>? { fun getStringList(rule: String?, isUrl: Boolean = false): List<String>? {
if (TextUtils.isEmpty(rule)) return null if (rule.isNullOrEmpty()) return null
val ruleList = splitSourceRule(rule) val ruleList = splitSourceRule(rule)
return getStringList(ruleList, isUrl) return getStringList(ruleList, isUrl)
} }
@Throws(Exception::class) @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 var result: Any? = null
content?.let { o -> val content = this.content
if (ruleList.isNotEmpty()) { if (content != null && ruleList.isNotEmpty()) {
if (ruleList.isNotEmpty()) result = o result = content
if (content is NativeObject) {
result = content[ruleList[0].rule]?.toString()
} else {
for (sourceRule in ruleList) { for (sourceRule in ruleList) {
putRule(sourceRule.putMap) putRule(sourceRule.putMap)
sourceRule.makeUpRule(result) sourceRule.makeUpRule(result)
@ -582,12 +585,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/ */
@Throws(Exception::class) @Throws(Exception::class)
private fun evalJS(jsStr: String, result: Any?): Any? { private fun evalJS(jsStr: String, result: Any?): Any? {
try {
val bindings = SimpleBindings() val bindings = SimpleBindings()
bindings["java"] = this bindings["java"] = this
bindings["book"] = book bindings["book"] = book
bindings["result"] = result bindings["result"] = result
bindings["baseUrl"] = baseUrl bindings["baseUrl"] = baseUrl
return SCRIPT_ENGINE.eval(jsStr, bindings) 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.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.Pattern.EXP_PATTERN import io.legado.app.constant.Pattern.EXP_PATTERN
import io.legado.app.constant.Pattern.JS_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.data.entities.BaseBook
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.help.http.AjaxWebView import io.legado.app.help.http.AjaxWebView
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.RequestMethod import io.legado.app.help.http.RequestMethod
import io.legado.app.help.http.Res 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 io.legado.app.utils.*
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -162,7 +162,7 @@ class AnalyzeUrl(
} }
if (urlArray.size > 1) { if (urlArray.size > 1) {
val options = GSON.fromJsonObject<Map<String, String>>(urlArray[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["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST }
options["headers"]?.let { headers -> options["headers"]?.let { headers ->
GSON.fromJsonObject<Map<String, String>>(headers)?.let { headerMap.putAll(it) } GSON.fromJsonObject<Map<String, String>>(headers)?.let { headerMap.putAll(it) }
@ -248,19 +248,19 @@ class AnalyzeUrl(
method == RequestMethod.POST -> { method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty()) {
HttpHelper HttpHelper
.getApiService<IHttpPostApi>(baseUrl) .getApiService<HttpPostApi>(baseUrl, charset)
.postMap(url, fieldMap, headerMap) .postMap(url, fieldMap, headerMap)
} else { } else {
HttpHelper HttpHelper
.getApiService<IHttpPostApi>(baseUrl) .getApiService<HttpPostApi>(baseUrl, charset)
.postBody(url, body!!, headerMap) .postBody(url, body!!, headerMap)
} }
} }
fieldMap.isEmpty() -> HttpHelper fieldMap.isEmpty() -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl) .getApiService<HttpGetApi>(baseUrl, charset)
.get(url, headerMap) .get(url, headerMap)
else -> HttpHelper else -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl) .getApiService<HttpGetApi>(baseUrl, charset)
.getMap(url, fieldMap, headerMap) .getMap(url, fieldMap, headerMap)
} }
} }
@ -284,24 +284,20 @@ class AnalyzeUrl(
method == RequestMethod.POST -> { method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty()) {
HttpHelper HttpHelper
.getApiService<IHttpPostApi>(baseUrl) .getApiService<HttpPostApi>(baseUrl, charset)
.postMapAsync(url, fieldMap, headerMap) .postMapAsync(url, fieldMap, headerMap)
.await()
} else { } else {
HttpHelper HttpHelper
.getApiService<IHttpPostApi>(baseUrl) .getApiService<HttpPostApi>(baseUrl, charset)
.postBodyAsync(url, body!!, headerMap) .postBodyAsync(url, body!!, headerMap)
.await()
} }
} }
fieldMap.isEmpty() -> HttpHelper fieldMap.isEmpty() -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl) .getApiService<HttpGetApi>(baseUrl, charset)
.getAsync(url, headerMap) .getAsync(url, headerMap)
.await()
else -> HttpHelper else -> HttpHelper
.getApiService<IHttpGetApi>(baseUrl) .getApiService<HttpGetApi>(baseUrl, charset)
.getMapAsync(url, fieldMap, headerMap) .getMapAsync(url, fieldMap, headerMap)
.await()
} }
return Res(NetworkUtils.getUrl(res), res.body()) return Res(NetworkUtils.getUrl(res), res.body())
} }

@ -0,0 +1,239 @@
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? {
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()) {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
}
}
}
return rules
}
}

@ -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.App
import io.legado.app.R import io.legado.app.R

@ -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.App
import io.legado.app.R import io.legado.app.R
@ -42,7 +42,9 @@ object BookInfo {
} }
Debug.log(bookSource.bookSourceUrl, "${book.author}") Debug.log(bookSource.bookSourceUrl, "${book.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
analyzeRule.getString(infoRule.kind).let { analyzeRule.getStringList(infoRule.kind)
?.joinToString(",")
?.let {
if (it.isNotEmpty()) book.kind = it if (it.isNotEmpty()) book.kind = it
} }
Debug.log(bookSource.bookSourceUrl, "${book.kind}") Debug.log(bookSource.bookSourceUrl, "${book.kind}")
@ -68,7 +70,7 @@ object BookInfo {
} }
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") 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.isEmpty()) book.tocUrl = baseUrl
if (book.tocUrl == baseUrl) { if (book.tocUrl == baseUrl) {
book.tocHtml = body 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.App
import io.legado.app.R import io.legado.app.R
@ -120,7 +120,7 @@ object BookList {
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类") 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.kind}")
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = analyzeRule.getString(wordCount) searchBook.wordCount = analyzeRule.getString(wordCount)
@ -170,7 +170,7 @@ object BookList {
searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor)) searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取分类", 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, "${searchBook.kind}", log)
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = analyzeRule.getString(ruleWordCount) 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 import io.legado.app.data.entities.BookChapter

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

Loading…
Cancel
Save