Merge pull request #35 from gedoor/master

up
pull/379/head
口口吕 4 years ago committed by GitHub
commit a911213c88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      .github/workflows/autoupdatefork.yml
  2. BIN
      .github/workflows/legado.jks
  3. 57
      .github/workflows/legado.yml
  4. 3
      .gitignore
  5. 13
      README.md
  6. 169
      api.md
  7. 82
      app/build.gradle
  8. 25
      app/proguard-rules.pro
  9. 11
      app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt
  10. 5
      app/src/debug/res/values-zh-rHK/strings.xml
  11. 5
      app/src/debug/res/values-zh-rTW/strings.xml
  12. 4
      app/src/debug/res/values-zh/strings.xml
  13. 4
      app/src/debug/res/values/strings.xml
  14. 6
      app/src/google/res/values-zh-rCN/strings.xml
  15. 6
      app/src/google/res/values-zh-rHK/strings.xml
  16. 6
      app/src/google/res/values/strings.xml
  17. 285
      app/src/main/AndroidManifest.xml
  18. 82
      app/src/main/assets/18PlusList.txt
  19. 4
      app/src/main/assets/disclaimer.md
  20. 2
      app/src/main/assets/help.md
  21. 62
      app/src/main/assets/httpTTS.json
  22. 28
      app/src/main/assets/readConfig.json
  23. 26
      app/src/main/assets/themeConfig.json
  24. 74
      app/src/main/assets/txtTocRule.json
  25. 498
      app/src/main/assets/updateLog.md
  26. 176
      app/src/main/assets/web/book.css
  27. 39
      app/src/main/assets/web/book.html
  28. 203
      app/src/main/assets/web/book.js
  29. 71
      app/src/main/assets/web/bookshelf.html
  30. 1
      app/src/main/assets/web/css/detail.42c41bd6.css
  31. 33
      app/src/main/assets/web/index.html
  32. 96
      app/src/main/assets/web/index.js
  33. 1
      app/src/main/assets/web/js/about.2589b5fe.js
  34. 1
      app/src/main/assets/web/js/about~detail.08c372e6.js
  35. 33
      app/src/main/assets/web/js/chunk-vendors.b3838a2d.js
  36. 1
      app/src/main/assets/web/js/detail.043d6e39.js
  37. 2
      app/src/main/assets/web/new/css/about.dbe575e1.css
  38. 0
      app/src/main/assets/web/new/css/app.e4c919b7.css
  39. 0
      app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css
  40. 1
      app/src/main/assets/web/new/css/detail.9ba76c69.css
  41. BIN
      app/src/main/assets/web/new/favicon.ico
  42. 0
      app/src/main/assets/web/new/fonts/element-icons.535877f5.woff
  43. 0
      app/src/main/assets/web/new/fonts/element-icons.732389de.ttf
  44. 0
      app/src/main/assets/web/new/fonts/iconfont.f9a3fb0e.woff
  45. 0
      app/src/main/assets/web/new/fonts/popfont.f39ecc1a.ttf
  46. 0
      app/src/main/assets/web/new/fonts/shelffont.6c094b6d.ttf
  47. 0
      app/src/main/assets/web/new/img/icons/android-chrome-192x192.png
  48. 0
      app/src/main/assets/web/new/img/icons/android-chrome-512x512.png
  49. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-120x120.png
  50. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-152x152.png
  51. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-180x180.png
  52. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-60x60.png
  53. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon-76x76.png
  54. 0
      app/src/main/assets/web/new/img/icons/apple-touch-icon.png
  55. 0
      app/src/main/assets/web/new/img/icons/favicon-16x16.png
  56. 0
      app/src/main/assets/web/new/img/icons/favicon-32x32.png
  57. 0
      app/src/main/assets/web/new/img/icons/msapplication-icon-144x144.png
  58. 0
      app/src/main/assets/web/new/img/icons/mstile-150x150.png
  59. 0
      app/src/main/assets/web/new/img/icons/safari-pinned-tab.svg
  60. 0
      app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg
  61. 3
      app/src/main/assets/web/new/index.html
  62. 1
      app/src/main/assets/web/new/js/about.59a63964.js
  63. 1
      app/src/main/assets/web/new/js/about~detail.1caf6ef5.js
  64. 2
      app/src/main/assets/web/new/js/app.d7843716.js
  65. 33
      app/src/main/assets/web/new/js/chunk-vendors.8dd9045a.js
  66. 1
      app/src/main/assets/web/new/js/detail.11777eca.js
  67. 0
      app/src/main/assets/web/new/manifest.json
  68. 74
      app/src/main/assets/web/new/precache-manifest.78eb8adcb8f052b2a72d462abe0dc498.js
  69. 2
      app/src/main/assets/web/new/robots.txt
  70. 34
      app/src/main/assets/web/new/service-worker.js
  71. 102
      app/src/main/java/io/legado/app/App.kt
  72. 207
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  73. 108
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  74. 19
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  75. 21
      app/src/main/java/io/legado/app/base/BaseFragment.kt
  76. 61
      app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt
  77. 8
      app/src/main/java/io/legado/app/base/BaseService.kt
  78. 5
      app/src/main/java/io/legado/app/base/VMBaseActivity.kt
  79. 299
      app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
  80. 23
      app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
  81. 24
      app/src/main/java/io/legado/app/constant/AppConst.kt
  82. 10
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  83. 2
      app/src/main/java/io/legado/app/constant/EventBus.kt
  84. 26
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  85. 16
      app/src/main/java/io/legado/app/constant/Theme.kt
  86. 118
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  87. 11
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  88. 14
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  89. 8
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  90. 23
      app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
  91. 6
      app/src/main/java/io/legado/app/data/dao/CookieDao.kt
  92. 31
      app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt
  93. 39
      app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
  94. 11
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  95. 2
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  96. 2
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  97. 4
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  98. 150
      app/src/main/java/io/legado/app/data/entities/Book.kt
  99. 11
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  100. 3
      app/src/main/java/io/legado/app/data/entities/BookProgress.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,32 @@
#更新fork
name: update fork
on:
schedule:
- cron: '0 16 * * *' #设置定时任务
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.event.repository.owner.id == github.event.sender.id && github.actor != 'gedoor' }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Install git
run: |
sudo apt-get update
sudo apt-get -y install git
- name: Set env
run: |
git config --global user.email "github-actions@github.com"
git config --global user.name "github-actions"
- name: Update fork
run: |
git remote add upstream https://github.com/gedoor/legado.git
git remote -v
git fetch upstream
git checkout master
git merge upstream/master
git push origin master

Binary file not shown.

@ -0,0 +1,57 @@
name: Android CI
on:
release:
types: [published]
push:
branches:
- master
# tags:
# - '3.*'
# pull_request:
# branches:
# - master
# watch:
# types: [started]
# schedule:
# - cron: '0 4 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: clear 18PlusList.txt
run: |
echo "清空18PlusList.txt"
echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt
- name: release apk sign
run: |
echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks
sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: apk live together
run: |
echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i
- name: build with gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease
- name : upload apk
uses: actions/upload-artifact@master
if: always()
with:
name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release

3
.gitignore vendored

@ -9,4 +9,7 @@
/release
/tmp
node_modules/
/app/app
/app/google
/app/gradle.properties
package-lock.json

@ -3,7 +3,18 @@
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
## 阅读3.0
书源规则 https://celeter.github.io
* [书源规则](https://alanskycn.gitee.io/teachme/)
* [更新日志](/app/src/main/assets/updateLog.md)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B4.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg)
### 阅读API
阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html

169
api.md

