Merge pull request #38 from gedoor/master

update code from master
pull/481/head
Antecer 5 years ago committed by GitHub
commit c9237aff3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 139
      app/build.gradle
  3. 4
      app/src/debug/res/values/strings.xml
  4. 145
      app/src/main/AndroidManifest.xml
  5. BIN
      app/src/main/assets/bg/午后沙滩.jpg
  6. BIN
      app/src/main/assets/bg/宁静夜色.jpg
  7. BIN
      app/src/main/assets/bg/小清新.jpg
  8. BIN
      app/src/main/assets/bg/山水墨影.jpg
  9. BIN
      app/src/main/assets/bg/山水画.jpg
  10. BIN
      app/src/main/assets/bg/护眼漫绿.jpg
  11. BIN
      app/src/main/assets/bg/新羊皮纸.jpg
  12. BIN
      app/src/main/assets/bg/明媚倾城.jpg
  13. BIN
      app/src/main/assets/bg/浅黄飘带.jpg
  14. BIN
      app/src/main/assets/bg/深宫魅影.jpg
  15. BIN
      app/src/main/assets/bg/清新.jpg
  16. BIN
      app/src/main/assets/bg/清新时光.jpg
  17. BIN
      app/src/main/assets/bg/羊皮纸1.jpg
  18. BIN
      app/src/main/assets/bg/羊皮纸2.jpg
  19. BIN
      app/src/main/assets/bg/羊皮纸3.jpg
  20. BIN
      app/src/main/assets/bg/羊皮纸4.jpg
  21. BIN
      app/src/main/assets/bg/边彩画布.jpg
  22. BIN
      app/src/main/assets/bg/雾花.jpg
  23. 16
      app/src/main/assets/disclaimer.md
  24. BIN
      app/src/main/assets/number.ttf
  25. 72
      app/src/main/assets/readConfig.json
  26. 38
      app/src/main/assets/txtChapterRule.json
  27. 5
      app/src/main/assets/updateLog.md
  28. 176
      app/src/main/assets/web/bookshelf.css
  29. 32
      app/src/main/assets/web/bookshelf.html
  30. 161
      app/src/main/assets/web/bookshelf.js
  31. BIN
      app/src/main/assets/web/favicon.ico
  32. 148
      app/src/main/assets/web/index.css
  33. 306
      app/src/main/assets/web/index.html
  34. 362
      app/src/main/assets/web/index.js
  35. BIN
      app/src/main/ic_launcher-web.png
  36. 160
      app/src/main/java/io/legado/app/App.kt
  37. 88
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  38. 51
      app/src/main/java/io/legado/app/base/BaseFragment.kt
  39. 21
      app/src/main/java/io/legado/app/base/BaseService.kt
  40. 77
      app/src/main/java/io/legado/app/base/BaseViewModel.kt
  41. 10
      app/src/main/java/io/legado/app/base/VMBaseActivity.kt
  42. 9
      app/src/main/java/io/legado/app/base/VMBaseFragment.kt
  43. 70
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  44. 2
      app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
  45. 7
      app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
  46. 4
      app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
  47. 10
      app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
  48. 2
      app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
  49. 3
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
  50. 3
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
  51. 3
      app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
  52. 14
      app/src/main/java/io/legado/app/constant/Action.kt
  53. 49
      app/src/main/java/io/legado/app/constant/AppConst.kt
  54. 7
      app/src/main/java/io/legado/app/constant/BookType.kt
  55. 16
      app/src/main/java/io/legado/app/constant/Bus.kt
  56. 8
      app/src/main/java/io/legado/app/constant/Pattern.kt
  57. 19
      app/src/main/java/io/legado/app/constant/RSSKeywords.kt
  58. 7
      app/src/main/java/io/legado/app/constant/Status.kt
  59. 40
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  60. 14
      app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
  61. 42
      app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt
  62. 53
      app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt
  63. 31
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  64. 49
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  65. 24
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  66. 82
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  67. 18
      app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
  68. 54
      app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
  69. 24
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  70. 60
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  71. 44
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  72. 35
      app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt
  73. 12
      app/src/main/java/io/legado/app/data/dao/SourceCookieDao.kt
  74. 26
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  75. 127
      app/src/main/java/io/legado/app/data/entities/Book.kt
  76. 32
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  77. 15
      app/src/main/java/io/legado/app/data/entities/BookGroup.kt
  78. 176
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  79. 21
      app/src/main/java/io/legado/app/data/entities/Bookmark.kt
  80. 28
      app/src/main/java/io/legado/app/data/entities/Chapter.kt
  81. 3
      app/src/main/java/io/legado/app/data/entities/EditEntity.kt
  82. 31
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  83. 29
      app/src/main/java/io/legado/app/data/entities/RssArticle.kt
  84. 33
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  85. 5
      app/src/main/java/io/legado/app/data/entities/Rule.kt
  86. 86
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  87. 17
      app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt
  88. 29
      app/src/main/java/io/legado/app/data/entities/SearchShow.kt
  89. 31
      app/src/main/java/io/legado/app/data/entities/Source.kt
  90. 12
      app/src/main/java/io/legado/app/data/entities/SourceCookie.kt
  91. 14
      app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt
  92. 14
      app/src/main/java/io/legado/app/data/entities/rule/BookListRule.kt
  93. 6
      app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt
  94. 14
      app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt
  95. 14
      app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt
  96. 8
      app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt
  97. 85
      app/src/main/java/io/legado/app/help/ActivityHelp.kt
  98. 30
      app/src/main/java/io/legado/app/help/AdapterDataObserverProxy.kt
  99. 60
      app/src/main/java/io/legado/app/help/BlurTransformation.kt
  100. 166
      app/src/main/java/io/legado/app/help/BookHelp.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,2 @@
# legado
## 阅读3.0

@ -2,29 +2,60 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders"
apply plugin: 'io.fabric'
androidExtensions {
experimental = true
}
static def releaseTime() {
return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8"))
}
def name = "legado"
def version = "0." + releaseTime()
def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim())
android {
compileSdkVersion 28
dataBinding {
enabled = true
signingConfigs {
myConfig {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
v1SigningEnabled true
v2SigningEnabled true
}
}
defaultConfig {
applicationId "io.legado.app"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
versionCode gitCommits
versionName version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
}
buildTypes {
release {
signingConfig signingConfigs.myConfig
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.myConfig
applicationIdSuffix '.debug'
versionNameSuffix 'debug'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${name}_${defaultConfig.versionName}.apk"
}
}
}
compileOptions {
@ -32,8 +63,14 @@ android {
targetCompatibility = '1.8'
}
kotlinOptions {
jvmTarget = "1.8"
}
}
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
kapt {
arguments {
@ -41,51 +78,83 @@ kapt {
}
}
kotlin{
experimental{
coroutines "enable"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.2.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX
implementation 'androidx.core:core-ktx:1.2.0-beta01'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
kapt 'androidx.lifecycle:lifecycle-compiler:2.0.0'
implementation 'androidx.room:room-runtime:2.0.0'
kapt 'androidx.room:room-compiler:2.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0-beta05'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'com.google.android:flexbox:1.1.0'
//lifecycle
def lifecycle_version = '2.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room
def room_version = '2.2.0'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//paging
implementation 'androidx.paging:paging-runtime:2.1.0'
//anko
def anko_version = '0.10.8'
implementation "org.jetbrains.anko:anko-sdk27:$anko_version"
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//liveEventBus
implementation 'com.jeremyliao:live-event-bus-x:1.4.5'
//
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
def coroutines_version = '1.2.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//
implementation 'pub.devrel:easypermissions:3.0.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.jayway.jsonpath:json-path:2.4.0'
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS
implementation 'com.github.gedoor:rhino-android:1.3'
//Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:3.14.0'//
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
//webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1'
implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
//
implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.6'
//
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-text:1.8'
}
apply plugin: 'com.google.gms.google-services'

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

@ -1,45 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name=".ui.search.SearchActivity">
</activity>
<activity android:name=".ui.about.AboutActivity">
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".ui.welcome.WelcomeActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
</activity>
<activity android:name=".ui.main.MainActivity" />
<activity
android:name=".ui.main.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" />
<activity
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<activity
android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".ui.rss.source.edit.RssSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity android:name=".ui.config.ConfigActivity" />
<activity android:name=".ui.replacerule.ReplaceRuleActivity" />
<activity android:name="io.legado.app.ui.book.search.SearchActivity" />
<activity android:name=".ui.about.AboutActivity" />
<activity android:name=".ui.qrcode.QrCodeActivity" />
<activity android:name=".ui.about.DonateActivity" />
<activity android:name=".ui.book.info.BookInfoActivity" />
<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.manage.BookSourceActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="booksource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<activity android:name=".ui.chapterlist.ChapterListActivity" />
<activity android:name=".ui.rss.read.ReadRssActivity" />
<activity android:name=".ui.audio.AudioPlayActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity
android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".ui.replacerule.ReplaceRuleActivity"/>
<service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" />
<service android:name=".service.UpdateService" />
<service android:name=".service.WebService" />
<service android:name=".service.TTSReadAloudService" />
<service android:name=".service.HttpReadAloudService" />
<receiver android:name=".receiver.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,16 @@
# 免责声明(Disclaimer)
* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。
各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。
任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。
您应该对使用搜索引擎的结果自行承担风险。
* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。
因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。
阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。
* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。
阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。
阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。
任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。
阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。

Binary file not shown.

@ -0,0 +1,72 @@
[
{
"bgStr": "羊皮纸2.jpg",
"bgType": 1,
"darkStatusIcon": true,
"textColor": "#5E432E",
"textSize": 22,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
},
{
"bgStr": "新羊皮纸.jpg",
"bgType": 1,
"darkStatusIcon": true,
"textColor": "#5E432E",
"textSize": 22,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
},
{
"bgStr": "#015A86",
"bgType": 0,
"darkStatusIcon": false,
"textColor": "#FFFFFF",
"textSize": 22,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
},
{
"bgStr": "宁静夜色",
"bgType": 1,
"darkStatusIcon": false,
"textColor": "#adadad",
"textSize": 22,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
},
{
"bgStr": "#000000",
"bgType": 0,
"darkStatusIcon": false,
"textColor": "#adadad",
"textSize": 22,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"lineSpacingMultiplier": 1.2,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 0,
"paddingBottom": 0
}
]

@ -0,0 +1,38 @@
[
{
"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,5 @@
## 本软件为开源软件,没有上架Google Play,请不要在任何地方购买!
### 关注公众号[开源阅读软件]点击广告可支持作者!
### 捐赠里点击红包搜索码可开启高级功能!
## 更新日志

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

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

@ -0,0 +1,161 @@
var $ = document.querySelector.bind(document)
, $$ = document.querySelectorAll.bind(document)
, $c = document.createElement.bind(document)
, randomImg = "http://acg.bakayun.cn/randbg.php?t=dfzh"
, randomImg2 = "http://img.xjh.me/random_img.php"
, books
;
var formatTime = value => {
return new Date(value).toLocaleString('zh-CN', {
hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
}).replace(/\//g, "-");
};
var apiMap = {
getBookshelf: "/getBookshelf",
getChapterList: "/getChapterList",
getBookContent: "/getBookContent",
saveBook: "/saveBook"
};
var apiAddress = (apiName, url) => {
let address = $('#address').value || window.location.host;
if (!(/^http|^\/\//).test(address)) {
address = "//" + address;
}
if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) {
address += ":1122";
}
localStorage.setItem('address', address);
return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
};
var init = () => {
$('#allcontent').classList.remove("read");
$('#books').innerHTML = "";
fetch(apiAddress("getBookshelf"), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(getBookshelf.errorMsg);
return;
}
books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);
books.forEach(book => {
let bookDiv = $c("div");
let img = $c("img");
img.src = book.bookInfoBean.coverUrl || randomImg;
img.setAttribute("data-series-num", book.serialNumber);
bookDiv.appendChild(img);
bookDiv.innerHTML += `<table><tbody>
<tr><td>书名</td><td>${book.bookInfoBean.name}</td></tr>
<tr><td>作者</td><td>${book.bookInfoBean.author}</td></tr>
<tr><td>阅读</td><td>${book.durChapterName}<br>${formatTime(book.finalDate)}</td></tr>
<tr><td>更新</td><td>${book.lastChapterName}<br>${formatTime(book.finalRefreshData)}</td></tr>
<tr><td>来源</td><td>${book.bookInfoBean.origin}</td></tr>
</tbody></table>`;
$('#books').appendChild(bookDiv);
});
$$('#books img').forEach(bookImg =>
bookImg.addEventListener("click", () => {
$('#allcontent').classList.add("read");
var book = books[bookImg.getAttribute("data-series-num")];
$("#info").innerHTML = `<img src="${bookImg.src}">
<p>  来源${book.bookInfoBean.origin}</p>
<p>  书名${book.bookInfoBean.name}</p>
<p>  作者${book.bookInfoBean.author}</p>
<p>阅读章节${book.durChapterName}</p>
<p>阅读时间${formatTime(book.finalDate)}</p>
<p>最新章节${book.lastChapterName}</p>
<p>检查时间${formatTime(book.finalRefreshData)}</p>
<p>  简介${book.bookInfoBean.introduce.trim().replace(/\n/g, "<br>")}</p>`;
window.location.hash = "";
window.location.hash = "#info";
$("#content").innerHTML = "章节列表加载中...";
$("#chapter").innerHTML = "";
fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "章节列表加载失败!";
return;
}
data.data.forEach(chapter => {
let ch = $c("button");
ch.setAttribute("data-url", chapter.durChapterUrl);
ch.setAttribute("title", chapter.durChapterName);
ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName;
$("#chapter").appendChild(ch);
});
$('#chapter').scrollTop = 0;
$("#content").innerHTML = "章节列表加载完成!";
});
}));
});
};
$("#back").addEventListener("click", () => {
if (window.location.hash === "#content") {
window.location.hash = "#chapter";
} else if (window.location.hash === "#chapter") {
window.location.hash = "#info";
} else {
$('#allcontent').classList.remove("read");
}
});
$("#refresh").addEventListener("click", init);
$('#hidebooks').addEventListener("click", () => {
$("#books").classList.toggle("hide");
$(".nav").classList.toggle("hide");
$("#allcontent").classList.toggle("allscreen");
});
$('#top').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#info";
});
$('#showchapter').addEventListener("click", () => {
window.location.hash = "";
window.location.hash = "#chapter";
});
$('#chapter').addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
var url = e.target.getAttribute("data-url");
var name = e.target.getAttribute("title");
if (!url) {
alert("未取得章节地址");
}
$("#content").innerHTML = "<p>" + name + " 加载中...</p>";
fetch(apiAddress("getBookContent", url), { mode: "cors" })
.then(res => res.json())
.then(data => {
if (!data.isSuccess) {
alert(data.errorMsg);
$("#content").innerHTML = "<p>" + name + " 加载失败!</p>";
return;
}
var content = data.data.trim().split("\n\n");
if (content.length === 2) {
$("#content").innerHTML = `<h2>${content[0]}</h2>  (全文 ${content[1].length} 字)<br><br>  ` + content[1].trim().replace(/\n/g, "<br><br>");
} else {
$("#content").innerHTML = `<h2>${name || e.target.innerHTML}</h2>  (全文 ${data.data.length} 字)<br><br>  ` + data.data.trim().replace(/\n/g, "<br><br>");
}
window.location.hash = "";
window.location.hash = "#content";
});
}
});
$('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host);
if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {
$('#address').value = localStorage.getItem('address');
}
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,148 @@
body {
margin: 0;
}
.editor {
display: flex;
align-items: stretch;
}
.setbox,
.menu,
.outbox {
flex: 1;
display: flex;
flex-flow: column;
max-height: 100vh;
overflow-y: auto;
}
.menu {
justify-content: center;
max-width: 90px;
margin: 0 5px;
}
.menu .button {
width: 90px;
height: 30px;
min-height: 30px;
margin: 5px 0px;
cursor: pointer;
}
@keyframes stroker {
0% {
stroke-dashoffset: 0
}
100% {
stroke-dashoffset: -240
}
}
.button rect {
width: 100%;
height: 100%;
fill: transparent;
stroke: #666;
stroke-width: 2px;
}
.button rect.busy {
stroke: #fD1850;
stroke-dasharray: 30 90;
animation: stroker 1s linear infinite;
}
.button text {
text-anchor: middle;
dominant-baseline: middle;
}
.setbox {
min-width: 40em;
}
.rules,
.tabbox {
flex: 1;
display: flex;
flex-flow: column;
}
.rules>* {
display: flex;
margin: 2px 0;
}
.rules textarea {
flex: 1;
margin-left: 5px;
}
.rules>*,
.rules>*>div,
.rules textarea {
min-height: 1em;
}
textarea {
word-break: break-all;
}
.tabtitle {
display: flex;
z-index: 1;
justify-content: flex-end;
}
.tabtitle>div {
cursor: pointer;
padding: 1px 10px 0 10px;
border-bottom: 3px solid transparent;
font-weight: bold;
}
.tabtitle>.this {
color: #4f9da6;
border-bottom-color: #4EBBE4;
}
.tabbody {
flex: 1;
display: flex;
margin-top: -1px;
border: 1px solid #A9A9A9;
height: 0;
}
.tabbody>* {
flex: 1;
flex-flow: column;
display: none;
}
.tabbody>.this {
display: flex;
}
.tabbody>*>.titlebar{
display: flex;
}
.tabbody>*>.titlebar>*{
flex: 1;
margin: 1px 1px 1px 1px;
}
.tabbody>*>.context {
flex: 1;
flex-flow: column;
border: 0;
padding: 5px;
overflow-y: auto;
}
.tabbody>*>.inputbox{
border: 0;
border-bottom: #A9A9A9 solid 1px;
height: 15px;
text-align:center;
}
.link>* {
display: flex;
margin: 5px;
border-bottom: 1px solid;
text-decoration: none;
}
#RuleList>label>* {
background: #eee;
padding-left: 3px;
margin: 2px 0;
cursor: pointer;
}
#RuleList input[type=radio] {
display: none;
}
#RuleList input[type="radio"]:checked+* {
background: #15cda8;
}
.isError {
color: #FF0000;
}

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>书源编辑器v3.8</title>
<link rel="stylesheet" type="text/css" href="/index.css"/>
</head>
<body>
<div class="editor">
<div class="setbox">
<div class="rules">
<div><b>书源基础信息</b></div>
<div>
<div>书源名称:</div>
<textarea rows="1" id="bookSourceName" placeholder="书源名称(bookSourceName) | 会显示在书源列表"></textarea>
</div>
<div>
<div>书源分组:</div>
<textarea rows="1" id="bookSourceGroup" placeholder="书源分组(bookSourceGroup) | 描述书源的特征信息"></textarea>
</div>
<div>
<div>书源域名:</div>
<textarea rows="1" id="bookSourceUrl"
placeholder="书源URL(bookSourceUrl) | 通常填写网站主页(标头不可省略),例: https://www.qidian.com"></textarea>
</div>
<div>
<div>登录网页:</div>
<textarea rows="1" id="loginUrl" placeholder="登录URL(loginUrl) | 填写网站登录网址,仅在需要登录的书源有用"></textarea>
</div>
<div><b>书籍发现规则</b></div>
<div>
<div>发现菜单:</div>
<textarea rows="5" id="ruleFindUrl"
placeholder="发现分类菜单规则(ruleFindUrl),将显示在发现菜单&#10;每行一条发现分类(网址域名可省略):&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
</div>
<div>
<div>结果列表:</div>
<textarea rows="1" id="ruleFindList"
placeholder="发现页列表规则(ruleFindList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleFindName"
placeholder="发现页书名规则(ruleFindName) | 选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleFindAuthor"
placeholder="发现页作者规则(ruleFindAuthor) | 选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleFindKind"
placeholder="发现页分类规则(ruleFindKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleFindLastChapter"
placeholder="发现页最新章节规则(ruleFindLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleFindIntroduce"
placeholder="发现页简介规则(ruleFindIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleFindCoverUrl"
placeholder="发现页封面规则(ruleFindCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>详情链接:</div>
<textarea rows="1" id="ruleFindNoteUrl"
placeholder="发现页详情规则(ruleFindNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea>
</div>
<div><b>书籍搜索规则</b></div>
<div>
<div>搜索网址:</div>
<textarea rows="1" id="ruleSearchUrl"
placeholder="搜索网址(ruleSearchUrl) | [域名可省略]/search.php@kw=searchKey|char=utf-8"></textarea>
</div>
<div>
<div>结果验证:</div>
<textarea rows="1" id="ruleBookUrlPattern"
placeholder="搜索页URL验证(ruleBookUrlPattern) | 正则验证URL是否为详情页,成功则跳过搜索页解析"></textarea>
</div>
<div>
<div>结果列表:</div>
<textarea rows="1" id="ruleSearchList"
placeholder="搜索页列表规则(ruleSearchList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleSearchName"
placeholder="搜索页书名规则(ruleSearchName) | 选择节点书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleSearchAuthor"
placeholder="搜索页作者规则(ruleSearchAuthor) | 选择节点作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleSearchKind"
placeholder="搜索页分类规则(ruleSearchKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleSearchLastChapter"
placeholder="搜索页最新章节规则(ruleSearchLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleSearchIntroduce"
placeholder="搜索页简介规则(ruleSearchIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleSearchCoverUrl"
placeholder="搜索页封面规则(ruleSearchCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>详情链接:</div>
<textarea rows="1" id="ruleSearchNoteUrl"
placeholder="搜索页详情规则(ruleSearchNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea>
</div>
<div><b>书籍详情规则</b></div>
<div>
<div>页面处理:</div>
<textarea rows="1" id="ruleBookInfoInit"
placeholder="详情页信息预处理(ruleBookInfoInit) | 用于加速详情信息检索"></textarea>
</div>
<div>
<div>书籍名称:</div>
<textarea rows="1" id="ruleBookName"
placeholder="书名规则(ruleBookName) | 选择详情页书名 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍作者:</div>
<textarea rows="1" id="ruleBookAuthor"
placeholder="作者规则(ruleBookAuthor) | 选择详情页作者 (规则结果为String)"></textarea>
</div>
<div>
<div>书籍分类:</div>
<textarea rows="1" id="ruleBookKind"
placeholder="分类规则(ruleBookKind) | 选择详情页分类信息 (规则结果为List&lt;String&gt;)"></textarea>
</div>
<div>
<div>最新章节:</div>
<textarea rows="1" id="ruleBookLastChapter"
placeholder="最新章节规则(ruleBookLastChapter) | 选择详情页最新章节 (规则结果为String)"></textarea>
</div>
<div>
<div>简介内容:</div>
<textarea rows="1" id="ruleIntroduce"
placeholder="简介规则(ruleIntroduce) | 选择详情页书籍简介 (规则结果为String)"></textarea>
</div>
<div>
<div>封面链接:</div>
<textarea rows="1" id="ruleCoverUrl" placeholder="封面规则(ruleCoverUrl) | 选择详情页书籍封面 (规则结果为Url)"></textarea>
</div>
<div>
<div>目录链接:</div>
<textarea rows="1" id="ruleChapterUrl"
placeholder="目录URL规则(ruleChapterUrl) | 选择目录页网址 (规则结果为Url, 与详情页相同时可省略)"></textarea>
</div>
<div><b>目录列表规则</b></div>
<div>
<div>目录翻页:</div>
<textarea rows="1" id="ruleChapterUrlNext"
placeholder="目录下一页规则(ruleChapterUrlNext) | 选择目录下一页链接 (规则结果为List&lt;Url&gt;)"></textarea>
</div>
<div>
<div>目录列表:</div>
<textarea rows="1" id="ruleChapterList"
placeholder="目录列表规则(ruleChapterList) | 选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
</div>
<div>
<div>章节名称:</div>
<textarea rows="1" id="ruleChapterName"
placeholder="章节名称规则(ruleChapterName) | 选择章节名称 (规则结果为String)"></textarea>
</div>
<div>
<div>章节链接:</div>
<textarea rows="1" id="ruleContentUrl"
placeholder="章节URL规则(ruleContentUrl) | 选择章节链接 (规则结果为Url)"></textarea>
</div>
<div><b>正文阅读规则</b></div>
<div>
<div>章节正文:</div>
<textarea rows="1" id="ruleBookContent"
placeholder="正文规则(ruleBookContent) | 选择正文内容 (规则结果为String)"></textarea>
</div>
<div>
<div>正文翻页:</div>
<textarea rows="1" id="ruleContentUrlNext"
placeholder="正文翻页URL规则(ruleContentUrlNext) | 选择下一分页(不是下一章)链接 (规则结果为Url)"></textarea>
</div>
<div><b>其它规则</b></div>
<div>
<div>浏览标识:</div>
<textarea rows="1" id="httpUserAgent"
placeholder="浏览器UA(HttpUserAgent) | 浏览器标识:User-Agent (可选)"></textarea>
</div>
<div>
<div>排序编号:</div>
<textarea rows="1" id="serialNumber" placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
</div>
<div>
<div>搜索权重:</div>
<textarea rows="1" id="weight" placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea>
</div>
<div>
<div>是否启用:</div>
<textarea rows="1" id="enable" placeholder="默认启用=true,手动启用=false (可选,默认true)"></textarea>
</div>
</div>
</div>
<div class="menu">
<svg class="button">
<text x="50%" y="55%">⇈推送书源</text>
<rect id="push"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇊拉取书源</text>
<rect id="pull"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋘编辑书源</text>
<rect id="editor"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⋙生成书源</text>
<rect id="conver"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✗清空表单</text>
<rect id="initial"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↶撤销操作</text>
<rect id="undo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">↷重做操作</text>
<rect id="redo"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">⇏调试书源</text>
<rect id="debug"></rect>
</svg>
<svg class="button">
<text x="50%" y="55%">✓保存书源</text>
<rect id="accept"></rect>
</svg>
</div>
<div class="outbox">
<div class="tabbox">
<div class="tabtitle">
<div name="编辑书源" class="tab1 this">编辑书源</div>
<div name="调试书源" class="tab2">调试书源</div>
<div name="书源列表" class="tab3">书源列表</div>
<div name="帮助信息" class="tab4">帮助信息</div>
</div>
<div class="tabbody">
<div class="tab1 this">
<textarea class="context" id="RuleJsonString" placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
</div>
<div class="tab2">
<input type="text" class="inputbox" id="DebugKey" placeholder="我的">
<textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
</div>
<div class="tab3">
<div class="titlebar">
<button id="Import">导入书源文件</button>
<button id="Export">导出书源文件</button>
<button id="Delete">删除选中书源</button>
<button id="ClrAll">清空当前列表</button>
</div>
<div class="context" id="RuleList"></div>
</div>
<div class="tab4">
<div class="context link">
<a target="_blank" href="https://gedoor.github.io/MyBookshelf/sourcerule.html">官方书源教程</a>
<a target="_blank" href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a>
<a target="_blank" href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
<a target="_blank" href="https://www.w3cschool.cn/regex_rmjc/?">正则表达式教程</a>
<a target="_blank" href="https://regexr.com/">正则表达式在线验证工具</a>
<div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义
<br>(?s) 前缀表示跨行解析
<br>(?m) 前缀表示逐行匹配
<br>(?i) 前缀表示忽略大小写
</div>
<a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="/index.js"></script>
</body>
</html>