@ -0,0 +1,169 @@
# 阅读API
## 对于Web的配置
您需要先在设置中启用"Web 服务"。
## 使用
### Web
以下说明假设您的操作在本机进行,且开放端口为1234。
如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。
#### 插入单个书源
```
URL = http://127.0.0.1:1234/saveSource
Method = POST
```
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
#### 插入多个书源
```
URL = http://127.0.0.1:1234/saveSources
Method = POST
```
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 获取书源
```
URL = http://127.0.0.1:1234/getSource?url=xxx
Method = GET
```
获取指定URL对应的书源信息。
#### 获取所有书源
```
URL = http://127.0.0.1:1234/getSources
Method = GET
```
获取APP内的所有书源。
#### 删除多个书源
```
URL = http://127.0.0.1:1234/deleteSources
Method = POST
```
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 插入书籍
```
URL = http://127.0.0.1:1234/saveBook
Method = POST
```
请求BODY内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
#### 获取所有书籍
```
URL = http://127.0.0.1:1234/getBookshelf
Method = GET
```
获取APP内的所有书籍。
#### 获取书籍章节列表
```
URL = http://127.0.0.1:1234/getChapterList?url=xxx
Method = GET
```
获取指定图书的章节列表。
#### 获取书籍内容
```
URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
Method = GET
```
获取指定图书的第`index`章节的文本内容。
### Content Provider
* 需声明`io.legado.READ_WRITE`权限
* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
* 以下出现的`providerHost`请自行替换
#### 插入单个书源
```
URL = content://providerHost/source/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
#### 插入多个书源
```
URL = content://providerHost/sources/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 获取书源
```
URL = content://providerHost/source/query?url=xxx
Method = query
```
获取指定URL对应的书源信息。
用`Cursor.getString(0)`取出返回结果。
#### 获取所有书源
```
URL = content://providerHost/sources/query
Method = query
```
获取APP内的所有书源。
用`Cursor.getString(0)`取出返回结果。
#### 删除多个书源
```
URL = content://providerHost/sources/delete
Method = delete
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
#### 插入书籍
```
URL = content://providerHost/book/insert
Method = insert
```
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
#### 获取所有书籍
```
URL = content://providerHost/books/query
Method = query
```
获取APP内的所有书籍。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍章节列表
```
URL = content://providerHost/book/chapter/query?url=xxx
Method = query
```
获取指定图书的章节列表。
用`Cursor.getString(0)`取出返回结果。
#### 获取书籍内容
```
URL = content://providerHost/book/content/query?url=xxx&index=1
Method = query
```
获取指定图书的第`index`章节的文本内容。
用`Cursor.getString(0)`取出返回结果。

@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "de.timfreiheit.resourceplaceholders"
apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'io.fabric'
androidExtensions {
@ -37,7 +37,6 @@ android {
targetSdkVersion 29
versionCode gitCommits
versionName version
flavorDimensions "versionCode"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
@ -45,7 +44,8 @@ android {
annotationProcessorOptions {
arguments = [
"room.incremental" : "true",
"room.expandProjection": "true"
"room.expandProjection": "true",
"room.schemaLocation" : "$projectDir/schemas".toString()
]
}
}
@ -56,7 +56,7 @@ android {
signingConfig signingConfigs.myConfig
}
applicationIdSuffix '.release'
minifyEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
@ -66,26 +66,30 @@ android {
applicationIdSuffix '.debug'
versionNameSuffix 'debug'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${name}_${defaultConfig.versionName}.apk"
def flavor = variant.productFlavors[0].name
outputFileName = "${name}_${flavor}_${defaultConfig.versionName}.apk"
}
}
}
productFlavors{
app{
manifestPlaceholders = [APP_CHANNEL_VALUE:"app"]
flavorDimensions "mode"
productFlavors {
app {
dimension "mode"
manifestPlaceholders = [APP_CHANNEL_VALUE: "app"]
}
google{
google {
dimension "mode"
applicationId "io.legado.play"
manifestPlaceholders = [APP_CHANNEL_VALUE:"google"]
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
}
}
compileOptions {
// Flag to enable support for the new language APIs
//coreLibraryDesugaringEnabled true
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -107,28 +111,30 @@ kapt {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "com.android.support:multidex:1.0.3"
//kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-core:17.2.3'
implementation 'com.google.firebase:firebase-core:17.5.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
//androidX
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.google.code.gson:gson:2.8.6'
//lifecycle
def lifecycle_version = '2.2.0'
@ -139,9 +145,10 @@ dependencies {
def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:2.2.5"
//paging
implementation 'androidx.paging:paging-runtime:2.1.2'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
//anko
def anko_version = '0.10.8'
@ -152,24 +159,25 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
//
def coroutines_version = '1.3.3'
def coroutines_version = '1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.4.0'
//JS rhino
implementation 'com.github.gedoor:rhino-android:1.4'
//Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
//
//noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
//webServer
implementation 'org.nanohttpd:nanohttpd:2.3.1'
@ -182,15 +190,23 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0'
//apache
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-lang3:3.11'
implementation 'org.apache.commons:commons-text:1.8'
//MarkDown
implementation 'ru.noties.markwon:core:3.0.2'
implementation 'ru.noties.markwon:core:3.1.0'
//
implementation 'com.github.houbb:opencc4j:1.4.0'
implementation 'com.hankcs:hanlp:portable-1.7.8'
//epub
implementation('com.positiondev.epublib:epublib-core:3.1') {
exclude group: 'org.slf4j'
exclude group: 'xmlpull'
}
//E-Ink
//implementation 'fadeapp.widgets:scrollless-recyclerView:1.0.2'
}
apply plugin: 'com.google.gms.google-services'

@ -58,14 +58,13 @@
# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
# 保留androidx下的所有类及其内部类
@ -128,16 +127,11 @@
}
# webView处理,项目中没有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用
# 记得proguard-android.txt中一定不要加-dontoptimize才起作用
@ -158,9 +152,7 @@
-keep class **.data.**{*;}
-dontwarn rx.**
-dontwarn okio.**
-dontwarn retrofit2.**
-dontwarn javax.annotation.**
-dontwarn org.apache.log4j.lf5.viewer.**
-dontnote org.apache.log4j.lf5.viewer.**
@ -171,21 +163,20 @@
-dontwarn org.conscrypt.**
-dontwarn com.jeremyliao.liveeventbus.**
-keep class com.google.gson.** { *; }
-keep class com.ke.gson.** { *; }
-keep class com.jeremyliao.liveeventbus.** { *; }
-keep class retrofit2.**{*;}
-keep class okhttp3.**{*;}
-keep class okio.**{*;}
-keep class com.hwangjr.rxbus.**{*;}
-keep class org.conscrypt.**{*;}
-keep class com.kunfei.bookshelf.widget.**{*;}
-keep class com.kunfei.bookshelf.bean.**{*;}
-keep class android.support.**{*;}
-keep class me.grantland.widget.**{*;}
-keep class de.hdodenhof.circleimageview.**{*;}
-keep class tyrant.explosionfield.**{*;}
-keep class tyrantgit.explosionfield.**{*;}
-keep class freemarker.**{*;}
-keep class com.gyf.barlibrary.* {*;}
-keep class com.gyf.barlibrary.** {*;}
##JSOUP
-keep class org.jsoup.**{*;}
-keep class **.xpath.**{*;}
@ -199,7 +190,7 @@
-dontwarn com.jayway.**
-keep class com.fasterxml.**{*;}
-keep class javax.swing..**{*;}
-keep class javax.swing.**{*;}
-dontwarn javax.swing.**
-keep class java.awt.**{*;}
-dontwarn java.awt.**
@ -220,7 +211,7 @@
-keep class javax.xml.**{*;}
-keep class org.xmlpull.**{*;}
-keep class org.simpleframework.xml.**{*;}
-keep class org.simpleframework.**{*;}
-dontwarn org.simpleframework.xml.**
-keepclassmembers class * {

@ -1,5 +1,7 @@
package io.legado.app
import android.net.Uri
import android.util.Log
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
@ -16,9 +18,12 @@ import org.junit.Assert.*
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
fun testContentProvider() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("cn.legado.book", appContext.packageName)
}
Log.d("test",
appContext.contentResolver.query(Uri.parse("content://io.legado.app.api.ReaderProvider/sources/query"),null,null,null,null)
!!.getString(0)
)
}
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜索</string>
</resources>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">閲讀.D</string>
<string name="receiving_shared_label">閲讀·D·搜索</string>
</resources>

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

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

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

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

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

@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app">
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
<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" />
@ -15,6 +18,7 @@
<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" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<application
android:name=".App"
@ -22,297 +26,352 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!--主入口-->
<!-- 主入口 -->
<activity android:name=".ui.welcome.WelcomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
android:launchMode="singleTask" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标1-->
<!-- 图标1 -->
<activity
android:name=".ui.welcome.Launcher1"
android:icon="@mipmap/launcher1"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher1">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标2-->
<!-- 图标2 -->
<activity
android:name=".ui.welcome.Launcher2"
android:icon="@mipmap/launcher2"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher2">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标3-->
<!-- 图标3 -->
<activity
android:name=".ui.welcome.Launcher3"
android:icon="@mipmap/launcher3"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher3">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标4-->
<!-- 图标4 -->
<activity
android:name=".ui.welcome.Launcher4"
android:icon="@mipmap/launcher4"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher4">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标5-->
<!-- 图标5 -->
<activity
android:name=".ui.welcome.Launcher5"
android:icon="@mipmap/launcher5"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher5">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--图标6-->
<!-- 图标6 -->
<activity
android:name=".ui.welcome.Launcher6"
android:icon="@mipmap/launcher6"
android:enabled="false">
android:enabled="false"
android:icon="@mipmap/launcher6">
<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" />
android:launchMode="singleTask"
android:resource="@xml/shortcuts" />
</activity>
<!--主界面-->
<!-- 主界面 -->
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask"
android:alwaysRetainTaskState="true" />
<!--阅读界面-->
android:alwaysRetainTaskState="true"
android:launchMode="singleTask" />
<!-- 阅读界面 -->
<activity
android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" />
<!--书籍详情页-->
<!-- 书籍详情页 -->
<activity
android:name=".ui.book.info.BookInfoActivity"
android:launchMode="singleTop" />
<!--书籍信息编辑-->
<!-- 书籍信息编辑 -->
<activity
android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity"
android:name=".ui.book.info.edit.BookInfoEditActivity"
android:launchMode="singleTask" />
<!--音频播放界面-->
<!-- 音频播放界面 -->
<activity
android:name=".ui.audio.AudioPlayActivity"
android:launchMode="singleTask" />
<!--授权界面-->
<!-- 授权界面 -->
<activity
android:name=".help.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<!--二维码扫描-->
<!-- 二维码扫描 -->
<activity
android:name=".ui.qrcode.QrCodeActivity"
android:launchMode="singleTask" />
<!--书源编辑-->
<!-- 书源编辑 -->
<activity
android:name=".ui.book.source.edit.BookSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--订阅源编辑-->
<!-- 订阅源编辑 -->
<activity
android:name=".ui.rss.source.edit.RssSourceEditActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!--配置界面-->
<!-- 配置界面 -->
<activity
android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" />
<!--搜索界面-->
<!-- 搜索界面 -->
<activity
android:name="io.legado.app.ui.book.search.SearchActivity"
android:name=".ui.book.search.SearchActivity"
android:launchMode="singleTask" />
<!--关于界面-->
<!-- 关于界面 -->
<activity
android:name=".ui.about.AboutActivity"
android:launchMode="singleTask" />
<!--捐赠界面-->
<!-- 捐赠界面 -->
<activity
android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" />
<!--书源管理-->
<!-- 书源管理 -->
<activity
android:name=".ui.book.source.manage.BookSourceActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="booksource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--订阅源管理-->
android:launchMode="singleTop" />
<!-- 订阅源管理 -->
<activity
android:name=".ui.rss.source.manage.RssSourceActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="rsssource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--替换规则界面-->
android:launchMode="singleTop" />
<!-- 替换规则界面 -->
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="replace"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--书籍管理-->
<!-- 书籍管理 -->
<activity
android:name=".ui.book.arrange.ArrangeBookActivity"
android:launchMode="singleTop" />
<!--书源调试-->
<!-- 书源调试 -->
<activity
android:name=".ui.book.source.debug.BookSourceDebugActivity"
android:launchMode="singleTop" />
<!--目录-->
<!-- 目录 -->
<activity
android:name=".ui.book.chapterlist.ChapterListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<activity
android:name=".ui.book.searchContent.SearchListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- RSS条目 -->
<activity
android:name="io.legado.app.ui.book.chapterlist.ChapterListActivity"
android:screenOrientation="behind"
android:name=".ui.rss.article.RssSortActivity"
android:launchMode="singleTop" />
<!--RSS阅读-->
<!-- RSS阅读 -->
<activity
android:name=".ui.rss.read.ReadRssActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTop" />
<!--导入书籍-->
<!-- 导入书籍 -->
<activity
android:name="io.legado.app.ui.book.local.ImportBookActivity"
android:name=".ui.book.local.ImportBookActivity"
android:launchMode="singleTop" />
<!--发现界面-->
<!-- 发现界面 -->
<activity
android:name="io.legado.app.ui.book.explore.ExploreShowActivity"
android:name=".ui.book.explore.ExploreShowActivity"
android:launchMode="singleTop" />
<!--订阅源调试-->
<!-- 订阅源调试 -->
<activity
android:name=".ui.rss.source.debug.RssSourceDebugActivity"
android:launchMode="singleTop" />
<!--订阅条目-->
<activity
android:name=".ui.rss.article.RssArticlesActivity"
android:launchMode="singleTop" />
<!--Rss收藏-->
<!-- Rss收藏 -->
<activity
android:name=".ui.rss.favorites.RssFavoritesActivity"
android:launchMode="singleTop" />
<!--下载界面-->
<!-- 缓存界面 -->
<activity
android:name="io.legado.app.ui.book.download.DownloadActivity"
android:name=".ui.book.cache.CacheActivity"
android:launchMode="singleTop" />
<!--书源登录-->
<!-- 书源登录 -->
<activity
android:name=".ui.login.SourceLogin"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!--文字处理-->
<!-- 阅读记录 -->
<activity
android:name=".ui.about.ReadRecordActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 文字处理 -->
<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.association.ImportBookSourceActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="booksource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!-- 导入订阅源 -->
<activity
android:name=".ui.config.FileAssociationActivity"
android:name=".ui.association.ImportRssSourceActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="rsssource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!-- 导入替换规则 -->
<activity
android:name=".ui.association.ImportReplaceRuleActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="replace"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!-- 打开文件 -->
<activity
android:name=".ui.association.FileAssociationActivity"
android:theme="@style/AppTheme.Transparent"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="text/plain" />
<data android:mimeType="application/json" />
<data android:mimeType="application/epub" />
</intent-filter>
</activity>
<service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" />
<service android:name=".service.CacheBookService" />
<service android:name=".service.WebService" />
<service android:name=".service.TTSReadAloudService" />
<service android:name=".service.HttpReadAloudService" />
<service android:name=".service.AudioPlayService" />
<service android:name=".service.DownloadService" />
<receiver android:name=".receiver.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<provider
android:name=".api.ReaderProvider"
android:authorities="${applicationId}.readerProvider"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedContentProvider" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"

@ -0,0 +1,82 @@
OGN5dS5jb20=
c2cwMC54eXo=
aXRyYWZmaWNuZXQuY29t
eGlhb3FpYW5nNTIw
MTIzeGlhb3FpYW5n
eGlhb3FpYW5neHM=
eGlhb3FpYW5nNTIw
MzM1eHM=
eGN4czk=
eGN4czUyMA==
c2h1YmFvYW4=
c2h1YmFvd2FuZzEyMw==
c2h1YmFvYW4=
aGFpdGFuZzEyMw==
eXV6aGFpd3VsYQ==
cG8xOA==
Ymwtbm92ZWw=
NXRucw==
c2hhb3NodWdl
amluamlzaHV3dQ==
NDJ3Zw==
eWlxdXNodQ==
c2h1YmFvd2FuZzEyMw==
M2hlYmFv
MzNoZWJhbw==
bHVvcWl1enc=
bXlzaHVnZQ==
c3NzeHN3
eWl0ZQ==
Y3Vpd2VpanV1
Y3Vpd2VpanV4cw==
Y3Vpd2VpanV4
eGlhb3FpYW5nd3g=
YXN6dw==
YXN6dzY=
c2FuaGFveHM=
ODdzaHV3dQ==
NDh3eA==
bG9uZ3Rlbmcy
NnF3eA==
bG9uZ3Rlbmd4cw==
aGF4ZHU=
M3R3eA==
aGF4d3g1
NjZsZXdlbg==
eGJhbnpodQ==
aGR5cA==
ZHliejk=
ZGl5aWJhbnpodTk=
ZGl5aWJhbnpodQ==
ZGl5aWJhbnpodTc=
YnoyMjI=
d29kZWFwaTAwMQ==
dGFuZ3poZWthbg==
YmF4aWFueHM=
eGlhb3NodW9zaGVuemhhbg==
ZGFtb2tl
emh3ZW5wZw==
eXV6aGFpZ2U=
d21wOA==
OXhpYW53ZW4=
bmFucmVudmlw
cmV5b28=
eWZ4aWFvc2h1b2U=
c2Fuaml1enc=
N3Fpbmc3
cWR4aWFvc2h1bw==
Y2hpbmVzZXpq
MzlzaHViYW8=
a3l4czU=
NTZtcw==
bml1c2hh
bWt4czY=
MjIyMjJ4cw==
OTVkdXNodQ==
YmFuemh1MjI=
d3JsdHh0
dHVkb3V0eHQ=
cm5neHM=
OTl3ZW5rdQ==
bGFvc2lqaXhz
ZnVzaHV6aGFpMQ==

@ -1,7 +1,7 @@
# 免责声明(Disclaimer)
* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。
* 阅读是一款解析指定规则并获取内容的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
* 当您搜索一本书的时,阅读会您所使用的规则将该书的书名以关键词的形式提交到各个第三方网络文学网站。
各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。
任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。

@ -1,7 +1,7 @@
## 常见问题
1.为什么第一次安装好之后什么东西都没有?
* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从QQ群、公众号“开源阅读软件”、酷安评论里获取由书友制作分享的书源。
* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号[开源阅读]()、QQ群、酷安评论里获取由书友制作分享的书源。
2.如何导入本地书源文件?
* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入);

@ -0,0 +1,62 @@
[
{
"id": 1598233029304,
"name": "度小美",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029305,
"name": "度小宇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029306,
"name": "度逍遥",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029307,
"name": "度丫丫",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029308,
"name": "度小娇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029309,
"name": "度米朵",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029310,
"name": "度博文",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029311,
"name": "度小童",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029312,
"name": "度小萌",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029313,
"name": "百度骚男",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029314,
"name": "百度评书",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029315,
"name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
}
]

@ -1,15 +1,13 @@
[
{
"bgStr": "#EBD9BB",
"bgStrNight": "#1E2021",
"textColor": "#63543C",
"textColorNight": "#DCDFE1",
"bgStr": "#FFFFFF",
"bgStrNight": "#000000",
"textColor": "#000000",
"textColorNight": "#FFFFFF",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10
"darkStatusIconNight": false
},
{
"bgStr": "#DDC090",
@ -19,9 +17,7 @@
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10
"darkStatusIconNight": false
},
{
"bgStr": "#C2D8AA",
@ -31,9 +27,7 @@
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10
"darkStatusIconNight": false
},
{
"bgStr": "#DBB8E2",
@ -43,9 +37,7 @@
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10
"darkStatusIconNight": false
},
{
"bgStr": "#ABCEE0",
@ -55,8 +47,6 @@
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": false,
"textSize": 24,
"letterSpacing": 0,
"lineSpacingExtra": 10
"darkStatusIconNight": false
}
]

@ -0,0 +1,26 @@
[
{
"themeName": "典雅蓝",
"isNightTheme": false,
"primaryColor": "#03A9F4",
"accentColor": "#AD1457",
"backgroundColor": "#F5F5F5",
"bottomBackground": "#EEEEEE"
},
{
"themeName": "黑白",
"isNightTheme": true,
"primaryColor": "#303030",
"accentColor": "#E0E0E0",
"backgroundColor": "#424242",
"bottomBackground": "#424242"
},
{
"themeName": "A屏黑",
"isNightTheme": true,
"primaryColor": "#000000",
"accentColor": "#FFFFFF",
"backgroundColor": "#000000",
"bottomBackground": "#000000"
}
]

@ -1,98 +1,128 @@
[
{
"id": -1,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![赛游])|篇(?!张))).{0,30}$",
"serialNumber": 0
},
{
"enable": false,
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"id": -2,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{
"id": -3,
"enable": false,
"name": "目录(简介)",
"rule": "(?<=[ \\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"name": "目录(匹配简介)",
"rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 2
},
{
"id": -4,
"enable": false,
"name": "目录(古典、轻小说备用)",
"rule": "^[  \\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 3
},
{
"id": -5,
"enable": false,
"name": "数字(纯数字标题)",
"rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$",
"serialNumber": 4
},
{
"id": -6,
"enable": true,
"name": "数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$",
"rule": "^[  \\t]{0,4}\\d{1,5}[,., 、_—\\-].{1,30}$",
"serialNumber": 5
},
{
"id": -7,
"enable": true,
"name": "大写数字 分隔符 标题名称",
"rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$",
"serialNumber": 6
},
{
"id": -8,
"enable": true,
"name": "正文 标题/序号",
"rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$",
"serialNumber": 6
"serialNumber": 7
},
{
"id": -9,
"enable": true,
"name": "Chapter/Section/Part/Episode 序号 标题",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 7
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO]\\.|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 8
},
{
"id": -10,
"enable": false,
"name": "Chapter(去简介)",
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 8
"rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 9
},
{
"id": -11,
"enable": true,
"name": "特殊符号 序号 标题",
"rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$",
"serialNumber": 9
"rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$",
"serialNumber": 10
},
{
"id": -12,
"enable": false,
"name": "特殊符号 标题(成对)",
"rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 10
"serialNumber": 11
},
{
"id": -13,
"enable":true,
"name": "特殊符号 标题(单个)",
"rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$",
"serialNumber": 11
"serialNumber": 12
},
{
"id": -14,
"enable": true,
"name": "章/卷 序号 标题",
"rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$",
"serialNumber": 12
"serialNumber": 13
},
{
"id": -15,
"enable":false,
"name": "顶格标题",
"rule": "^\\S.{1,20}$",
"serialNumber": 13
"serialNumber": 14
},
{
"id": -16,
"enable":false,
"name": "双标题(前向)",
"rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)",
"serialNumber": 14
"serialNumber": 15
},
{
"id": -17,
"enable":false,
"name": "双标题(后向)",
"rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$",
"serialNumber": 15
"serialNumber": 16
},
{
"id":-18,
"enable": true,
"name": "标题 特殊符号 序号",
"rule": "^.{1,20}[((][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$",
"serialNumber": 17
}
]

@ -1,10 +1,429 @@
## 更新日志
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。
* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来
# 更新日志
* 关注公众号 **[开源阅读]()** 菜单•软件下载 提前享受新版本。
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/09/15**
* 修复导入排版字体重复报错的bug
* 添加正文搜索 by [h11128](https://github.com/h11128)
**2020/09/12**
* web看书同步最新章
* web写源增加图片样式等规则
* 正文规则可以使用@get:{title}获取目录标题,js里使用title
**2020/09/11**
* 修复一些bug
* 背景配置自由添加
**2020/09/10**
* 修复自动换源的bug
* 修复保存主题的bug
* 书源排序,分享,注释优化 by [h11128](https://github.com/h11128)
**2020/09/09**
* 修复主题导入导出bug
* 优化分屏模式状态栏
* 书源基本属性增加“书源注释”
**2020/09/08**
* 页眉页脚跟随背景
* 主题导入导出
**2020/09/07**
* 订阅源和替换规则添加滑动选择
* 修复排版配置导入导出
* 订阅界面添加下载文件功能
**2020/09/06**
* 优化翻页
* EInk模式独立背景
* 阅读排版配置导入导出,包括背景和字体,支持网络导入
**2020/09/03**
* 修复替换中的回车消失的bug
* 所有内容恢复htmlFormat, 在想其它办法解决丢失一些内容的问题
* 图片(漫画)支持导出
**2020/09/02**
* 搜索url支持put,get,js里使用java.put,java.get
* 对于搜索重定向的源,可以使用此方法获得重定向后的url
```
<js>
var url='https://www.yooread.net/e/search/index.php,'+JSON.stringify({
"method":"POST",
"body":"show=title&tempid=1&keyboard="+key
});
String(java.connect(url).raw().request().url())
</js>
```
* 正文合并后替换规则支持所有规则写法,包括js
**2020/09/01**
* 导入书源列表添加全不选
* 详情页菜单添加清理缓存,清理当前书籍缓存
* 修复滑动选择,选择数量不更新的bug
* 字体跟随背景,每个背景对应一个字体
* 优化图片下载
**2020/08/29**
* 修复一个null引起的崩溃bug
* 修复我的界面滚动时图标消失的bug
* 修复从详情页目录打开章节内容不对的bug
* 书源选择增加滑动选择, 选择框区域滑动时进行选择 by [Mupceet](https://github.com/Mupceet)
* 请求头,支持http代理,socks4 socks5代理设置 by [10bits](https://github.com/10bits)
```
socks5代理
{
"proxy":"socks5://127.0.0.1:1080"
}
http代理
{
"proxy":"http://127.0.0.1:1080"
}
支持代理服务器验证
{
"proxy":"socks5://127.0.0.1:1080@用户名@密码"
}
注意:这些请求头是无意义的,会被忽略掉
```
**2020/08/28**
* 修复一些bug
* 换源不再改变书名和作者,防止换到一些错误的书源后不能再换源
**2020/08/27**
* 修复主题bug
* 修复封面bug
* 优化书籍更新,搜索,换源
* e-ink模式不再固定背景
**2020/08/26**
* js添加java.encodeURI(speakText),用于解决js编码时有~的语句朗读不出来
* 修复书名太长删除阅读记录按钮不显示的bug
* 完成本地书籍编码选择
**2020/08/25**
* 阅读记录可以删除了
* 修复翻页模式选择颜色问题
* 修复toolbar在一些情况下文字颜色不对的bug
* 多设备阅读记录叠加
* 封面链接支持修改headers
**2020/08/24**
* 应用被杀死时停止朗读
* 默认封面添加删除操作
* 备份阅读记录
* 书源添加移除分组支持多选,多个分组以逗号(中英均可)隔开
* 可以自定义在线朗读了
**2020/08/22**
* 添加阅读时间记录
**2020/08/21**
* 图片(漫画源)支持离线下载了
**2020/08/20**
* 正文图片(漫画源)链接支持修改headers
**2020/08/19**
* 选择文本替换时带入书名和书源
**2020/08/16**
* 添加亮度调节控件显示开关
* 添加应用内语言切换
* 底栏颜色限制去除,自动适配
**2020/08/12**
* 增加了Content Provider 接口支持 by [w568w](https://github.com/w568w)
* 修复阅读界面加入书架后,书籍详情页还是显示加入书架按钮的bug
* 修复低版本手机自动阅读速度拉动最左边崩溃的bug
* 给亮度调节加个半透明背景,很多人找不到
* 修复替换分组选择无效的bug
* 备份添加书签
* 修复web端进度更新后手机端进入阅读界面进度不变的bug
* 增加了txt目录规则备份
* 优化了导入功能,导入之前对比已有书源,可选择性导入
* 其它一些bug修复
**2020/08/08**
* 继续适配主题,现在应该所有地方都按照主题变色了
* 朗读定时增加到3个小时,朗读暂停恢复后继续定时
* 优化了主题颜色选择,会影响体验的颜色禁止选,会有提示
* 订阅规则下一页支持页数,下一页规则填page
**2020/08/07**
* 修复其它一些主题色没有适配到的地方
* 添加默认启用替换配置
**2020/08/06**
* 菜单背景根随主题色
* 修复其它一些主题色没有适配到的地方
* 取消图片颜色为FULL时的自动滚动
* 其它一些优化,升级库文件之类
* 显示订阅加入恢复忽略列表
**2020/08/03**
* 修复一些主题色没有适配到的地方
* 尝试修复书架最新章节更新不及时的bug
**2020/08/02**
* 阅读菜单底部几个按钮的背景动态设置为底部操作栏颜色
* 优化书签功能,解决一些bug
**2020/07/29**
* 正文图片样式为FULL的自动为滚动模式
**2020/07/28**
* 长图正文规则添加图片样式FULL,可以滚动浏览了
**2020/07/26**
* 优化翻页,加快翻页速度
**2020/07/25**
* 正文规则添加多页合并后的替换规则,格式同样是##regex##replaceTo
* 正文图片添加长按缩放
* 正文规则添加图片样式规则,可以设置为FULL
* 其它一些bug修复
**2020/07/21**
* 优化文字选择,不再缓存
* 添加忽略恢复列表,方便不同手机配置不同
* 其它一些bug修复
**2020/07/19**
* 添加自定义默认封面
* 修复封面选择本地图片时书架不显示的bug
**2020/07/14**
* 添加英文语言 by [52fisher](https://github.com/52fisher)
**2020/07/13**
* 在线阅读图片支持测试成功,最好把替换净化关了,防止图片url不对
* 书源保留img标签就行,@html自动保留标签
**2020/07/12**
* epub显示图片,未完善
* 在线阅读也支持图片,还未测试
**2020/07/11**
* epub可以正确识别书名和作者了
* epub封面正确显示
**2020/07/10**
* 修复一些窗口再墨水屏上背景透明的问题
* 添加epub支持
* web阅读时记住进度
* 导入书源时系统文件选择器可以选择json文件
**2020/07/06**
* 优化下载
**2020/07/05**
* 修复夜间模式底栏颜色调整无效的bug
* 【web看书】加了翻页、排序等 by [Celeter](https://github.com/Celeter)
* 两部xx' is recognized as a title by [52fisher](https://github.com/52fisher)
* 添加下载错误日志,从下载菜单浏览
* 修复vip标识引发的bug
**2020/07/04**
* 修复滚动bug
* 其它一些优化
**2020/07/03**
* 修复关闭两端对齐是朗读高亮不准确的bug
* 添加文字底部对齐开关
**2020/06/25**
* E-Ink模式合并到主题模式里, E-Ink模式不能修改阅读界面背景和文字颜色
* 添加判断,防止背景透明引起重影,花屏问题
**2020/06/22**
* 修复xpath获取正文多了许多逗号的bug
* 修复检验有效书源移除失效分组失败的bug
**2020/06/21**
* 双击书架图标返回顶部
**2020/06/20**
* 适配NavigationBar
**2020/06/19**
* 修复eInk bug
* 修复分组下载bug
* 导入本地添加滚动条
**2020/06/18**
* fadeapp.widgets:scrollless-recyclerView导致有些手机重影,暂时去除
* 下载界面添加分组
* 修复eInk bug
**2020/06/17**
* 修复更新书架时更新禁止更新的问题
* 修复导入旧版本数据问题
**2020/06/16**
* 刷新时只刷新当前书架
* 修复恢复备份需要退出重进的问题
* 保存打开 E-Ink 模式前的主题、翻页动画,关闭后恢复之前的配置, 现在可以切着玩了
* 修复因繁体语言导致的崩溃bug
**2020/06/15**
* 添加 E-Ink 模式 by [Modificator](https://github.com/Modificator)
* 修复发现打开书时可能的错误
**2020/06/14**
* 修复txt文件目录识别
* 书源分组添加已启用已禁用
**2020/06/13**
* 优化搜索
**2020/06/12**
* 修复分组变化的bug
**2020/06/10**
* 正文字体的粗细选择增加可以选择细体(Android O生效) by [hingbong](https://github.com/hingbong)
* 修复bug
**2020/06/09**
* 修复从发现界面打开已在书架的书时,显示不对的问题
**2020/06/07**
* 优化书源检测,自定义搜索关键词
* 失效书源如果校验为有效会去掉失效标志
**2020/06/06**
* 修复一些bug,包括从阅读界面退出后还是显示红色更新的bug
**2020/06/03**
* zh-TW translation by david082321
* 修复音频播放时播放速度调节会再下一章失效的bug
**2020/05/31**
* 更新到android studio 4.0
* 书源排序添加按url
* 去除朗读通知的进度条
* 修复恢复问题,暂时去除混淆
**2020/05/24**
* 添加自动翻页速度调节
**2020/05/23**
* 添加文字两端对齐配置
**2020/05/20**
* Rss列表增加一种显示样式
**2020/05/18**
* 修复http://alanskycn.gitee.io/书源导入失败问题,被屏蔽UA了
* Rss列表添加样式切换
**2020/05/17**
* 自动翻页功能完成
* 替换规则输入时弹出辅助输入条
**2020/05/10**
* 添加识别rss分组中的频道信息,在菜单中可以切换频道 from [yangyxd](https://github.com/yangyxd)
* 源管理添加置底,批量置顶,批量置地
* 封面选择本地图片完成
**2020/05/04**
* 优化txt文件目录解析
**2020/05/03**
* 优化一些界面显示问题
* 订阅源添加style
* 修复一些重复目录的bug
**2020/05/02**
* 修复不停换源的bug
* 修复本地书籍自动换源
* 修复书源校验的一些问题
**2020/05/01**
* 尝试修复朗读时可能错位的bug
* 添加自动换源配置
* 换源添加禁用菜单
**2020/04/29**
* 修复bug
* 订阅界面添加长按菜单
**2020/04/26**
* 添加导入旧的书源转换
* 修复不自动朗读下一章的bug
**2020/04/25**
* 修复翻页按键设置为空时崩溃的bug
* 翻页按键优先自定义按键,可覆盖音量按键
* 写书源时的辅助键盘添加※
* 更改了书源格式,不再需要转义符
**2020/04/24**
* 坚果云最近调整了策略,必须使用应用密码才能备份,用户信息,安全,第三方应用
* text目录规则添加id字段,负值为系统自带规则
* 其它一些优化
**2020/04/20**
* 优化阅读界面信息显示
**2020/04/19**
* 添加阅读界面各种信息设置
**2020/04/18**
* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, [hingbong](https://github.com/hingbong)
* 修复更新时间不对的bug
**2020/04/13**
* 去除rss朗读时的引号
**2020/04/13**
* 修复调用webView返回结果多了引号的bug
**2020/04/12**
* 解决无法取消加粗的bug
* 修复换源自动加入书架的bug
**2020/04/09**
* 修复书架刷新闪烁
**2020/04/08**
* 可以隐藏书架未分组
**2020/04/07**
* 书架添加未分组,有未分组书籍时自动显示
* 其它一些优化
**2020/04/04**
* 优化备份逻辑
* 修复订阅分类太多显示不全的bug
* 修复一些分类要手动刷新的问题
**2020/04/02**
* 书架书名和作者作为唯一值
* 添加订阅分类,分类规则和发现一样,分类一::url1 && 分类2::url2
**2020/03/29**
* 添加退出软件后是否响应耳机按键的开关
* 优化书源校验
**2020/03/26**
* 修复txt目录bug
* 最近工作比较忙,只有晚上有时间写软件,bug之类的不要催,白天不回消息
**2020/03/25**
* 修复7.1.1的网络问题,是retrofit2库最新版本的bug,暂时退回上版本
* 去除下载路径的配置,减少错误
* 添加隐藏状态栏是否扩展到刘海
**2020/03/24**
* txt文件第一章之前的文字不再放到简介里
* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别
* 修复文件关联 by [wqfantexi](https://github.com/wqfantexi)
**2020/03/22**
* 添加文件关联 by wqfantexi
* 添加文件关联 by [wqfantexi](https://github.com/wqfantexi)
* 手动排序可以了,在书架整理里面拖动排序
* 删除分组时同时删除书籍里的分组信息,下次添加新分组时不会自动出现在分组内
* 修复换源丢失分组信息的bug
@ -19,14 +438,14 @@
* 修复rss请求头无效bug
**2020/03/19**
* 美化界面我的 by yangyxd
* 美化界面我的 by [yangyxd](https://github.com/yangyxd)
* 优化搜索
**2020/03/18**
* 尝试修复搜索时崩溃
* 解决看过书籍的移到顶部需要向上滚动才能看到的bug
* 只有再书源被删除找不到书源时才会自动换源
* 美化界面 by yangyxd
* 美化界面 by [yangyxd](https://github.com/yangyxd)
* 订阅后台播放
**2020/03/16**
@ -51,8 +470,8 @@
**2020/03/11**
* 修复调节上边距时下边距一起动的bug
* 适配沚水的web阅读 by 六月
* 分组管理页面调整 by yangyxd
* 适配沚水的web阅读 by [六月](https://github.com/Celeter)
* 分组管理页面调整 by [yangyxd](https://github.com/yangyxd)
**2020/03/10**
* 优化文字选择菜单弹出位置
@ -61,13 +480,13 @@
**2020/03/09**
* 底部文字对齐
* 主题添加阴影调节 by yangyxd
* 主题添加阴影调节 by [yangyxd](https://github.com/yangyxd)
**2020/03/08**
* 订阅长按保存图片
* 订阅全屏播放
* 书架全部分组可以隐藏了
* 内置web书架基本能用了 by 六月
* 内置web书架基本能用了 by [六月](https://github.com/Celeter)
* 书架整理加入未分组
* 显示总进度
* 隐藏状态栏时,标题显示在上方
@ -139,7 +558,7 @@
**2020/02/26**
* 修复仿真翻页
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by hingbong
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by [hingbong](https://github.com/hingbong)
**2020/02/25**
* 优化文本选择和滚动,感觉很完美了
@ -263,66 +682,11 @@
* 适配Android 10 权限
* 导入旧版本配置不在需要存储权限
* 选择字体不在需要存储权限
**2019/12/30**
* 修改书源调试
- 调试搜索>>输入关键字,如:`系统`
- 调试发现>>输入发现URL,如:`月票榜::https://www.qidian.com/rank/yuepiao?page={{page}}`
- 调试详情页>>输入详情页URL,如:`https://m.qidian.com/book/1015609210`
- 调试目录页>>输入目录页URL,如:`++https://www.zhaishuyuan.com/read/30394`
- 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996`
* 修改订阅中自动添加style的情景
订阅源的内容规则中存在`<style>``style=`
**2019/12/28**
* 添加下载界面
* 添加分组备份
**2019/12/23**
* 修复每次打开翻页模式恢复默认的bug
* 修复m3u8报错问题
**2019/12/22**
* 更新音频播放界面
**2019/12/20**
* 阅读界面的下载菜单可以用了
* rss阅读bug修复,webView内可以用返回建返回上一链接
* 优化搜索界面逻辑
* 导入旧书源时url里的&不再转为&&
* 修复书架封面显示不全的bug
**2019/12/16**
* 添加几个主题选择
* 音频播放添加header支持
**2019/12/15**
* 修复清理缓存会把其他文件删除的问题
* 详情页模糊背景
* 修复朗读时最后一页显示不正确的问题
* 最近感冒了,发热咳嗽还没好,继续咸鱼
**2019/12/12**
* web服务停止问题
* 默认显示沉浸式状态栏
**2019/12/09**
* 其他设置->清理缓存
* 调整深色模式配色,预适配Android10
* 启用web服务
**2019/12/03**
* from Celeter:
- 调试log修改
- 增加书源字符串分享(RSS未包含)
- 增加导出选中的源(包含书源、RSS、替换规则)
* 一键缓存
* 修复bug
**2019/12/02**
* 修复一些bug,书架网格布局完成
* 音频添加速度调整
* 后台朗读,长按返回,或点击按钮
* 订阅可以隐藏
* 优化老版本规则导入from Celeter

@ -1,176 +0,0 @@
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;
}

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

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

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

File diff suppressed because one or more lines are too long

@ -14,7 +14,7 @@
<div class="rules">
<div><b>基本</b></div>
<div>
<div>URL :</div>
<div>域名 :</div>
<textarea rows="1" id="bookSourceUrl" class="base" title="bookSourceUrl"
placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
</div>
@ -33,6 +33,11 @@
<textarea rows="1" id="bookSourceGroup" class="base" title="bookSourceGroup"
placeholder="&lt;选填&gt;描述书源的特征信息"></textarea>
</div>
<div>
<div>源注释 :</div>
<textarea rows="1" id="bookSourceComment" class="base" title="bookSourceComment"
placeholder="&lt;选填&gt;描述书源作者和状态"></textarea>
</div>
<div>
<div>登录地址:</div>
<textarea rows="1" id="loginUrl" class="base" title="loginUrl"
@ -51,7 +56,7 @@
<div><b>搜索</b></div>
<div>
<div>搜索地址:</div>
<textarea rows="1" id="searchUrl" class="base" title="searchUrl"
<textarea rows="3" id="searchUrl" class="base" title="searchUrl"
placeholder="[域名可省略]/search.php@kw={{key}}"></textarea>
</div>
<div>
@ -245,13 +250,23 @@
<div>
<div>脚本注入:</div>
<textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
placeholder="注入javascript,用于模拟鼠标点击等,无返回结果"></textarea>
placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
</div>
<div>
<div>资源正则:</div>
<textarea rows="1" id="ruleContent_sourceRegex" class="ruleContent" title="sourceRegex"
placeholder="匹配资源的url特征,用于嗅探"></textarea>
</div>
<div>
<div>替换规则:</div>
<textarea rows="1" id="ruleContent_replaceRegex" class="ruleContent" title="replaceRegex"
placeholder="多页内容合并后替换,用于正文净化"></textarea>
</div>
<div>
<div>图片样式:</div>
<textarea rows="1" id="ruleContent_imageStyle" class="ruleContent" title="imageStyle"
placeholder="FULL:铺满 不填:默认样式"></textarea>
</div>
<p></p>
<div><b>其它规则</b></div>
<div>
@ -348,19 +363,19 @@
</div>
<div class="tab4">
<div class="context link">
<a target="_blank" href="https://celeter.github.io">源制作教程</a>
<a target="_blank" href="https://alanskycn.gitee.io/teachme">源制作教程</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>
<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>
<a target="_blank" href="book.html">阅读书架(经典)</a>
<a target="_blank" href="bookshelf.html">阅读书架(新潮)</a>
<a target="_blank" href="https://www.beta.browxy.com">代码在线运行工具</a>
<a target="_blank" href="bookshelf.html">阅读书架(经典)</a>
<a target="_blank" href="new/index.html">阅读书架(新潮)</a>
</div>
</div>
</div>

@ -18,7 +18,7 @@ function hashParam(key, val) {
}
}
// 创建书源规则容器对象
const RuleJSON = (() => {
function Container() {
let ruleJson = {};
let searchJson = {};
let exploreJson = {};
@ -36,26 +36,31 @@ const RuleJSON = (() => {
// 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
ruleJson.ruleSearch = JSON.stringify(searchJson);
//ruleJson.ruleSearch = JSON.stringify(searchJson);
ruleJson.ruleSearch = searchJson;
// 发现规则
$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
ruleJson.ruleExplore = JSON.stringify(exploreJson);
//ruleJson.ruleExplore = JSON.stringify(exploreJson);
ruleJson.ruleExplore = exploreJson;
// 详情页规则
$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
//ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
ruleJson.ruleBookInfo = bookInfoJson;
// 目录规则
$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
ruleJson.ruleToc = JSON.stringify(tocJson);
//ruleJson.ruleToc = JSON.stringify(tocJson);
ruleJson.ruleToc = tocJson;
// 正文规则
$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
ruleJson.ruleContent = JSON.stringify(contentJson);
//ruleJson.ruleContent = JSON.stringify(contentJson);
ruleJson.ruleContent = contentJson;
return ruleJson;
})();
}
// 选项卡Tab切换事件处理
function showTab(tabName) {
$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
@ -101,6 +106,7 @@ function HttpPost(url, data) {
}
// 将书源表单转化为书源对象
function rule2json() {
let RuleJSON = Container();
// 转换base
Object.keys(RuleJSON).forEach(key => {
if (!key.startsWith("rule")) {
@ -110,38 +116,53 @@ function rule2json() {
// 转换搜索规则
let searchJson = {};
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
searchJson[key] = $('#' + 'ruleSearch_' + key).value;
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
if ($('#' + 'ruleSearch_' + key).value)
searchJson[key] = $('#' + 'ruleSearch_' + key).value;
});
RuleJSON.ruleSearch = JSON.stringify(searchJson);
//RuleJSON.ruleSearch = JSON.stringify(searchJson);
RuleJSON.ruleSearch = searchJson;
// 转换发现规则
let exploreJson = {};
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
Object.keys(RuleJSON.ruleExplore).forEach(key => {
if ($('#' + 'ruleExplore_' + key).value)
exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
});
RuleJSON.ruleExplore = JSON.stringify(exploreJson);
//RuleJSON.ruleExplore = JSON.stringify(exploreJson);
RuleJSON.ruleExplore = exploreJson;
// 转换详情页规则
let bookInfoJson = {};
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
if ($('#' + 'ruleBookInfo_' + key).value)
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
});
RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
//RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
RuleJSON.ruleBookInfo = bookInfoJson;
// 转换目录规则
let tocJson = {};
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
tocJson[key] = $('#' + 'ruleToc_' + key).value;
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
Object.keys(RuleJSON.ruleToc).forEach(key => {
if ($('#' + 'ruleToc_' + key).value)
tocJson[key] = $('#' + 'ruleToc_' + key).value;
});
RuleJSON.ruleToc = JSON.stringify(tocJson);
//RuleJSON.ruleToc = JSON.stringify(tocJson);
RuleJSON.ruleToc = tocJson;
// 转换正文规则
let contentJson = {};
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
contentJson[key] = $('#' + 'ruleContent_' + key).value;
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
Object.keys(RuleJSON.ruleContent).forEach(key => {
if ($('#' + 'ruleContent_' + key).value)
contentJson[key] = $('#' + 'ruleContent_' + key).value;
});
RuleJSON.ruleContent = JSON.stringify(contentJson);
//RuleJSON.ruleContent = JSON.stringify(contentJson);
RuleJSON.ruleContent = contentJson;
RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime);
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
@ -153,6 +174,7 @@ function rule2json() {
}
// 将书源对象填充到书源表单
function json2rule(RuleEditor) {
let RuleJSON = Container();
// 转换base
Object.keys(RuleJSON).forEach(key => {
if (!key.startsWith("rule")) {
@ -171,40 +193,50 @@ function json2rule(RuleEditor) {
// 转换搜索规则
if (RuleEditor.ruleSearch) {
let searchJson = JSON.parse(RuleEditor.ruleSearch);
Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
//let searchJson = JSON.parse(RuleEditor.ruleSearch);
let searchJson = RuleEditor.ruleSearch;
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
});
}
// 转换发现规则
if (RuleEditor.ruleExplore) {
let exploreJson = JSON.parse(RuleEditor.ruleExplore);
Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
//let exploreJson = JSON.parse(RuleEditor.ruleExplore);
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
let exploreJson = RuleEditor.ruleExplore;
Object.keys(RuleJSON.ruleExplore).forEach(key => {
$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
});
}
// 转换详情页规则
if (RuleEditor.ruleBookInfo) {
let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
//let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
let bookInfoJson = RuleEditor.ruleBookInfo;
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
});
}
// 转换目录规则
if (RuleEditor.ruleToc) {
let tocJson = JSON.parse(RuleEditor.ruleToc);
Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
//let tocJson = JSON.parse(RuleEditor.ruleToc);
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
let tocJson = RuleEditor.ruleToc;
Object.keys(RuleJSON.ruleToc).forEach(key => {
$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
});
}
// 转换正文规则
if (RuleEditor.ruleContent) {
let contentJson = JSON.parse(RuleEditor.ruleContent);
Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
//let contentJson = JSON.parse(RuleEditor.ruleContent);
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
let contentJson = RuleEditor.ruleContent;
Object.keys(RuleJSON.ruleContent).forEach(key => {
$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

@ -0,0 +1,3 @@
<!DOCTYPE html><html lang=en style="padding: 0;height:100%"><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="favicon.ico" /><![endif]--><title>Legado Bookshelf</title><link href=css/about.dbe575e1.css rel=prefetch><link href=css/detail.9ba76c69.css rel=prefetch><link href=js/about.59a63964.js rel=prefetch><link href=js/about~detail.1caf6ef5.js rel=prefetch><link href=js/detail.11777eca.js rel=prefetch><link href=css/app.e4c919b7.css rel=preload as=style><link href=css/chunk-vendors.ad4ff18f.css rel=preload as=style><link href=js/app.d7843716.js rel=preload as=script><link href=js/chunk-vendors.8dd9045a.js rel=preload as=script><link href=css/chunk-vendors.ad4ff18f.css rel=stylesheet><link href=css/app.e4c919b7.css rel=stylesheet><link rel=icon type=image/png sizes=32x32 href=img/icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=img/icons/favicon-16x16.png><link rel=manifest href=manifest.json><meta name=theme-color content=#4DBA87><meta name=apple-mobile-web-app-capable content=no><meta name=apple-mobile-web-app-status-bar-style content=default><meta name=apple-mobile-web-app-title content=yd-web-tool><link rel=apple-touch-icon href=img/icons/apple-touch-icon-152x152.png><link rel=mask-icon href=img/icons/safari-pinned-tab.svg color=#4DBA87><meta name=msapplication-TileImage content=img/icons/msapplication-icon-144x144.png><meta name=msapplication-TileColor content=#000000></head><style>body::-webkit-scrollbar {
display: none;
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=js/chunk-vendors.8dd9045a.js></script><script src=js/app.d7843716.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,74 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "a77097c019b699bc81ee",
"url": "css/about.dbe575e1.css"
},
{
"revision": "4d729c4b428d537ebd8d",
"url": "css/app.e4c919b7.css"
},
{
"revision": "3e91096748e0f4d6bb89",
"url": "css/chunk-vendors.ad4ff18f.css"
},
{
"revision": "8f2124417070a994ebbd",
"url": "css/detail.9ba76c69.css"
},
{
"revision": "535877f50039c0cb49a6196a5b7517cd",
"url": "fonts/element-icons.535877f5.woff"
},
{
"revision": "732389ded34cb9c52dd88271f1345af9",
"url": "fonts/element-icons.732389de.ttf"
},
{
"revision": "f9a3fb0e145017e166dd4d91d9280cc4",
"url": "fonts/iconfont.f9a3fb0e.woff"
},
{
"revision": "f39ecc1a1d2a1eff3aca8aadd818bb61",
"url": "fonts/popfont.f39ecc1a.ttf"
},
{
"revision": "6c094b6d4ae9404dbed273c41b06fae8",
"url": "fonts/shelffont.6c094b6d.ttf"
},
{
"revision": "b5c48bc1e1fe73212a31be704875b71f",
"url": "img/noCover.b5c48bc1.jpeg"
},
{
"revision": "ad9f43586bb9220e0df71ce8fad92d8b",
"url": "index.html"
},
{
"revision": "a77097c019b699bc81ee",
"url": "js/about.59a63964.js"
},
{
"revision": "2c81bd893f3a92f018d8",
"url": "js/about~detail.1caf6ef5.js"
},
{
"revision": "4d729c4b428d537ebd8d",
"url": "js/app.d7843716.js"
},
{
"revision": "3e91096748e0f4d6bb89",
"url": "js/chunk-vendors.8dd9045a.js"
},
{
"revision": "8f2124417070a994ebbd",
"url": "js/detail.11777eca.js"
},
{
"revision": "b46d04eb43bc31ca0f9f95121646440d",
"url": "manifest.json"
},
{
"revision": "b6216d61c03e6ce0c9aea6ca7808f7ca",
"url": "robots.txt"
}
]);

@ -0,0 +1,2 @@
User-agent: *
Disallow:

@ -0,0 +1,34 @@
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"precache-manifest.78eb8adcb8f052b2a72d462abe0dc498.js"
);
workbox.core.setCacheNameDetails({prefix: "yd-web-tool"});
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

@ -1,28 +1,31 @@
package io.legado.app
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
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.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.AppDatabase
import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.*
@Suppress("DEPRECATION")
class App : Application() {
class App : MultiDexApplication() {
companion object {
@JvmStatic
@ -32,15 +35,18 @@ class App : Application() {
@JvmStatic
lateinit var db: AppDatabase
private set
}
var versionCode = 0
var versionName = ""
lateinit var androidId: String
var versionCode = 0
var versionName = ""
}
override fun onCreate() {
super.onCreate()
INSTANCE = this
CrashHandler().init(this)
androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID)
CrashHandler(this)
LanguageUtils.setConfigurationOld(this)
db = AppDatabase.createDatabase(INSTANCE)
packageManager.getPackageInfo(packageName, 0)?.let {
versionCode = it.versionCode
@ -60,7 +66,8 @@ class App : Application() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES, Configuration.UI_MODE_NIGHT_NO -> applyDayNight()
Configuration.UI_MODE_NIGHT_YES,
Configuration.UI_MODE_NIGHT_NO -> applyDayNight()
}
}
@ -68,28 +75,58 @@ class App : Application() {
* 更新主题
*/
fun applyTheme() {
if (AppConfig.isNightTheme) {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
).accentColor(
getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800))
).backgroundColor(
getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
).bottomBackground(
getPrefInt("colorBottomBackgroundNight", getCompatColor(R.color.md_grey_850))
).apply()
} else {
ThemeStore.editTheme(this)
.primaryColor(
getPrefInt("colorPrimary", getCompatColor(R.color.md_indigo_800))
).accentColor(
getPrefInt("colorAccent", getCompatColor(R.color.md_red_600))
).backgroundColor(
getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
).bottomBackground(
getPrefInt("colorBottomBackground", getCompatColor(R.color.md_grey_200))
).apply()
when {
AppConfig.isEInkMode -> {
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(Color.WHITE)
.accentColor(Color.BLACK)
.backgroundColor(Color.WHITE)
.bottomBackground(Color.WHITE)
.apply()
}
AppConfig.isNightTheme -> {
val primary =
getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600))
val accent =
getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_deep_orange_800))
var background =
getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900))
if (ColorUtils.isColorLight(background)) {
background = getCompatColor(R.color.md_grey_900)
putPrefInt(PreferKey.cNBackground, background)
}
val bBackground =
getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_850))
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(ColorUtils.withAlpha(primary, 1f))
.accentColor(ColorUtils.withAlpha(accent, 1f))
.backgroundColor(ColorUtils.withAlpha(background, 1f))
.bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
.apply()
}
else -> {
val primary =
getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500))
val accent =
getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_red_600))
var background =
getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100))
if (!ColorUtils.isColorLight(background)) {
background = getCompatColor(R.color.md_grey_100)
putPrefInt(PreferKey.cBackground, background)
}
val bBackground =
getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200))
ThemeStore.editTheme(this)
.coloredNavigationBar(true)
.primaryColor(ColorUtils.withAlpha(primary, 1f))
.accentColor(ColorUtils.withAlpha(accent, 1f))
.backgroundColor(ColorUtils.withAlpha(background, 1f))
.bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
.apply()
}
}
}
@ -97,6 +134,7 @@ class App : Application() {
ReadBookConfig.upBg()
applyTheme()
initNightMode()
postEvent(EventBus.RECREATE, "")
}
private fun initNightMode() {
@ -118,7 +156,7 @@ class App : Application() {
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.download_offline),
getString(R.string.action_download),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel

@ -0,0 +1,207 @@
/*
* Copyright (C) 2020 w568w
*/
package io.legado.app.api
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.ContentValues
import android.content.UriMatcher
import android.database.CharArrayBuffer
import android.database.ContentObserver
import android.database.Cursor
import android.database.DataSetObserver
import android.net.Uri
import android.os.Bundle
import com.google.gson.Gson
import io.legado.app.web.controller.BookshelfController
import io.legado.app.web.controller.SourceController
import io.legado.app.web.utils.ReturnData
import java.util.*
/**
* Export book data to other app.
*/
class ReaderProvider : ContentProvider() {
private enum class RequestCode {
SaveSource, SaveSources, SaveBook, DeleteSources, GetSource, GetSources, GetBookshelf, GetChapterList, GetBookContent
}
private val postBodyKey = "json"
private val sMatcher by lazy {
UriMatcher(UriMatcher.NO_MATCH).apply {
"${context?.applicationInfo?.packageName}.readerProvider".also { authority ->
addURI(authority, "source/insert", RequestCode.SaveSource.ordinal)
addURI(authority, "sources/insert", RequestCode.SaveSources.ordinal)
addURI(authority, "book/insert", RequestCode.SaveBook.ordinal)
addURI(authority, "sources/delete", RequestCode.DeleteSources.ordinal)
addURI(authority, "source/query", RequestCode.GetSource.ordinal)
addURI(authority, "sources/query", RequestCode.GetSources.ordinal)
addURI(authority, "books/query", RequestCode.GetBookshelf.ordinal)
addURI(authority, "book/chapter/query", RequestCode.GetChapterList.ordinal)
addURI(authority, "book/content/query", RequestCode.GetBookContent.ordinal)
}
}
}
override fun onCreate() = false
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
if (sMatcher.match(uri) < 0) return -1
when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.DeleteSources -> SourceController.deleteSources(selection)
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
)
}
return 0
}
override fun getType(uri: Uri) = throw UnsupportedOperationException("Not yet implemented")
override fun insert(uri: Uri, values: ContentValues?): Uri? {
if (sMatcher.match(uri) < 0) return null
when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.SaveSource -> values?.let {
SourceController.saveSource(values.getAsString(postBodyKey))
}
RequestCode.SaveBook -> values?.let {
BookshelfController.saveBook(values.getAsString(postBodyKey))
}
RequestCode.SaveSources -> values?.let {
SourceController.saveSources(values.getAsString(postBodyKey))
}
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
)
}
return null
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
val map: MutableMap<String, ArrayList<String>> = HashMap()
uri.getQueryParameter("url")?.let {
map["url"] = arrayListOf(it)
}
uri.getQueryParameter("index")?.let {
map["index"] = arrayListOf(it)
}
return if (sMatcher.match(uri) < 0) null else when (RequestCode.values()[sMatcher.match(uri)]) {
RequestCode.GetSource -> SimpleCursor(SourceController.getSource(map))
RequestCode.GetSources -> SimpleCursor(SourceController.sources)
RequestCode.GetBookshelf -> SimpleCursor(BookshelfController.bookshelf)
RequestCode.GetBookContent -> SimpleCursor(BookshelfController.getBookContent(map))
RequestCode.GetChapterList -> SimpleCursor(BookshelfController.getChapterList(map))
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
)
}
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = throw UnsupportedOperationException("Not yet implemented")
/**
* Simple inner class to deliver json callback data.
*
* Only getString() makes sense.
*/
private class SimpleCursor(data: ReturnData?) : Cursor {
private val mData: String = Gson().toJson(data)
override fun getCount() = 1
override fun getPosition() = 0
override fun move(i: Int) = true
override fun moveToPosition(i: Int) = true
override fun moveToFirst() = true
override fun moveToLast() = true
override fun moveToNext() = true
override fun moveToPrevious() = true
override fun isFirst() = true
override fun isLast() = true
override fun isBeforeFirst() = true
override fun isAfterLast() = true
override fun getColumnIndex(s: String) = 0
@Throws(IllegalArgumentException::class)
override fun getColumnIndexOrThrow(s: String): Int {
throw UnsupportedOperationException("Not yet implemented")
}
override fun getColumnName(i: Int) = null as String?
override fun getColumnNames() = arrayOf<String>()
override fun getColumnCount() = 0
override fun getBlob(i: Int) = ByteArray(0)
override fun getString(i: Int) = mData
override fun copyStringToBuffer(
i: Int,
charArrayBuffer: CharArrayBuffer
) {
}
override fun getShort(i: Int) = 0.toShort()
override fun getInt(i: Int) = 0
override fun getLong(i: Int) = 0L
override fun getFloat(i: Int) = 0F
override fun getDouble(i: Int) = 0.toDouble()
override fun getType(i: Int) = 0
override fun isNull(i: Int) = false
override fun deactivate() {}
override fun requery() = false
override fun close() {}
override fun isClosed() = false
override fun registerContentObserver(contentObserver: ContentObserver) {}
override fun unregisterContentObserver(contentObserver: ContentObserver) {}
override fun registerDataSetObserver(dataSetObserver: DataSetObserver) {}
override fun unregisterDataSetObserver(dataSetObserver: DataSetObserver) {}
override fun setNotificationUri(contentResolver: ContentResolver, uri: Uri) {}
override fun getNotificationUri() = null as Uri?
override fun getWantsAllOnMoveCalls() = false
override fun setExtras(bundle: Bundle) {}
override fun getExtras() = null as Bundle?
override fun respond(bundle: Bundle) = null as Bundle?
}
}

@ -1,20 +1,23 @@
package io.legado.app.base
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ColorUtils
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyOpenTint
import io.legado.app.utils.applyTint
import io.legado.app.utils.disableAutoFill
import io.legado.app.utils.hideSoftInput
import io.legado.app.ui.widget.TitleBar
import io.legado.app.utils.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@ -22,21 +25,66 @@ import kotlinx.coroutines.cancel
abstract class BaseActivity(
private val layoutID: Int,
private val fullScreen: Boolean = true,
private val theme: Theme = Theme.Auto
val fullScreen: Boolean = true,
private val theme: Theme = Theme.Auto,
private val toolBarTheme: Theme = Theme.Auto,
private val transparent: Boolean = false
) : AppCompatActivity(),
CoroutineScope by MainScope() {
val isInMultiWindow: Boolean
get() {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
isInMultiWindowMode
} else {
false
}
}
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LanguageUtils.setConfiguration(newBase))
}
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
if (AppConst.menuViewNames.contains(name) && parent?.parent is FrameLayout) {
(parent.parent as View).setBackgroundColor(backgroundColor)
}
return super.onCreateView(parent, name, context, attrs)
}
override fun onCreate(savedInstanceState: Bundle?) {
window.decorView.disableAutoFill()
initTheme()
setupSystemBar()
super.onCreate(savedInstanceState)
setContentView(layoutID)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
findViewById<TitleBar>(R.id.title_bar)
?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
}
onActivityCreated(savedInstanceState)
observeLiveBus()
}
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
findViewById<TitleBar>(R.id.title_bar)
?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
setupSystemBar()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
findViewById<TitleBar>(R.id.title_bar)
?.onMultiWindowModeChanged(isInMultiWindow, fullScreen)
setupSystemBar()
}
override fun onDestroy() {
super.onDestroy()
cancel()
@ -47,7 +95,7 @@ abstract class BaseActivity(
final override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return menu?.let {
val bool = onCompatCreateOptionsMenu(it)
it.applyTint(this, theme)
it.applyTint(this, toolBarTheme)
bool
} ?: super.onCreateOptionsMenu(menu)
}
@ -60,9 +108,7 @@ abstract class BaseActivity(
return true
}
open fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
return super.onCreateOptionsMenu(menu)
}
open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu)
final override fun onOptionsItemSelected(item: MenuItem?): Boolean {
item?.let {
@ -74,25 +120,32 @@ abstract class BaseActivity(
return item != null && onCompatOptionsItemSelected(item)
}
open fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item)
}
open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item)
private fun initTheme() {
ATH.applyBackgroundTint(window.decorView)
when (theme) {
Theme.Dark -> setTheme(R.style.AppTheme_Dark)
Theme.Light -> setTheme(R.style.AppTheme_Light)
else -> if (ColorUtils.isColorLight(primaryColor)) {
setTheme(R.style.AppTheme_Light)
} else {
Theme.Transparent -> setTheme(R.style.AppTheme_Transparent)
Theme.Dark -> {
setTheme(R.style.AppTheme_Dark)
ATH.applyBackgroundTint(window.decorView)
}
Theme.Light -> {
setTheme(R.style.AppTheme_Light)
ATH.applyBackgroundTint(window.decorView)
}
else -> {
if (ColorUtils.isColorLight(primaryColor)) {
setTheme(R.style.AppTheme_Light)
} else {
setTheme(R.style.AppTheme_Dark)
}
ATH.applyBackgroundTint(window.decorView)
}
}
}
private fun setupSystemBar() {
if (fullScreen) {
if (fullScreen && !isInMultiWindow) {
window.clearFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
@ -102,11 +155,16 @@ abstract class BaseActivity(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
ATH.setStatusBarColorAuto(this, fullScreen)
if (theme == Theme.Dark) {
ATH.setLightStatusBar(this, false)
} else if (theme == Theme.Light) {
ATH.setLightStatusBar(this, true)
if (toolBarTheme == Theme.Dark) {
ATH.setLightStatusBar(window, false)
} else if (toolBarTheme == Theme.Light) {
ATH.setLightStatusBar(window, true)
}
upNavigationBarColor()
}
open fun upNavigationBarColor() {
ATH.setNavigationBarColorAuto(this)
}
open fun observeLiveBus() {

@ -3,12 +3,15 @@ package io.legado.app.base
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.ThemeStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@ -21,12 +24,24 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(ThemeStore.backgroundColor())
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun show(manager: FragmentManager, tag: String?) {
try {
//在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit()
super.show(manager, tag)
} catch (e: Exception) {
//同一实例使用不同的tag会异常,这里捕获一下
e.printStackTrace()
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
@ -36,9 +51,7 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
) = Coroutine.async(scope, context) { block() }
open fun observeLiveBus() {
}

@ -1,11 +1,14 @@
package io.legado.app.base
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Bundle
import android.view.*
import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import io.legado.app.R
import io.legado.app.ui.widget.TitleBar
import io.legado.app.utils.applyTint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -36,12 +39,30 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onMultiWindowModeChanged()
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) {
super.onMultiWindowModeChanged(isInMultiWindowMode)
onMultiWindowModeChanged()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
onMultiWindowModeChanged()
}
private fun onMultiWindowModeChanged() {
(activity as? BaseActivity)?.let {
view?.findViewById<TitleBar>(R.id.title_bar)
?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen)
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()

@ -0,0 +1,61 @@
package io.legado.app.base
import android.annotation.SuppressLint
import androidx.fragment.app.DialogFragment
import androidx.preference.*
import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
import io.legado.app.ui.widget.prefs.ListPreferenceDialog
import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
@SuppressLint("RestrictedApi")
override fun onDisplayPreferenceDialog(preference: Preference) {
var handled = false
if (callbackFragment is OnPreferenceDisplayDialogCallback) {
handled =
(callbackFragment as OnPreferenceDisplayDialogCallback)
.onPreferenceDisplayDialog(this, preference)
}
if (!handled && activity is OnPreferenceDisplayDialogCallback) {
handled = (activity as OnPreferenceDisplayDialogCallback)
.onPreferenceDisplayDialog(this, preference)
}
if (handled) {
return
}
// check if dialog is already showing
if (parentFragmentManager.findFragmentByTag(dialogFragmentTag) != null) {
return
}
val f: DialogFragment = when (preference) {
is EditTextPreference -> {
EditTextPreferenceDialog.newInstance(preference.getKey())
}
is ListPreference -> {
ListPreferenceDialog.newInstance(preference.getKey())
}
is MultiSelectListPreference -> {
MultiSelectListPreferenceDialog.newInstance(preference.getKey())
}
else -> {
throw IllegalArgumentException(
"Cannot display dialog for an unknown Preference type: "
+ preference.javaClass.simpleName
+ ". Make sure to implement onPreferenceDisplayDialog() to handle "
+ "displaying a custom dialog for this Preference."
)
}
}
f.setTargetFragment(this, 0)
f.show(parentFragmentManager, dialogFragmentTag)
}
}

@ -16,13 +16,9 @@ abstract class BaseService : Service(), CoroutineScope by MainScope() {
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
) = Coroutine.async(scope, context) { block() }
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onBind(intent: Intent?) = null
override fun onDestroy() {
super.onDestroy()

@ -6,8 +6,9 @@ import io.legado.app.constant.Theme
abstract class VMBaseActivity<VM : ViewModel>(
layoutID: Int,
fullScreen: Boolean = true,
theme: Theme = Theme.Auto
) : BaseActivity(layoutID, fullScreen, theme) {
theme: Theme = Theme.Auto,
toolBarTheme: Theme = Theme.Auto
) : BaseActivity(layoutID, fullScreen, theme, toolBarTheme) {
protected abstract val viewModel: VM

@ -10,73 +10,72 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.*
/**
* Created by Invincible on 2017/11/24.
*
* 通用的adapter 可添加headerfooter以及不同类型item
*/
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context):
RecyclerView.Adapter<ItemViewHolder>() {
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) {
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>): this(context) {
addItemViewDelegates(*delegates)
}
constructor(
context: Context,
vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>
) : this(context) {
): this(context) {
addItemViewDelegates(*delegates)
}
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var headerItems: SparseArray<View>? = null
private var footerItems: SparseArray<View>? = null
private val itemDelegates: HashMap<Int, ItemViewDelegate<ITEM>> = hashMapOf()
private val items: MutableList<ITEM> = mutableListOf()
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
private var itemAnimation: ItemAnimation? = null
// 这个用Kotlin的setter就行了, 不需要手动开一个函数进行设置
var itemAnimation: ItemAnimation? = null
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
itemClickListener = listener
}
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
itemLongClickListener = listener
}
fun bindToRecyclerView(recyclerView: RecyclerView) {
recyclerView.adapter = this
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
fun <DELEGATE: ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
itemDelegates[viewType] = delegate
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) {
fun <DELEGATE: ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) {
itemDelegates[itemDelegates.size] = delegate
}
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) {
fun <DELEGATE: ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) {
delegates.forEach {
addItemViewDelegate(it)
}
}
fun addItemViewDelegates(vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) {
fun addItemViewDelegates(vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) =
delegates.forEach {
addItemViewDelegate(it.first, it.second)
}
}
fun addHeaderView(header: View) {
synchronized(lock) {
if (headerItems == null) {
@ -89,8 +88,8 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun addFooterView(footer: View) {
fun addFooterView(footer: View) =
synchronized(lock) {
if (footerItems == null) {
footerItems = SparseArray()
@ -101,9 +100,9 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
notifyItemInserted(index)
}
}
}
fun removeHeaderView(header: View) {
fun removeHeaderView(header: View) =
synchronized(lock) {
headerItems?.let {
val index = it.indexOfValue(header)
@ -113,9 +112,8 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
}
fun removeFooterView(footer: View) {
fun removeFooterView(footer: View) =
synchronized(lock) {
footerItems?.let {
val index = it.indexOfValue(footer)
@ -125,8 +123,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
}
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
@ -138,7 +135,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
notifyDataSetChanged()
}
}
fun setItems(items: List<ITEM>?, diffResult: DiffUtil.DiffResult) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
@ -150,7 +147,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
diffResult.dispatchUpdatesTo(this)
}
}
fun setItem(position: Int, item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
@ -160,7 +157,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun addItem(item: ITEM) {
synchronized(lock) {
val oldSize = getActualItemCount()
@ -169,7 +166,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) {
if (this.items.addAll(position, newItems)) {
@ -177,7 +174,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun addItems(newItems: List<ITEM>) {
synchronized(lock) {
val oldSize = getActualItemCount()
@ -190,7 +187,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (this.items.removeAt(position) != null) {
@ -198,7 +195,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun removeItem(item: ITEM) {
synchronized(lock) {
if (this.items.remove(item)) {
@ -206,7 +203,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun removeItems(items: List<ITEM>) {
synchronized(lock) {
if (this.items.removeAll(items)) {
@ -214,7 +211,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun swapItem(oldPosition: Int, newPosition: Int) {
synchronized(lock) {
val size = getActualItemCount()
@ -227,8 +224,8 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun updateItem(item: ITEM) {
fun updateItem(item: ITEM) =
synchronized(lock) {
val index = this.items.indexOf(item)
if (index >= 0) {
@ -236,18 +233,16 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
notifyItemChanged(index)
}
}
}
fun updateItem(position: Int, payload: Any) {
fun updateItem(position: Int, payload: Any) =
synchronized(lock) {
val size = getActualItemCount()
if (position in 0 until size) {
notifyItemChanged(position + getHeaderCount(), payload)
}
}
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) =
synchronized(lock) {
val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) {
@ -258,119 +253,94 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
)
}
}
}
fun clearItems() {
fun clearItems() =
synchronized(lock) {
this.items.clear()
notifyDataSetChanged()
}
}
fun isEmpty(): Boolean {
return items.isEmpty()
}
fun isNotEmpty(): Boolean {
return items.isNotEmpty()
}
fun isEmpty() = items.isEmpty()
fun isNotEmpty() = items.isNotEmpty()
/**
* 除去header和footer
*/
fun getActualItemCount(): Int {
return items.size
}
fun getHeaderCount(): Int {
return headerItems?.size() ?: 0
}
fun getFooterCount(): Int {
return footerItems?.size() ?: 0
}
fun getActualItemCount() = items.size
fun getHeaderCount() = headerItems?.size() ?: 0
fun getFooterCount() = footerItems?.size() ?: 0
fun getItem(position: Int): ITEM? = items.getOrNull(position)
fun getItemByLayoutPosition(position: Int): ITEM? {
val pos = position - getHeaderCount()
return items.getOrNull(pos)
}
fun getItemByLayoutPosition(position: Int) = items.getOrNull(position - getHeaderCount())
fun getItems(): List<ITEM> = items
protected open fun getItemViewType(item: ITEM, position: Int): Int {
return 0
}
protected open fun getItemViewType(item: ITEM, position: Int) = 0
/**
* grid 模式下使用
*/
protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int): Int {
return 1
}
final override fun getItemCount(): Int {
return getActualItemCount() + getHeaderCount() + getFooterCount()
}
final override fun getItemViewType(position: Int): Int {
return when {
isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
else -> getItem(getActualPosition(position))?.let {
getItemViewType(it, getActualPosition(position))
} ?: 0
protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int) = 1
final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount()
final override fun getItemViewType(position: Int) = when {
isHeader(position) -> TYPE_HEADER_VIEW + position
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
else -> getItem(getActualPosition(position))?.let {
getItemViewType(it, getActualPosition(position))
} ?: 0
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when {
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {
ItemViewHolder(headerItems!!.get(viewType))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return when {
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {
ItemViewHolder(headerItems!!.get(viewType))
}
viewType >= TYPE_FOOTER_VIEW -> {
ItemViewHolder(footerItems!!.get(viewType))
}
else -> {
val holder = ItemViewHolder(
inflater.inflate(
itemDelegates.getValue(viewType).layoutId,
parent,
false
)
viewType >= TYPE_FOOTER_VIEW -> {
ItemViewHolder(footerItems!!.get(viewType))
}
else -> {
val holder = ItemViewHolder(
inflater.inflate(
itemDelegates.getValue(viewType).layoutId,
parent,
false
)
itemDelegates.getValue(viewType)
.registerListener(holder)
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let {
itemClickListener?.invoke(holder, it)
}
)
itemDelegates.getValue(viewType)
.registerListener(holder)
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let {
itemClickListener?.invoke(holder, it)
}
}
if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener {
getItem(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it) ?: true
} ?: true
}
}
if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener {
getItem(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it) ?: true
} ?: true
}
holder
}
holder
}
}
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
}
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}
final override fun onBindViewHolder(
holder: ItemViewHolder,
position: Int,
@ -383,19 +353,19 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
super.onViewAttachedToWindow(holder)
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
addAnimation(holder)
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
val manager = recyclerView.layoutManager
if (manager is GridLayoutManager) {
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
manager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return getItem(position)?.let {
if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize(
@ -406,23 +376,13 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
fun setItemAnimation(item: ItemAnimation) {
itemAnimation = item
}
private fun isHeader(position: Int): Boolean {
return position < getHeaderCount()
}
private fun isFooter(position: Int): Boolean {
return position >= getActualItemCount() + getHeaderCount()
}
private fun getActualPosition(position: Int): Int {
return position - getHeaderCount()
}
private fun isHeader(position: Int) = position < getHeaderCount()
private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount()
private fun getActualPosition(position: Int) = position - getHeaderCount()
private fun addAnimation(holder: ItemViewHolder) {
itemAnimation?.let {
if (it.itemAnimEnabled) {
@ -433,8 +393,7 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
item.itemAnimation?.let {
for (anim in it.getAnimators(holder.itemView)) {
@ -443,12 +402,12 @@ abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) :
}
}
}
companion object {
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE
private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
}
}

@ -16,22 +16,19 @@ class ItemAnimation private constructor() {
var itemAnimDuration: Long = 300L
var itemAnimStartPosition: Int = -1
fun interpolator(interpolator: Interpolator): ItemAnimation {
fun interpolator(interpolator: Interpolator) = apply {
itemAnimInterpolator = interpolator
return this
}
fun duration(duration: Long): ItemAnimation {
fun duration(duration: Long) = apply {
itemAnimDuration = duration
return this
}
fun startPostion(startPos: Int): ItemAnimation {
fun startPosition(startPos: Int) = apply {
itemAnimStartPosition = startPos
return this
}
fun animation(animationType: Int = NONE, animation: BaseAnimation? = null): ItemAnimation {
fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply {
if (animation != null) {
itemAnimation = animation
} else {
@ -43,17 +40,14 @@ class ItemAnimation private constructor() {
RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation()
}
}
return this
}
fun enabled(enabled: Boolean): ItemAnimation {
fun enabled(enabled: Boolean) = apply {
itemAnimEnabled = enabled
return this
}
fun firstOnly(firstOnly: Boolean): ItemAnimation {
fun firstOnly(firstOnly: Boolean) = apply {
itemAnimFirstOnly = firstOnly
return this
}
companion object {
@ -79,8 +73,7 @@ class ItemAnimation private constructor() {
*/
const val RIGHT_SLIDE_IN: Int = 0x00000005
fun create(): ItemAnimation {
return ItemAnimation()
}
fun create() = ItemAnimation()
}
}

@ -20,7 +20,7 @@ object AppConst {
const val UA_NAME = "User-Agent"
val userAgent: String by lazy {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
}
val SCRIPT_ENGINE: ScriptEngine by lazy {
@ -41,17 +41,35 @@ object AppConst {
val keyboardToolChars: List<String> by lazy {
arrayListOf(
"@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
"href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag"
"", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\",
"$", "#", "!", ".", "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))
val bookGroupNone = BookGroup(-4, App.INSTANCE.getString(R.string.no_group))
const val notificationIdRead = 1144771
const val notificationIdAudio = 1144772
const val notificationIdWeb = 1144773
const val notificationIdDownload = 1144774
val urlOption: String by lazy {
"""
,{
"charset": "",
"method": "POST",
"body": "",
"headers": {"User-Agent": ""}
}
""".trimIndent()
}
val menuViewNames = arrayOf(
"com.android.internal.view.menu.ListMenuItemView",
"androidx.appcompat.view.menu.ListMenuItemView"
)
}

@ -3,8 +3,14 @@ package io.legado.app.constant
import java.util.regex.Pattern
object AppPattern {
val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val JS_PATTERN: Pattern =
Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
val imgPattern: Pattern =
Pattern.compile("<img .*?src.*?=.*?\"(.*?(?:,\\{.*\\})?)\".*?>", Pattern.CASE_INSENSITIVE)
val authorRegex = "\\s*者\\s*[::]".toRegex()
val nameRegex = Regex("\\s+作\\s*者.*")
val authorRegex = Regex(".*?作\\s*?者[::]")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val splitGroupRegex = Regex("[,;,;]")
}

@ -3,7 +3,7 @@ package io.legado.app.constant
object EventBus {
const val MEDIA_BUTTON = "mediaButton"
const val RECREATE = "RECREATE"
const val UP_BOOK = "sourceDebugLog"
const val UP_BOOK = "upBookToc"
const val ALOUD_STATE = "aloud_state"
const val TTS_PROGRESS = "ttsStart"
const val TTS_DS = "ttsDs"

@ -2,16 +2,16 @@ package io.legado.app.constant
object PreferKey {
const val versionCode = "versionCode"
const val language = "language"
const val themeMode = "themeMode"
const val hideStatusBar = "hideStatusBar"
const val clickTurnPage = "clickTurnPage"
const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch"
const val readAloudOnLine = "readAloudOnLine"
const val speakEngine = "speakEngine"
const val readAloudByPage = "readAloudByPage"
const val ttsSpeechRate = "ttsSpeechRate"
const val ttsSpeechPer = "ttsSpeechPer"
const val prevKey = "prevKeyCode"
const val nextKey = "nextKeyCode"
const val showRss = "showRss"
@ -22,9 +22,9 @@ object PreferKey {
const val cleanCache = "cleanCache"
const val saveTabPosition = "saveTabPosition"
const val pageAnim = "pageAnim"
const val readBookFont = "readBookFont"
const val fontFolder = "fontFolder"
const val backupPath = "backupUri"
const val restoreIgnore = "restoreIgnore"
const val threadCount = "threadCount"
const val webPort = "webPort"
const val keepLight = "keep_light"
@ -42,4 +42,24 @@ object PreferKey {
const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces"
const val readBodyToLh = "readBodyToLh"
const val textFullJustify = "textFullJustify"
const val textBottomJustify = "textBottomJustify"
const val autoReadSpeed = "autoReadSpeed"
const val barElevation = "barElevation"
const val transparentStatusBar = "transparentStatusBar"
const val defaultCover = "defaultCover"
const val replaceEnableDefault = "replaceEnableDefault"
const val showBrightnessView = "showBrightnessView"
const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent"
const val cBackground = "colorBackground"
const val cBBackground = "colorBottomBackground"
const val cNPrimary = "colorPrimaryNight"
const val cNAccent = "colorAccentNight"
const val cNBackground = "colorBackgroundNight"
const val cNBBackground = "colorBottomBackgroundNight"
}

@ -1,15 +1,19 @@
package io.legado.app.constant
import io.legado.app.help.AppConfig
import io.legado.app.utils.ColorUtils
enum class Theme {
Dark, Light, Auto;
Dark, Light, Auto, Transparent;
companion object {
fun getTheme(): Theme {
return if (AppConfig.isNightTheme) {
Dark
} else Light
}
fun getTheme() =
if (AppConfig.isNightTheme) Dark
else Light
fun getTheme(backgroundColor: Int) =
if (ColorUtils.isColorLight(backgroundColor)) Light
else Dark
}
}

@ -4,41 +4,121 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.App
import io.legado.app.data.dao.*
import io.legado.app.data.entities.*
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 8,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class],
version = 20,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract class AppDatabase: RoomDatabase() {
companion object {
private const val DATABASE_NAME = "legado.db"
fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
fun createDatabase(context: Context) =
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addCallback(object : Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) }
}
})
.addMigrations(
migration_10_11,
migration_11_12,
migration_12_13,
migration_13_14,
migration_14_15,
migration_15_17,
migration_17_18,
migration_18_19,
migration_19_20
)
.allowMainThreadQueries()
.build()
private val migration_10_11 = object: Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules")
database.execSQL(
"""
CREATE TABLE txtTocRules(id INTEGER NOT NULL,
name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL,
enable INTEGER NOT NULL, PRIMARY KEY (id))
"""
)
}
}
private val migration_11_12 = object: Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD style TEXT ")
}
}
private val migration_12_13 = object: Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
}
}
private val migration_13_14 = object: Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL,
`name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT,
`customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL,
`lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT,
`durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL,
`order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))
"""
)
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books_new` (`name`, `author`) ")
database.execSQL("INSERT INTO books_new select * from books ")
database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO books")
}
}
private val migration_14_15 = object: Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
}
}
private val migration_15_17 = object: Migration(15, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
}
}
private val migration_17_18 = object: Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
private val migration_18_19 = object: Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))")
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
}
}
private val migration_19_20 = object: Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
}
}
}
abstract fun bookDao(): BookDao
abstract fun bookGroupDao(): BookGroupDao
abstract fun bookSourceDao(): BookSourceDao
@ -52,4 +132,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun rssStarDao(): RssStarDao
abstract fun cookieDao(): CookieDao
abstract fun txtTocRule(): TxtTocRuleDao
abstract fun readRecordDao(): ReadRecordDao
abstract fun httpTTSDao(): HttpTTSDao
}

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

@ -21,15 +21,15 @@ interface BookDao {
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'")
fun observeLocalUri(): LiveData<List<String>>
@Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0")
fun observeDownload(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun observeByGroup(group: Int): LiveData<List<Book>>
@Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroup(): LiveData<List<Book>>
@Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
fun observeNoGroupSize(): LiveData<Int>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun liveDataSearch(key: String): LiveData<List<Book>>
@ -42,6 +42,12 @@ interface BookDao {
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
fun getBook(bookUrl: String): Book?
@Query("SELECT * FROM books WHERE name = :name and author = :author")
fun getBook(name: String, author: String): Book?
@get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
val noGroupSize: Int
@get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0")
val webBooks: List<Book>
@ -78,7 +84,7 @@ interface BookDao {
@Query("update books set `group` = :newGroupId where `group` = :oldGroupId")
fun upGroup(oldGroupId: Int, newGroupId: Int)
@get:Query("select bookUrl, durChapterIndex, durChapterPos, durChapterTime, durChapterTitle from books")
@get:Query("select bookUrl, tocUrl, origin, originName, durChapterIndex, durChapterPos, durChapterTime, durChapterTitle from books")
val allBookProgress: List<BookProgress>
@Query(

@ -11,9 +11,15 @@ 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")
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey or bookSourceComment like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
fun liveDataEnabled(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 0 order by customOrder asc")
fun liveDataDisabled(): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun liveExplore(): LiveData<List<BookSource>>

@ -1,21 +1,22 @@
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 androidx.room.*
import io.legado.app.data.entities.Bookmark
@Dao
interface BookmarkDao {
@Query("select * from bookmarks")
fun all(): List<Bookmark>
@get:Query("select * from bookmarks")
val all: List<Bookmark>
@Query("select * from bookmarks where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): DataSource.Factory<Int, Bookmark>
@Query("select * from bookmarks where bookUrl = :bookUrl or (bookName = :bookName and bookAuthor = :bookAuthor)")
fun observeByBook(
bookUrl: String,
bookName: String,
bookAuthor: String
): DataSource.Factory<Int, Bookmark>
@Query("SELECT * FROM bookmarks where bookUrl = :bookUrl and chapterName like '%'||:key||'%' or content like '%'||:key||'%'")
fun liveDataSearch(bookUrl: String, key: String): DataSource.Factory<Int, Bookmark>
@ -23,6 +24,12 @@ interface BookmarkDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookmark: Bookmark)
@Update
fun update(bookmark: Bookmark)
@Delete
fun delete(vararg bookmark: Bookmark)
@Query("delete from bookmarks where bookUrl = :bookUrl and chapterName like '%'||:chapterName||'%'")
fun delByBookmark(bookUrl: String, chapterName: String)

@ -9,6 +9,9 @@ interface CookieDao {
@Query("SELECT * FROM cookies Where url = :url")
fun get(url: String): Cookie?
@Query("select * from cookies where url like '%|%'")
fun getOkHttpCookies(): List<Cookie>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg cookie: Cookie)
@ -17,4 +20,7 @@ interface CookieDao {
@Query("delete from cookies where url = :url")
fun delete(url: String)
@Query("delete from cookies where url like '%|%'")
fun deleteOkHttp()
}

@ -0,0 +1,31 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.HttpTTS
@Dao
interface HttpTTSDao {
@get:Query("select * from httpTTS order by name")
val all: List<HttpTTS>
@Query("select * from httpTTS order by name")
fun observeAll(): LiveData<List<HttpTTS>>
@get:Query("select count(*) from httpTTS")
val count: Int
@Query("select * from httpTTS where id = :id")
fun get(id: Long): HttpTTS?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg httpTTS: HttpTTS)
@Delete
fun delete(vararg httpTTS: HttpTTS)
@Update
fun update(vararg httpTTS: HttpTTS)
}

@ -0,0 +1,39 @@
package io.legado.app.data.dao
import androidx.room.*
import io.legado.app.data.entities.ReadRecord
import io.legado.app.data.entities.ReadRecordShow
@Dao
interface ReadRecordDao {
@get:Query("select * from readRecord")
val all: List<ReadRecord>
@get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName")
val allShow: List<ReadRecordShow>
@get:Query("select sum(readTime) from readRecord")
val allTime: Long
@Query("select sum(readTime) from readRecord where bookName = :bookName")
fun getReadTime(bookName: String): Long?
@Query("select readTime from readRecord where androidId = :androidId and bookName = :bookName")
fun getReadTime(androidId: String, bookName: String): Long?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg readRecord: ReadRecord)
@Update
fun update(vararg record: ReadRecord)
@Delete
fun delete(vararg record: ReadRecord)
@Query("delete from readRecord")
fun clear()
@Query("delete from readRecord where bookName = :bookName")
fun deleteByName(bookName: String)
}

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

@ -19,7 +19,7 @@ interface SearchBookDao {
@Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): 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 limit 1")
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources) order by originOrder limit 1")
fun getFirstByNameAuthor(name: String, author: String): SearchBook?
@Query(

@ -28,4 +28,6 @@ interface TxtTocRuleDao {
@Delete
fun delete(vararg rule: TxtTocRule)
@Query("delete from txtTocRules where id < 0")
fun deleteDefault()
}

@ -3,8 +3,10 @@ package io.legado.app.data.entities
import io.legado.app.utils.splitNotBlank
interface BaseBook {
var name: String
var author: String
var bookUrl: String
var variableMap: HashMap<String, String>?
val variableMap: HashMap<String, String>
var kind: String?
var wordCount: String?

@ -5,9 +5,13 @@ 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.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.help.AppConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
@ -15,15 +19,18 @@ import java.nio.charset.Charset
import kotlin.math.max
@Parcelize
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
@Entity(
tableName = "books",
indices = [Index(value = ["name", "author"], unique = true)]
)
data class Book(
@PrimaryKey
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 or 本地书籍文件名
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var name: String = "", // 书籍名称(书源获取)
override var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
@ -46,83 +53,124 @@ data class Book(
var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
var order: Int = 0, // 手动排序
var originOrder: Int = 0, //书源排序
var useReplaceRule: Boolean = true, // 正文使用净化替换规则
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则
var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
) : Parcelable, BaseBook {
): Parcelable, BaseBook {
fun isLocalBook(): Boolean {
return origin == BookType.local
}
fun isTxt(): Boolean {
fun isLocalTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true)
}
fun isEpub(): Boolean {
return originName.endsWith(".epub", true)
}
fun isOnLineTxt(): Boolean {
return !isLocalBook() && type == 0
}
override fun equals(other: Any?): Boolean {
if (other is Book) {
return other.bookUrl == bookUrl
}
return false
}
override fun hashCode(): Int {
return bookUrl.hashCode()
}
@Ignore
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null
get() {
if (field == null) {
field = GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
}
return field
}
override val variableMap by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
}
override fun putVariable(key: String, value: String) {
variableMap[key] = value
variable = GSON.toJson(variableMap)
}
@Ignore
@IgnoredOnParcel
override var infoHtml: String? = null
@Ignore
@IgnoredOnParcel
override var tocHtml: String? = null
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
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?.put(key, value)
variable = GSON.toJson(variableMap)
}
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
fun getFolderName(): String {
return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl)
}
fun toSearchBook() = SearchBook(
name = name,
author = author,
kind = kind,
bookUrl = bookUrl,
origin = origin,
originName = originName,
type = type,
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
}
fun changeTo(newBook: Book) {
newBook.group = group
newBook.order = order
newBook.customCoverUrl = customCoverUrl
newBook.customIntro = customIntro
newBook.customTag = customTag
newBook.canUpdate = canUpdate
newBook.useReplaceRule = useReplaceRule
delete()
App.db.bookDao().insert(newBook)
}
fun toSearchBook(): SearchBook {
return SearchBook(
name = name,
author = author,
kind = kind,
bookUrl = bookUrl,
origin = origin,
originName = originName,
type = type,
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
fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null
}
App.db.bookDao().delete(this)
}
fun upInfoFromOld(oldBook: Book?) {
oldBook?.let {
group = oldBook.group
durChapterIndex = oldBook.durChapterIndex
durChapterPos = oldBook.durChapterPos
durChapterTitle = oldBook.durChapterTitle
customCoverUrl = oldBook.customCoverUrl
customIntro = oldBook.customIntro
order = oldBook.order
if (coverUrl.isNullOrEmpty()) {
coverUrl = oldBook.getDisplayCover()
}
}
}
}

@ -52,16 +52,9 @@ data class BookChapter(
variable = GSON.toJson(variableMap)
}
override fun hashCode(): Int {
return url.hashCode()
}
override fun hashCode() = url.hashCode()
override fun equals(other: Any?): Boolean {
if (other is BookChapter) {
return other.url == url
}
return false
}
override fun equals(other: Any?) = if (other is BookChapter) other.url == url else false
}

@ -2,6 +2,9 @@ package io.legado.app.data.entities
data class BookProgress(
val bookUrl: String,
val tocUrl: String = "",
var origin: String = "",
var originName: String = "",
val durChapterIndex: Int,
val durChapterPos: Int,
val durChapterTime: Long,

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

Loading…
Cancel
Save