@ -0,0 +1,362 @@
// 简化js原生选择器
function $(selector) { return document.querySelector(selector); }
function $$(selector) { return document.querySelectorAll(selector); }
// 读写Hash值(val未赋值时为读取)
function hashParam(key, val) {
let hashstr = decodeURIComponent(window.location.hash);
let regKey = new RegExp(`${key}=([^&]*)`);
let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;
if (val == undefined) return getVal;
if (hashstr == '' || hashstr == '#') {
window.location.hash = `#${key}=${val}`;
}
else {
if (getVal) window.location.hash = hashstr.replace(getVal, val);
else {
window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;
}
}
}
// 创建书源规则容器对象
const RuleJSON = (() => {
let ruleJson = {};
$$('.rules textarea').forEach(item => ruleJson[item.id] = '');
// for (let item of $$('.rules textarea')) ruleJson[item.id] = '';
ruleJson.serialNumber = 0;
ruleJson.weight = 0;
ruleJson.enable = true;
return ruleJson;
})();
// 选项卡Tab切换事件处理
function showTab(tabName) {
$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
$$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });
$(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';
$(`.tabtitle>*[name=${tabName}]`).className += ' this';
hashParam('tab', tabName);
}
// 书源列表列表标签构造函数
function newRule(rule) {
return `<label for="${rule.bookSourceUrl}"><input type="radio" name="rule" id="${rule.bookSourceUrl}"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;
}
// 缓存规则列表
var RuleSources = [];
if (localStorage.getItem('RuleSources')) {
RuleSources = JSON.parse(localStorage.getItem('RuleSources'));
RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
}
// 页面加载完成事件
window.onload = () => {
$$('.tabtitle>*').forEach(item => {
item.addEventListener('click', () => {
showTab(item.innerHTML);
});
});
if (hashParam('tab')) showTab(hashParam('tab'));
}
// 获取数据
function HttpGet(url) {
return fetch(hashParam('domain') ? hashParam('domain') + url : url)
.then(res => res.json()).catch(err => console.error('Error:', err));
}
// 提交数据
function HttpPost(url, data) {
return fetch(hashParam('domain') ? hashParam('domain') + url : url, {
body: JSON.stringify(data),
method: 'POST',
mode: "cors",
headers: new Headers({
'Content-Type': 'application/json;charset=utf-8'
})
}).then(res => res.json()).catch(err => console.error('Error:', err));
}
// 将书源表单转化为书源对象
function rule2json() {
Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value);
RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
return RuleJSON;
}
// 将书源对象填充到书源表单
function json2rule(RuleEditor) {
Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : '');
}
// 记录操作过程
var course = { "old": [], "now": {}, "new": [] };
if (localStorage.getItem('course')) {
course = JSON.parse(localStorage.getItem('course'));
json2rule(course.now);
}
else {
course.now = rule2json();
window.localStorage.setItem('course', JSON.stringify(course));
}
function todo() {
course.old.push(Object.assign({}, course.now));
course.now = rule2json();
course.new = [];
if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小
localStorage.setItem('course', JSON.stringify(course));
}
function undo() {
course = JSON.parse(localStorage.getItem('course'));
if (course.old.length > 0) {
course.new.push(course.now);
course.now = course.old.pop();
localStorage.setItem('course', JSON.stringify(course));
json2rule(course.now);
}
}
function redo() {
course = JSON.parse(localStorage.getItem('course'));
if (course.new.length > 0) {
course.old.push(course.now);
course.now = course.new.pop();
localStorage.setItem('course', JSON.stringify(course));
json2rule(course.now);
}
}
function setRule(editRule) {
let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);
if ($(`input[id="${editRule.bookSourceUrl}"]`)) {
Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });
$(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;
} else {
RuleSources.push(editRule);
$('#RuleList').innerHTML += newRule(editRule);
}
}
$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });
$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });
// 处理按钮点击事件
$('.menu').addEventListener('click', e => {
let thisNode = e.target;
thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :
thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;
if (!thisNode) return;
if (thisNode.getAttribute('class') == 'busy') return;
thisNode.setAttribute('class', 'busy');
switch (thisNode.id) {
case 'push':
$$('#RuleList>label>div').forEach(item => { item.className = ''; });
(async () => {
await HttpPost(`/saveSources`, RuleSources).then(json => {
if (json.isSuccess) {
let okData = json.data;
if (Array.isArray(okData)) {
let failMsg = ``;
if (RuleSources.length > okData.length) {
RuleSources.forEach(item => {
if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }
else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }
});
failMsg = '\n推送失败的书源将用红色字体标注!';
}
alert(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length}\n成功: ${okData.length}\n失败: ${RuleSources.length - okData.length}${failMsg}`);
}
else {
alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length}`);
}
}
else {
alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`);
}
}).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
case 'pull':
showTab('书源列表');
(async () => {
await HttpGet(`/getSources`).then(json => {
if (json.isSuccess) {
$('#RuleList').innerHTML = ''
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
alert(`成功拉取 ${RuleSources.length} 条书源`);
}
else {
alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`);
}
}).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
case 'editor':
if ($('#RuleJsonString').value == '') break;
try {
json2rule(JSON.parse($('#RuleJsonString').value));
todo();
} catch (error) {
console.log(error);
alert(error);
}
break;
case 'conver':
showTab('编辑书源');
$('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);
break;
case 'initial':
$$('.rules textarea').forEach(item => { item.value = '' });
todo();
break;
case 'undo':
undo()
break;
case 'redo':
redo()
break;
case 'debug':
showTab('调试书源');
let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1));
let DebugInfos = $('#DebugConsole');
function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
let saveRule = [rule2json()];
HttpPost(`/saveSources`, saveRule).then(sResult => {
if (sResult.isSuccess) {
let sKey = DebugKey.value ? DebugKey.value : '我的';
$('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
let ws = new WebSocket(`${wsOrigin}/sourceDebug`);
ws.onopen = () => {
ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
};
ws.onmessage = (msg) => {
DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data);
if (msg.data == 'finish') setRule(saveRule[0]);
};
ws.onerror = (err) => {
throw `${err.data}`;
}
ws.onclose = () => {
thisNode.setAttribute('class', '');
DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`);
}
} else throw `${sResult.errorMsg}`;
}).catch(err => {
DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
thisNode.setAttribute('class', '');
});
return;
case 'accept':
(async () => {
let saveRule = [rule2json()];
await HttpPost(`/saveSources`, saveRule).then(json => {
alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
setRule(saveRule[0]);
}).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\n${err}`); });
thisNode.setAttribute('class', '');
})();
return;
default:
}
setTimeout(() => { thisNode.setAttribute('class', ''); }, 500);
});
$('#DebugKey').addEventListener('keydown', e => {
if (e.keyCode == 13) {
let clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent("click", true, false);
$('#debug').dispatchEvent(clickEvent);
}
});
// 列表规则更改事件
$('#RuleList').addEventListener('click', e => {
let editRule = null;
if (e.target && e.target.getAttribute('name') == 'rule') {
editRule = rule2json();
json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));
} else return;
if (editRule.bookSourceUrl == '') return;
if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
setRule(editRule);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
});
// 处理列表按钮事件
$('.tab3>.titlebar').addEventListener('click', e => {
let thisNode = e.target;
if (thisNode.nodeName != 'BUTTON') return;
switch (thisNode.id) {
case 'Import':
let fileImport = document.createElement('input');
fileImport.type = 'file';
fileImport.accept = '.json';
fileImport.addEventListener('change', () => {
let file = fileImport.files[0];
let reader = new FileReader();
reader.onloadend = function (evt) {
if (evt.target.readyState == FileReader.DONE) {
let fileText = evt.target.result;
try {
let fileJson = JSON.parse(fileText);
let newSources = [];
newSources.push(...fileJson);
if (window.confirm(`如何处理导入的书源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources));
$('#RuleList').innerHTML = ''
RuleSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
}
else {
newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
RuleSources.push(...newSources);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
newSources.forEach(item => {
$('#RuleList').innerHTML += newRule(item);
});
}
alert(`成功导入 ${newSources.length} 条书源`);
}
catch (err) {
alert(`导入书源文件失败!\n${err}`);
}
}
};
reader.readAsText(file);
}, false);
fileImport.click();
break;
case 'Export':
let fileExport = document.createElement('a');
fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;
let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" });
fileExport.href = window.URL.createObjectURL(myBlob);
fileExport.click();
break;
case 'Delete':
let selectRule = $('#RuleList input:checked');
if (!selectRule) {
alert(`没有书源被选中!`);
return;
}
if (confirm(`确定要删除选定书源吗?\n(同时删除APP内书源)`)) {
let selectRuleUrl = selectRule.id;
let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的书源
let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl)); // 提取待留下的书源
HttpPost(`/deleteSources`, deleteSources).then(json => {
if (json.isSuccess) {
let selectNode = document.getElementById(selectRuleUrl).parentNode;
selectNode.parentNode.removeChild(selectNode);
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));
if ($('#bookSourceUrl').value == selectRuleUrl) {
$$('.rules textarea').forEach(item => { item.value = '' });
todo();
}
console.log(deleteSources);
console.log(`以上书源已删除!`)
}
}).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); });
}
break;
case 'ClrAll':
if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) {
localStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));
$('#RuleList').innerHTML = ''
}
break;
default:
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

@ -1,8 +1,29 @@
package io.legado.app
import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.data.AppDatabase
import io.legado.app.help.ActivityHelp
import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.widget.page.ChapterProvider
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.isNightTheme
@Suppress("DEPRECATION")
class App : Application() {
companion object {
@ -15,9 +36,148 @@ class App : Application() {
private set
}
var versionCode = 0
var versionName = ""
override fun onCreate() {
super.onCreate()
INSTANCE = this
CrashHandler().init(this)
db = AppDatabase.createDatabase(INSTANCE)
packageManager.getPackageInfo(packageName, 0)?.let {
versionCode = it.versionCode
versionName = it.versionName
}
if (!ThemeStore.isConfigured(this, versionCode)) applyTheme()
initNightTheme()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId()
LiveEventBus.get()
.config()
.supportBroadcast(this)
.lifecycleObserverAlwaysActive(true)
.autoClear(false)
registerActivityLife()
}
/**
* 更新主题
*/
fun applyTheme() {
if (isNightTheme) {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
)
.accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
)
.backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.md_grey_800))
)
.apply()
} else {
ThemeStore.editTheme(this)
.primaryColor(getPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)))
.accentColor(getPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)))
.backgroundColor(getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)))
.apply()
}
ChapterProvider.upReadAloudSpan()
}
fun applyDayNight() {
ReadBookConfig.upBg()
applyTheme()
initNightTheme()
}
private fun initNightTheme() {
val targetMode = if (isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(targetMode)
}
/**
* 创建通知ID
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannelId() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.let {
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.download_offline),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
downloadChannel.enableLights(false)
downloadChannel.enableVibration(false)
downloadChannel.setSound(null, null)
//用唯一的ID创建渠道对象
val readAloudChannel = NotificationChannel(
channelIdReadAloud,
getString(R.string.read_aloud),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
readAloudChannel.enableLights(false)
readAloudChannel.enableVibration(false)
readAloudChannel.setSound(null, null)
//用唯一的ID创建渠道对象
val webChannel = NotificationChannel(
channelIdWeb,
getString(R.string.web_service),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel
webChannel.enableLights(false)
webChannel.enableVibration(false)
webChannel.setSound(null, null)
//向notification manager 提交channel
it.createNotificationChannels(listOf(downloadChannel, readAloudChannel, webChannel))
}
}
private fun registerActivityLife() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
ActivityHelp.remove(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
ActivityHelp.add(activity)
}
})
}
}

@ -1,58 +1,98 @@
package io.legado.app.base
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModel
import io.legado.app.R
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ColorUtils
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.disableAutoFill
import io.legado.app.utils.hideSoftInput
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
abstract class BaseActivity<BD : ViewDataBinding, VM : ViewModel> : AppCompatActivity() {
protected lateinit var dataBinding: BD
private set
protected abstract val viewModel: VM
protected abstract val layoutID: Int
abstract class BaseActivity(private val layoutID: Int, private val fullScreen: Boolean = true) :
AppCompatActivity(),
CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
window.decorView.disableAutoFill()
initTheme()
setupSystemBar()
super.onCreate(savedInstanceState)
dataBinding = DataBindingUtil.setContentView(this, layoutID)
onViewModelCreated(viewModel, savedInstanceState)
setContentView(layoutID)
onActivityCreated(savedInstanceState)
observeLiveBus()
}
open fun onViewModelCreated(viewModel: VM, savedInstanceState: Bundle?){
override fun onDestroy() {
super.onDestroy()
cancel()
}
abstract fun onActivityCreated(savedInstanceState: Bundle?)
final override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return menu?.let {
val bool = onCompatCreateOptionsMenu(it)
it.applyTint(this)
bool
} ?: super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
open fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
return super.onCreateOptionsMenu(menu)
}
final override fun onOptionsItemSelected(item: MenuItem?): Boolean {
item?.let {
if (it.itemId == android.R.id.home) {
supportFinishAfterTransition()
return true
}
}
return if (item == null) false else onCompatOptionsItemSelected(item)
return item != null && onCompatOptionsItemSelected(item)
}
open fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
return true
return super.onOptionsItemSelected(item)
}
override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title
private fun initTheme() {
ATH.applyBackgroundTint(window.decorView)
if (ColorUtils.isColorLight(primaryColor)) {
setTheme(R.style.AppTheme_Light)
} else {
setTheme(R.style.AppTheme_Dark)
}
}
override fun setTitle(titleId: Int) {
supportActionBar?.setTitle(titleId)
private fun setupSystemBar() {
if (fullScreen) {
window.clearFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
ATH.setStatusBarColorAuto(this, fullScreen)
}
fun setSubTitle(subtitle: CharSequence?) {
supportActionBar?.subtitle = subtitle
open fun observeLiveBus() {
}
fun setSubTitle(subtitleId: Int) {
supportActionBar?.setSubtitle(subtitleId)
override fun finish() {
currentFocus?.hideSoftInput()
super.finish()
}
}

@ -0,0 +1,51 @@
package io.legado.app.base
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import io.legado.app.utils.applyTint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
CoroutineScope by MainScope() {
var supportToolbar: Toolbar? = null
private set
val menuInflater: MenuInflater
get() = SupportMenuInflater(requireContext())
override fun onDestroy() {
super.onDestroy()
cancel()
}
fun setSupportToolbar(toolbar: Toolbar) {
supportToolbar = toolbar
supportToolbar?.let {
it.menu.apply {
onCompatCreateOptionsMenu(this)
applyTint(requireContext())
}
it.setOnMenuItemClickListener { item ->
onCompatOptionsItemSelected(item)
true
}
}
}
open fun onCompatCreateOptionsMenu(menu: Menu) {
}
open fun onCompatOptionsItemSelected(item: MenuItem) {
}
}

@ -0,0 +1,21 @@
package io.legado.app.base
import android.app.Service
import android.content.Intent
import android.os.IBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
abstract class BaseService : Service(), CoroutineScope by MainScope() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}

@ -1,52 +1,65 @@
package io.legado.app.base
import android.app.Application
import android.content.Context
import androidx.annotation.CallSuper
import androidx.lifecycle.AndroidViewModel
import io.legado.app.App
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.*
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
open class BaseViewModel(application: Application) : AndroidViewModel(application),
CoroutineScope by MainScope(),
AnkoLogger {
val context: Context by lazy { this.getApplication<App>() }
private val launchManager: MutableList<Job> = mutableListOf()
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
fun <R> submit(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> Deferred<R>
): Coroutine<R> {
return Coroutine.async(scope, context) { block().await() }
}
protected fun launchOnUI(
tryBlock: suspend CoroutineScope.() -> Unit,//成功
errorBlock: suspend CoroutineScope.(Throwable) -> Unit,//失败
finallyBlock: suspend CoroutineScope.() -> Unit//结束
) {
launchOnUI {
tryCatch(tryBlock, errorBlock, finallyBlock)
@CallSuper
override fun onCleared() {
super.onCleared()
cancel()
}
open fun toast(message: Int) {
launch {
context.toast(message)
}
}
/**
* add launch task to [launchManager]
*/
private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
val job = launch { block() }//主线程
launchManager.add(job)
job.invokeOnCompletion { launchManager.remove(job) }
open fun toast(message: CharSequence) {
launch {
context.toast(message)
}
}
private suspend fun tryCatch(
tryBlock: suspend CoroutineScope.() -> Unit,
errorBlock: suspend CoroutineScope.(Throwable) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
try {
coroutineScope { tryBlock() }
} catch (e: Throwable) {
coroutineScope { errorBlock(e) }
} finally {
coroutineScope { finallyBlock() }
open fun longToast(message: Int) {
launch {
context.toast(message)
}
}
override fun onCleared() {
super.onCleared()
launchManager.clear()
open fun longToast(message: CharSequence) {
launch {
context.toast(message)
}
}
}

@ -0,0 +1,10 @@
package io.legado.app.base
import androidx.lifecycle.ViewModel
abstract class VMBaseActivity<VM : ViewModel>(layoutID: Int, fullScreen: Boolean = true) :
BaseActivity(layoutID, fullScreen) {
protected abstract val viewModel: VM
}

@ -0,0 +1,9 @@
package io.legado.app.base
import androidx.lifecycle.ViewModel
abstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) {
protected abstract val viewModel: VM
}

@ -53,11 +53,11 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
itemDelegates.put(viewType, delegate)
itemDelegates[viewType] = delegate
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) {
itemDelegates.put(itemDelegates.size, delegate)
itemDelegates[itemDelegates.size] = delegate
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) {
@ -122,7 +122,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
}
fun setItems(items: List<ITEM>?) {
fun setItems(items: List<ITEM>?, notify: Boolean = true) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
@ -130,7 +130,9 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
if (items != null) {
this.items.addAll(items)
}
notifyDataSetChanged()
if (notify) {
notifyDataSetChanged()
}
}
}
@ -148,26 +150,15 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.add(item)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemInserted(oldSize + getHeaderCount())
}
notifyItemInserted(oldSize + getHeaderCount())
}
}
}
fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
if (if (oldSize == 0) this.items.addAll(newItems) else this.items.addAll(position, newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(position + getHeaderCount(), newItems.size)
}
}
if (this.items.addAll(position, newItems)) {
notifyItemRangeInserted(position + getHeaderCount(), newItems.size)
}
}
}
@ -176,33 +167,32 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) {
if (oldSize == 0) {
notifyDataSetChanged()
} else {
notifyItemRangeChanged(oldSize + getHeaderCount(), newItems.size)
}
notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size)
}
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (this.items.removeAt(position) != null)
if (this.items.removeAt(position) != null) {
notifyItemRemoved(position + getHeaderCount())
}
}
}
fun removeItem(item: ITEM) {
synchronized(lock) {
if (this.items.remove(item))
if (this.items.remove(item)) {
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
}
}
}
fun removeItems(items: List<ITEM>) {
synchronized(lock) {
if (this.items.removeAll(items))
if (this.items.removeAll(items)) {
notifyDataSetChanged()
}
}
}
@ -210,8 +200,21 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
synchronized(lock) {
val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) {
Collections.swap(this.items, oldPosition + getHeaderCount(), newPosition + getHeaderCount())
notifyDataSetChanged()
val srcPosition = oldPosition + getHeaderCount()
val targetPosition = newPosition + getHeaderCount()
Collections.swap(this.items, srcPosition, targetPosition)
notifyItemChanged(srcPosition)
notifyItemChanged(targetPosition)
}
}
}
fun updateItem(item: ITEM) {
synchronized(lock) {
val index = this.items.indexOf(item)
if (index >= 0) {
this.items[index] = item
notifyItemChanged(index)
}
}
}
@ -234,6 +237,13 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
}
fun clearItems() {
synchronized(lock) {
this.items.clear()
notifyDataSetChanged()
}
}
fun isEmpty(): Boolean {
return items.isEmpty()
}
@ -295,7 +305,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
}
else -> {
val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutID, parent, false))
val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutId, parent, false))
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
@ -324,7 +334,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : Rec
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
getItem(holder.layoutPosition)?.let {
getItem(holder.layoutPosition - getHeaderCount())?.let {
itemDelegates.getValue(getItemViewType(holder.layoutPosition))
.convert(holder, it, payloads)
}

@ -14,7 +14,7 @@ abstract class InfiniteScrollListener() : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// if (dy < 0 || dataLoading.isDataLoading()) return
val layoutManager:LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
val layoutManager: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
val visibleItemCount = recyclerView.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()

@ -2,12 +2,7 @@ package io.legado.app.base.adapter
import android.view.animation.Interpolator
import android.view.animation.LinearInterpolator
import io.legado.app.base.adapter.animations.AlphaInAnimation
import io.legado.app.base.adapter.animations.BaseAnimation
import io.legado.app.base.adapter.animations.ScaleInAnimation
import io.legado.app.base.adapter.animations.SlideInBottomAnimation
import io.legado.app.base.adapter.animations.SlideInLeftAnimation
import io.legado.app.base.adapter.animations.SlideInRightAnimation
import io.legado.app.base.adapter.animations.*
/**
* Created by Invincible on 2017/12/15.

@ -7,9 +7,7 @@ import android.content.Context
*
* item代理
*/
abstract class ItemViewDelegate<in ITEM>(protected val context: Context) {
abstract val layoutID: Int
abstract class ItemViewDelegate<ITEM>(protected val context: Context, val layoutId: Int) {
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)

@ -5,22 +5,18 @@ import android.content.Context
/**
* Created by Invincible on 2017/12/15.
*/
abstract class SimpleRecyclerAdapter<ITEM>(context: Context) : CommonRecyclerAdapter<ITEM>(context) {
abstract class SimpleRecyclerAdapter<ITEM>(context: Context, private val layoutId: Int) :
CommonRecyclerAdapter<ITEM>(context) {
init {
addItemViewDelegate(object : ItemViewDelegate<ITEM>(context) {
override val layoutID: Int
get() = this@SimpleRecyclerAdapter.layoutID
addItemViewDelegate(object : ItemViewDelegate<ITEM>(context, layoutId) {
override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>) {
this@SimpleRecyclerAdapter.convert(holder, item, payloads)
}
})
}
abstract val layoutID: Int
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>)
}

@ -8,7 +8,7 @@ import android.view.View
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
companion object {

@ -3,11 +3,10 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInBottomAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
}

@ -3,12 +3,11 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInLeftAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
}

@ -3,12 +3,11 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInRightAnimation : BaseAnimation {
override fun getAnimators(view: View): Array<Animator> =
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
}

@ -0,0 +1,14 @@
package io.legado.app.constant
object Action {
const val play = "play"
const val stop = "stop"
const val resume = "resume"
const val pause = "pause"
const val addTimer = "addTimer"
const val setTimer = "setTimer"
const val prevParagraph = "prevParagraph"
const val nextParagraph = "nextParagraph"
const val upTtsSpeechRate = "upTtsSpeechRate"
}

@ -1,11 +1,56 @@
package io.legado.app.constant
import android.annotation.SuppressLint
import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.BookGroup
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.putPrefBoolean
import java.text.SimpleDateFormat
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
@SuppressLint("SimpleDateFormat")
object AppConst {
const val APP_TAG = "Legado"
const val channelIdDownload = "channel_download"
const val channelIdReadAloud = "channel_read_aloud"
const val channelIdWeb = "channel_web"
const val APP_TAG = "Legado"
const val RC_IMPORT_YUEDU_DATA = 100
val userAgent: String by lazy {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
}
val SCRIPT_ENGINE: ScriptEngine by lazy {
ScriptEngineManager().getEngineByName("rhino")
}
val TIME_FORMAT: SimpleDateFormat by lazy {
SimpleDateFormat("HH:mm")
}
val keyboardToolChars: List<String> by lazy {
arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
"href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag"
)
}
val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
var bookGroupLocalShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
}
var bookGroupAudioShow: Boolean
get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
set(value) {
App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
}
}

@ -0,0 +1,7 @@
package io.legado.app.constant
object BookType {
const val default = 0
const val audio = 1
const val local = "loc_book"
}

@ -0,0 +1,16 @@
package io.legado.app.constant
object Bus {
const val RECREATE = "RECREATE"
const val UP_BOOK = "sourceDebugLog"
const val ALOUD_STATE = "aloud_state"
const val TTS_START = "ttsStart"
const val TTS_TURN_PAGE = "ttsTurnPage"
const val TTS_DS = "ttsDs"
const val BATTERY_CHANGED = "batteryChanged"
const val TIME_CHANGED = "timeChanged"
const val READ_ALOUD_BUTTON = "readAloudButton"
const val UP_CONFIG = "upConfig"
const val OPEN_CHAPTER = "openChapter"
const val REPLACE = "replace"
}

@ -0,0 +1,8 @@
package io.legado.app.constant
import java.util.regex.Pattern
object Pattern {
val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
}

@ -0,0 +1,19 @@
package io.legado.app.constant
object RSSKeywords {
const val RSS_ITEM = "item"
const val RSS_ITEM_TITLE = "title"
const val RSS_ITEM_LINK = "link"
const val RSS_ITEM_AUTHOR = "author"
const val RSS_ITEM_CATEGORY = "category"
const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
const val RSS_ITEM_ENCLOSURE = "enclosure"
const val RSS_ITEM_DESCRIPTION = "description"
const val RSS_ITEM_CONTENT = "content:encoded"
const val RSS_ITEM_PUB_DATE = "pubDate"
const val RSS_ITEM_TIME = "time"
const val RSS_ITEM_URL = "url"
const val RSS_ITEM_TYPE = "type"
const val RSS_ITEM_GUID = "guid"
}

@ -0,0 +1,7 @@
package io.legado.app.constant
object Status {
const val STOP = 0
const val PLAY = 1
const val PAUSE = 3
}

@ -6,23 +6,22 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.data.dao.BookDao
import io.legado.app.data.dao.ReplaceRuleDao
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Chapter
import io.legado.app.data.entities.ReplaceRule
import javax.xml.transform.Source
import io.legado.app.data.dao.*
import io.legado.app.data.entities.*
@Database(entities = [Book::class, Chapter::class, ReplaceRule::class], version = 1, exportSchema = true)
// @TypeConverters(Converters::class)
@Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class,
SearchBook::class, SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class,
RssArticle::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DATABASE_NAME = "legado.db"
private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
@ -35,23 +34,30 @@ abstract class AppDatabase : RoomDatabase() {
}
fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
)
// .addMigrations(MIGRATION_1_2)
// .addMigrations(MIGRATION_2_3)
// .addMigrations(MIGRATION_3_4)
// .addMigrations(MIGRATION_4_5)
// .addMigrations(MIGRATION_5_6)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
.build()
}
}
abstract fun bookDao(): BookDao
abstract fun bookGroupDao(): BookGroupDao
abstract fun bookSourceDao(): BookSourceDao
abstract fun bookChapterDao(): BookChapterDao
abstract fun replaceRuleDao(): ReplaceRuleDao
abstract fun searchBookDao(): SearchBookDao
abstract fun searchKeywordDao(): SearchKeywordDao
abstract fun sourceCookieDao(): SourceCookieDao
abstract fun rssSourceDao(): RssSourceDao
abstract fun bookmarkDao(): BookmarkDao
abstract fun rssArticleDao(): RssArticleDao
}

@ -1,14 +0,0 @@
package io.legado.app.data.api
import kotlinx.coroutines.Deferred
import retrofit2.http.*
interface CommonHttpApi {
@GET
fun get(@Url url: String, @QueryMap map: Map<String, String>): Deferred<String>
@FormUrlEncoded
@POST
fun post(@Url url: String, @FieldMap map: Map<String, String>): Deferred<String>
}

@ -0,0 +1,42 @@
package io.legado.app.data.api
import kotlinx.coroutines.Deferred
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.QueryMap
import retrofit2.http.Url
/**
* Created by GKF on 2018/1/21.
* get web content
*/
interface IHttpGetApi {
@GET
fun getAsync(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
@GET
fun getMapAsync(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
@GET
fun get(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
fun getMap(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<String>
}

@ -0,0 +1,53 @@
package io.legado.app.data.api
import kotlinx.coroutines.Deferred
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.*
/**
* Created by GKF on 2018/1/29.
* post
*/
interface IHttpPostApi {
@FormUrlEncoded
@POST
fun postMapAsync(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
@POST
fun postBodyAsync(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Deferred<Response<String>>
@FormUrlEncoded
@POST
fun postMap(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<String>
@POST
fun postBody(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Call<String>
@FormUrlEncoded
@POST
fun postMapByte(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
}

@ -0,0 +1,31 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legado.app.data.entities.BookChapter
@Dao
interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): LiveData<List<BookChapter>>
@Query("select * from chapters where bookUrl = :bookUrl")
fun getChapterList(bookUrl: String): List<BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
fun getChapter(bookUrl: String, index: Int): BookChapter?
@Query("select count(url) from chapters where bookUrl = :bookUrl")
fun getChapterCount(bookUrl: String): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookChapter: BookChapter)
@Query("delete from chapters where bookUrl = :bookUrl")
fun delByBook(bookUrl: String)
}

@ -1,19 +1,56 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Query
import androidx.room.*
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
@Dao
interface BookDao {
@Query("SELECT * FROM books order by durChapterTime desc")
fun observeAll(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE type = ${BookType.audio} order by durChapterTime desc")
fun observeAudio(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocal(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `group` = :group")
fun observeByGroup(group: Int): DataSource.Factory<Int, Book>
fun observeByGroup(group: Int): LiveData<List<Book>>
@Query("SELECT descUrl FROM books WHERE `group` = :group")
@Query("SELECT bookUrl FROM books WHERE `group` = :group")
fun observeUrlsByGroup(group: Int): LiveData<List<String>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `name` in (:names)")
fun findByName(vararg names: String): List<Book>
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
fun getBook(bookUrl: String): Book?
@get:Query("SELECT * FROM books")
val allBooks: List<Book>
@get:Query("SELECT * FROM books ORDER BY durChapterTime DESC limit 1")
val lastReadBook: Book?
@get:Query("SELECT bookUrl FROM books")
val allBookUrls: List<String>
@get:Query("SELECT COUNT(*) FROM books")
val allBookCount: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg book: Book)
@Update
fun update(vararg books: Book)
@Query("delete from books where bookUrl = :bookUrl")
fun delete(bookUrl: String)
}

@ -0,0 +1,24 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.BookGroup
@Dao
interface BookGroupDao {
@Query("SELECT * FROM book_groups ORDER BY `order`")
fun liveDataAll(): LiveData<List<BookGroup>>
@get:Query("SELECT MAX(groupId) FROM book_groups")
val maxId: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookGroup: BookGroup)
@Update
fun update(vararg bookGroup: BookGroup)
@Delete
fun delete(vararg bookGroup: BookGroup)
}

@ -0,0 +1,82 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.*
import io.legado.app.data.entities.BookSource
@Dao
interface BookSourceDao {
@Query("select * from book_sources order by customOrder asc")
fun liveDataAll(): LiveData<List<BookSource>>
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
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")
fun liveExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''")
fun liveGroupEnabled(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)")
fun enableSection(vararg sourceUrls: String)
@Query("update book_sources set enabled = 0 where bookSourceUrl in (:sourceUrls)")
fun disableSection(vararg sourceUrls: String)
@Query("delete from book_sources where bookSourceUrl in (:sourceUrls)")
fun delSection(vararg sourceUrls: String)
@Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource>
@Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'")
fun getByGroup(group: String): List<BookSource>
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
val noGroup: List<BookSource>
@get:Query("select * from book_sources order by customOrder asc")
val all: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 order by customOrder asc")
val allEnabled: List<BookSource>
@Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource?
@Query("select count(*) from book_sources")
fun allCount(): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookSource: BookSource)
@Update
fun update(vararg bookSource: BookSource)
@Delete
fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String)
@get:Query("select min(customOrder) from book_sources")
val minOrder: Int
@get:Query("select max(customOrder) from book_sources")
val maxOrder: Int
}

@ -0,0 +1,18 @@
package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Query
import io.legado.app.data.entities.Bookmark
@Dao
interface BookmarkDao {
@Query("select * from bookmarks")
fun all(): List<Bookmark>
@Query("select * from bookmarks where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): DataSource.Factory<Int, Bookmark>
}

@ -1,7 +1,6 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.*
import io.legado.app.data.entities.ReplaceRule
@ -10,10 +9,13 @@ import io.legado.app.data.entities.ReplaceRule
interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
fun observeAll(): DataSource.Factory<Int, ReplaceRule>
fun liveDataAll(): LiveData<List<ReplaceRule>>
@Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC")
fun observeAllIds(): LiveData<List<Int>>
@Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
fun liveDataSearch(key: String): LiveData<List<ReplaceRule>>
@get:Query("SELECT MIN(sortOrder) FROM replace_rules")
val minOrder: Int
@get:Query("SELECT MAX(sortOrder) FROM replace_rules")
val maxOrder: Int
@ -25,25 +27,53 @@ interface ReplaceRuleDao {
val allEnabled: List<ReplaceRule>
@Query("SELECT * FROM replace_rules WHERE id = :id")
fun findById(id: Int): ReplaceRule?
fun findById(id: Long): ReplaceRule?
@Query("SELECT * FROM replace_rules WHERE id in (:ids)")
fun findByIds(vararg ids: Int): 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("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'")
@Query("delete from replace_rules where id in (:ids)")
fun delSection(vararg ids: Long)
@Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1
AND (scope LIKE '%' || :scope || '%' or scope = null or scope = '')"""
)
fun findEnabledByScope(scope: String): List<ReplaceRule>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg replaceRules: ReplaceRule)
@Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope = null or scope = '')"""
)
fun findEnabledByScope(name: String, origin: String): List<ReplaceRule>
@Query("select `group` from replace_rules where `group` is not null and `group` <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("select * from replace_rules where `group` like '%' || :group || '%'")
fun getByGroup(group: String): List<ReplaceRule>
@get:Query("select * from replace_rules where `group` is null or `group` = ''")
val noGroup: List<ReplaceRule>
@get:Query("SELECT COUNT(*) - SUM(isEnabled) FROM replace_rules")
val summary: Int
@Query("UPDATE replace_rules SET isEnabled = :enable")
fun enableAll(enable: Boolean)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(replaceRule: ReplaceRule): Long
fun insert(vararg replaceRule: ReplaceRule): List<Long>
@Update
fun update(vararg replaceRules: ReplaceRule)
@Delete
fun delete(vararg replaceRules: ReplaceRule)
}

@ -0,0 +1,24 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RssArticle
@Dao
interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin and title = :title")
fun get(origin: String, title: String): RssArticle?
@Query("select * from rssArticles where origin = :origin order by `order` desc")
fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(vararg rssArticle: RssArticle)
@Update
fun update(vararg rssArticle: RssArticle)
@Query("delete from rssArticles where origin = :origin")
fun delete(origin: String)
}

@ -0,0 +1,60 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RssSource
@Dao
interface RssSourceDao {
@Query("select * from rssSources where sourceUrl = :key")
fun getByKey(key: String): RssSource?
@get:Query("SELECT * FROM rssSources")
val all: List<RssSource>
@Query("SELECT * FROM rssSources order by customOrder")
fun liveAll(): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder")
fun liveSearch(key: String): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun liveEnabled(): LiveData<List<RssSource>>
@Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
fun liveGroup(): LiveData<List<String>>
@Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)")
fun enableSection(vararg sourceUrls: String)
@Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)")
fun disableSection(vararg sourceUrls: String)
@get:Query("select min(customOrder) from rssSources")
val minOrder: Int
@get:Query("select max(customOrder) from rssSources")
val maxOrder: Int
@Query("delete from rssSources where sourceUrl in (:sourceUrls)")
fun delSection(vararg sourceUrls: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rssSource: RssSource)
@Update
fun update(vararg rssSource: RssSource)
@Delete
fun delete(vararg rssSource: RssSource)
@Query("delete from rssSources where sourceUrl = :sourceUrl")
fun delete(sourceUrl: String)
@get:Query("select * from rssSources where sourceGroup is null or sourceGroup = ''")
val noGroup: List<RssSource>
@Query("select * from rssSources where sourceGroup like '%' || :group || '%'")
fun getByGroup(group: String): List<RssSource>
}

@ -0,0 +1,44 @@
package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchShow
@Dao
interface SearchBookDao {
@Query(
"""SELECT name, author, min(time) time, max(kind) kind, max(coverUrl) coverUrl,max(intro) intro, max(wordCount) wordCount,
max(latestChapterTitle) latestChapterTitle, count(origin) originCount
FROM searchBooks where time >= :time
group by name, author
order by case when name = :key then 1 when author = :key then 2 when name like '%'||:key||'%' then 3 when author like '%'||:key||'%' then 4 else 5 end, time"""
)
fun observeShow(key: String, time: Long): DataSource.Factory<Int, SearchShow>
@Query("SELECT * FROM searchBooks")
fun observeAll(): DataSource.Factory<Int, SearchBook>
@Query("SELECT * FROM searchBooks where time >= :time")
fun observeNew(time: Long): DataSource.Factory<Int, SearchBook>
@Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author order by originOrder limit 1")
fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query("select * from searchBooks where name = :name and author = :author order by originOrder")
fun getByNameAuthor(name: String, author: String): List<SearchBook>
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder")
fun getByNameAuthorEnable(name: String, author: String): List<SearchBook>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long>
}

@ -0,0 +1,35 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.SearchKeyword
@Dao
interface SearchKeywordDao {
@Query("SELECT * FROM search_keywords ORDER BY usage DESC")
fun liveDataByUsage(): LiveData<List<SearchKeyword>>
@Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC")
fun liveDataByTime(): LiveData<List<SearchKeyword>>
@Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC")
fun liveDataSearch(key: String): LiveData<List<SearchKeyword>>
@Query("select * from search_keywords where word = :key")
fun get(key: String): SearchKeyword?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg keywords: SearchKeyword)
@Update
fun update(vararg keywords: SearchKeyword)
@Delete
fun delete(vararg keywords: SearchKeyword)
@Query("DELETE FROM search_keywords")
fun deleteAll()
}

@ -0,0 +1,12 @@
package io.legado.app.data.dao
import androidx.room.Dao
import androidx.room.Query
@Dao
interface SourceCookieDao {
@Query("SELECT cookie FROM cookies Where url = :url")
fun getCookieByUrl(url: String): String?
}

@ -0,0 +1,26 @@
package io.legado.app.data.entities
import io.legado.app.utils.splitNotBlank
interface BaseBook {
var variableMap: HashMap<String, String>
var kind: String?
var wordCount: String?
var infoHtml: String?
var tocHtml: String?
fun putVariable(key: String, value: String)
fun getKindList(): List<String> {
val kindList = arrayListOf<String>()
wordCount?.let {
if (it.isNotBlank()) kindList.add(it)
}
kind?.let {
val kinds = it.splitNotBlank(",", "\n")
kindList.addAll(kinds)
}
return kindList
}
}

@ -1,50 +1,93 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.*
import io.legado.app.utils.strim
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.constant.BookType
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import kotlin.math.max
@Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["descUrl"], unique = true))])
data class Book(@PrimaryKey
var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍)
var name: String = "", // 书籍名称(书源获取)
var customName: String? = null, // 书籍名称(用户修改)
var author: String? = null, // 作者名称(书源获取)
var customAuthor: String? = null, // 作者名称(用户修改)
var tag: String? = null, // 分类信息(书源获取)
var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
var customCoverUrl: String? = null, // 封面Url(用户修改)
var description: String? = null, // 简介内容(书源获取)
var customDescription: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // 0: 文本读物, 1: 有声读物
var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = 0, // 最新章节标题更新时间
var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间
var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
var totalChapterNum: Int = 0, // 书籍目录总数
var durChapterTitle: String? = null, // 当前章节名称
var durChapterIndex: Int = 0, // 当前章节索引
var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间)
var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
) : Parcelable {
fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayName() = customName.strim() ?: name
fun getDisplayAuthor() = customAuthor.strim() ?: author
fun getDisplayCover() = customCoverUrl.strim() ?: coverUrl
fun getDisplayDescription() = customDescription.strim() ?: description
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
data class Book(
@PrimaryKey
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
var customCoverUrl: String? = null, // 封面Url(用户修改)
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType
var group: Int = 0, // 自定义分组索引号
var latestChapterTitle: String? = null, // 最新章节标题
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间
var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间
var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
var totalChapterNum: Int = 0, // 书籍目录总数
var durChapterTitle: String? = null, // 当前章节名称
var durChapterIndex: Int = 0, // 当前章节索引
var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
var durChapterTime: Long = System.currentTimeMillis(), // 最近一次阅读书籍的时间(打开正文的时间)
override var wordCount: String? = null,
var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
var order: Int = 0, // 手动排序
var originOrder: Int = 0, //书源排序
var useReplaceRule: Boolean = true, // 正文使用净化替换规则
var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
) : Parcelable, BaseBook {
@Ignore
@IgnoredOnParcel
override var variableMap: HashMap<String, String> = GSON.fromJsonObject(variable) ?: HashMap()
@Ignore
@IgnoredOnParcel
override var infoHtml: String? = null
@Ignore
@IgnoredOnParcel
override var tocHtml: String? = null
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
override fun putVariable(key: String, value: String) {
variableMap[key] = value
variable = GSON.toJson(variableMap)
}
fun toSearchBook(): SearchBook {
return SearchBook(
name = name,
author = author,
kind = kind,
bookUrl = bookUrl,
origin = origin,
originName = originName,
wordCount = wordCount,
latestChapterTitle = latestChapterTitle,
coverUrl = coverUrl,
intro = intro,
tocUrl = tocUrl,
originOrder = originOrder,
variable = variable
).apply {
this.infoHtml = this@Book.infoHtml
this.tocHtml = this@Book.tocHtml
}
}
}

@ -0,0 +1,32 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(
tableName = "chapters",
primaryKeys = ["url", "bookUrl"],
indices = [(Index(value = ["bookUrl"], unique = false)), (Index(value = ["bookUrl", "index"], unique = true))],
foreignKeys = [(ForeignKey(
entity = Book::class,
parentColumns = ["bookUrl"],
childColumns = ["bookUrl"],
onDelete = ForeignKey.CASCADE
))]
) // 删除书籍时自动删除章节
data class BookChapter(
var url: String = "", // 章节地址
var title: String = "", // 章节标题
var bookUrl: String = "", // 书籍地址
var index: Int = 0, // 章节序号
var resourceUrl: String? = null, // 音频真实URL
var tag: String? = null, //
var start: Long? = null, // 章节起始位置
var end: Long? = null // 章节终止位置
) : Parcelable

@ -0,0 +1,15 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "book_groups")
data class BookGroup(
@PrimaryKey
var groupId: Int = 0,
var groupName: String,
var order: Int = 0
) : Parcelable

@ -0,0 +1,176 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.userAgent
import io.legado.app.data.entities.rule.*
import io.legado.app.help.JsExtensions
import io.legado.app.utils.ACache
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefString
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.util.*
import javax.script.SimpleBindings
@Parcelize
@Entity(
tableName = "book_sources",
indices = [(Index(value = ["bookSourceUrl"], unique = false))]
)
data class BookSource(
var bookSourceName: String = "", // 名称
var bookSourceGroup: String? = null, // 分组
@PrimaryKey
var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = 0, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null,
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, //启用发现
var header: String? = null, // header
var loginUrl: String? = null, // 登录地址
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, //发现url
var ruleExplore: String? = null, // 发现规则
var searchUrl: String? = null, //搜索url
var ruleSearch: String? = null, // 搜索规则
var ruleBookInfo: String? = null, // 书籍信息页规则
var ruleToc: String? = null, // 目录页规则
var ruleContent: String? = null // 正文页规则
) : Parcelable {
@Ignore
@IgnoredOnParcel
var searchRuleV: SearchRule? = null
@Ignore
@IgnoredOnParcel
var exploreRuleV: ExploreRule? = null
@Ignore
@IgnoredOnParcel
var bookInfoRuleV: BookInfoRule? = null
@Ignore
@IgnoredOnParcel
var tocRuleV: TocRule? = null
@Ignore
@IgnoredOnParcel
var contentRuleV: ContentRule? = null
@Throws(Exception::class)
fun getHeaderMap(): Map<String, String> {
val headerMap = HashMap<String, String>()
headerMap["User-Agent"] = App.INSTANCE.getPrefString("user_agent") ?: userAgent
header?.let {
val header1 = when {
it.startsWith("@js:", true) ->
evalJS(it.substring(4)).toString()
it.startsWith("<js>", true) ->
evalJS(it.substring(4, it.lastIndexOf("<"))).toString()
else -> it
}
GSON.fromJsonObject<Map<String, String>>(header1)?.let { map ->
headerMap.putAll(map)
}
}
return headerMap
}
fun getSearchRule(): SearchRule {
searchRuleV ?: let {
searchRuleV = GSON.fromJsonObject<SearchRule>(ruleSearch)
searchRuleV ?: let { searchRuleV = SearchRule() }
}
return searchRuleV!!
}
fun getExploreRule(): ExploreRule {
exploreRuleV ?: let {
exploreRuleV = GSON.fromJsonObject<ExploreRule>(ruleExplore)
exploreRuleV ?: let { exploreRuleV = ExploreRule() }
}
return exploreRuleV!!
}
fun getBookInfoRule(): BookInfoRule {
bookInfoRuleV ?: let {
bookInfoRuleV = GSON.fromJsonObject<BookInfoRule>(ruleBookInfo)
bookInfoRuleV ?: let { bookInfoRuleV = BookInfoRule() }
}
return bookInfoRuleV!!
}
fun getTocRule(): TocRule {
tocRuleV ?: let {
tocRuleV = GSON.fromJsonObject<TocRule>(ruleToc)
tocRuleV ?: let { tocRuleV = TocRule() }
}
return tocRuleV!!
}
fun getContentRule(): ContentRule {
contentRuleV ?: let {
contentRuleV = GSON.fromJsonObject<ContentRule>(ruleContent)
contentRuleV ?: let { contentRuleV = ContentRule() }
}
return contentRuleV!!
}
fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>()
exploreUrl?.let {
var a = it
if (a.isNotBlank()) {
try {
if (it.startsWith("<js>", false)) {
val aCache = ACache.get(App.INSTANCE, "explore")
a = aCache.getAsString(bookSourceUrl) ?: ""
if (a.isBlank()) {
val bindings = SimpleBindings()
bindings["baseUrl"] = bookSourceUrl
bindings["java"] = JsExtensions
a = AppConst.SCRIPT_ENGINE.eval(
it.substring(4, it.lastIndexOf("<")),
bindings
).toString()
aCache.put(bookSourceUrl, a)
}
}
val b = a.split("(&&|\n)+".toRegex())
b.map { c ->
val d = c.split("::")
if (d.size > 1)
exploreKinds.add(ExploreKind(d[0], d[1]))
}
} catch (e: Exception) {
exploreKinds.add(ExploreKind(e.localizedMessage))
}
}
}
return exploreKinds
}
/**
* 执行JS
*/
@Throws(Exception::class)
private fun evalJS(jsStr: String): Any {
val bindings = SimpleBindings()
bindings["java"] = JsExtensions
return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings)
}
data class ExploreKind(
var title: String,
var url: String? = null
)
}

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

@ -1,28 +0,0 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "chapters",
indices = [(Index(value = ["url"], unique = true)), (Index(value = ["bookUrl", "index"], unique = true))],
foreignKeys = [(ForeignKey(entity = Book::class,
parentColumns = ["descUrl"],
childColumns = ["bookUrl"],
onDelete = ForeignKey.CASCADE))]) // 删除书籍时自动删除章节
data class Chapter(@PrimaryKey
var url: String = "", // 章节地址
var title: String = "", // 章节标题
var bookUrl: String = "", // 书籍地址
var index: Int = 0, // 章节序号
var resourceUrl: String? = null, // 音频真实URL
var tag: String? = null, //
var start: Long? = null, // 章节起始位置
var end: Long? = null // 章节终止位置
) : Parcelable

@ -0,0 +1,3 @@
package io.legado.app.data.entities
data class EditEntity(var key: String, var value: String?, var hint: Int)

@ -8,19 +8,20 @@ import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "replace_rules",
indices = [(Index(value = ["id"]))])
@Entity(
tableName = "replace_rules",
indices = [(Index(value = ["id"]))]
)
data class ReplaceRule(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
var name: String? = null,
var pattern: String? = null,
var replacement: String? = null,
var scope: String? = null,
var isEnabled: Boolean = true,
var isRegex: Boolean = true,
@ColumnInfo(name = "sortOrder")
var order: Int = 0
) : Parcelable
@PrimaryKey(autoGenerate = true)
var id: Long = System.currentTimeMillis(),
var name: String = "",
var group: String? = null,
var pattern: String = "",
var replacement: String = "",
var scope: String? = null,
var isEnabled: Boolean = true,
var isRegex: Boolean = true,
@ColumnInfo(name = "sortOrder")
var order: Int = 0
) : Parcelable

@ -0,0 +1,29 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.Ignore
@Entity(
tableName = "rssArticles",
primaryKeys = ["origin", "title"]
)
data class RssArticle(
var origin: String = "",
var title: String = "",
var order: Long = 0,
var author: String? = null,
var link: String? = null,
var pubDate: String? = null,
var description: String? = null,
var content: String? = null,
var image: String? = null,
var categories: String? = null,
var read: Boolean = false,
var star: Boolean = false
) {
@Ignore
var categoryList: MutableList<String> = mutableListOf()
}

@ -0,0 +1,33 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))])
data class RssSource(
@PrimaryKey
var sourceUrl: String = "",
var sourceName: String = "",
var sourceIcon: String = "",
var sourceGroup: String? = null,
var enabled: Boolean = true,
//列表规则
var ruleArticles: String? = null,
var ruleNextPage: String? = null,
var ruleTitle: String? = null,
var rulePubDate: String? = null,
//类别规则
var ruleCategories: String? = null,
//webView规则
var ruleDescription: String? = null,
var ruleImage: String? = null,
var ruleLink: String? = null,
var ruleContent: String? = null,
var enableJs: Boolean = false,
var loadWithBaseUrl: Boolean = false,
var customOrder: Int = 0
) : Parcelable

@ -1,5 +0,0 @@
package io.legado.app.data.entities
class Rule {
}

@ -1,3 +1,87 @@
package io.legado.app.data.entities
class SearchBook
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "searchBooks", indices = [(Index(value = ["bookUrl"], unique = true))])
data class SearchBook(
@PrimaryKey
var bookUrl: String = "",
var origin: String = "", // 书源规则
var originName: String = "",
var name: String = "",
var author: String = "",
override var kind: String? = null,
var coverUrl: String? = null,
var intro: String? = null,
override var wordCount: String? = null,
var latestChapterTitle: String? = null,
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var time: Long = System.currentTimeMillis(),
var variable: String? = null,
var originOrder: Int = 0
) : Parcelable, BaseBook, Comparable<SearchBook> {
@Ignore
@IgnoredOnParcel
override var infoHtml: String? = null
@Ignore
@IgnoredOnParcel
override var tocHtml: String? = null
override fun equals(other: Any?): Boolean {
if (other is SearchBook) {
if (other.bookUrl == bookUrl) {
return true
}
}
return false
}
override fun hashCode(): Int {
return bookUrl.hashCode()
}
override fun compareTo(other: SearchBook): Int {
return other.originOrder - this.originOrder
}
@IgnoredOnParcel
@Ignore
override var variableMap: HashMap<String, String> = GSON.fromJsonObject(variable) ?: HashMap()
override fun putVariable(key: String, value: String) {
variableMap[key] = value
variable = GSON.toJson(variableMap)
}
fun toBook(): Book {
return Book(
name = name,
author = author,
kind = kind,
bookUrl = bookUrl,
origin = origin,
originName = originName,
wordCount = wordCount,
latestChapterTitle = latestChapterTitle,
coverUrl = coverUrl,
intro = intro,
tocUrl = tocUrl,
originOrder = originOrder,
variable = variable
).apply {
this.infoHtml = this@SearchBook.infoHtml
this.tocUrl = this@SearchBook.tocUrl
}
}
}

@ -0,0 +1,17 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))])
data class SearchKeyword(
@PrimaryKey
var word: String = "", // 搜索关键词
var usage: Int = 1, // 使用次数
var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间
) : Parcelable

@ -0,0 +1,29 @@
package io.legado.app.data.entities
import io.legado.app.utils.splitNotBlank
data class SearchShow(
var name: String = "",
var author: String = "",
var kind: String? = null,
var coverUrl: String? = null,
var intro: String? = null,
var wordCount: String? = null,
var latestChapterTitle: String? = null,
var time: Long = 0L,
var originCount: Int = 0
) {
fun getKindList(): List<String> {
val kindList = arrayListOf<String>()
wordCount?.let {
if (it.isNotBlank()) kindList.add(it)
}
kind?.let {
val kinds = it.splitNotBlank(",", "\n")
kindList.addAll(kinds)
}
return kindList
}
}

@ -1,31 +0,0 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
@Entity(tableName = "sources",
indices = [(Index(value = ["sourceId"]))])
data class Source(@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "sourceId")
var id: Int = 0, // 编号
var name: String = "", // 名称
var origin: String = "", // 地址,包括 http/https
var type: Int = 0, // 类型,0 文本,1 音频
var group: String? = null, // 分组
var header: String? = null, // header
var loginUrl: String? = null, // 登录地址
var isEnabled: Boolean = true, // 是否启用
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var customOrder: Int = 0, // 手动排序编号
var weight: Int = 0, // 智能排序的权重
var exploreRule: String? = null, // 发现规则
var searchRule: String? = null, // 搜索规则
var bookInfoRule: String? = null, // 书籍信息页规则
var tocRule: String? = null, // 目录页规则
var contentRule: String? = null // 正文页规则
) : Parcelable

@ -0,0 +1,12 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))])
data class SourceCookie(
@PrimaryKey
var url: String = "",
var cookie: String = ""
)

@ -0,0 +1,14 @@
package io.legado.app.data.entities.rule
data class BookInfoRule(
var init: String? = null,
var name: String? = null,
var author: String? = null,
var intro: String? = null,
var kind: String? = null,
var lastChapter: String? = null,
var updateTime: String? = null,
var coverUrl: String? = null,
var tocUrl: String? = null,
var wordCount: String? = null
)

@ -0,0 +1,14 @@
package io.legado.app.data.entities.rule
interface BookListRule {
var bookList: String?
var name: String?
var author: String?
var intro: String?
var kind: String?
var lastChapter: String?
var updateTime: String?
var bookUrl: String?
var coverUrl: String?
var wordCount: String?
}

@ -0,0 +1,6 @@
package io.legado.app.data.entities.rule
data class ContentRule(
var content: String? = null,
var nextContentUrl: String? = null
)

@ -0,0 +1,14 @@
package io.legado.app.data.entities.rule
data class ExploreRule(
override var bookList: String? = null,
override var name: String? = null,
override var author: String? = null,
override var intro: String? = null,
override var kind: String? = null,
override var lastChapter: String? = null,
override var updateTime: String? = null,
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : BookListRule

@ -0,0 +1,14 @@
package io.legado.app.data.entities.rule
data class SearchRule(
override var bookList: String? = null,
override var name: String? = null,
override var author: String? = null,
override var intro: String? = null,
override var kind: String? = null,
override var lastChapter: String? = null,
override var updateTime: String? = null,
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : BookListRule

@ -0,0 +1,8 @@
package io.legado.app.data.entities.rule
data class TocRule(
var chapterList: String? = null,
var chapterName: String? = null,
var chapterUrl: String? = null,
var nextTocUrl: String? = null
)

@ -0,0 +1,85 @@
package io.legado.app.help
import android.app.Activity
import java.lang.ref.WeakReference
import java.util.*
/**
* Activity管理器,管理项目中Activity的状态
*/
object ActivityHelp {
private val activities: MutableList<WeakReference<Activity>> = arrayListOf()
/**
* 判断指定Activity是否存在
*/
fun isExist(activityClass: Class<*>): Boolean {
for (item in activities) {
if (item.get()?.javaClass == activityClass) {
return true
}
}
return false
}
/**
* 添加Activity
*/
fun add(activity: Activity) {
activities.add(WeakReference(activity))
}
/**
* 移除Activity
*/
fun remove(activity: Activity) {
for (temp in activities) {
if (null != temp.get() && temp.get() === activity) {
activities.remove(temp)
break
}
}
}
/**
* 移除Activity
*/
fun remove(activityClass: Class<*>) {
val iterator = activities.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
if (item.get()?.javaClass == activityClass) {
iterator.remove()
}
}
}
/**
* 关闭指定 activity
*/
fun finishActivity(vararg activities: Activity) {
for (activity in activities) {
activity.finish()
}
}
/**
* 关闭指定 activity(class)
*/
fun finishActivity(vararg activityClasses: Class<*>) {
val waitFinish = ArrayList<WeakReference<Activity>>()
for (temp in activities) {
for (activityClass in activityClasses) {
if (temp.get()?.javaClass == activityClass) {
waitFinish.add(temp)
break
}
}
}
for (activityWeakReference in waitFinish) {
activityWeakReference.get()?.finish()
}
}
}

@ -0,0 +1,30 @@
package io.legado.app.help
import androidx.recyclerview.widget.RecyclerView
internal class AdapterDataObserverProxy(var adapterDataObserver: RecyclerView.AdapterDataObserver, var headerCount: Int) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
adapterDataObserver.onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount)
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload)
}
// 当第n个数据被获取,更新第n+1个position
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount)
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount)
}
}

@ -0,0 +1,60 @@
package io.legado.app.help
import android.annotation.TargetApi
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
/**
* 模糊
*/
class BlurTransformation(context: Context, private val radius: Int) : BitmapTransformation() {
private val rs: RenderScript = RenderScript.create(context)
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val blurredBitmap = toTransform.copy(Bitmap.Config.ARGB_8888, true)
// Allocate memory for Renderscript to work with
//分配用于渲染脚本的内存
val input = Allocation.createFromBitmap(
rs,
blurredBitmap,
Allocation.MipmapControl.MIPMAP_FULL,
Allocation.USAGE_SHARED
)
val output = Allocation.createTyped(rs, input.type)
// Load up an instance of the specific script that we want to use.
//加载我们想要使用的特定脚本的实例。
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
script.setInput(input)
// Set the blur radius
//设置模糊半径
script.setRadius(radius.toFloat())
// Start the ScriptIntrinsicBlur
//启动 ScriptIntrinsicBlur,
script.forEach(output)
// Copy the output to the blurred bitmap
//将输出复制到模糊的位图
output.copyTo(blurredBitmap)
return blurredBitmap
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update("blur transformation".toByteArray())
}
}

@ -0,0 +1,166 @@
package io.legado.app.help
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.ReplaceRule
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File
import kotlin.math.min
object BookHelp {
private var downloadPath: String =
App.INSTANCE.getPrefString("downloadPath")
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
fun upDownloadPath() {
downloadPath =
App.INSTANCE.getPrefString("downloadPath")
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
}
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
val filePath = getChapterPath(book, bookChapter)
val file = FileHelp.getFile(filePath)
file.writeText(content)
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter)
runCatching {
val file = File(filePath)
if (file.exists()) {
return true
}
}
return false
}
fun getContent(book: Book, bookChapter: BookChapter): String? {
val filePath = getChapterPath(book, bookChapter)
runCatching {
val file = File(filePath)
if (file.exists()) {
return file.readText()
}
}
return null
}
fun delContent(book: Book, bookChapter: BookChapter) {
val filePath = getChapterPath(book, bookChapter)
kotlin.runCatching {
val file = File(filePath)
if (file.exists()) {
file.delete()
}
}
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
val bookFolder = formatFolderName(book.name + book.bookUrl)
val chapterFile =
String.format("%05d-%s", bookChapter.index, formatFolderName(bookChapter.title))
return "$downloadPath${File.separator}book_cache${File.separator}$bookFolder${File.separator}$chapterFile.nb"
}
private fun formatFolderName(folderName: String): String {
return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "")
}
fun formatAuthor(author: String?): String {
return author
?.replace("\\s*者[\\s::]*".toRegex(), "")
?.replace("\\s+".toRegex(), " ")
?.trim { it <= ' ' }
?: ""
}
fun getDurChapterIndexByChapterTitle(
title: String?,
index: Int,
chapters: List<BookChapter>
): Int {
if (title.isNullOrEmpty()) {
return min(index, chapters.lastIndex)
}
if (chapters.size > index && title == chapters[index].title) {
return index
}
var newIndex = 0
val jaccardSimilarity = JaccardSimilarity()
var similarity = if (chapters.size > index) {
jaccardSimilarity.apply(title, chapters[index].title)
} else 0.0
if (similarity == 1.0) {
return index
} else {
for (i in 1..50) {
if (index - i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index - i].title).let {
if (it > similarity) {
similarity = it
newIndex = index - i
if (similarity == 1.0) {
return newIndex
}
}
}
}
if (index + i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index + i].title).let {
if (it > similarity) {
similarity = it
newIndex = index + i
if (similarity == 1.0) {
return newIndex
}
}
}
}
}
}
return newIndex
}
var bookName: String? = null
var bookOrigin: String? = null
var replaceRules: List<ReplaceRule> = arrayListOf()
fun disposeContent(
name: String, origin: String?,
content: String,
enableReplace: Boolean
): String {
var c = content
synchronized(this) {
if (enableReplace && (bookName != name || bookOrigin != origin)) {
replaceRules = if (origin.isNullOrEmpty()) {
App.db.replaceRuleDao().findEnabledByScope(name)
} else {
App.db.replaceRuleDao().findEnabledByScope(name, origin)
}
}
}
for (item in replaceRules) {
item.pattern.let {
if (it.isNotEmpty()) {
c = if (item.isRegex) {
c.replace(it.toRegex(), item.replacement)
} else {
c.replace(it, item.replacement)
}
}
}
}
val indent = App.INSTANCE.getPrefInt("textIndent", 2)
return c.replace("\\s*\\n+\\s*".toRegex(), "\n" + " ".repeat(indent))
}
}

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

Loading…
Cancel
Save