pull/995/head
fisher 4 years ago
commit ce21648ca8
  1. 99
      .github/scripts/lzy_web.py
  2. 70
      .github/workflows/legado.yml
  3. 4
      README.md
  4. 52
      app/build.gradle
  5. 48
      app/src/main/AndroidManifest.xml
  6. 160
      app/src/main/assets/defaultData/httpTTS.json
  7. 55
      app/src/main/assets/defaultData/readConfig.json
  8. 20
      app/src/main/assets/defaultData/rssSources.json
  9. 120
      app/src/main/assets/updateLog.md
  10. 2
      app/src/main/assets/web/index.js
  11. 2
      app/src/main/assets/web/new/css/detail.79c91c30.css
  12. 4
      app/src/main/assets/web/new/index.html
  13. 0
      app/src/main/assets/web/new/js/about.32f651f9.js
  14. 1
      app/src/main/assets/web/new/js/about~detail.1caf6ef5.js
  15. 1
      app/src/main/assets/web/new/js/about~detail.c1b29cbc.js
  16. 1
      app/src/main/assets/web/new/js/app.3f3311a5.js
  17. 1
      app/src/main/assets/web/new/js/app.d60dad33.js
  18. 33
      app/src/main/assets/web/new/js/chunk-vendors.060eee33.js
  19. 33
      app/src/main/assets/web/new/js/chunk-vendors.305d5536.js
  20. 1
      app/src/main/assets/web/new/js/detail.0cc184f2.js
  21. 1
      app/src/main/assets/web/new/js/detail.6a5cc86c.js
  22. 32
      app/src/main/assets/web/new/precache-manifest.63ef7cbb041538ea344680ff651c5232.js
  23. 2
      app/src/main/assets/web/new/service-worker.js
  24. 7
      app/src/main/java/io/legado/app/App.kt
  25. 32
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  26. 2
      app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt
  27. 6
      app/src/main/java/io/legado/app/base/VMBaseActivity.kt
  28. 27
      app/src/main/java/io/legado/app/constant/AppConst.kt
  29. 27
      app/src/main/java/io/legado/app/constant/Const.kt
  30. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  31. 15
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  32. 57
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  33. 4
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  34. 6
      app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
  35. 2
      app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
  36. 3
      app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt
  37. 9
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  38. 41
      app/src/main/java/io/legado/app/data/entities/Book.kt
  39. 2
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  40. 14
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  41. 4
      app/src/main/java/io/legado/app/data/entities/ReadRecord.kt
  42. 31
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  43. 167
      app/src/main/java/io/legado/app/help/AppConfig.kt
  44. 106
      app/src/main/java/io/legado/app/help/BookHelp.kt
  45. 79
      app/src/main/java/io/legado/app/help/ContentHelp.kt
  46. 26
      app/src/main/java/io/legado/app/help/ContentProcessor.kt
  47. 69
      app/src/main/java/io/legado/app/help/CrashHandler.kt
  48. 4
      app/src/main/java/io/legado/app/help/DefaultData.kt
  49. 231
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  50. 53
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  51. 48
      app/src/main/java/io/legado/app/help/MediaHelp.kt
  52. 40
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  53. 6
      app/src/main/java/io/legado/app/help/ThemeConfig.kt
  54. 4
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  55. 180
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  56. 122
      app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt
  57. 12
      app/src/main/java/io/legado/app/help/http/RetryInterceptor.kt
  58. 13
      app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt
  59. 14
      app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt
  60. 40
      app/src/main/java/io/legado/app/help/http/parser/StrResponseParser.kt
  61. 33
      app/src/main/java/io/legado/app/help/http/parser/TextParser.kt
  62. 7
      app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt
  63. 7
      app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt
  64. 9
      app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt
  65. 10
      app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt
  66. 2
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  67. 11
      app/src/main/java/io/legado/app/help/storage/BookWebDav.kt
  68. 32
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  69. 2
      app/src/main/java/io/legado/app/lib/permission/ActivitySource.kt
  70. 2
      app/src/main/java/io/legado/app/lib/permission/FragmentSource.kt
  71. 7
      app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt
  72. 7
      app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt
  73. 9
      app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt
  74. 8
      app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt
  75. 28
      app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt
  76. 2
      app/src/main/java/io/legado/app/lib/permission/Permissions.kt
  77. 31
      app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt
  78. 44
      app/src/main/java/io/legado/app/lib/permission/Request.kt
  79. 6
      app/src/main/java/io/legado/app/lib/permission/RequestManager.kt
  80. 2
      app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt
  81. 2
      app/src/main/java/io/legado/app/lib/permission/RequestSource.kt
  82. 8
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  83. 4
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt
  84. 163
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  85. 12
      app/src/main/java/io/legado/app/model/Debug.kt
  86. 20
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  87. 24
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  88. 81
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  89. 302
      app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java
  90. 4
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  91. 185
      app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt
  92. 227
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  93. 86
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  94. 7
      app/src/main/java/io/legado/app/model/rss/Rss.kt
  95. 4
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  96. 224
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  97. 86
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  98. 24
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  99. 77
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  100. 31
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,99 @@
import requests, os, datetime, sys
# Cookie 中 phpdisk_info 的值
cookie_phpdisk_info = os.environ.get('phpdisk_info')
# Cookie 中 ylogin 的值
cookie_ylogin = os.environ.get('ylogin')
# 请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Referer': 'https://pc.woozooo.com/account.php?action=login'
}
# 小饼干
cookie = {
'ylogin': cookie_ylogin,
'phpdisk_info': cookie_phpdisk_info
}
# 日志打印
def log(msg):
utc_time = datetime.datetime.utcnow()
china_time = utc_time + datetime.timedelta(hours=8)
print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}")
# 检查是否已登录
def login_by_cookie():
url_account = "https://pc.woozooo.com/account.php"
if cookie['phpdisk_info'] is None:
log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!')
return False
if cookie['ylogin'] is None:
log('ERROR: 请指定 Cookie 中 ylogin 的值!')
return False
res = requests.get(url_account, headers=headers, cookies=cookie, verify=True)
if '网盘用户登录' in res.text:
log('ERROR: 登录失败,请更新Cookie')
return False
else:
log('登录成功')
return True
# 上传文件
def upload_file(file_dir, folder_id):
file_name = os.path.basename(file_dir)
url_upload = "https://up.woozooo.com/fileup.php"
headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}'
post_data = {
"task": "1",
"folder_id": folder_id,
"id": "WU_FILE_0",
"name": file_name,
}
files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')}
res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120,
verify=True).json()
log(f"{file_dir} -> {res['info']}")
return res['zt'] == 1
# 上传文件夹内的文件
def upload_folder(folder_dir, folder_id):
file_list = os.listdir(folder_dir)
for file in file_list:
path = os.path.join(folder_dir, file)
if os.path.isfile(path):
upload_file(path, folder_id)
else:
upload_folder(path, folder_id)
# 上传
def upload(dir, folder_id):
if dir is None:
log('ERROR: 请指定上传的文件路径')
return
if folder_id is None:
log('ERROR: 请指定蓝奏云的文件夹id')
return
if os.path.isfile(dir):
upload_file(dir, str(folder_id))
else:
upload_folder(dir, str(folder_id))
if __name__ == '__main__':
argv = sys.argv[1:]
if len(argv) != 2:
log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id')
# 需上传的路径
upload_path = argv[0]
# 蓝奏云文件夹id
lzy_folder_id = argv[1]
if login_by_cookie():
upload(upload_path, lzy_folder_id)

@ -6,11 +6,6 @@ on:
push: push:
branches: branches:
- master - master
# tags:
# - '3.*'
# pull_request:
# branches:
# - master
# watch: # watch:
# types: [started] # types: [started]
# schedule: # schedule:
@ -20,21 +15,24 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
# 登录蓝奏云后查看cookie # 登录蓝奏云后在控制台运行document.cookie
# LANZOU_ID : woozooo.com -> Cookie -> ylogin ylogin: ${{ secrets.LANZOU_ID }}
# LANZOU_PSD : pc.woozooo.com -> Cookie -> phpdisk_info phpdisk_info: ${{ secrets.LANZOU_PSD }}
LANZOU_ID: ${{ secrets.LANZOU_ID }} # 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_PSD: ${{ secrets.LANZOU_PSD }} LANZOU_FOLDER_ID: '2670621'
# 蓝奏云里的文件夹名(需手动在蓝奏云创建)
LANZOU_FOLDER: '阅读3测试版'
# 是否上传到artifact # 是否上传到artifact
UPLOAD_ARTIFACT: 'true' UPLOAD_ARTIFACT: 'true'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set Up JDK - uses: actions/cache@v2
uses: actions/setup-java@v1
with: with:
java-version: 14 path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt - name: Clear 18PlusList.txt
run: | run: |
echo "清空18PlusList.txt" echo "清空18PlusList.txt"
@ -56,35 +54,27 @@ jobs:
run: | run: |
echo "开始进行release构建" echo "开始进行release构建"
chmod +x gradlew chmod +x gradlew
./gradlew assembleAppRelease ./gradlew assembleAppRelease --build-cache --parallel
- name: Upload App To Artifact - name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }} if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: legado apk name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk
- name: Set Up Python 3
if: ${{ env.LANZOU_PSD }}
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Dependence Install
if: ${{ env.LANZOU_PSD }}
run: |
pip install requests
pip install lanzou-api
- name: Download Script
if: ${{ env.LANZOU_PSD }}
shell: python
run: |
import requests,os
res = requests.get('https://raw.githubusercontent.com/Celeter/build/master/.github/scripts/API_lanzou.py')
with open(os.environ["FILE_PATH"], 'wb') as f:
f.write(res.content)
env:
FILE_PATH: ${{ github.workspace }}/.github/lzy.py
- name: Upload App To Lanzou - name: Upload App To Lanzou
if: ${{ env.LANZOU_PSD }} if: ${{ env.ylogin }}
run: python $GITHUB_WORKSPACE/.github/lzy.py run: |
env: path="$GITHUB_WORKSPACE/app/build/outputs/apk/app/release"
UPLOAD_FOLDER: ${{ github.workspace }}/app/build/outputs/apk/app/release/ files=$(ls $path)
for f in $files
do
if [[ $f == *"apk" ]]; then
file=$f
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file"
break
fi
done
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzous.com/b0f810h4b"

@ -45,9 +45,13 @@ com.positiondev.epublib:epublib-core
## 免责声明 ## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/disclaimer.html
## 界面
![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%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%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%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%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%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) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg)
## 其它
其它网友做的IOS版本: https://github.com/kaich/Yuedu

@ -3,7 +3,6 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'de.timfreiheit.resourceplaceholders' apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'com.google.firebase.crashlytics'
static def releaseTime() { static def releaseTime() {
return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8")) return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8"))
@ -98,6 +97,9 @@ android {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
buildToolsVersion '30.0.3' buildToolsVersion '30.0.3'
// tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:unchecked"
// }
} }
resourcePlaceholders { resourcePlaceholders {
@ -111,36 +113,34 @@ kapt {
} }
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' implementation project(path: ':epublib')
implementation fileTree(dir: 'libs', include: ['*.jar']) coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
testImplementation 'junit:junit:4.13.1' implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "com.android.support:multidex:1.0.3" implementation 'androidx.multidex:multidex:2.0.1'
//kotlin //kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
//fireBase
implementation 'com.google.firebase:firebase-analytics-ktx:18.0.0'
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.0'
//androidX //androidX
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.activity:activity-ktx:1.1.0" implementation "androidx.activity:activity-ktx:1.2.3"
implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.fragment:fragment-ktx:1.3.3"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation "androidx.collection:collection-ktx:1.1.0" implementation "androidx.collection:collection-ktx:1.1.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.6'
//media //media
def media2_version = "1.1.2" def media2_version = "1.1.3"
implementation "androidx.media2:media2-session:$media2_version" implementation "androidx.media2:media2-session:$media2_version"
implementation "androidx.media:media:1.3.1"
//implementation "androidx.media2:media2-player:$media2_version" //implementation "androidx.media2:media2-player:$media2_version"
//implementation 'com.google.android.exoplayer:exoplayer:2.13.0' //implementation 'com.google.android.exoplayer:exoplayer:2.13.0'
@ -151,24 +151,20 @@ dependencies {
implementation("com.louiscad.splitties:splitties-views:$splitties_version") implementation("com.louiscad.splitties:splitties-views:$splitties_version")
//lifecycle //lifecycle
def lifecycle_version = '2.2.0' def lifecycle_version = '2.3.1'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.6' def room_version = '2.3.0'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:$room_version" testImplementation "androidx.room:room-testing:$room_version"
//paging
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
//liveEventBus //liveEventBus
implementation 'com.jeremyliao:live-event-bus-x:1.5.7' implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
// //
def coroutines_version = '1.3.7' def coroutines_version = '1.4.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -182,9 +178,7 @@ dependencies {
implementation 'com.github.gedoor:rhino-android:1.5' implementation 'com.github.gedoor:rhino-android:1.5'
// //
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.ljx.rxhttp:rxhttp:2.5.3'
kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.3'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
@ -194,7 +188,7 @@ dependencies {
implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
// //
implementation 'com.king.zxing:zxing-lite:2.0.2' implementation 'com.king.zxing:zxing-lite:2.0.3'
// //
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'
@ -209,14 +203,6 @@ dependencies {
implementation 'io.noties.markwon:html:4.6.0' implementation 'io.noties.markwon:html:4.6.0'
// //
implementation 'com.hankcs:hanlp:portable-1.7.8' implementation 'com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.1.3'
//epub
implementation('com.positiondev.epublib:epublib-core:3.1') {
exclude group: 'org.slf4j'
exclude group: 'xmlpull'
}
} }
apply plugin: 'com.google.gms.google-services'

@ -173,7 +173,7 @@
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!-- 授权界面 --> <!-- 授权界面 -->
<activity <activity
android:name=".help.permission.PermissionActivity" android:name="io.legado.app.lib.permission.PermissionActivity"
android:theme="@style/Activity.Permission" /> android:theme="@style/Activity.Permission" />
<!-- 二维码扫描 --> <!-- 二维码扫描 -->
<activity <activity
@ -286,7 +286,7 @@
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!-- 书源登录 --> <!-- 书源登录 -->
<activity <activity
android:name=".ui.login.SourceLogin" android:name=".ui.login.SourceLoginActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" /> android:hardwareAccelerated="true" />
<!-- 阅读记录 --> <!-- 阅读记录 -->
@ -294,10 +294,17 @@
android:name=".ui.about.ReadRecordActivity" android:name=".ui.about.ReadRecordActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" /> android:hardwareAccelerated="true" />
<!-- 选择文件 -->
<activity
android:name=".ui.document.FilePickerActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme.Transparent" />
<!-- 文字处理 --> <!-- 文字处理 -->
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label"> android:label="@string/receiving_shared_label"
android:theme="@style/AppTheme.Transparent">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" /> <action android:name="android.intent.action.PROCESS_TEXT" />
@ -366,16 +373,47 @@
android:name=".ui.association.FileAssociationActivity" android:name=".ui.association.FileAssociationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/AppTheme.Transparent"> android:theme="@style/AppTheme.Transparent">
<!-- VIEW (Open with) action -->
<!-- Works when an app knows the media type of a file, e.g. Gmail or Chrome. -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" /> <data android:scheme="app" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" />
<!-- text -->
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
<!-- json -->
<data android:mimeType="application/json" /> <data android:mimeType="application/json" />
<data android:mimeType="application/epub" /> <!-- EPUB -->
<data android:mimeType="application/epub+zip" />
</intent-filter>
<!-- Works when an app doesn't know the media type, e.g. Dropbox -->
<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="*" />
<data android:scheme="app" />
<data android:scheme="content" />
<data android:scheme="file" />
<!-- This media type is necessary, otherwise it won't match on the file extension -->
<data android:mimeType="*/*" />
<!--TXT-->
<data android:pathPattern=".*\\.txt" />
<data android:pathPattern=".*\\.TXT" />
<!--JSON-->
<data android:pathPattern=".*\\.json" />
<data android:pathPattern=".*\\.JSON" />
<!-- EPUB -->
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.EPUB" />
</intent-filter> </intent-filter>
</activity> </activity>

@ -1,143 +1,233 @@
[ [
{
"id": -100,
"name": "0",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}"
},
{
"id": -99,
"name": "zaixianai.cn",
"url": "<js>\nlet url='https://www.zaixianai.cn/voiceCompose';\n\nlet ua=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\";\n\nlet doc=java.get(url,{\"User-Agent\":ua});\nlet cookie=String(doc.header(\"set-cookie\")).match(/laravel_session=[^\\n]+/)[0];\nlet token=String(doc.body()).match(/token=\"([^\"]+)/)[1];\n\nurl='https://www.zaixianai.cn/Api_getVoice,'+JSON.stringify({\n\"method\": \"POST\",\n\"body\": \"content=\" + java.encodeURI(speakText) + \"&volume=50&speech_rate=0&voice=Aixia&_token=\"+token,\n\"headers\": {\n\"User-Agent\": ua,\n\"cookie\": cookie\n}\n});\n\nlet res=java.ajax(url);\n\n'https://www.zaixianai.cn/voice/'+JSON.parse(res).data.file_name+','+JSON.stringify({\n\"headers\": {\n\"User-Agent\": ua,\n\"accept\": \"*/*\",\n\"referer\": \"https://www.zaixianai.cn/voiceCompose\",\n\"cookie\": cookie,\n\"accept-encoding\": \"identity;q=1, *;q=0\"\n}\n})\n</js>"
},
{
"id": -98,
"name": "台湾女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4007&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{ {
"id": -1, "id": -1,
"name": "度丫丫", "name": "度丫丫",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -2, "id": -2,
"name": "度博文①", "name": "度博文①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -3, "id": -3,
"name": "度博文②", "name": "度博文②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -4, "id": -4,
"name": "度博文③", "name": "度博文③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -97,
"name": "度小乔",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}"
}, },
{ {
"id": -5, "id": -5,
"name": "度小娇", "name": "度小娇",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -6, "id": -6,
"name": "度小宇", "name": "度小宇",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=2&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -7, "id": -7,
"name": "度小童", "name": "度小童",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -96,
"name": "度小童",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -95,
"name": "度小粤",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=cte&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -8, "id": -8,
"name": "度小美", "name": "度小美",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -94,
"name": "度小芳",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4125&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -9, "id": -9,
"name": "度小萌", "name": "度小萌",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -93,
"name": "度小贤",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"}"
},
{
"id": -92,
"name": "度小雯",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -10, "id": -10,
"name": "度小鹿①", "name": "度小鹿①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -11, "id": -12,
"name": "度小鹿②", "name": "度小鹿②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -12, "id": -11,
"name": "度小鹿③", "name": "度小鹿③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -91,
"name": "度灵儿",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -13, "id": -13,
"name": "度米朵①", "name": "度米朵①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -14, "id": -14,
"name": "度米朵②", "name": "度米朵②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -15, "id": -15,
"name": "度逍遥-基础", "name": "度逍遥-基础",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -16, "id": -16,
"name": "度逍遥-精品①", "name": "度逍遥-精品①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -17, "id": -17,
"name": "度逍遥-精品②", "name": "度逍遥-精品②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -18, "id": -18,
"name": "情感女声", "name": "情感女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -19, "id": -19,
"name": "情感男声", "name": "情感男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -20, "id": -20,
"name": "标准女声", "name": "标准女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -90,
"name": "标准女声-基础",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -21, "id": -21,
"name": "标准男声", "name": "标准男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -22, "id": -89,
"name": "治愈女声", "name": "温柔女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5120&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4126&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -23, "id": -88,
"name": "治愈男声", "name": "甜美女声①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1200&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=&rate=32=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -24, "id": -24,
"name": "甜美女声", "name": "甜美女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -25, "id": -25,
"name": "甜美女声②", "name": "甜美女声③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -22,
"name": "电台女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5120&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=&rate=32=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -23,
"name": "电台男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -26, "id": -26,
"name": "百度主持", "name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{
"id": -87,
"name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -86,
"name": "百度解说①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4123&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -85,
"name": "百度解说②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4128&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -84,
"name": "百度解说③",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4129&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{ {
"id": -27, "id": -27,
"name": "百度评书①", "name": "百度评书①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -28, "id": -28,
"name": "百度评书②", "name": "百度评书②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -83,
"name": "萝莉少女音",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5201&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -29, "id": -29,

@ -1,5 +1,56 @@
[ [
{ {
"bgStr": "#ffc0edc6",
"bgStrEInk": "#FFFFFF",
"bgStrNight": "#000000",
"bgType": 0,
"bgTypeEInk": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"darkStatusIconEInk": true,
"darkStatusIconNight": false,
"footerMode": 0,
"footerPaddingBottom": 10,
"footerPaddingLeft": 13,
"footerPaddingRight": 17,
"footerPaddingTop": 0,
"headerMode": 0,
"headerPaddingBottom": 0,
"headerPaddingLeft": 19,
"headerPaddingRight": 16,
"headerPaddingTop": 10,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"name": "微信读书",
"paddingBottom": 4,
"paddingLeft": 22,
"paddingRight": 22,
"paddingTop": 5,
"pageAnim": 3,
"pageAnimEInk": 3,
"paragraphIndent": "  ",
"paragraphSpacing": 6,
"showFooterLine": true,
"showHeaderLine": true,
"textBold": 0,
"textColor": "#ff0b0b0b",
"textColorEInk": "#000000",
"textColorNight": "#ADADAD",
"textSize": 24,
"tipColor": -10461088,
"tipFooterLeft": 7,
"tipFooterMiddle": 0,
"tipFooterRight": 6,
"tipHeaderLeft": 1,
"tipHeaderMiddle": 0,
"tipHeaderRight": 2,
"titleBottomSpacing": 0,
"titleMode": 0,
"titleSize": 4,
"titleTopSpacing": 0
},
{
"name": "预设1",
"bgStr": "#FFFFFF", "bgStr": "#FFFFFF",
"bgStrNight": "#000000", "bgStrNight": "#000000",
"textColor": "#000000", "textColor": "#000000",
@ -10,6 +61,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设2",
"bgStr": "#DDC090", "bgStr": "#DDC090",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#3E3422", "textColor": "#3E3422",
@ -20,6 +72,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设3",
"bgStr": "#C2D8AA", "bgStr": "#C2D8AA",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#596C44", "textColor": "#596C44",
@ -30,6 +83,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设4",
"bgStr": "#DBB8E2", "bgStr": "#DBB8E2",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#68516C", "textColor": "#68516C",
@ -40,6 +94,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设5",
"bgStr": "#ABCEE0", "bgStr": "#ABCEE0",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#3D4C54", "textColor": "#3D4C54",

@ -1,9 +1,18 @@
[ [
{ {
"articleStyle": 0,
"customOrder": 1, "customOrder": 1,
"enableJs": true, "enableJs": true,
"enabled": true, "enabled": true,
"singleUrl": true,
"sourceIcon": "http:\/\/ku.mumuceo.com\/static\/images\/applogo\/yuedu.png",
"sourceName": "使用说明",
"sourceUrl": "https://www.yuque.com/legado"
},
{
"articleStyle": 0,
"customOrder": 2,
"enableJs": true,
"enabled": true,
"loadWithBaseUrl": false, "loadWithBaseUrl": false,
"ruleArticles": "class.release-main-section||tbody@tr", "ruleArticles": "class.release-main-section||tbody@tr",
"ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<a href=\"([^\"]+)\".*?><.*?>([^<]+)<\\\/span><\\\/a>\/g,'<h4><a href=\"$1\" style=\"color:#000\">$2<\/a><\/h4><p style=\"font-size:10px\">$1<\/p>\\n\\n')}\nelse{\nhtml=String(java.getString(\"@@class.markdown-body@html\",false)+java.getString(\"@@class.flex-justify-between.-1@html\",false));\nhref='https:\/\/github.com'+String(java.getString(\"@@class.flex-justify-between.-1@class.Box-body.0@a@href\",false))\nhtml+'<br><p>下载地址:<\/p><a style=\"font-size:13px\">'+href+'<\/a>'\n}", "ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<a href=\"([^\"]+)\".*?><.*?>([^<]+)<\\\/span><\\\/a>\/g,'<h4><a href=\"$1\" style=\"color:#000\">$2<\/a><\/h4><p style=\"font-size:10px\">$1<\/p>\\n\\n')}\nelse{\nhtml=String(java.getString(\"@@class.markdown-body@html\",false)+java.getString(\"@@class.flex-justify-between.-1@html\",false));\nhref='https:\/\/github.com'+String(java.getString(\"@@class.flex-justify-between.-1@class.Box-body.0@a@href\",false))\nhtml+'<br><p>下载地址:<\/p><a style=\"font-size:13px\">'+href+'<\/a>'\n}",
@ -17,5 +26,14 @@
"sourceName": "阅读下载", "sourceName": "阅读下载",
"sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases", "sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases",
"style": "" "style": ""
},
{
"customOrder": 3,
"enableJs": true,
"enabled": true,
"singleUrl": true,
"sourceIcon": "http:\/\/mmbiz.qpic.cn\/mmbiz_png\/hpfMV8hEuL2eS6vnCxvTzoOiaCAibV6exBzJWq9xMic9xDg3YXAick87tsfafic0icRwkQ5ibV0bJ84JtSuxhPuEDVquA\/0?wx_fmt=png",
"sourceName": "小说拾遗",
"sourceUrl": "snssdk1128:\/\/user\/profile\/562564899806367"
} }
] ]

@ -3,6 +3,126 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2021/05/16**
* 添加总是使用默认封面配置
* 添加一种语言 ptbr translation by mezysinc
* epublib 修bug by ag2s20150909
**2021/05/12**
* 修复预下载bug
* 添加显示未读开关,书架布局中
**2021/05/11**
* 修复部分webDav bug
* 修复图片太大时会崩溃的bug
* 修复其它一些bug
**2021/05/10**
* 修复书签bug
* 修改弃用类ViewPager
**2021/05/09**
* 网页访问404依然返回body
* 添加书籍批量导出
**2021/05/08**
* 预下载章节可调整数目
* 修复低版本Android使用TTS闪退。 by ag2s20150909
* 修复WebDav报错
* 优化翻页动画点击翻页
**2021/05/06**
* 修复bug
* url参数添加重置次数,retry
* 修改默认tts, 手动导入
* 升级android studio
**2021/04/30**
* epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909
* 添加高刷设置
* 其它一些优化
* pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本
**2021/04/16**
* 去掉google统计,解决华为手机使用崩溃的bug
* 添加规则订阅时判断重复提醒
* 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局
**2021/04/13**
* 修复导入布局bug
* 修复订阅切换布局bug
**2021/04/08**
* 缓存时重新检查并缓存图片
* 订阅源调试添加源码查看
* web调试不输出源码
* 修复bug
* 换源优化
--- by ag2s20150909
* 修复localBook获取书名作者名的逻辑
* 修复导出的epub的标题文字过大的bug
* 优化图片排版
**2021/04/02**
* 修复bug
* 书源调试添加源码查看
* 添加导出epub by ag2s20150909
* 换源添加是否校验作者选项
**2021/03/31**
* 优化epubLib by ag2s20150909
* 升级库,修改弃用方法
* tts引擎添加导入导出功能
**2021/03/23**
* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右
* js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?>
* 优化目录并发访问
* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909
**2021/03/19**
* 修复图片地址参数缺少的bug
* 修复更改替换规则时多次重新加载正文导致朗读多次停顿的bug
* 修复是否使用替换默认值修改后不及时生效的bug
* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 by hoodie13
* 百度tsn改为tts
**2021/03/15**
* 优化图片TEXT样式显示
* 图片url在解析正文时就拼接成绝对url
* 修复一些bug
**2021/03/08**
* 阅读页面停留10分钟之后自动备份进度
* 添加了针对中文的断行排版处理-by hoodie13, 需要再阅读界面设置里手动开启
* 添加朗读快捷方式
* 优化Epub解析 by hoodie13
* epub书籍增加cache by hoodie13
* 修复切换书籍或者章节时的断言崩溃问题。看漫画容易复现。 by hoodie13
* 修正增加书签alert的正文内容较多时,确定键溢出屏幕问题 by hoodie13
* 图片样式添加TEXT, 阅读界面菜单里可以选择图片样式
**2021/02/26**
* 添加反转内容功能
* 更新章节时若无目录url将自动加载详情页
* 添加变量nextChapterUrl
* 订阅跳转外部应用时提示
* 修复恢复bug
* 详情页拼接url改为重定向后的地址
* 不重复解析详情页
**2021/02/21**
* 下一页规则改为在内容规则之后执行
* 书籍导出增加编码设置和导出文件夹设置,使用替换设置
* 导入源添加等待框
* 修复一些崩溃bug
**2021/02/16**
* 修复分享内容不对的bug
* 优化主题颜色,添加透明度
* rss分类url支持js
* 打开阅读时同步阅读进度
**2021/02/09** **2021/02/09**
* 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低 * 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低
* 修复保存书源时不更新书源时间的bug * 修复保存书源时不更新书源时间的bug

@ -152,7 +152,7 @@ function rule2json() {
RuleJSON.lastUpdateTime = new Date().getTime(); RuleJSON.lastUpdateTime = new Date().getTime();
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder); RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.bookSourceType);
RuleJSON.enabled = RuleJSON.enabled == '' || String(RuleJSON.enabled).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true'; RuleJSON.enabled = RuleJSON.enabled == '' || String(RuleJSON.enabled).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
RuleJSON.enabledExplore = RuleJSON.enabledExplore == '' || String(RuleJSON.enabledExplore).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true'; RuleJSON.enabledExplore = RuleJSON.enabledExplore == '' || String(RuleJSON.enabledExplore).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
return RuleJSON; return RuleJSON;

@ -1,3 +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.b4324411.css" rel="prefetch"><link href="js/about.d8b0dc91.js" rel="prefetch"><link href="js/about~detail.1caf6ef5.js" rel="prefetch"><link href="js/detail.6a5cc86c.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.d60dad33.js" rel="preload" as="script"><link href="js/chunk-vendors.060eee33.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 { <!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.79c91c30.css" rel="prefetch"><link href="js/about.32f651f9.js" rel="prefetch"><link href="js/about~detail.c1b29cbc.js" rel="prefetch"><link href="js/detail.0cc184f2.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.3f3311a5.js" rel="preload" as="script"><link href="js/chunk-vendors.305d5536.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; 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.060eee33.js"></script><script src="js/app.d60dad33.js"></script></body></html> }</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.305d5536.js"></script><script src="js/app.3f3311a5.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

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

@ -1,19 +1,19 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([ self.__precacheManifest = (self.__precacheManifest || []).concat([
{ {
"revision": "414c7503fa98c3f2e328", "revision": "a4809f9e102ca093c5e0",
"url": "css/about.dbe575e1.css" "url": "css/about.dbe575e1.css"
}, },
{ {
"revision": "13925596cfa342cbb55d", "revision": "92b120ee6d7003919840",
"url": "css/app.e4c919b7.css" "url": "css/app.e4c919b7.css"
}, },
{ {
"revision": "d53f06b957b78ddb78b7", "revision": "d14a57da901d79a81631",
"url": "css/chunk-vendors.ad4ff18f.css" "url": "css/chunk-vendors.ad4ff18f.css"
}, },
{ {
"revision": "0607c99f49945c348397", "revision": "329d93821b9c998393c1",
"url": "css/detail.b4324411.css" "url": "css/detail.79c91c30.css"
}, },
{ {
"revision": "535877f50039c0cb49a6196a5b7517cd", "revision": "535877f50039c0cb49a6196a5b7517cd",
@ -40,28 +40,28 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "img/noCover.b5c48bc1.jpeg" "url": "img/noCover.b5c48bc1.jpeg"
}, },
{ {
"revision": "8ff165c058a9618dc10065e65133a4e7", "revision": "6b1059341e07bf6fefd98e31613ba6da",
"url": "index.html" "url": "index.html"
}, },
{ {
"revision": "414c7503fa98c3f2e328", "revision": "a4809f9e102ca093c5e0",
"url": "js/about.d8b0dc91.js" "url": "js/about.32f651f9.js"
}, },
{ {
"revision": "2c81bd893f3a92f018d8", "revision": "2828d7869f38c2483b33",
"url": "js/about~detail.1caf6ef5.js" "url": "js/about~detail.c1b29cbc.js"
}, },
{ {
"revision": "13925596cfa342cbb55d", "revision": "92b120ee6d7003919840",
"url": "js/app.d60dad33.js" "url": "js/app.3f3311a5.js"
}, },
{ {
"revision": "d53f06b957b78ddb78b7", "revision": "d14a57da901d79a81631",
"url": "js/chunk-vendors.060eee33.js" "url": "js/chunk-vendors.305d5536.js"
}, },
{ {
"revision": "0607c99f49945c348397", "revision": "329d93821b9c998393c1",
"url": "js/detail.6a5cc86c.js" "url": "js/detail.0cc184f2.js"
}, },
{ {
"revision": "b46d04eb43bc31ca0f9f95121646440d", "revision": "b46d04eb43bc31ca0f9f95121646440d",

@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts( importScripts(
"precache-manifest.00f143f3962d167b482bfdc0fc9c4e77.js" "precache-manifest.63ef7cbb041538ea344680ff651c5232.js"
); );
workbox.core.setCacheNameDetails({prefix: "yd-web-tool"}); workbox.core.setCacheNameDetails({prefix: "yd-web-tool"});

@ -7,7 +7,6 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.channelIdDownload import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb import io.legado.app.constant.AppConst.channelIdWeb
@ -15,10 +14,8 @@ import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.ThemeConfig.applyDayNight import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.HttpHelper
import io.legado.app.utils.LanguageUtils import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
import rxhttp.wrapper.param.RxHttp
class App : MultiDexApplication() { class App : MultiDexApplication() {
@ -26,10 +23,6 @@ class App : MultiDexApplication() {
super.onCreate() super.onCreate()
CrashHandler(this) CrashHandler(this)
LanguageUtils.setConfiguration(this) LanguageUtils.setConfiguration(this)
RxHttp.init(HttpHelper.client, BuildConfig.DEBUG)
RxHttp.setOnParamAssembly {
it.addHeader(AppConst.UA_NAME, AppConfig.userAgent)
}
createNotificationChannels() createNotificationChannels()
applyDayNight(this) applyDayNight(this)
LiveEventBus.config() LiveEventBus.config()

@ -14,6 +14,7 @@ import androidx.viewbinding.ViewBinding
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
@ -32,7 +33,8 @@ abstract class BaseActivity<VB : ViewBinding>(
val fullScreen: Boolean = true, val fullScreen: Boolean = true,
private val theme: Theme = Theme.Auto, private val theme: Theme = Theme.Auto,
private val toolBarTheme: Theme = Theme.Auto, private val toolBarTheme: Theme = Theme.Auto,
private val transparent: Boolean = false private val transparent: Boolean = false,
private val imageBg: Boolean = true
) : AppCompatActivity(), ) : AppCompatActivity(),
CoroutineScope by MainScope() { CoroutineScope by MainScope() {
@ -66,6 +68,26 @@ abstract class BaseActivity<VB : ViewBinding>(
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
getPrefBoolean(PreferKey.highBrush)
) {
/**
* 添加高刷新率支持
*/
// 获取系统window支持的模式
@Suppress("DEPRECATION")
val modes = window.windowManager.defaultDisplay.supportedModes
// 对获取的模式,基于刷新率的大小进行排序,从小到大排序
modes.sortBy {
it.refreshRate
}
window.let {
val lp = it.attributes
// 取出最大的那一个刷新率,直接设置给window
lp.preferredDisplayModeId = modes.last().modeId
it.attributes = lp
}
}
window.decorView.disableAutoFill() window.decorView.disableAutoFill()
initTheme() initTheme()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -152,9 +174,13 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView) ATH.applyBackgroundTint(window.decorView)
} }
} }
if (AppConfig.isGooglePlay) { if (imageBg) {
ThemeConfig.getBgImage(this)?.let { ThemeConfig.getBgImage(this)?.let {
window.decorView.background = it try {
window.decorView.background = it
} catch (e: OutOfMemoryError) {
toastOnUi("Image Bg Out Of Memory")
}
} }
} }
} }

@ -53,7 +53,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
) )
} }
} }
@Suppress("DEPRECATION")
f.setTargetFragment(this, 0) f.setTargetFragment(this, 0)
f.show(parentFragmentManager, dialogFragmentTag) f.show(parentFragmentManager, dialogFragmentTag)
} }

@ -7,8 +7,10 @@ import io.legado.app.constant.Theme
abstract class VMBaseActivity<VB : ViewBinding, VM : ViewModel>( abstract class VMBaseActivity<VB : ViewBinding, VM : ViewModel>(
fullScreen: Boolean = true, fullScreen: Boolean = true,
theme: Theme = Theme.Auto, theme: Theme = Theme.Auto,
toolBarTheme: Theme = Theme.Auto toolBarTheme: Theme = Theme.Auto,
) : BaseActivity<VB>(fullScreen, theme, toolBarTheme) { transparent: Boolean = false,
imageBg: Boolean = true
) : BaseActivity<VB>(fullScreen, theme, toolBarTheme, transparent, imageBg) {
protected abstract val viewModel: VM protected abstract val viewModel: VM

@ -1,6 +1,7 @@
package io.legado.app.constant package io.legado.app.constant
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.provider.Settings
import io.legado.app.R import io.legado.app.R
import splitties.init.appCtx import splitties.init.appCtx
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -76,4 +77,30 @@ object AppConst {
document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a'; document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a';
""".trimIndent() """.trimIndent()
} }
val androidId: String by lazy {
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID)
}
val appInfo: AppInfo by lazy {
val appInfo = AppInfo()
appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)?.let {
appInfo.versionName = it.versionName
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
appInfo.versionCode = it.longVersionCode
} else {
@Suppress("DEPRECATION")
appInfo.versionCode = it.versionCode.toLong()
}
}
appInfo
}
val charsets =
arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII")
data class AppInfo(
var versionCode: Long = 0L,
var versionName: String = ""
)
} }

@ -1,27 +0,0 @@
package io.legado.app.constant
import android.provider.Settings
import splitties.init.appCtx
val androidId: String by lazy {
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID)
}
val appInfo: AppInfo by lazy {
val appInfo = AppInfo()
appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)?.let {
appInfo.versionName = it.versionName
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
appInfo.versionCode = it.longVersionCode
} else {
@Suppress("DEPRECATION")
appInfo.versionCode = it.versionCode.toLong()
}
}
appInfo
}
data class AppInfo(
var versionCode: Long = 0L,
var versionName: String = ""
)

@ -22,6 +22,5 @@ object EventBus {
const val SAVE_CONTENT = "saveContent" const val SAVE_CONTENT = "saveContent"
const val CHECK_SOURCE = "checkSource" const val CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone" const val CHECK_SOURCE_DONE = "checkSourceDone"
const val REPLACE_RULE_SAVE = "replaceRuleSave"
const val TIP_COLOR = "tipColor" const val TIP_COLOR = "tipColor"
} }

@ -4,6 +4,8 @@ object PreferKey {
const val language = "language" const val language = "language"
const val themeMode = "themeMode" const val themeMode = "themeMode"
const val userAgent = "userAgent" const val userAgent = "userAgent"
const val showUnread = "showUnread"
const val useDefaultCover = "useDefaultCover"
const val hideStatusBar = "hideStatusBar" const val hideStatusBar = "hideStatusBar"
const val clickActionTL = "clickActionTopLeft" const val clickActionTL = "clickActionTopLeft"
const val clickActionTC = "clickActionTopCenter" const val clickActionTC = "clickActionTopCenter"
@ -39,7 +41,9 @@ object PreferKey {
const val webDavAccount = "web_dav_account" const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password" const val webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir" const val webDavCreateDir = "webDavCreateDir"
const val webDavCacheBackup = "webDavCacheBackup" const val exportToWebDav = "webDavCacheBackup"
const val exportType = "exportType"
const val changeSourceCheckAuthor = "changeSourceCheckAuthor"
const val changeSourceLoadToc = "changeSourceLoadToc" const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo" const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType" const val chineseConverterType = "chineseConverterType"
@ -64,7 +68,14 @@ object PreferKey {
const val importKeepName = "importKeepName" const val importKeepName = "importKeepName"
const val screenOrientation = "screenOrientation" const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress" const val syncBookProgress = "syncBookProgress"
const val preDownload = "preDownload" const val preDownloadNum = "preDownloadNum"
const val autoRefresh = "auto_refresh"
const val defaultToRead = "defaultToRead"
const val exportCharset = "exportCharset"
const val exportUseReplace = "exportUseReplace"
const val useZhLayout = "useZhLayout"
const val fullScreenGesturesSupport = "fullScreenGesturesSupport"
const val highBrush = "highBrush"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -7,9 +7,10 @@ import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.androidId import io.legado.app.constant.AppConst.androidId
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx import splitties.init.appCtx
import java.util.* import java.util.*
@ -23,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class], RuleSub::class],
version = 28, version = 32,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -40,7 +41,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val rssArticleDao: RssArticleDao abstract val rssArticleDao: RssArticleDao
abstract val rssStarDao: RssStarDao abstract val rssStarDao: RssStarDao
abstract val cookieDao: CookieDao abstract val cookieDao: CookieDao
abstract val txtTocRule: TxtTocRuleDao abstract val txtTocRuleDao: TxtTocRuleDao
abstract val readRecordDao: ReadRecordDao abstract val readRecordDao: ReadRecordDao
abstract val httpTTSDao: HttpTTSDao abstract val httpTTSDao: HttpTTSDao
abstract val cacheDao: CacheDao abstract val cacheDao: CacheDao
@ -58,7 +59,8 @@ abstract class AppDatabase : RoomDatabase() {
migration_14_15, migration_15_17, migration_17_18, migration_18_19, migration_14_15, migration_15_17, migration_17_18, migration_18_19,
migration_19_20, migration_20_21, migration_21_22, migration_22_23, migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25, migration_25_26, migration_26_27, migration_23_24, migration_24_25, migration_25_26, migration_26_27,
migration_27_28 migration_27_28, migration_28_29, migration_29_30, migration_30_31,
migration_31_32
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -87,6 +89,13 @@ abstract class AppDatabase : RoomDatabase() {
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})"""
) )
if (AppConfig.isGooglePlay) {
db.execSQL(
"""
delete from rssSources where sourceUrl = 'https://github.com/gedoor/legado/releases'
"""
)
}
} }
} }
@ -255,6 +264,46 @@ abstract class AppDatabase : RoomDatabase() {
database.execSQL("ALTER TABLE rssStars ADD variable TEXT") database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
} }
} }
private val migration_28_29 = object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
}
}
private val migration_29_30 = object : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `epubChapters`
(`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT,
PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
"""
)
database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
}
}
private val migration_30_31 = object : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))
"""
)
database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
}
}
private val migration_31_32 = object : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `epubChapters`")
}
}
} }
} }

@ -1,7 +1,6 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.* import androidx.room.*
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -41,9 +40,6 @@ interface BookSourceDao {
@Query("select distinct bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''") @Query("select distinct bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''")
fun liveExploreGroup(): LiveData<List<String>> fun liveExploreGroup(): LiveData<List<String>>
@Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource>
@Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'") @Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'")
fun getByGroup(group: String): List<BookSource> fun getByGroup(group: String): List<BookSource>

@ -1,6 +1,6 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.paging.DataSource import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
@ -16,10 +16,10 @@ interface BookmarkDao {
bookUrl: String, bookUrl: String,
bookName: String, bookName: String,
bookAuthor: String bookAuthor: String
): DataSource.Factory<Int, Bookmark> ): LiveData<List<Bookmark>>
@Query("SELECT * FROM bookmarks where bookUrl = :bookUrl and chapterName like '%'||:key||'%' or content like '%'||:key||'%'") @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> fun liveDataSearch(bookUrl: String, key: String): LiveData<List<Bookmark>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookmark: Bookmark) fun insert(vararg bookmark: Bookmark)

@ -19,7 +19,7 @@ interface ReadRecordDao {
@Query("select sum(readTime) from readRecord where bookName = :bookName") @Query("select sum(readTime) from readRecord where bookName = :bookName")
fun getReadTime(bookName: String): Long? fun getReadTime(bookName: String): Long?
@Query("select readTime from readRecord where androidId = :androidId and bookName = :bookName") @Query("select readTime from readRecord where deviceId = :androidId and bookName = :bookName")
fun getReadTime(androidId: String, bookName: String): Long? fun getReadTime(androidId: String, bookName: String): Long?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)

@ -16,6 +16,9 @@ interface RuleSubDao {
@get:Query("select customOrder from ruleSubs order by customOrder limit 0,1") @get:Query("select customOrder from ruleSubs order by customOrder limit 0,1")
val maxOrder: Int val maxOrder: Int
@Query("select * from ruleSubs where url = :url")
fun findByUrl(url: String): RuleSub?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg ruleSub: RuleSub) fun insert(vararg ruleSub: RuleSub)

@ -1,6 +1,5 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
@ -10,12 +9,6 @@ import io.legado.app.data.entities.SearchBook
@Dao @Dao
interface SearchBookDao { interface SearchBookDao {
@Query("SELECT * FROM searchBooks")
fun observeAll(): DataSource.Factory<Int, SearchBook>
@Query("SELECT * FROM searchBooks where time >= :time")
fun observeNew(time: Long): DataSource.Factory<Int, SearchBook>
@Query("select * from searchBooks where bookUrl = :bookUrl") @Query("select * from searchBooks where bookUrl = :bookUrl")
fun getSearchBook(bookUrl: String): SearchBook? fun getSearchBook(bookUrl: String): SearchBook?
@ -27,7 +20,7 @@ interface SearchBookDao {
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
from searchBooks as t1 inner join book_sources as t2 from searchBooks as t1 inner join book_sources as t2
on t1.origin = t2.bookSourceUrl on t1.origin = t2.bookSourceUrl
where t1.name = :name and t1.author = :author and t2.enabled = 1 and t2.bookSourceGroup like '%'||:sourceGroup||'%' where t1.name = :name and t1.author like '%'||:author||'%' and t2.enabled = 1 and t2.bookSourceGroup like '%'||:sourceGroup||'%'
order by t2.customOrder order by t2.customOrder
""" """
) )

@ -107,6 +107,8 @@ data class Book(
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
@ -146,6 +148,31 @@ data class Book(
config().pageAnim = pageAnim config().pageAnim = pageAnim
} }
fun getImageStyle(): String? {
return config().imageStyle
}
fun setImageStyle(imageStyle: String?) {
config().imageStyle = imageStyle
}
fun getDelParagraph(): Int {
return config().delParagraph
}
fun setDelParagraph(num: Int) {
config().delParagraph = num
}
fun setDelTag(tag: Long) {
config().delTag =
if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag
}
fun getDelTag(tag: Long): Boolean {
return config().delTag and tag == tag
}
fun getFolderName(): String { fun getFolderName(): String {
//防止书名过长,只取9位 //防止书名过长,只取9位
var folderName = name.replace(AppPattern.fileNameRegex, "") var folderName = name.replace(AppPattern.fileNameRegex, "")
@ -207,11 +234,23 @@ data class Book(
} }
} }
companion object {
const val hTag = 2L
const val rubyTag = 4L
const val imgTag = 8L
const val imgStyleDefault = "DEFAULT"
const val imgStyleFull = "FULL"
const val imgStyleText = "TEXT"
}
@Parcelize @Parcelize
data class ReadConfig( data class ReadConfig(
var pageAnim: Int = -1, var pageAnim: Int = -1,
var reSegment: Boolean = false, var reSegment: Boolean = false,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则 var imageStyle: String? = null,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则
var delParagraph: Int = 0,//去除段首
var delTag: Long = 0L,//去除标签
) : Parcelable ) : Parcelable
class Converters { class Converters {

@ -37,6 +37,8 @@ data class BookChapter(
var tag: String? = null, // var tag: String? = null, //
var start: Long? = null, // 章节起始位置 var start: Long? = null, // 章节起始位置
var end: Long? = null, // 章节终止位置 var end: Long? = null, // 章节终止位置
var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId
var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId
var variable: String? = null //变量 var variable: String? = null //变量
) : Parcelable { ) : Parcelable {

@ -105,7 +105,9 @@ data class BookSource(
var a = urlRule var a = urlRule
if (a.isNotBlank()) { if (a.isNotBlank()) {
kotlin.runCatching { kotlin.runCatching {
if (urlRule.startsWith("<js>", false)) { if (urlRule.startsWith("<js>", false)
|| urlRule.startsWith("@js", false)
) {
val aCache = ACache.get(appCtx, "explore") val aCache = ACache.get(appCtx, "explore")
a = aCache.getAsString(bookSourceUrl) ?: "" a = aCache.getAsString(bookSourceUrl) ?: ""
if (a.isBlank()) { if (a.isBlank()) {
@ -114,10 +116,12 @@ data class BookSource(
bindings["java"] = this bindings["java"] = this
bindings["cookie"] = CookieStore bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager bindings["cache"] = CacheManager
a = AppConst.SCRIPT_ENGINE.eval( val jsStr = if (urlRule.startsWith("@")) {
urlRule.substring(4, urlRule.lastIndexOf("<")), urlRule.substring(3)
bindings } else {
).toString() urlRule.substring(4, urlRule.lastIndexOf("<"))
}
a = AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString()
aCache.put(bookSourceUrl, a) aCache.put(bookSourceUrl, a)
} }
} }

@ -2,9 +2,9 @@ package io.legado.app.data.entities
import androidx.room.Entity import androidx.room.Entity
@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"]) @Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"])
data class ReadRecord( data class ReadRecord(
var androidId: String = "", var deviceId: String = "",
var bookName: String = "", var bookName: String = "",
var readTime: Long = 0L var readTime: Long = 0L
) )

@ -9,9 +9,11 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.CacheManager import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.help.http.CookieStore import io.legado.app.help.http.CookieStore
import io.legado.app.utils.ACache
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import splitties.init.appCtx
import java.util.* import java.util.*
import javax.script.SimpleBindings import javax.script.SimpleBindings
@ -23,6 +25,7 @@ data class RssSource(
var sourceName: String = "", var sourceName: String = "",
var sourceIcon: String = "", var sourceIcon: String = "",
var sourceGroup: String? = null, var sourceGroup: String? = null,
var sourceComment: String? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var sortUrl: String? = null, var sortUrl: String? = null,
var singleUrl: Boolean = false, var singleUrl: Boolean = false,
@ -104,9 +107,30 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
} }
fun sortUrls(): LinkedHashMap<String, String> = fun sortUrls(): LinkedHashMap<String, String> = linkedMapOf<String, String>().apply {
linkedMapOf<String, String>().apply { kotlin.runCatching {
sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c -> var a = sortUrl
if (sortUrl?.startsWith("<js>", false) == true
|| sortUrl?.startsWith("@js", false) == true
) {
val aCache = ACache.get(appCtx, "rssSortUrl")
a = aCache.getAsString(sourceUrl) ?: ""
if (a.isBlank()) {
val bindings = SimpleBindings()
bindings["baseUrl"] = sourceUrl
bindings["java"] = this
bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager
val jsStr = if (sortUrl!!.startsWith("@")) {
sortUrl!!.substring(3)
} else {
sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<"))
}
a = AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString()
aCache.put(sourceUrl, a)
}
}
a?.split("(&&|\n)+".toRegex())?.forEach { c ->
val d = c.split("::") val d = c.split("::")
if (d.size > 1) if (d.size > 1)
this[d[0]] = d[1] this[d[0]] = d[1]
@ -115,4 +139,5 @@ data class RssSource(
this[""] = sourceUrl this[""] = sourceUrl
} }
} }
}
} }

@ -2,7 +2,6 @@ package io.legado.app.help
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.utils.* import io.legado.app.utils.*
@ -10,44 +9,44 @@ import splitties.init.appCtx
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
private val context get() = appCtx val isGooglePlay = appCtx.channel == "google"
val isGooglePlay = context.channel == "google"
var userAgent: String = getPrefUserAgent() var userAgent: String = getPrefUserAgent()
var replaceEnableDefault = context.getPrefBoolean(PreferKey.replaceEnableDefault, true) var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
var isEInkMode = context.getPrefString(PreferKey.themeMode) == "3" var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
var clickActionTL = context.getPrefInt(PreferKey.clickActionTL, 2) var clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2)
var clickActionTC = context.getPrefInt(PreferKey.clickActionTC, 2) var clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1)
var clickActionTR = context.getPrefInt(PreferKey.clickActionTR, 1) var clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2)
var clickActionML = context.getPrefInt(PreferKey.clickActionML, 2) var clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0)
var clickActionMC = context.getPrefInt(PreferKey.clickActionMC, 0) var clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1)
var clickActionMR = context.getPrefInt(PreferKey.clickActionMR, 1) var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2)
var clickActionBL = context.getPrefInt(PreferKey.clickActionBL, 2) var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1)
var clickActionBC = context.getPrefInt(PreferKey.clickActionBC, 1) var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1)
var clickActionBR = context.getPrefInt(PreferKey.clickActionBR, 1)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
PreferKey.themeMode -> isEInkMode = context.getPrefString(PreferKey.themeMode) == "3" PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
PreferKey.clickActionTL -> clickActionTL = PreferKey.clickActionTL -> clickActionTL =
context.getPrefInt(PreferKey.clickActionTL, 2) appCtx.getPrefInt(PreferKey.clickActionTL, 2)
PreferKey.clickActionTC -> clickActionTC = PreferKey.clickActionTC -> clickActionTC =
context.getPrefInt(PreferKey.clickActionTC, 2) appCtx.getPrefInt(PreferKey.clickActionTC, 2)
PreferKey.clickActionTR -> clickActionTR = PreferKey.clickActionTR -> clickActionTR =
context.getPrefInt(PreferKey.clickActionTR, 2) appCtx.getPrefInt(PreferKey.clickActionTR, 2)
PreferKey.clickActionML -> clickActionML = PreferKey.clickActionML -> clickActionML =
context.getPrefInt(PreferKey.clickActionML, 2) appCtx.getPrefInt(PreferKey.clickActionML, 2)
PreferKey.clickActionMC -> clickActionMC = PreferKey.clickActionMC -> clickActionMC =
context.getPrefInt(PreferKey.clickActionMC, 2) appCtx.getPrefInt(PreferKey.clickActionMC, 2)
PreferKey.clickActionMR -> clickActionMR = PreferKey.clickActionMR -> clickActionMR =
context.getPrefInt(PreferKey.clickActionMR, 2) appCtx.getPrefInt(PreferKey.clickActionMR, 2)
PreferKey.clickActionBL -> clickActionBL = PreferKey.clickActionBL -> clickActionBL =
context.getPrefInt(PreferKey.clickActionBL, 2) appCtx.getPrefInt(PreferKey.clickActionBL, 2)
PreferKey.clickActionBC -> clickActionBC = PreferKey.clickActionBC -> clickActionBC =
context.getPrefInt(PreferKey.clickActionBC, 2) appCtx.getPrefInt(PreferKey.clickActionBC, 2)
PreferKey.clickActionBR -> clickActionBR = PreferKey.clickActionBR -> clickActionBR =
context.getPrefInt(PreferKey.clickActionBR, 2) appCtx.getPrefInt(PreferKey.clickActionBR, 2)
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
context.getPrefBoolean(PreferKey.readBodyToLh, true) appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
PreferKey.useZhLayout -> ReadBookConfig.useZhLayout =
appCtx.getPrefBoolean(PreferKey.useZhLayout)
PreferKey.userAgent -> userAgent = getPrefUserAgent() PreferKey.userAgent -> userAgent = getPrefUserAgent()
} }
} }
@ -62,97 +61,153 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
} }
var isNightTheme: Boolean var isNightTheme: Boolean
get() = isNightTheme(context) get() = isNightTheme(appCtx)
set(value) { set(value) {
if (isNightTheme != value) { if (isNightTheme != value) {
if (value) { if (value) {
context.putPrefString(PreferKey.themeMode, "2") appCtx.putPrefString(PreferKey.themeMode, "2")
} else { } else {
context.putPrefString(PreferKey.themeMode, "1") appCtx.putPrefString(PreferKey.themeMode, "1")
} }
} }
} }
var showUnread: Boolean
get() = appCtx.getPrefBoolean(PreferKey.showUnread, true)
set(value) {
appCtx.putPrefBoolean(PreferKey.showUnread, value)
}
val useDefaultCover: Boolean
get() = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false)
val isTransparentStatusBar: Boolean val isTransparentStatusBar: Boolean
get() = context.getPrefBoolean(PreferKey.transparentStatusBar, true) get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
val immNavigationBar: Boolean val immNavigationBar: Boolean
get() = context.getPrefBoolean(PreferKey.immNavigationBar, true) get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
val screenOrientation: String? val screenOrientation: String?
get() = context.getPrefString(PreferKey.screenOrientation) get() = appCtx.getPrefString(PreferKey.screenOrientation)
var backupPath: String? var backupPath: String?
get() = context.getPrefString(PreferKey.backupPath) get() = appCtx.getPrefString(PreferKey.backupPath)
set(value) { set(value) {
if (value.isNullOrEmpty()) { if (value.isNullOrEmpty()) {
context.removePref(PreferKey.backupPath) appCtx.removePref(PreferKey.backupPath)
} else { } else {
context.putPrefString(PreferKey.backupPath, value) appCtx.putPrefString(PreferKey.backupPath, value)
} }
} }
val isShowRSS: Boolean val isShowRSS: Boolean
get() = context.getPrefBoolean(PreferKey.showRss, true) get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
val autoRefreshBook: Boolean val autoRefreshBook: Boolean
get() = context.getPrefBoolean(R.string.pk_auto_refresh) get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
var threadCount: Int var threadCount: Int
get() = context.getPrefInt(PreferKey.threadCount, 16) get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
set(value) { set(value) {
context.putPrefInt(PreferKey.threadCount, value) appCtx.putPrefInt(PreferKey.threadCount, value)
} }
var importBookPath: String? var importBookPath: String?
get() = context.getPrefString("importBookPath") get() = appCtx.getPrefString("importBookPath")
set(value) { set(value) {
if (value == null) { if (value == null) {
context.removePref("importBookPath") appCtx.removePref("importBookPath")
} else { } else {
context.putPrefString("importBookPath", value) appCtx.putPrefString("importBookPath", value)
} }
} }
var ttsSpeechRate: Int var ttsSpeechRate: Int
get() = context.getPrefInt(PreferKey.ttsSpeechRate, 5) get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, 5)
set(value) { set(value) {
context.putPrefInt(PreferKey.ttsSpeechRate, value) appCtx.putPrefInt(PreferKey.ttsSpeechRate, value)
} }
var chineseConverterType: Int var chineseConverterType: Int
get() = context.getPrefInt(PreferKey.chineseConverterType) get() = appCtx.getPrefInt(PreferKey.chineseConverterType)
set(value) { set(value) {
context.putPrefInt(PreferKey.chineseConverterType, value) appCtx.putPrefInt(PreferKey.chineseConverterType, value)
} }
var systemTypefaces: Int var systemTypefaces: Int
get() = context.getPrefInt(PreferKey.systemTypefaces) get() = appCtx.getPrefInt(PreferKey.systemTypefaces)
set(value) { set(value) {
context.putPrefInt(PreferKey.systemTypefaces, value) appCtx.putPrefInt(PreferKey.systemTypefaces, value)
} }
var elevation: Int var elevation: Int
get() = context.getPrefInt(PreferKey.barElevation, AppConst.sysElevation) get() = appCtx.getPrefInt(PreferKey.barElevation, AppConst.sysElevation)
set(value) {
appCtx.putPrefInt(PreferKey.barElevation, value)
}
var exportCharset: String
get() {
val c = appCtx.getPrefString(PreferKey.exportCharset)
if (c.isNullOrBlank()) {
return "UTF-8"
}
return c
}
set(value) {
appCtx.putPrefString(PreferKey.exportCharset, value)
}
var exportUseReplace: Boolean
get() = appCtx.getPrefBoolean(PreferKey.exportUseReplace, true)
set(value) {
appCtx.putPrefBoolean(PreferKey.exportUseReplace, value)
}
var exportToWebDav: Boolean
get() = appCtx.getPrefBoolean(PreferKey.exportToWebDav)
set(value) { set(value) {
context.putPrefInt(PreferKey.barElevation, value) appCtx.putPrefBoolean(PreferKey.exportToWebDav, value)
}
var exportType: Int
get() = appCtx.getPrefInt(PreferKey.exportType)
set(value) {
appCtx.putPrefInt(PreferKey.exportType, value)
}
var changeSourceCheckAuthor: Boolean
get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor)
set(value) {
appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value)
} }
val autoChangeSource: Boolean val autoChangeSource: Boolean
get() = context.getPrefBoolean(PreferKey.autoChangeSource, true) get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
val changeSourceLoadInfo get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc)
val changeSourceLoadToc get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc)
val changeSourceLoadInfo get() = context.getPrefBoolean(PreferKey.changeSourceLoadToc) val importKeepName get() = appCtx.getPrefBoolean(PreferKey.importKeepName)
val changeSourceLoadToc get() = context.getPrefBoolean(PreferKey.changeSourceLoadToc) val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true)
var preDownloadNum
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
set(value) {
appCtx.putPrefInt(PreferKey.preDownloadNum, value)
}
val importKeepName get() = context.getPrefBoolean(PreferKey.importKeepName) val mediaButtonOnExit get() = appCtx.getPrefBoolean("mediaButtonOnExit", true)
val syncBookProgress get() = context.getPrefBoolean(PreferKey.syncBookProgress, true) val replaceEnableDefault get() = appCtx.getPrefBoolean(PreferKey.replaceEnableDefault, true)
val preDownload get() = context.getPrefBoolean(PreferKey.preDownload, true) val fullScreenGesturesSupport: Boolean
get() = appCtx.getPrefBoolean(PreferKey.fullScreenGesturesSupport, false)
private fun getPrefUserAgent(): String { private fun getPrefUserAgent(): String {
val ua = context.getPrefString(PreferKey.userAgent) val ua = appCtx.getPrefString(PreferKey.userAgent)
if (ua.isNullOrBlank()) { if (ua.isNullOrBlank()) {
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36" return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
} }

@ -1,5 +1,6 @@
package io.legado.app.help package io.legado.app.help
import android.net.Uri
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
@ -55,6 +56,28 @@ object BookHelp {
} }
} }
fun getEpubFile(book: Book,): File {
val file = FileUtils.getFile(
downloadDir,
cacheFolderName,
book.getFolderName(),
"index.epubx"
)
if(!file.exists()){
val input = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl)
appCtx.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
if (input != null) {
FileUtils.writeInputStream(file, input)
}
}
return file
}
suspend fun saveContent(book: Book, bookChapter: BookChapter, content: String) { suspend fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return if (content.isEmpty()) return
//保存文本 //保存文本
@ -77,31 +100,6 @@ object BookHelp {
postEvent(EventBus.SAVE_CONTENT, bookChapter) postEvent(EventBus.SAVE_CONTENT, bookChapter)
} }
@Suppress("unused")
fun saveFont(book: Book, bookChapter: BookChapter, font: ByteArray) {
FileUtils.createFileIfNotExist(
downloadDir,
cacheFolderName,
book.getFolderName(),
"font",
bookChapter.getFontName()
).writeBytes(font)
}
fun getFontPath(book: Book, bookChapter: BookChapter): String? {
val fontFile = FileUtils.getFile(
downloadDir,
cacheFolderName,
book.getFolderName(),
"font",
bookChapter.getFontName()
)
if (fontFile.exists()) {
return fontFile.absolutePath
}
return null
}
suspend fun saveImage(book: Book, src: String) { suspend fun saveImage(book: Book, src: String) {
while (downloadImages.contains(src)) { while (downloadImages.contains(src)) {
delay(100) delay(100)
@ -138,7 +136,7 @@ object BookHelp {
) )
} }
private fun getImageSuffix(src: String): String { fun getImageSuffix(src: String): String {
var suffix = src.substringAfterLast(".").substringBefore(",") var suffix = src.substringAfterLast(".").substringBefore(",")
if (suffix.length > 5) { if (suffix.length > 5) {
suffix = ".jpg" suffix = ".jpg"
@ -148,7 +146,7 @@ object BookHelp {
fun getChapterFiles(book: Book): List<String> { fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>() val fileNameList = arrayListOf<String>()
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return fileNameList return fileNameList
} }
FileUtils.createFolderIfNotExist( FileUtils.createFolderIfNotExist(
@ -162,7 +160,7 @@ object BookHelp {
// 检测该章节是否下载 // 检测该章节是否下载
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
return if (book.isLocalBook()) { return if (book.isLocalTxt()) {
true true
} else { } else {
FileUtils.exists( FileUtils.exists(
@ -174,9 +172,38 @@ object BookHelp {
} }
} }
fun hasImageContent(book: Book, bookChapter: BookChapter): Boolean {
if (!hasContent(book, bookChapter)) {
return false
}
getContent(book, bookChapter)?.let {
val matcher = AppPattern.imgPattern.matcher(it)
while (matcher.find()) {
matcher.group(1)?.let { src ->
val image = getImage(book, src)
if (!image.exists()) {
return false
}
}
}
}
return true
}
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return LocalBook.getContext(book, bookChapter) return LocalBook.getContext(book, bookChapter)
} else if (book.isEpub() && !hasContent(book, bookChapter)) {
val string = LocalBook.getContext(book, bookChapter)
string?.let {
FileUtils.createFileIfNotExist(
downloadDir,
cacheFolderName,
book.getFolderName(),
bookChapter.getFileName(),
).writeText(it)
}
return string
} else { } else {
val file = FileUtils.getFile( val file = FileUtils.getFile(
downloadDir, downloadDir,
@ -191,8 +218,27 @@ object BookHelp {
return null return null
} }
fun reverseContent(book: Book, bookChapter: BookChapter) {
if (!book.isLocalBook()) {
val file = FileUtils.getFile(
downloadDir,
cacheFolderName,
book.getFolderName(),
bookChapter.getFileName()
)
if (file.exists()) {
val text = file.readText()
val stringBuilder = StringBuilder()
text.toStringArray().forEach {
stringBuilder.insert(0, it)
}
file.writeText(stringBuilder.toString())
}
}
}
fun delContent(book: Book, bookChapter: BookChapter) { fun delContent(book: Book, bookChapter: BookChapter) {
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return return
} else { } else {
FileUtils.createFileIfNotExist( FileUtils.createFileIfNotExist(

@ -17,22 +17,8 @@ object ContentHelp {
*/ */
fun reSegment(content: String, chapterName: String): String { fun reSegment(content: String, chapterName: String): String {
var content1 = content var content1 = content
val content2: String val dict = makeDict(content1)
val chapterNameLength = chapterName.trim { it <= ' ' }.length var p = content1
content2 = if (chapterNameLength > 1) {
val regexp =
chapterName.trim { it <= ' ' }.replace("\\s+".toRegex(), "(\\\\s*)")
// 质量较低的页面,章节内可能重复出现章节标题
if (chapterNameLength > 5) content1.replace(regexp.toRegex(), "")
.trim { it <= ' ' } else content1.replaceFirst(
"^\\s*" + regexp.toRegex(),
""
).trim { it <= ' ' }
} else {
content1
}
val dict = makeDict(content2)
var p = content2
.replace("&quot;".toRegex(), "") .replace("&quot;".toRegex(), "")
.replace("[::]['\"‘”“]+".toRegex(), ":“") .replace("[::]['\"‘”“]+".toRegex(), ":“")
.replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "\n") .replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "\n")
@ -315,14 +301,6 @@ object ContentHelp {
if (loop2Mod1 < 0 && loop2Mod2 > 0) { if (loop2Mod1 < 0 && loop2Mod2 > 0) {
if (match(MARK_SENTENCES_END, string[j])) insN.add(j) if (match(MARK_SENTENCES_END, string[j])) insN.add(j)
} }
/*
else if (mod[i - 1] > 0 && mod[i] < 0) {
if (j > 0) {
if (match(MARK_SENTENCES_END, string.charAt(j)))
ins_n.add(j);
}
}
*/
loop2Mod1 = loop2Mod2 loop2Mod1 = loop2Mod2
i++ i++
} }
@ -441,28 +419,6 @@ object ContentHelp {
insN = ArrayList(HashSet(insN)) insN = ArrayList(HashSet(insN))
insN.sort() insN.sort()
// 输出log进行检验
/*
System.out.println("quote[i]:position/mod\t" + string);
for (int i = 0; i < array_quote.size(); i++) {
System.out.print(" [" + i + "]" + array_quote.get(i) + "/" + mod[i]);
}
System.out.print("\n");
System.out.print("ins_q:");
for (int i = 0; i < ins_quote.length; i++) {
System.out.print(" " + ins_quote[i]);
}
System.out.print("\n");
System.out.print("ins_n:");
for (int i : ins_n) {
System.out.print(" " + i);
}
System.out.print("\n");
*/
// 完成字符串拼接(从string复制、插入引号和换行 // 完成字符串拼接(从string复制、插入引号和换行
// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' // ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
// ins_n 插入换行。数组的值表示插入换行符的位置 // ins_n 插入换行。数组的值表示插入换行符的位置
@ -626,37 +582,6 @@ object ContentHelp {
return -1 return -1
} }
/**
* 计算字符串与字典的距离
*
* @param str 数据字符串
* @param form 从第几个字符开始匹配
* @param to 匹配到第几个字符串结束
* @param inOrder 是否从前向后匹配
* @param words 可变长参数构成的字典每个字符串代表一个字符
* @return 匹配结果注意这个距离是使用第一个字符进行计算的
*/
private fun seekWordsIndex(
str: String,
form: Int,
to: Int,
inOrder: Boolean,
vararg words: String
): Int {
if (words.isEmpty()) return -2
val i = seekIndex(str, words[0], form, to, inOrder)
if (i < 0) return i
for (j in 1 until words.size) {
val k = seekIndex(str, words[j], form, to, inOrder)
if (inOrder) {
if (i + j != k) return -3
} else {
if (i - j != k) return -3
}
}
return i
}
/* 搜寻引号并进行分段处理了一五三类常见情况 /* 搜寻引号并进行分段处理了一五三类常见情况
参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。
完整引用说话内容在反引号内侧有断句标点例如 完整引用说话内容在反引号内侧有断句标点例如

@ -1,12 +1,10 @@
package io.legado.app.help package io.legado.app.help
import com.hankcs.hanlp.HanLP import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
class ContentProcessor(private val bookName: String, private val bookOrigin: String) { class ContentProcessor(private val bookName: String, private val bookOrigin: String) {
@ -23,14 +21,16 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
replaceRules.addAll(appDb.replaceRuleDao.findEnabledByScope(bookName, bookOrigin)) replaceRules.addAll(appDb.replaceRuleDao.findEnabledByScope(bookName, bookOrigin))
} }
suspend fun getContent( @Synchronized
fun getContent(
book: Book, book: Book,
title: String, //已经经过简繁转换 title: String, //已经经过简繁转换
content: String, content: String,
isRead: Boolean = true isRead: Boolean = true,
useReplace: Boolean = book.getUseReplaceRule()
): List<String> { ): List<String> {
var content1 = content var content1 = content
if (book.getUseReplaceRule()) { if (useReplace) {
replaceRules.forEach { item -> replaceRules.forEach { item ->
if (item.pattern.isNotEmpty()) { if (item.pattern.isNotEmpty()) {
try { try {
@ -40,9 +40,7 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
content1.replace(item.pattern, item.replacement) content1.replace(item.pattern, item.replacement)
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { appCtx.toastOnUi("${item.name}替换出错")
appCtx.toastOnUi("${item.name}替换出错")
}
} }
} }
} }
@ -53,18 +51,16 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
} }
try { try {
when (AppConfig.chineseConverterType) { when (AppConfig.chineseConverterType) {
1 -> content1 = HanLP.convertToSimplifiedChinese(content1) 1 -> content1 = ChineseUtils.t2s(content1)
2 -> content1 = HanLP.convertToTraditionalChinese(content1) 2 -> content1 = ChineseUtils.s2t(content1)
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { appCtx.toastOnUi("简繁转换出错")
appCtx.toastOnUi("简繁转换出错")
}
} }
} }
val contents = arrayListOf<String>() val contents = arrayListOf<String>()
content1.split("\n").forEach { content1.split("\n").forEach {
val str = it.replace("^[\\n\\s\\r]+".toRegex(), "") val str = it.replace("^[\\n\\r]+".toRegex(), "").trim()
if (contents.isEmpty()) { if (contents.isEmpty()) {
contents.add(title) contents.add(title)
if (str != title && str.isNotEmpty()) { if (str != title && str.isNotEmpty()) {

@ -4,12 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.longToastOnUi
import io.legado.app.utils.msg
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -26,7 +25,7 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
/** /**
* 系统默认UncaughtExceptionHandler * 系统默认UncaughtExceptionHandler
*/ */
private var mDefaultHandler: Thread.UncaughtExceptionHandler? = null private var mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
/** /**
* 存储异常和参数信息 * 存储异常和参数信息
@ -40,7 +39,6 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
private val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") private val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
init { init {
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
//设置该CrashHandler为系统默认的 //设置该CrashHandler为系统默认的
Thread.setDefaultUncaughtExceptionHandler(this) Thread.setDefaultUncaughtExceptionHandler(this)
} }
@ -63,18 +61,9 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
collectDeviceInfo(context) collectDeviceInfo(context)
//添加自定义信息 //添加自定义信息
addCustomInfo() addCustomInfo()
kotlin.runCatching {
//使用Toast来显示异常信息
Handler(Looper.getMainLooper()).post {
Toast.makeText(
context,
ex.message,
Toast.LENGTH_LONG
).show()
}
}
//保存日志文件 //保存日志文件
saveCrashInfo2File(ex) saveCrashInfo2File(ex)
context.longToastOnUi(ex.msg)
} }
/** /**
@ -116,35 +105,33 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
* 保存错误信息到文件中 * 保存错误信息到文件中
*/ */
private fun saveCrashInfo2File(ex: Throwable) { private fun saveCrashInfo2File(ex: Throwable) {
kotlin.runCatching { val sb = StringBuilder()
val sb = StringBuilder() for ((key, value) in paramsMap) {
for ((key, value) in paramsMap) { sb.append(key).append("=").append(value).append("\n")
sb.append(key).append("=").append(value).append("\n") }
}
val writer = StringWriter() val writer = StringWriter()
val printWriter = PrintWriter(writer) val printWriter = PrintWriter(writer)
ex.printStackTrace(printWriter) ex.printStackTrace(printWriter)
var cause: Throwable? = ex.cause var cause: Throwable? = ex.cause
while (cause != null) { while (cause != null) {
cause.printStackTrace(printWriter) cause.printStackTrace(printWriter)
cause = cause.cause cause = cause.cause
} }
printWriter.close() printWriter.close()
val result = writer.toString() val result = writer.toString()
sb.append(result) sb.append(result)
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
val time = format.format(Date()) val time = format.format(Date())
val fileName = "crash-$time-$timestamp.log" val fileName = "crash-$time-$timestamp.log"
context.externalCacheDir?.let { rootFile -> context.externalCacheDir?.let { rootFile ->
FileUtils.getFile(rootFile, "crash").listFiles()?.forEach { FileUtils.getFile(rootFile, "crash").listFiles()?.forEach {
if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) { if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) {
it.delete() it.delete()
}
} }
FileUtils.createFileIfNotExist(rootFile, "crash", fileName)
.writeText(sb.toString())
} }
FileUtils.createFileIfNotExist(rootFile, "crash", fileName)
.writeText(sb.toString())
} }
} }

@ -61,8 +61,8 @@ object DefaultData {
} }
fun importDefaultTocRules() { fun importDefaultTocRules() {
appDb.txtTocRule.deleteDefault() appDb.txtTocRuleDao.deleteDefault()
appDb.txtTocRule.insert(*txtTocRules.toTypedArray()) appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray())
} }
fun importDefaultRssSources() { fun importDefaultRssSources() {

@ -4,17 +4,16 @@ import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.help.http.CookieStore import io.legado.app.help.http.*
import io.legado.app.help.http.SSLHelper
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.net.URLEncoder import java.net.URLEncoder
@ -40,6 +39,25 @@ interface JsExtensions {
} }
} }
/**
* 并发访问网络
*/
fun ajaxAll(urlList: Array<String>): Array<StrResponse?> {
return runBlocking {
val asyncArray = Array(urlList.size) {
async(IO) {
val url = urlList[it]
val analyzeUrl = AnalyzeUrl(url)
analyzeUrl.getStrResponse(url)
}
}
val resArray = Array<StrResponse?>(urlList.size) {
asyncArray[it].await()
}
resArray
}
}
/** /**
* 访问网络,返回Response<String> * 访问网络,返回Response<String>
*/ */
@ -62,8 +80,8 @@ interface JsExtensions {
fun downloadFile(content: String, url: String): String { fun downloadFile(content: String, url: String): String {
val type = AnalyzeUrl(url).type ?: return "" val type = AnalyzeUrl(url).type ?: return ""
val zipPath = FileUtils.getPath( val zipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}" "${MD5Utils.md5Encode16(url)}.${type}"
) )
FileUtils.deleteFile(zipPath) FileUtils.deleteFile(zipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath) val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -81,8 +99,8 @@ interface JsExtensions {
fun unzipFile(zipPath: String): String { fun unzipFile(zipPath: String): String {
if (zipPath.isEmpty()) return "" if (zipPath.isEmpty()) return ""
val unzipPath = FileUtils.getPath( val unzipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
FileUtils.getNameExcludeExtension(zipPath) FileUtils.getNameExcludeExtension(zipPath)
) )
FileUtils.deleteFile(unzipPath) FileUtils.deleteFile(unzipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath) val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -104,7 +122,7 @@ interface JsExtensions {
for (f in it) { for (f in it) {
val charsetName = EncodingDetect.getEncode(f) val charsetName = EncodingDetect.getEncode(f)
contents.append(String(f.readBytes(), charset(charsetName))) contents.append(String(f.readBytes(), charset(charsetName)))
.append("\n") .append("\n")
} }
contents.deleteCharAt(contents.length - 1) contents.deleteCharAt(contents.length - 1)
} }
@ -118,12 +136,12 @@ interface JsExtensions {
*/ */
fun get(urlStr: String, headers: Map<String, String>): Connection.Response { fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr) return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true) .ignoreContentType(true)
.followRedirects(false) .followRedirects(false)
.headers(headers) .headers(headers)
.method(Connection.Method.GET) .method(Connection.Method.GET)
.execute() .execute()
} }
/** /**
@ -131,13 +149,13 @@ interface JsExtensions {
*/ */
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response { fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr) return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true) .ignoreContentType(true)
.followRedirects(false) .followRedirects(false)
.requestBody(body) .requestBody(body)
.headers(headers) .headers(headers)
.method(Connection.Method.POST) .method(Connection.Method.POST)
.execute() .execute()
} }
/** /**
@ -227,7 +245,7 @@ interface JsExtensions {
} }
fun htmlFormat(str: String): String { fun htmlFormat(str: String): String {
return str.htmlFormat() return HtmlFormatter.formatKeepImg(str)
} }
/** /**
@ -257,6 +275,10 @@ interface JsExtensions {
return null return null
} }
/**
* 返回字体解析类
* @param str 支持url,本地文件,base64,自动判断,自动缓存
*/
fun queryTTF(str: String?): QueryTTF? { fun queryTTF(str: String?): QueryTTF? {
str ?: return null str ?: return null
val key = md5Encode16(str) val key = md5Encode16(str)
@ -266,7 +288,7 @@ interface JsExtensions {
str.isAbsUrl() -> runBlocking { str.isAbsUrl() -> runBlocking {
var x = CacheManager.getByteArray(key) var x = CacheManager.getByteArray(key)
if (x == null) { if (x == null) {
x = RxHttp.get(str).toByteArray().await() x = okHttpClient.newCall { url(str) }.bytes()
x.let { x.let {
CacheManager.put(key, it) CacheManager.put(key, it)
} }
@ -283,15 +305,20 @@ interface JsExtensions {
return qTTF return qTTF
} }
/**
* @param text 包含错误字体的内容
* @param font1 错误的字体
* @param font2 正确的字体
*/
fun replaceFont( fun replaceFont(
text: String, text: String,
font1: QueryTTF?, font1: QueryTTF?,
font2: QueryTTF? font2: QueryTTF?
): String { ): String {
if (font1 == null || font2 == null) return text if (font1 == null || font2 == null) return text
val contentArray = text.toCharArray() val contentArray = text.toCharArray()
contentArray.forEachIndexed { index, s -> contentArray.forEachIndexed { index, s ->
val oldCode = s.toInt() val oldCode = s.code
if (font1.inLimit(s)) { if (font1.inLimit(s)) {
val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode)) val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode))
if (code != 0) contentArray[index] = code.toChar() if (code != 0) contentArray[index] = code.toChar()
@ -303,7 +330,153 @@ interface JsExtensions {
/** /**
* 输出调试日志 * 输出调试日志
*/ */
fun log(msg: String) { fun log(msg: String): String {
Debug.log(msg) Debug.log(msg)
return msg
}
/**
* AES 解码为 ByteArray
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesDecodeToByteArray(
str: String,
key: String,
transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.decryptAES(
data = str.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* AES 解码为 String
* @param str 传入的AES加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesDecodeToString(
str: String,
key: String,
transformation: String,
iv: String = ""
): String? {
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
/**
* 已经base64的AES 解码为 ByteArray
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesBase64DecodeToByteArray(
str: String,
key: String,
transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.decryptBase64AES(
data = str.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 已经base64的AES 解码为 String
* @param str 传入的AES Base64加密的数据
* @param key AES 解密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesBase64DecodeToString(
str: String,
key: String,
transformation: String,
iv: String = ""
): String? {
return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
} }
/**
* 加密aes为ByteArray
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToByteArray(
data: String, key: String, transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.encryptAES(
data.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 加密aes为String
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToString(
data: String, key: String, transformation: String,
iv: String = ""
): String? {
return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) }
}
/**
* 加密aes后Base64化的ByteArray
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToBase64ByteArray(
data: String, key: String, transformation: String,
iv: String = ""
): ByteArray? {
return EncoderUtils.encryptAES2Base64(
data.encodeToByteArray(),
key = key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)
}
/**
* 加密aes后Base64化的String
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式
* @param iv ECB模式的偏移向量
*/
fun aesEncodeToBase64String(
data: String, key: String, transformation: String,
iv: String = ""
): String? {
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
}
} }

@ -10,6 +10,33 @@ object LocalConfig {
private val localConfig = private val localConfig =
appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) appCtx.getSharedPreferences("local", Context.MODE_PRIVATE)
val readHelpVersionIsLast: Boolean
get() = isLastVersion(1, "readHelpVersion", "firstRead")
val backupHelpVersionIsLast: Boolean
get() = isLastVersion(1, "backupHelpVersion", "firstBackup")
val readMenuHelpVersionIsLast: Boolean
get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu")
val bookSourcesHelpVersionIsLast: Boolean
get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources")
val debugHelpVersionIsLast: Boolean
get() = isLastVersion(1, "debugHelpVersion")
val ruleHelpVersionIsLast: Boolean
get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean
get() = !isLastVersion(3, "httpTtsVersion")
val hasUpTxtTocRule: Boolean
get() = !isLastVersion(1, "txtTocRuleVersion")
val hasUpRssSources: Boolean
get() = !isLastVersion(3, "rssSourceVersion")
var versionCode var versionCode
get() = localConfig.getLong(versionCodeKey, 0) get() = localConfig.getLong(versionCodeKey, 0)
set(value) { set(value) {
@ -46,30 +73,4 @@ object LocalConfig {
return true return true
} }
val readHelpVersionIsLast: Boolean
get() = isLastVersion(1, "readHelpVersion", "firstRead")
val backupHelpVersionIsLast: Boolean
get() = isLastVersion(1, "backupHelpVersion", "firstBackup")
val readMenuHelpVersionIsLast: Boolean
get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu")
val bookSourcesHelpVersionIsLast: Boolean
get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources")
val debugHelpVersionIsLast: Boolean
get() = isLastVersion(1, "debugHelpVersion")
val ruleHelpVersionIsLast: Boolean
get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean
get() = !isLastVersion(2, "httpTtsVersion")
val hasUpTxtTocRule: Boolean
get() = !isLastVersion(1, "txtTocRuleVersion")
val hasUpRssSources: Boolean
get() = !isLastVersion(1, "rssSourceVersion")
} }

@ -1,12 +1,12 @@
package io.legado.app.help package io.legado.app.help
import android.content.Context import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import io.legado.app.R import io.legado.app.R
object MediaHelp { object MediaHelp {
@ -32,20 +32,16 @@ object MediaHelp {
or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED) or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED)
fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequest? { fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequestCompat? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val mPlaybackAttributes = AudioAttributesCompat.Builder()
val mPlaybackAttributes = AudioAttributes.Builder() .setUsage(AudioAttributesCompat.USAGE_MEDIA)
.setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()
.build() return AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(mPlaybackAttributes)
.setAudioAttributes(mPlaybackAttributes) //.setAcceptsDelayedFocusGain(true)
.setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(audioFocusChangeListener)
.setOnAudioFocusChangeListener(audioFocusChangeListener) .build()
.build()
} else {
null
}
} }
/** /**
@ -53,21 +49,11 @@ object MediaHelp {
*/ */
fun requestFocus( fun requestFocus(
audioManager: AudioManager, audioManager: AudioManager,
listener: AudioManager.OnAudioFocusChangeListener, focusRequest: AudioFocusRequestCompat?
focusRequest: AudioFocusRequest?
): Boolean { ): Boolean {
val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val request: Int =
focusRequest?.let { focusRequest?.let { AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) }
audioManager.requestAudioFocus(focusRequest) ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
} ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
listener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
} }

@ -20,9 +20,8 @@ import java.io.File
object ReadBookConfig { object ReadBookConfig {
const val configFileName = "readConfig.json" const val configFileName = "readConfig.json"
const val shareConfigFileName = "shareReadConfig.json" const val shareConfigFileName = "shareReadConfig.json"
val context get() = appCtx val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
val configFilePath = FileUtils.getPath(context.filesDir, configFileName) val shareConfigFilePath = FileUtils.getPath(appCtx.filesDir, shareConfigFileName)
val shareConfigFilePath = FileUtils.getPath(context.filesDir, shareConfigFileName)
val configList: ArrayList<Config> = arrayListOf() val configList: ArrayList<Config> = arrayListOf()
lateinit var shareConfig: Config lateinit var shareConfig: Config
var durConfig var durConfig
@ -84,7 +83,7 @@ object ReadBookConfig {
} }
fun upBg() { fun upBg() {
val resources = context.resources val resources = appCtx.resources
val dm = resources.displayMetrics val dm = resources.displayMetrics
val width = dm.widthPixels val width = dm.widthPixels
val height = dm.heightPixels val height = dm.heightPixels
@ -133,30 +132,31 @@ object ReadBookConfig {
} }
//配置写入读取 //配置写入读取
var readBodyToLh = context.getPrefBoolean(PreferKey.readBodyToLh, true) var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
var autoReadSpeed = context.getPrefInt(PreferKey.autoReadSpeed, 46) var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46)
set(value) { set(value) {
field = value field = value
context.putPrefInt(PreferKey.autoReadSpeed, value) appCtx.putPrefInt(PreferKey.autoReadSpeed, value)
} }
var styleSelect = context.getPrefInt(PreferKey.readStyleSelect) var styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
set(value) { set(value) {
field = value field = value
if (context.getPrefInt(PreferKey.readStyleSelect) != value) { if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) {
context.putPrefInt(PreferKey.readStyleSelect, value) appCtx.putPrefInt(PreferKey.readStyleSelect, value)
} }
} }
var shareLayout = context.getPrefBoolean(PreferKey.shareLayout) var shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout)
set(value) { set(value) {
field = value field = value
if (context.getPrefBoolean(PreferKey.shareLayout) != value) { if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) {
context.putPrefBoolean(PreferKey.shareLayout, value) appCtx.putPrefBoolean(PreferKey.shareLayout, value)
} }
} }
val textFullJustify get() = context.getPrefBoolean(PreferKey.textFullJustify, true) val textFullJustify get() = appCtx.getPrefBoolean(PreferKey.textFullJustify, true)
val textBottomJustify get() = context.getPrefBoolean(PreferKey.textBottomJustify, true) val textBottomJustify get() = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true)
var hideStatusBar = context.getPrefBoolean(PreferKey.hideStatusBar) var hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = context.getPrefBoolean(PreferKey.hideNavigationBar) var hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
var useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout)
val config get() = if (shareLayout) shareConfig else durConfig val config get() = if (shareLayout) shareConfig else durConfig
@ -490,7 +490,7 @@ object ReadBookConfig {
fun curBgDrawable(width: Int, height: Int): Drawable { fun curBgDrawable(width: Int, height: Int): Drawable {
var bgDrawable: Drawable? = null var bgDrawable: Drawable? = null
val resources = context.resources val resources = appCtx.resources
try { try {
bgDrawable = when (curBgType()) { bgDrawable = when (curBgType()) {
0 -> ColorDrawable(Color.parseColor(curBgStr())) 0 -> ColorDrawable(Color.parseColor(curBgStr()))
@ -498,7 +498,7 @@ object ReadBookConfig {
BitmapDrawable( BitmapDrawable(
resources, resources,
BitmapUtils.decodeAssetsBitmap( BitmapUtils.decodeAssetsBitmap(
context, appCtx,
"bg" + File.separator + curBgStr(), "bg" + File.separator + curBgStr(),
width, width,
height height
@ -513,7 +513,7 @@ object ReadBookConfig {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
return bgDrawable ?: ColorDrawable(context.getCompatColor(R.color.background)) return bgDrawable ?: ColorDrawable(appCtx.getCompatColor(R.color.background))
} }
} }
} }

@ -52,10 +52,8 @@ object ThemeConfig {
} }
fun upConfig() { fun upConfig() {
getConfigs()?.let { getConfigs()?.forEach { config ->
it.forEach { config -> addConfig(config)
addConfig(config)
}
} }
} }

@ -197,7 +197,9 @@ class Coroutine<T>(
return withContext(scope.coroutineContext.plus(context)) { return withContext(scope.coroutineContext.plus(context)) {
if (timeMillis > 0L) withTimeout(timeMillis) { if (timeMillis > 0L) withTimeout(timeMillis) {
block() block()
} else block() } else {
block()
}
} }
} }

@ -1,90 +1,38 @@
package io.legado.app.help.http package io.legado.app.help.http
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.ConnectionSpec
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
@Suppress("unused") private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
object HttpHelper { ConcurrentHashMap()
}
private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
ConcurrentHashMap()
}
val client: OkHttpClient by lazy {
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
.retryOnConnectionFailure(true)
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
.connectionSpecs(specs)
.followRedirects(true)
.followSslRedirects(true)
.protocols(listOf(Protocol.HTTP_1_1))
.addInterceptor(getHeaderInterceptor())
builder.build()
}
/** val okHttpClient: OkHttpClient by lazy {
* 缓存代理okHttp val specs = arrayListOf(
*/ ConnectionSpec.MODERN_TLS,
fun getProxyClient(proxy: String? = null): OkHttpClient { ConnectionSpec.COMPATIBLE_TLS,
if (proxy.isNullOrBlank()) { ConnectionSpec.CLEARTEXT
return client )
}
proxyClientCache[proxy]?.let {
return it
}
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = r.findAll(proxy)
val group = ms.first()
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
val type = if (group.groupValues[1] == "http") "http" else "socks"
val host = group.groupValues[2]
val port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
if (type != "direct" && host != "") {
val builder = client.newBuilder()
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
}
val proxyClient = builder.build()
proxyClientCache[proxy] = proxyClient
return proxyClient
}
return client
}
private fun getHeaderInterceptor(): Interceptor { val builder = OkHttpClient.Builder()
return Interceptor { chain -> .connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
.retryOnConnectionFailure(true)
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
.connectionSpecs(specs)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(Interceptor { chain ->
val request = chain.request() val request = chain.request()
.newBuilder() .newBuilder()
.addHeader("Keep-Alive", "300") .addHeader("Keep-Alive", "300")
@ -92,28 +40,72 @@ object HttpHelper {
.addHeader("Cache-Control", "no-cache") .addHeader("Cache-Control", "no-cache")
.build() .build()
chain.proceed(request) chain.proceed(request)
})
builder.build()
}
/**
* 缓存代理okHttp
*/
fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) {
return okHttpClient
}
proxyClientCache[proxy]?.let {
return it
}
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = r.findAll(proxy)
val group = ms.first()
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
val type = if (group.groupValues[1] == "http") "http" else "socks"
val host = group.groupValues[2]
val port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
if (type != "direct" && host != "") {
val builder = okHttpClient.newBuilder()
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
} }
val proxyClient = builder.build()
proxyClientCache[proxy] = proxyClient
return proxyClient
} }
return okHttpClient
}
suspend fun ajax(params: AjaxWebView.AjaxParams): StrResponse = suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse =
suspendCancellableCoroutine { block -> suspendCancellableCoroutine { block ->
val webView = AjaxWebView() val webView = AjaxWebView()
block.invokeOnCancellation { block.invokeOnCancellation {
webView.destroyWebView() webView.destroyWebView()
} }
webView.callback = object : AjaxWebView.Callback() { webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) { override fun onResult(response: StrResponse) {
if (!block.isCompleted) if (!block.isCompleted)
block.resume(response) block.resume(response)
} }
override fun onError(error: Throwable) { override fun onError(error: Throwable) {
if (!block.isCompleted) if (!block.isCompleted)
block.cancel(error) block.cancel(error)
}
} }
webView.load(params)
} }
webView.load(params)
} }

@ -0,0 +1,122 @@
package io.legado.app.help.http
import io.legado.app.constant.AppConst
import io.legado.app.help.AppConfig
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.nio.charset.Charset
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun OkHttpClient.newCall(
retry: Int = 0,
builder: Request.Builder.() -> Unit
): ResponseBody {
val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent)
requestBuilder.apply(builder)
var response: Response? = null
for (i in 0..retry) {
response = this.newCall(requestBuilder.build()).await()
if (response.isSuccessful) {
return response.body!!
}
}
return response!!.body ?: throw IOException(response.message)
}
suspend fun OkHttpClient.newCallStrResponse(
retry: Int = 0,
builder: Request.Builder.() -> Unit
): StrResponse {
val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent)
requestBuilder.apply(builder)
var response: Response? = null
for (i in 0..retry) {
response = this.newCall(requestBuilder.build()).await()
if (response.isSuccessful) {
return StrResponse(response, response.body!!.text())
}
}
return StrResponse(response!!, response.body?.text() ?: response.message)
}
suspend fun Call.await(): Response = suspendCancellableCoroutine { block ->
block.invokeOnCancellation {
cancel()
}
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
block.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
block.resume(response)
}
})
}
fun ResponseBody.text(encode: String? = null): String {
val responseBytes = UTF8BOMFighter.removeUTF8BOM(bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
fun Request.Builder.addHeaders(headers: Map<String, String>) {
headers.forEach {
addHeader(it.key, it.value)
}
}
fun Request.Builder.get(url: String, queryMap: Map<String, String>, encoded: Boolean = false) {
val httpBuilder = url.toHttpUrl().newBuilder()
queryMap.forEach {
if (encoded) {
httpBuilder.addEncodedQueryParameter(it.key, it.value)
} else {
httpBuilder.addQueryParameter(it.key, it.value)
}
}
url(httpBuilder.build())
}
fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
val formBody = FormBody.Builder()
form.forEach {
if (encoded) {
formBody.addEncoded(it.key, it.value)
} else {
formBody.add(it.key, it.value)
}
}
post(formBody.build())
}
fun Request.Builder.postJson(json: String?) {
json?.let {
val requestBody = json.toRequestBody("application/json; charset=UTF-8".toMediaType())
post(requestBody)
}
}

@ -0,0 +1,12 @@
package io.legado.app.help.http
import okhttp3.Interceptor
import okhttp3.Response
class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
TODO("Not yet implemented")
}
}

@ -1,13 +0,0 @@
package io.legado.app.help.http.parser
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
@Parser(name = "ByteArray")
class ByteParser : rxhttp.wrapper.parse.Parser<ByteArray> {
override fun onParse(response: Response): ByteArray {
return response.body!!.bytes()
}
}

@ -1,14 +0,0 @@
package io.legado.app.help.http.parser
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import java.io.InputStream
@Parser(name = "InputStream")
class InputStreamParser : rxhttp.wrapper.parse.Parser<InputStream> {
override fun onParse(response: Response): InputStream {
return response.body!!.byteStream()
}
}

@ -1,40 +0,0 @@
package io.legado.app.help.http.parser
import io.legado.app.help.http.StrResponse
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset
@Parser(name = "StrResponse")
class StrResponseParser(private val encode: String? = null) :
rxhttp.wrapper.parse.Parser<StrResponse> {
override fun onParse(response: Response): StrResponse {
val body = getString(response)
return StrResponse(response, body)
}
private fun getString(response: Response): String {
val responseBody = response.body ?: throw HttpStatusCodeException(response, "内容为空")
val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
responseBody.contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
}

@ -1,33 +0,0 @@
package io.legado.app.help.http.parser
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset
@Parser(name = "Text")
class TextParser(private val encode: String? = null) : rxhttp.wrapper.parse.Parser<String> {
override fun onParse(response: Response): String {
val responseBody = response.body ?: throw HttpStatusCodeException(response, "内容为空")
val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
responseBody.contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
}

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

@ -1,7 +0,0 @@
package io.legado.app.help.permission
interface OnPermissionsGrantedCallback {
fun onPermissionsGranted(requestCode: Int)
}

@ -1,9 +0,0 @@
package io.legado.app.help.permission
interface OnPermissionsResultCallback {
fun onPermissionsGranted(requestCode: Int)
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)
}

@ -1,10 +0,0 @@
package io.legado.app.help.permission
import android.content.Intent
interface OnRequestPermissionsResultCallback {
fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray)
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
}

@ -67,7 +67,7 @@ object Backup {
writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath) writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath)
writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath) writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath)
writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath) writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath)
writeListToJson(appDb.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath) writeListToJson(appDb.txtTocRuleDao.all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(appDb.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) writeListToJson(appDb.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath)
GSON.toJson(ReadBookConfig.configList).let { GSON.toJson(ReadBookConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)

@ -38,7 +38,7 @@ object BookWebDav {
return url return url
} }
suspend fun initWebDav(): Boolean { private suspend fun initWebDav(): Boolean {
val account = appCtx.getPrefString(PreferKey.webDavAccount) val account = appCtx.getPrefString(PreferKey.webDavAccount)
val password = appCtx.getPrefString(PreferKey.webDavPassword) val password = appCtx.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
@ -122,19 +122,16 @@ object BookWebDav {
} }
} }
suspend fun exportWebDav(path: String, fileName: String) { suspend fun exportWebDav(byteArray: ByteArray, fileName: String) {
try { try {
if (initWebDav()) { if (initWebDav()) {
// 默认导出到legado文件夹下exports目录 // 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/" val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话 // 在legado文件夹创建exports目录,如果不存在的话
WebDav(exportsWebDavUrl).makeAsDir() WebDav(exportsWebDavUrl).makeAsDir()
val file = File("${path}${File.separator}${fileName}")
// 如果导出的本地文件存在,开始上传 // 如果导出的本地文件存在,开始上传
if (file.exists()) { val putUrl = exportsWebDavUrl + fileName
val putUrl = exportsWebDavUrl + fileName WebDav(putUrl).upload(byteArray)
WebDav(putUrl).upload("${path}${File.separator}${fileName}")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {

@ -1,5 +1,7 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -9,23 +11,24 @@ import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext import com.jayway.jsonpath.ParseContext
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.AppConst.androidId
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.androidId
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import splitties.systemservices.alarmManager
import java.io.File import java.io.File
import kotlin.system.exitProcess
object Restore { object Restore {
private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json") private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json")
@ -138,7 +141,7 @@ object Restore {
appDb.ruleSubDao.insert(*it.toTypedArray()) appDb.ruleSubDao.insert(*it.toTypedArray())
} }
fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let { fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let {
appDb.txtTocRule.insert(*it.toTypedArray()) appDb.txtTocRuleDao.insert(*it.toTypedArray())
} }
fileToListT<HttpTTS>(path, DefaultData.httpTtsFileName)?.let { fileToListT<HttpTTS>(path, DefaultData.httpTtsFileName)?.let {
appDb.httpTTSDao.insert(*it.toTypedArray()) appDb.httpTTSDao.insert(*it.toTypedArray())
@ -146,11 +149,11 @@ object Restore {
fileToListT<ReadRecord>(path, "readRecord.json")?.let { fileToListT<ReadRecord>(path, "readRecord.json")?.let {
it.forEach { readRecord -> it.forEach { readRecord ->
//判断是不是本机记录 //判断是不是本机记录
if (readRecord.androidId != androidId) { if (readRecord.deviceId != androidId) {
appDb.readRecordDao.insert(readRecord) appDb.readRecordDao.insert(readRecord)
} else { } else {
val time = appDb.readRecordDao val time = appDb.readRecordDao
.getReadTime(readRecord.androidId, readRecord.bookName) .getReadTime(readRecord.deviceId, readRecord.bookName)
if (time == null || time < readRecord.readTime) { if (time == null || time < readRecord.readTime) {
appDb.readRecordDao.insert(readRecord) appDb.readRecordDao.insert(readRecord)
} }
@ -207,7 +210,6 @@ object Restore {
is Long -> edit.putLong(it.key, value) is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value) is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value) is String -> edit.putString(it.key, value)
else -> Unit
} }
} }
} }
@ -220,17 +222,19 @@ object Restore {
hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46) autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46)
} }
ChapterProvider.upStyle()
ReadBook.loadContent(resetPageOffset = false)
} }
appCtx.toastOnUi(R.string.restore_success)
withContext(Main) { withContext(Main) {
appCtx.toastOnUi(R.string.restore_success) delay(100)
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon)) LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon))
} }
LanguageUtils.setConfiguration(appCtx) appCtx.packageManager.getLaunchIntentForPackage(appCtx.packageName)?.let { intent ->
ThemeConfig.applyDayNight(appCtx) val restartIntent =
postEvent(EventBus.SHOW_RSS, "") PendingIntent.getActivity(appCtx, 0, intent, PendingIntent.FLAG_ONE_SHOT)
alarmManager[AlarmManager.RTC, System.currentTimeMillis() + 300] = restartIntent
exitProcess(0)
}
} }
} }

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

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

@ -0,0 +1,7 @@
package io.legado.app.lib.permission
interface OnPermissionsGrantedCallback {
fun onPermissionsGranted()
}

@ -0,0 +1,9 @@
package io.legado.app.lib.permission
interface OnPermissionsResultCallback {
fun onPermissionsGranted()
fun onPermissionsDenied(deniedPermissions: Array<String>)
}

@ -0,0 +1,8 @@
package io.legado.app.lib.permission
interface OnRequestPermissionsResultCallback {
fun onRequestPermissionsResult(permissions: Array<String>, grantResults: IntArray)
fun onSettingActivityResult()
}

@ -1,10 +1,11 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import io.legado.app.R import io.legado.app.R
@ -12,6 +13,12 @@ import io.legado.app.utils.toastOnUi
class PermissionActivity : AppCompatActivity() { class PermissionActivity : AppCompatActivity() {
private val settingActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
RequestPlugins.sRequestCallback?.onSettingActivityResult()
finish()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -30,7 +37,7 @@ class PermissionActivity : AppCompatActivity() {
-> try { -> try {
val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
settingIntent.data = Uri.fromParts("package", packageName, null) settingIntent.data = Uri.fromParts("package", packageName, null)
startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING) settingActivityResult.launch(settingIntent)
} catch (e: Exception) { } catch (e: Exception) {
toastOnUi(R.string.tip_cannot_jump_setting_page) toastOnUi(R.string.tip_cannot_jump_setting_page)
finish() finish()
@ -39,15 +46,16 @@ class PermissionActivity : AppCompatActivity() {
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
RequestPlugins.sRequestCallback?.onRequestPermissionsResult(requestCode, permissions, grantResults) RequestPlugins.sRequestCallback?.onRequestPermissionsResult(
finish() permissions,
} grantResults
)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
RequestPlugins.sRequestCallback?.onActivityResult(requestCode, resultCode, data)
finish() finish()
} }

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
@Suppress("unused") @Suppress("unused")
object Permissions { object Permissions {

@ -1,9 +1,8 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import java.util.*
@Suppress("unused") @Suppress("unused")
class PermissionsCompat private constructor() { class PermissionsCompat private constructor() {
@ -14,17 +13,6 @@ class PermissionsCompat private constructor() {
RequestManager.pushRequest(request) RequestManager.pushRequest(request)
} }
companion object {
// 检查权限, 如果已经拥有返回 true
fun check(activity: AppCompatActivity, vararg permissions: String): Boolean {
val request = Request(activity)
val pers = ArrayList<String>()
pers.addAll(listOf(*permissions))
val data = request.getDeniedPermissions(pers.toTypedArray())
return data == null
}
}
class Builder { class Builder {
private val request: Request private val request: Request
@ -41,24 +29,19 @@ class PermissionsCompat private constructor() {
return this return this
} }
fun requestCode(requestCode: Int): Builder { fun onGranted(callback: () -> Unit): Builder {
request.setRequestCode(requestCode)
return this
}
fun onGranted(callback: (requestCode: Int) -> Unit): Builder {
request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {
override fun onPermissionsGranted(requestCode: Int) { override fun onPermissionsGranted() {
callback(requestCode) callback()
} }
}) })
return this return this
} }
fun onDenied(callback: (requestCode: Int, deniedPermissions: Array<String>) -> Unit): Builder { fun onDenied(callback: (deniedPermissions: Array<String>) -> Unit): Builder {
request.setOnDeniedCallback(object : OnPermissionsDeniedCallback { request.setOnDeniedCallback(object : OnPermissionsDeniedCallback {
override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) { override fun onPermissionsDenied(deniedPermissions: Array<String>) {
callback(requestCode, deniedPermissions) callback(deniedPermissions)
} }
}) })
return this return this

@ -1,6 +1,5 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -12,6 +11,7 @@ import io.legado.app.R
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import java.util.* import java.util.*
@Suppress("MemberVisibilityCanBePrivate")
internal class Request : OnRequestPermissionsResultCallback { internal class Request : OnRequestPermissionsResultCallback {
internal val requestTime: Long internal val requestTime: Long
@ -46,10 +46,6 @@ internal class Request : OnRequestPermissionsResultCallback {
this.permissions?.addAll(listOf(*permissions)) this.permissions?.addAll(listOf(*permissions))
} }
fun setRequestCode(requestCode: Int) {
this.requestCode = requestCode
}
fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) {
grantedCallback = callback grantedCallback = callback
} }
@ -75,19 +71,16 @@ internal class Request : OnRequestPermissionsResultCallback {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (deniedPermissions == null) { if (deniedPermissions == null) {
onPermissionsGranted(requestCode) onPermissionsGranted()
} else { } else {
val rationale = val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { showSettingDialog(rationale) {
onPermissionsDenied( onPermissionsDenied(deniedPermissions)
requestCode,
deniedPermissions
)
} }
} else { } else {
onPermissionsDenied(requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} }
} else { } else {
@ -98,7 +91,7 @@ internal class Request : OnRequestPermissionsResultCallback {
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
} }
} else { } else {
onPermissionsGranted(requestCode) onPermissionsGranted()
} }
} }
} }
@ -151,26 +144,25 @@ internal class Request : OnRequestPermissionsResultCallback {
} }
} }
private fun onPermissionsGranted(requestCode: Int) { private fun onPermissionsGranted() {
try { try {
grantedCallback?.onPermissionsGranted(requestCode) grantedCallback?.onPermissionsGranted()
} catch (ignore: Exception) { } catch (ignore: Exception) {
} }
RequestPlugins.sResultCallback?.onPermissionsGranted(requestCode) RequestPlugins.sResultCallback?.onPermissionsGranted()
} }
private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) { private fun onPermissionsDenied(deniedPermissions: Array<String>) {
try { try {
deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions) deniedCallback?.onPermissionsDenied(deniedPermissions)
} catch (ignore: Exception) { } catch (ignore: Exception) {
} }
RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions) RequestPlugins.sResultCallback?.onPermissionsDenied(deniedPermissions)
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>, permissions: Array<String>,
grantResults: IntArray grantResults: IntArray
) { ) {
@ -179,21 +171,21 @@ internal class Request : OnRequestPermissionsResultCallback {
val rationale = val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } showSettingDialog(rationale) { onPermissionsDenied(deniedPermissions) }
} else { } else {
onPermissionsDenied(requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} else { } else {
onPermissionsGranted(requestCode) onPermissionsGranted()
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onSettingActivityResult() {
val deniedPermissions = deniedPermissions val deniedPermissions = deniedPermissions
if (deniedPermissions == null) { if (deniedPermissions == null) {
onPermissionsGranted(this.requestCode) onPermissionsGranted()
} else { } else {
onPermissionsDenied(this.requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} }

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -58,11 +58,11 @@ internal object RequestManager : OnPermissionsResultCallback {
} }
} }
override fun onPermissionsGranted(requestCode: Int) { override fun onPermissionsGranted() {
startNextRequest() startNextRequest()
} }
override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) { override fun onPermissionsDenied(deniedPermissions: Array<String>) {
startNextRequest() startNextRequest()
} }

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
internal object RequestPlugins { internal object RequestPlugins {

@ -1,4 +1,4 @@
package io.legado.app.help.permission package io.legado.app.lib.permission
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

@ -16,7 +16,7 @@ import android.widget.ScrollView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -219,9 +219,9 @@ object ATH {
} }
} }
fun setEdgeEffectColor(viewPager: ViewPager?, @ColorInt color: Int) { fun setEdgeEffectColor(viewPager: ViewPager2?, @ColorInt color: Int) {
try { try {
val clazz = ViewPager::class.java val clazz = ViewPager2::class.java
for (name in arrayOf("mLeftEdge", "mRightEdge")) { for (name in arrayOf("mLeftEdge", "mRightEdge")) {
val field = clazz.getDeclaredField(name) val field = clazz.getDeclaredField(name)
field.isAccessible = true field.isAccessible = true
@ -280,7 +280,7 @@ object ATH {
fun applyEdgeEffectColor(view: View?) { fun applyEdgeEffectColor(view: View?) {
when (view) { when (view) {
is RecyclerView -> view.edgeEffectFactory = DEFAULT_EFFECT_FACTORY is RecyclerView -> view.edgeEffectFactory = DEFAULT_EFFECT_FACTORY
is ViewPager -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) is ViewPager2 -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context))
is ScrollView -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) is ScrollView -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context))
} }
} }

@ -101,9 +101,9 @@ val Fragment.secondaryDisabledTextColor: Int
val Context.buttonDisabledColor: Int val Context.buttonDisabledColor: Int
get() = if (isDarkTheme) { get() = if (isDarkTheme) {
ContextCompat.getColor(this, R.color.ate_button_disabled_dark) ContextCompat.getColor(this, R.color.md_dark_disabled)
} else { } else {
ContextCompat.getColor(this, R.color.ate_button_disabled_light) ContextCompat.getColor(this, R.color.md_light_disabled)
} }
val Context.isDarkTheme: Boolean val Context.isDarkTheme: Boolean

@ -1,16 +1,14 @@
package io.legado.app.lib.webdav package io.legado.app.lib.webdav
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.newCall
import io.legado.app.utils.await import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toInputStream
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
@ -23,16 +21,16 @@ class WebDav(urlStr: String) {
// 指定返回哪些属性 // 指定返回哪些属性
private const val DIR = private const val DIR =
"""<?xml version="1.0"?> """<?xml version="1.0"?>
<a:propfind xmlns:a="DAV:"> <a:propfind xmlns:a="DAV:">
<a:prop> <a:prop>
<a:displayname/> <a:displayname/>
<a:resourcetype/> <a:resourcetype/>
<a:getcontentlength/> <a:getcontentlength/>
<a:creationdate/> <a:creationdate/>
<a:getlastmodified/> <a:getlastmodified/>
%s %s
</a:prop> </a:prop>
</a:propfind>""" </a:propfind>"""
} }
private val url: URL = URL(urlStr) private val url: URL = URL(urlStr)
@ -59,64 +57,50 @@ class WebDav(urlStr: String) {
* @return 远程文件是否存在 * @return 远程文件是否存在
*/ */
suspend fun indexFileInfo(): Boolean { suspend fun indexFileInfo(): Boolean {
propFindResponse(ArrayList())?.let { response -> return !propFindResponse(ArrayList()).isNullOrEmpty()
if (!response.isSuccessful) {
this.exists = false
return false
}
response.body?.let {
@Suppress("BlockingMethodInNonBlockingContext")
if (it.string().isNotEmpty()) {
return true
}
}
}
return false
} }
/** /**
* 列出当前路径下的文件 * 列出当前路径下的文件
* *
* @param propsList 指定列出文件的哪些属性
* @return 文件列表 * @return 文件列表
*/ */
suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { suspend fun listFiles(): List<WebDav> {
propFindResponse(propsList)?.let { response -> propFindResponse()?.let { body ->
if (response.isSuccessful) { return parseDir(body)
response.body?.let { body ->
@Suppress("BlockingMethodInNonBlockingContext")
return parseDir(body.string())
}
}
} }
return ArrayList() return ArrayList()
} }
@Throws(IOException::class) /**
private suspend fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? { * @param propsList 指定列出文件的哪些属性
*/
private suspend fun propFindResponse(propsList: List<String> = emptyList()): String? {
val requestProps = StringBuilder() val requestProps = StringBuilder()
for (p in propsList) { for (p in propsList) {
requestProps.append("<a:").append(p).append("/>\n") requestProps.append("<a:").append(p).append("/>\n")
} }
val requestPropsStr: String val requestPropsStr: String = if (requestProps.toString().isEmpty()) {
requestPropsStr = if (requestProps.toString().isEmpty()) {
DIR.replace("%s", "") DIR.replace("%s", "")
} else { } else {
String.format(DIR, requestProps.toString() + "\n") String.format(DIR, requestProps.toString() + "\n")
} }
httpUrl?.let { url -> val url = httpUrl
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 val auth = HttpAuth.auth
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 if (url != null && auth != null) {
val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType()) return kotlin.runCatching {
val request = Request.Builder() okHttpClient.newCall {
.url(url) url(url)
.method("PROPFIND", requestBody) addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
addHeader("Depth", "1")
HttpAuth.auth?.let { // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
request.header("Authorization", Credentials.basic(it.user, it.pass)) // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
} val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType())
request.header("Depth", if (depth < 0) "infinity" else depth.toString()) method("PROPFIND", requestBody)
return HttpHelper.client.newCall(request.build()).await() }.text()
}.onFailure {
it.printStackTrace()
}.getOrNull()
} }
return null return null
} }
@ -157,15 +141,20 @@ class WebDav(urlStr: String) {
/** /**
* 根据自己的URL在远程处创建对应的文件夹 * 根据自己的URL在远程处创建对应的文件夹
*
* @return 是否创建成功 * @return 是否创建成功
*/ */
suspend fun makeAsDir(): Boolean { suspend fun makeAsDir(): Boolean {
httpUrl?.let { url -> val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(url) if (url != null && auth != null) {
.method("MKCOL", null) //防止报错
return execRequest(request) return kotlin.runCatching {
okHttpClient.newCall {
url(url)
method("MKCOL", null)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}.close()
}.isSuccess
} }
return false return false
} }
@ -199,11 +188,16 @@ class WebDav(urlStr: String) {
if (!file.exists()) return false if (!file.exists()) return false
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = file.asRequestBody(contentType?.toMediaType()) val fileBody = file.asRequestBody(contentType?.toMediaType())
httpUrl?.let { val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(it) if (url != null && auth != null) {
.put(fileBody) return kotlin.runCatching {
return execRequest(request) okHttpClient.newCall {
url(url)
put(fileBody)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}.close()
}.isSuccess
} }
return false return false
} }
@ -211,37 +205,30 @@ class WebDav(urlStr: String) {
suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean {
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = byteArray.toRequestBody(contentType?.toMediaType()) val fileBody = byteArray.toRequestBody(contentType?.toMediaType())
httpUrl?.let { val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(it) if (url != null && auth != null) {
.put(fileBody) return kotlin.runCatching {
return execRequest(request) okHttpClient.newCall {
url(url)
put(fileBody)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}.close()
}.isSuccess
} }
return false return false
} }
/**
* 执行请求获取响应结果
* @param requestBuilder 因为还需要追加验证信息所以此处传递Request.Builder的对象而不是Request的对象
* @return 请求执行的结果
*/
@Throws(IOException::class)
private suspend fun execRequest(requestBuilder: Request.Builder): Boolean {
HttpAuth.auth?.let {
requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass))
}
val response = HttpHelper.client.newCall(requestBuilder.build()).await()
return response.isSuccessful
}
@Throws(IOException::class)
private suspend fun getInputStream(): InputStream? { private suspend fun getInputStream(): InputStream? {
val url = httpUrl val url = httpUrl
val auth = HttpAuth.auth val auth = HttpAuth.auth
if (url != null && auth != null) { if (url != null && auth != null) {
return RxHttp.get(url) return kotlin.runCatching {
.addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) okHttpClient.newCall {
.toInputStream().await() url(url)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}.byteStream()
}.getOrNull()
} }
return null return null
} }

@ -8,7 +8,7 @@ import io.legado.app.data.entities.RssSource
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.rss.Rss import io.legado.app.model.rss.Rss
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.htmlFormat import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.msg import io.legado.app.utils.msg
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -36,7 +36,7 @@ object Debug {
if (debugSource != sourceUrl || callback == null || !print) return if (debugSource != sourceUrl || callback == null || !print) return
var printMsg = msg ?: "" var printMsg = msg ?: ""
if (isHtml) { if (isHtml) {
printMsg = printMsg.htmlFormat() printMsg = HtmlFormatter.format(msg)
} }
if (showTime) { if (showTime) {
printMsg = printMsg =
@ -186,6 +186,12 @@ object Debug {
} }
private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) { private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
if (book.tocUrl.isNotBlank()) {
log(debugSource, "目录url不为空,详情页已解析")
log(debugSource, showTime = false)
tocDebug(scope, webBook, book)
return
}
log(debugSource, "︾开始解析详情页") log(debugSource, "︾开始解析详情页")
val info = webBook.getBookInfo(scope, book) val info = webBook.getBookInfo(scope, book)
.onSuccess { .onSuccess {
@ -206,7 +212,7 @@ object Debug {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成") log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null val nextChapterUrl = it.getOrNull(1)?.url
contentDebug(scope, webBook, book, it[0], nextChapterUrl) contentDebug(scope, webBook, book, it[0], nextChapterUrl)
} else { } else {
log(debugSource, "︽目录列表为空", state = -1) log(debugSource, "︽目录列表为空", state = -1)

@ -97,16 +97,16 @@ class AnalyzeByJSoup(doc: Any) {
} }
val results = ArrayList<List<String>>() val results = ArrayList<List<String>>()
for (ruleStrX in ruleStrS) { for (ruleStrX in ruleStrS) {
val temp: List<String>? val temp: List<String>? =
temp = if (sourceRule.isCss) { if (sourceRule.isCss) {
val lastIndex = ruleStrX.lastIndexOf('@') val lastIndex = ruleStrX.lastIndexOf('@')
getResultLast( getResultLast(
element.select(ruleStrX.substring(0, lastIndex)), element.select(ruleStrX.substring(0, lastIndex)),
ruleStrX.substring(lastIndex + 1) ruleStrX.substring(lastIndex + 1)
) )
} else { } else {
getResultList(ruleStrX) getResultList(ruleStrX)
} }
if (!temp.isNullOrEmpty()) { if (!temp.isNullOrEmpty()) {
results.add(temp) results.add(temp)
if (results.isNotEmpty() && elementsType == "|") { if (results.isNotEmpty() && elementsType == "|") {

@ -27,9 +27,10 @@ import kotlin.collections.HashMap
class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions { class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
var book: BaseBook? = null var book: BaseBook? = null
var chapter: BookChapter? = null var chapter: BookChapter? = null
private var content: Any? = null var nextChapterUrl: String? = null
private var baseUrl: String? = null var content: Any? = null
private var baseURL: URL? = null var baseUrl: String? = null
var redirectUrl: URL? = null
private var isJSON: Boolean = false private var isJSON: Boolean = false
private var isRegex: Boolean = false private var isRegex: Boolean = false
@ -62,15 +63,17 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
fun setBaseUrl(baseUrl: String?): AnalyzeRule { fun setBaseUrl(baseUrl: String?): AnalyzeRule {
baseUrl?.let { baseUrl?.let {
this.baseUrl = baseUrl this.baseUrl = baseUrl
kotlin.runCatching {
baseURL = URL(baseUrl.substringBefore(","))
}.onFailure {
it.printStackTrace()
}
} }
return this return this
} }
fun setRedirectUrl(url: String): URL? {
kotlin.runCatching {
redirectUrl = URL(url.split(AnalyzeUrl.splitUrlRegex, 1)[0])
}
return redirectUrl
}
/** /**
* 获取XPath解析类 * 获取XPath解析类
*/ */
@ -169,7 +172,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
val urlList = ArrayList<String>() val urlList = ArrayList<String>()
if (result is List<*>) { if (result is List<*>) {
for (url in result as List<*>) { for (url in result as List<*>) {
val absoluteURL = NetworkUtils.getAbsoluteURL(baseURL, url.toString()) val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString())
if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) { if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) {
urlList.add(absoluteURL) urlList.add(absoluteURL)
} }
@ -238,7 +241,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
return if (str.isBlank()) { return if (str.isBlank()) {
baseUrl ?: "" baseUrl ?: ""
} else { } else {
NetworkUtils.getAbsoluteURL(baseURL, str) NetworkUtils.getAbsoluteURL(redirectUrl, str)
} }
} }
return str return str
@ -646,6 +649,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
bindings["chapter"] = chapter bindings["chapter"] = chapter
bindings["title"] = chapter?.title bindings["title"] = chapter?.title
bindings["src"] = content bindings["src"] = content
bindings["nextChapterUrl"] = nextChapterUrl
return runBlocking { return runBlocking {
SCRIPT_ENGINE.eval(jsStr, bindings) SCRIPT_ENGINE.eval(jsStr, bindings)
} }

@ -16,9 +16,6 @@ import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.help.http.* import io.legado.app.help.http.*
import io.legado.app.utils.* import io.legado.app.utils.*
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import rxhttp.wrapper.param.toStrResponse
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -58,6 +55,7 @@ class AnalyzeUrl(
private var charset: String? = null private var charset: String? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
private var proxy: String? = null private var proxy: String? = null
private var retry: Int = 0
init { init {
baseUrl = baseUrl.split(splitUrlRegex, 1)[0] baseUrl = baseUrl.split(splitUrlRegex, 1)[0]
@ -200,6 +198,7 @@ class AnalyzeUrl(
option.js?.let { option.js?.let {
evalJS(it) evalJS(it)
} }
retry = option.retry
} }
} }
headerMap[UA_NAME] ?: let { headerMap[UA_NAME] ?: let {
@ -305,62 +304,43 @@ class AnalyzeUrl(
params.sourceRegex = sourceRegex params.sourceRegex = sourceRegex
params.postData = body?.toByteArray() params.postData = body?.toByteArray()
params.tag = tag params.tag = tag
return HttpHelper.ajax(params) return getWebViewSrc(params)
} }
return when (method) { return getProxyClient(proxy).newCallStrResponse(retry) {
RequestMethod.POST -> { removeHeader(UA_NAME)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { addHeaders(headerMap)
RxHttp.postForm(url) when (method) {
.setAssemblyEnabled(false) RequestMethod.POST -> {
.setOkClient(HttpHelper.getProxyClient(proxy)) url(url)
.addAllEncoded(fieldMap) if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
.addAllHeader(headerMap) postForm(fieldMap, true)
.toStrResponse().await() } else {
} else { postJson(body)
RxHttp.postJson(url) }
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toStrResponse().await()
} }
else -> get(url, fieldMap, true)
} }
else -> RxHttp.get(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} }
} }
suspend fun getByteArray(tag: String? = null): ByteArray { suspend fun getByteArray(tag: String? = null): ByteArray {
setCookie(tag) setCookie(tag)
return when (method) { @Suppress("BlockingMethodInNonBlockingContext")
RequestMethod.POST -> { return getProxyClient(proxy).newCall(retry) {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { removeHeader(UA_NAME)
RxHttp.postForm(url) addHeaders(headerMap)
.setAssemblyEnabled(false) when (method) {
.setOkClient(HttpHelper.getProxyClient(proxy)) RequestMethod.POST -> {
.addAllEncoded(fieldMap) url(url)
.addAllHeader(headerMap) if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
.toByteArray().await() postForm(fieldMap, true)
} else { } else {
RxHttp.postJson(url) postJson(body)
.setAssemblyEnabled(false) }
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toByteArray().await()
} }
else -> get(url, fieldMap, true)
} }
else -> RxHttp.get(url) }.bytes()
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
}
} }
private fun setCookie(tag: String?) { private fun setCookie(tag: String?) {
@ -393,7 +373,8 @@ class AnalyzeUrl(
val headers: Any?, val headers: Any?,
val body: Any?, val body: Any?,
val type: String?, val type: String?,
val js: String? val js: String?,
val retry: Int = 0
) )
} }

@ -11,6 +11,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@SuppressWarnings({"FieldCanBeLocal", "StatementWithEmptyBody", "unused"})
public class QueryTTF { public class QueryTTF {
private static class Header { private static class Header {
public int majorVersion; public int majorVersion;
@ -84,7 +85,7 @@ public class QueryTTF {
public int maxComponentDepth; public int maxComponentDepth;
} }
private class CmapLayout { private static class CmapLayout {
public int version; public int version;
public int numTables; public int numTables;
public List<CmapRecord> records = new LinkedList<>(); public List<CmapRecord> records = new LinkedList<>();
@ -146,19 +147,19 @@ public class QueryTTF {
} }
private static class ByteArrayReader { private static class ByteArrayReader {
public int Index; public int index;
public byte[] Buffer; public byte[] buffer;
public ByteArrayReader(byte[] buffer, int index) { public ByteArrayReader(byte[] buffer, int index) {
Buffer = buffer; this.buffer = buffer;
Index = index; this.index = index;
} }
public long ReadUIntX(long len) { public long ReadUIntX(long len) {
long result = 0; long result = 0;
for (long i = 0; i < len; ++i) { for (long i = 0; i < len; ++i) {
result <<= 8; result <<= 8;
result |= Buffer[Index++] & 0xFF; result |= buffer[index++] & 0xFF;
} }
return result; return result;
} }
@ -186,17 +187,17 @@ public class QueryTTF {
public String ReadStrings(int len, Charset charset) { public String ReadStrings(int len, Charset charset) {
byte[] result = len > 0 ? new byte[len] : null; byte[] result = len > 0 ? new byte[len] : null;
for (int i = 0; i < len; ++i) result[i] = Buffer[Index++]; for (int i = 0; i < len; ++i) result[i] = buffer[index++];
return new String(result, charset); return new String(result, charset);
} }
public byte GetByte() { public byte GetByte() {
return Buffer[Index++]; return buffer[index++];
} }
public byte[] GetBytes(int len) { public byte[] GetBytes(int len) {
byte[] result = len > 0 ? new byte[len] : null; byte[] result = len > 0 ? new byte[len] : null;
for (int i = 0; i < len; ++i) result[i] = Buffer[Index++]; for (int i = 0; i < len; ++i) result[i] = buffer[index++];
return result; return result;
} }
@ -213,16 +214,17 @@ public class QueryTTF {
} }
} }
private final ByteArrayReader FontReader; private final ByteArrayReader fontReader;
private final Header fileHeader = new Header(); private final Header fileHeader = new Header();
private final List<Directory> Directorys = new LinkedList<>(); private final List<Directory> directorys = new LinkedList<>();
private final NameLayout Name = new NameLayout(); private final NameLayout name = new NameLayout();
private final HeadLayout head = new HeadLayout(); private final HeadLayout head = new HeadLayout();
private final MaxpLayout maxp = new MaxpLayout(); private final MaxpLayout maxp = new MaxpLayout();
private final List<Integer> loca = new LinkedList<>(); private final List<Integer> loca = new LinkedList<>();
private final CmapLayout Cmap = new CmapLayout(); private final CmapLayout Cmap = new CmapLayout();
private final List<GlyfLayout> glyf = new LinkedList<>(); private final List<GlyfLayout> glyf = new LinkedList<>();
private final Pair[] pps = new Pair[]{ @SuppressWarnings("unchecked")
private final Pair<Integer, Integer>[] pps = new Pair[]{
Pair.of(3, 10), Pair.of(3, 10),
Pair.of(0, 4), Pair.of(0, 4),
Pair.of(3, 1), Pair.of(3, 1),
@ -231,10 +233,10 @@ public class QueryTTF {
Pair.of(0, 1) Pair.of(0, 1)
}; };
public final Map<Integer, String> CodeToGlyph = new HashMap<>(); public final Map<Integer, String> codeToGlyph = new HashMap<>();
public final Map<String, Integer> GlyphToCode = new HashMap<>(); public final Map<String, Integer> glyphToCode = new HashMap<>();
private int LimitMix = 0; private int limitMix = 0;
private int LimitMax = 0; private int limitMax = 0;
/** /**
* 构造函数 * 构造函数
@ -242,162 +244,162 @@ public class QueryTTF {
* @param buffer 传入TTF字体二进制数组 * @param buffer 传入TTF字体二进制数组
*/ */
public QueryTTF(byte[] buffer) { public QueryTTF(byte[] buffer) {
FontReader = new ByteArrayReader(buffer, 0); fontReader = new ByteArrayReader(buffer, 0);
// 获取文件头 // 获取文件头
fileHeader.majorVersion = FontReader.ReadUInt16(); fileHeader.majorVersion = fontReader.ReadUInt16();
fileHeader.minorVersion = FontReader.ReadUInt16(); fileHeader.minorVersion = fontReader.ReadUInt16();
fileHeader.numOfTables = FontReader.ReadUInt16(); fileHeader.numOfTables = fontReader.ReadUInt16();
fileHeader.searchRange = FontReader.ReadUInt16(); fileHeader.searchRange = fontReader.ReadUInt16();
fileHeader.entrySelector = FontReader.ReadUInt16(); fileHeader.entrySelector = fontReader.ReadUInt16();
fileHeader.rangeShift = FontReader.ReadUInt16(); fileHeader.rangeShift = fontReader.ReadUInt16();
// 获取目录 // 获取目录
for (int i = 0; i < fileHeader.numOfTables; ++i) { for (int i = 0; i < fileHeader.numOfTables; ++i) {
Directory d = new Directory(); Directory d = new Directory();
d.tag = FontReader.ReadStrings(4, StandardCharsets.US_ASCII); d.tag = fontReader.ReadStrings(4, StandardCharsets.US_ASCII);
d.checkSum = FontReader.ReadUInt32(); d.checkSum = fontReader.ReadUInt32();
d.offset = FontReader.ReadUInt32(); d.offset = fontReader.ReadUInt32();
d.length = FontReader.ReadUInt32(); d.length = fontReader.ReadUInt32();
Directorys.add(d); directorys.add(d);
} }
// 解析表 name (字体信息,包含版权、名称、作者等...) // 解析表 name (字体信息,包含版权、名称、作者等...)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("name")) { if (Temp.tag.equals("name")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
Name.format = FontReader.ReadUInt16(); name.format = fontReader.ReadUInt16();
Name.count = FontReader.ReadUInt16(); name.count = fontReader.ReadUInt16();
Name.stringOffset = FontReader.ReadUInt16(); name.stringOffset = fontReader.ReadUInt16();
for (int i = 0; i < Name.count; ++i) { for (int i = 0; i < name.count; ++i) {
NameRecord record = new NameRecord(); NameRecord record = new NameRecord();
record.platformID = FontReader.ReadUInt16(); record.platformID = fontReader.ReadUInt16();
record.encodingID = FontReader.ReadUInt16(); record.encodingID = fontReader.ReadUInt16();
record.languageID = FontReader.ReadUInt16(); record.languageID = fontReader.ReadUInt16();
record.nameID = FontReader.ReadUInt16(); record.nameID = fontReader.ReadUInt16();
record.length = FontReader.ReadUInt16(); record.length = fontReader.ReadUInt16();
record.offset = FontReader.ReadUInt16(); record.offset = fontReader.ReadUInt16();
Name.records.add(record); name.records.add(record);
} }
} }
} }
// 解析表 head (获取 head.indexToLocFormat) // 解析表 head (获取 head.indexToLocFormat)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("head")) { if (Temp.tag.equals("head")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
head.majorVersion = FontReader.ReadUInt16(); head.majorVersion = fontReader.ReadUInt16();
head.minorVersion = FontReader.ReadUInt16(); head.minorVersion = fontReader.ReadUInt16();
head.fontRevision = FontReader.ReadUInt32(); head.fontRevision = fontReader.ReadUInt32();
head.checkSumAdjustment = FontReader.ReadUInt32(); head.checkSumAdjustment = fontReader.ReadUInt32();
head.magicNumber = FontReader.ReadUInt32(); head.magicNumber = fontReader.ReadUInt32();
head.flags = FontReader.ReadUInt16(); head.flags = fontReader.ReadUInt16();
head.unitsPerEm = FontReader.ReadUInt16(); head.unitsPerEm = fontReader.ReadUInt16();
head.created = FontReader.ReadUInt64(); head.created = fontReader.ReadUInt64();
head.modified = FontReader.ReadUInt64(); head.modified = fontReader.ReadUInt64();
head.xMin = FontReader.ReadInt16(); head.xMin = fontReader.ReadInt16();
head.yMin = FontReader.ReadInt16(); head.yMin = fontReader.ReadInt16();
head.xMax = FontReader.ReadInt16(); head.xMax = fontReader.ReadInt16();
head.yMax = FontReader.ReadInt16(); head.yMax = fontReader.ReadInt16();
head.macStyle = FontReader.ReadUInt16(); head.macStyle = fontReader.ReadUInt16();
head.lowestRecPPEM = FontReader.ReadUInt16(); head.lowestRecPPEM = fontReader.ReadUInt16();
head.fontDirectionHint = FontReader.ReadInt16(); head.fontDirectionHint = fontReader.ReadInt16();
head.indexToLocFormat = FontReader.ReadInt16(); head.indexToLocFormat = fontReader.ReadInt16();
head.glyphDataFormat = FontReader.ReadInt16(); head.glyphDataFormat = fontReader.ReadInt16();
} }
} }
// 解析表 maxp (获取 maxp.numGlyphs) // 解析表 maxp (获取 maxp.numGlyphs)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("maxp")) { if (Temp.tag.equals("maxp")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
maxp.majorVersion = FontReader.ReadUInt16(); maxp.majorVersion = fontReader.ReadUInt16();
maxp.minorVersion = FontReader.ReadUInt16(); maxp.minorVersion = fontReader.ReadUInt16();
maxp.numGlyphs = FontReader.ReadUInt16(); maxp.numGlyphs = fontReader.ReadUInt16();
maxp.maxPoints = FontReader.ReadUInt16(); maxp.maxPoints = fontReader.ReadUInt16();
maxp.maxContours = FontReader.ReadUInt16(); maxp.maxContours = fontReader.ReadUInt16();
maxp.maxCompositePoints = FontReader.ReadUInt16(); maxp.maxCompositePoints = fontReader.ReadUInt16();
maxp.maxCompositeContours = FontReader.ReadUInt16(); maxp.maxCompositeContours = fontReader.ReadUInt16();
maxp.maxZones = FontReader.ReadUInt16(); maxp.maxZones = fontReader.ReadUInt16();
maxp.maxTwilightPoints = FontReader.ReadUInt16(); maxp.maxTwilightPoints = fontReader.ReadUInt16();
maxp.maxStorage = FontReader.ReadUInt16(); maxp.maxStorage = fontReader.ReadUInt16();
maxp.maxFunctionDefs = FontReader.ReadUInt16(); maxp.maxFunctionDefs = fontReader.ReadUInt16();
maxp.maxInstructionDefs = FontReader.ReadUInt16(); maxp.maxInstructionDefs = fontReader.ReadUInt16();
maxp.maxStackElements = FontReader.ReadUInt16(); maxp.maxStackElements = fontReader.ReadUInt16();
maxp.maxSizeOfInstructions = FontReader.ReadUInt16(); maxp.maxSizeOfInstructions = fontReader.ReadUInt16();
maxp.maxComponentElements = FontReader.ReadUInt16(); maxp.maxComponentElements = fontReader.ReadUInt16();
maxp.maxComponentDepth = FontReader.ReadUInt16(); maxp.maxComponentDepth = fontReader.ReadUInt16();
} }
} }
// 解析表 loca (轮廓数据偏移地址表) // 解析表 loca (轮廓数据偏移地址表)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("loca")) { if (Temp.tag.equals("loca")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
int offset = head.indexToLocFormat == 0 ? 2 : 4; int offset = head.indexToLocFormat == 0 ? 2 : 4;
for (long i = 0; i < Temp.length; i += offset) { for (long i = 0; i < Temp.length; i += offset) {
loca.add(offset == 2 ? FontReader.ReadUInt16() << 1 : FontReader.ReadUInt32()); loca.add(offset == 2 ? fontReader.ReadUInt16() << 1 : fontReader.ReadUInt32());
} }
} }
} }
// 解析表 cmap (Unicode编码轮廓索引对照表) // 解析表 cmap (Unicode编码轮廓索引对照表)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("cmap")) { if (Temp.tag.equals("cmap")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
Cmap.version = FontReader.ReadUInt16(); Cmap.version = fontReader.ReadUInt16();
Cmap.numTables = FontReader.ReadUInt16(); Cmap.numTables = fontReader.ReadUInt16();
for (int i = 0; i < Cmap.numTables; ++i) { for (int i = 0; i < Cmap.numTables; ++i) {
CmapRecord record = new CmapRecord(); CmapRecord record = new CmapRecord();
record.platformID = FontReader.ReadUInt16(); record.platformID = fontReader.ReadUInt16();
record.encodingID = FontReader.ReadUInt16(); record.encodingID = fontReader.ReadUInt16();
record.offset = FontReader.ReadUInt32(); record.offset = fontReader.ReadUInt32();
Cmap.records.add(record); Cmap.records.add(record);
} }
for (int i = 0; i < Cmap.numTables; ++i) { for (int i = 0; i < Cmap.numTables; ++i) {
int fmtOffset = Cmap.records.get(i).offset; int fmtOffset = Cmap.records.get(i).offset;
FontReader.Index = Temp.offset + fmtOffset; fontReader.index = Temp.offset + fmtOffset;
int EndIndex = FontReader.Index; int EndIndex = fontReader.index;
int format = FontReader.ReadUInt16(); int format = fontReader.ReadUInt16();
if (Cmap.tables.containsKey(fmtOffset)) continue; if (Cmap.tables.containsKey(fmtOffset)) continue;
if (format == 0) { if (format == 0) {
CmapFormat f = new CmapFormat(); CmapFormat f = new CmapFormat();
f.format = format; f.format = format;
f.length = FontReader.ReadUInt16(); f.length = fontReader.ReadUInt16();
f.language = FontReader.ReadUInt16(); f.language = fontReader.ReadUInt16();
f.glyphIdArray = FontReader.GetBytes(f.length - 6); f.glyphIdArray = fontReader.GetBytes(f.length - 6);
Cmap.tables.put(fmtOffset, f); Cmap.tables.put(fmtOffset, f);
} else if (format == 4) { } else if (format == 4) {
CmapFormat4 f = new CmapFormat4(); CmapFormat4 f = new CmapFormat4();
f.format = format; f.format = format;
f.length = FontReader.ReadUInt16(); f.length = fontReader.ReadUInt16();
f.language = FontReader.ReadUInt16(); f.language = fontReader.ReadUInt16();
f.segCountX2 = FontReader.ReadUInt16(); f.segCountX2 = fontReader.ReadUInt16();
int segCount = f.segCountX2 >> 1; int segCount = f.segCountX2 >> 1;
f.searchRange = FontReader.ReadUInt16(); f.searchRange = fontReader.ReadUInt16();
f.entrySelector = FontReader.ReadUInt16(); f.entrySelector = fontReader.ReadUInt16();
f.rangeShift = FontReader.ReadUInt16(); f.rangeShift = fontReader.ReadUInt16();
f.endCode = FontReader.GetUInt16Array(segCount); f.endCode = fontReader.GetUInt16Array(segCount);
f.reservedPad = FontReader.ReadUInt16(); f.reservedPad = fontReader.ReadUInt16();
f.startCode = FontReader.GetUInt16Array(segCount); f.startCode = fontReader.GetUInt16Array(segCount);
f.idDelta = FontReader.GetInt16Array(segCount); f.idDelta = fontReader.GetInt16Array(segCount);
f.idRangeOffset = FontReader.GetUInt16Array(segCount); f.idRangeOffset = fontReader.GetUInt16Array(segCount);
f.glyphIdArray = FontReader.GetUInt16Array((EndIndex + f.length - FontReader.Index) >> 1); f.glyphIdArray = fontReader.GetUInt16Array((EndIndex + f.length - fontReader.index) >> 1);
Cmap.tables.put(fmtOffset, f); Cmap.tables.put(fmtOffset, f);
} else if (format == 6) { } else if (format == 6) {
CmapFormat6 f = new CmapFormat6(); CmapFormat6 f = new CmapFormat6();
f.format = format; f.format = format;
f.length = FontReader.ReadUInt16(); f.length = fontReader.ReadUInt16();
f.language = FontReader.ReadUInt16(); f.language = fontReader.ReadUInt16();
f.firstCode = FontReader.ReadUInt16(); f.firstCode = fontReader.ReadUInt16();
f.entryCount = FontReader.ReadUInt16(); f.entryCount = fontReader.ReadUInt16();
f.glyphIdArray = FontReader.GetUInt16Array(f.entryCount); f.glyphIdArray = fontReader.GetUInt16Array(f.entryCount);
Cmap.tables.put(fmtOffset, f); Cmap.tables.put(fmtOffset, f);
} else if (format == 12) { } else if (format == 12) {
CmapFormat12 f = new CmapFormat12(); CmapFormat12 f = new CmapFormat12();
f.format = format; f.format = format;
f.reserved = FontReader.ReadUInt16(); f.reserved = fontReader.ReadUInt16();
f.length = FontReader.ReadUInt32(); f.length = fontReader.ReadUInt32();
f.language = FontReader.ReadUInt32(); f.language = fontReader.ReadUInt32();
f.numGroups = FontReader.ReadUInt32(); f.numGroups = fontReader.ReadUInt32();
f.groups = new ArrayList<>(f.numGroups); f.groups = new ArrayList<>(f.numGroups);
for (int n = 0; n < f.numGroups; ++n) { for (int n = 0; n < f.numGroups; ++n) {
f.groups.add(Triple.of(FontReader.ReadUInt32(), FontReader.ReadUInt32(), FontReader.ReadUInt32())); f.groups.add(Triple.of(fontReader.ReadUInt32(), fontReader.ReadUInt32(), fontReader.ReadUInt32()));
} }
Cmap.tables.put(fmtOffset, f); Cmap.tables.put(fmtOffset, f);
} }
@ -405,30 +407,30 @@ public class QueryTTF {
} }
} }
// 解析表 glyf (字体轮廓数据表) // 解析表 glyf (字体轮廓数据表)
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (Temp.tag.equals("glyf")) { if (Temp.tag.equals("glyf")) {
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
for (int i = 0; i < maxp.numGlyphs; ++i) { for (int i = 0; i < maxp.numGlyphs; ++i) {
FontReader.Index = Temp.offset + loca.get(i); fontReader.index = Temp.offset + loca.get(i);
short numberOfContours = FontReader.ReadInt16(); short numberOfContours = fontReader.ReadInt16();
if (numberOfContours > 0) { if (numberOfContours > 0) {
GlyfLayout g = new GlyfLayout(); GlyfLayout g = new GlyfLayout();
g.numberOfContours = numberOfContours; g.numberOfContours = numberOfContours;
g.xMin = FontReader.ReadInt16(); g.xMin = fontReader.ReadInt16();
g.yMin = FontReader.ReadInt16(); g.yMin = fontReader.ReadInt16();
g.xMax = FontReader.ReadInt16(); g.xMax = fontReader.ReadInt16();
g.yMax = FontReader.ReadInt16(); g.yMax = fontReader.ReadInt16();
g.endPtsOfContours = FontReader.GetUInt16Array(numberOfContours); g.endPtsOfContours = fontReader.GetUInt16Array(numberOfContours);
g.instructionLength = FontReader.ReadUInt16(); g.instructionLength = fontReader.ReadUInt16();
g.instructions = FontReader.GetBytes(g.instructionLength); g.instructions = fontReader.GetBytes(g.instructionLength);
int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1; int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1;
// 获取轮廓点描述标志 // 获取轮廓点描述标志
g.flags = new byte[flagLength]; g.flags = new byte[flagLength];
for (int n = 0; n < flagLength; ++n) { for (int n = 0; n < flagLength; ++n) {
g.flags[n] = FontReader.GetByte(); g.flags[n] = fontReader.GetByte();
if ((g.flags[n] & 0x08) != 0x00) { if ((g.flags[n] & 0x08) != 0x00) {
for (int m = FontReader.ReadUInt8(); m > 0; --m) { for (int m = fontReader.ReadUInt8(); m > 0; --m) {
g.flags[++n] = g.flags[n - 1]; g.flags[++n] = g.flags[n - 1];
} }
} }
@ -438,9 +440,9 @@ public class QueryTTF {
for (int n = 0; n < flagLength; ++n) { for (int n = 0; n < flagLength; ++n) {
short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1); short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1);
if ((g.flags[n] & 0x02) != 0) { if ((g.flags[n] & 0x02) != 0) {
g.xCoordinates[n] = (short) (same * FontReader.ReadUInt8()); g.xCoordinates[n] = (short) (same * fontReader.ReadUInt8());
} else { } else {
g.xCoordinates[n] = same == 1 ? (short) 0 : FontReader.ReadInt16(); g.xCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16();
} }
} }
// 获取轮廓点描述y轴相对值 // 获取轮廓点描述y轴相对值
@ -448,9 +450,9 @@ public class QueryTTF {
for (int n = 0; n < flagLength; ++n) { for (int n = 0; n < flagLength; ++n) {
short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1); short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1);
if ((g.flags[n] & 0x04) != 0) { if ((g.flags[n] & 0x04) != 0) {
g.yCoordinates[n] = (short) (same * FontReader.ReadUInt8()); g.yCoordinates[n] = (short) (same * fontReader.ReadUInt8());
} else { } else {
g.yCoordinates[n] = same == 1 ? (short) 0 : FontReader.ReadInt16(); g.yCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16();
} }
} }
// 相对坐标转绝对坐标 // 相对坐标转绝对坐标
@ -477,11 +479,11 @@ public class QueryTTF {
for (short b : glyf.get(gid).xCoordinates) sb.append(b); for (short b : glyf.get(gid).xCoordinates) sb.append(b);
for (short b : glyf.get(gid).yCoordinates) sb.append(b); for (short b : glyf.get(gid).yCoordinates) sb.append(b);
String val = sb.toString(); String val = sb.toString();
if (LimitMix == 0) LimitMix = key; if (limitMix == 0) limitMix = key;
LimitMax = key; limitMax = key;
CodeToGlyph.put(key, val); codeToGlyph.put(key, val);
if (GlyphToCode.containsKey(val)) continue; if (glyphToCode.containsKey(val)) continue;
GlyphToCode.put(val, key); glyphToCode.put(val, key);
} }
} }
@ -492,15 +494,15 @@ public class QueryTTF {
* @return 返回查询结果字符串 * @return 返回查询结果字符串
*/ */
public String getNameById(int nameId) { public String getNameById(int nameId) {
for (Directory Temp : Directorys) { for (Directory Temp : directorys) {
if (!Temp.tag.equals("name")) continue; if (!Temp.tag.equals("name")) continue;
FontReader.Index = Temp.offset; fontReader.index = Temp.offset;
break; break;
} }
for (NameRecord record : Name.records) { for (NameRecord record : name.records) {
if (record.nameID != nameId) continue; if (record.nameID != nameId) continue;
FontReader.Index += Name.stringOffset + record.offset; fontReader.index += name.stringOffset + record.offset;
return FontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE); return fontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE);
} }
return "error"; return "error";
} }
@ -514,7 +516,7 @@ public class QueryTTF {
private int getGlyfIndex(int code) { private int getGlyfIndex(int code) {
if (code == 0) return 0; if (code == 0) return 0;
int fmtKey = 0; int fmtKey = 0;
for (@SuppressWarnings("unchecked") Pair<Integer, Integer> item : pps) { for (Pair<Integer, Integer> item : pps) {
for (CmapRecord record : Cmap.records) { for (CmapRecord record : Cmap.records) {
if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) { if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) {
fmtKey = record.offset; fmtKey = record.offset;
@ -527,6 +529,7 @@ public class QueryTTF {
int glyfID = 0; int glyfID = 0;
CmapFormat table = Cmap.tables.get(fmtKey); CmapFormat table = Cmap.tables.get(fmtKey);
assert table != null;
int fmt = table.format; int fmt = table.format;
if (fmt == 0) { if (fmt == 0) {
if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF; if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF;
@ -575,7 +578,7 @@ public class QueryTTF {
* @return 返回bool查询结果 * @return 返回bool查询结果
*/ */
public boolean inLimit(char code) { public boolean inLimit(char code) {
return (LimitMix <= code) && (code < LimitMax); return (limitMix <= code) && (code < limitMax);
} }
/** /**
@ -585,7 +588,7 @@ public class QueryTTF {
* @return 返回轮廓数组的String值 * @return 返回轮廓数组的String值
*/ */
public String getGlyfByCode(int key) { public String getGlyfByCode(int key) {
return CodeToGlyph.getOrDefault(key, ""); return codeToGlyph.getOrDefault(key, "");
} }
/** /**
@ -595,6 +598,7 @@ public class QueryTTF {
* @return 返回Unicode十进制值 * @return 返回Unicode十进制值
*/ */
public int getCodeByGlyf(String val) { public int getCodeByGlyf(String val) {
return GlyphToCode.getOrDefault(val, 0); //noinspection ConstantConditions
return glyphToCode.getOrDefault(val, 0);
} }
} }

@ -277,10 +277,10 @@ class AnalyzeTxtFile {
} }
private fun getTocRules(): List<TxtTocRule> { private fun getTocRules(): List<TxtTocRule> {
var rules = appDb.txtTocRule.enabled var rules = appDb.txtTocRuleDao.enabled
if (rules.isEmpty()) { if (rules.isEmpty()) {
rules = DefaultData.txtTocRules.apply { rules = DefaultData.txtTocRules.apply {
appDb.txtTocRule.insert(*this.toTypedArray()) appDb.txtTocRuleDao.insert(*this.toTypedArray())
}.filter { }.filter {
it.enable it.enable
} }

@ -1,185 +0,0 @@
package io.legado.app.model.localBook
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.text.TextUtils
import io.legado.app.data.entities.BookChapter
import io.legado.app.utils.*
import nl.siegmann.epublib.domain.Book
import nl.siegmann.epublib.domain.TOCReference
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
class EPUBFile(val book: io.legado.app.data.entities.Book) {
companion object {
private var eFile: EPUBFile? = null
@Synchronized
private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile {
if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) {
eFile = EPUBFile(book)
return eFile!!
}
return eFile!!
}
@Synchronized
fun getChapterList(book: io.legado.app.data.entities.Book): ArrayList<BookChapter> {
return getEFile(book).getChapterList()
}
@Synchronized
fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? {
return getEFile(book).getContent(chapter)
}
@Synchronized
fun getImage(
book: io.legado.app.data.entities.Book,
href: String
): InputStream? {
return getEFile(book).getImage(href)
}
}
private var epubBook: Book? = null
private var mCharset: Charset = Charset.defaultCharset()
init {
try {
val epubReader = EpubReader()
val inputStream = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl)
appCtx.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
epubBook = epubReader.readEpub(inputStream)
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
appCtx.externalFilesDir,
"covers",
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
)
}
if (!File(book.coverUrl!!).exists()) {
epubBook!!.coverImage?.inputStream?.use {
val cover = BitmapFactory.decodeStream(it)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getContent(chapter: BookChapter): String? {
epubBook?.let { eBook ->
val resource = eBook.resources.getByHref(chapter.url)
val doc = Jsoup.parse(String(resource.data, mCharset))
val elements = doc.body().children()
elements.select("script").remove()
elements.select("style").remove()
return elements.outerHtml().htmlFormat()
}
return null
}
private fun getImage(href: String): InputStream? {
val abHref = href.replace("../", "")
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook ->
val metadata = eBook.metadata
book.name = metadata.firstTitle
if (metadata.authors.size > 0) {
val author =
metadata.authors[0].toString().replace("^, |, $".toRegex(), "")
book.author = author
}
if (metadata.descriptions.size > 0) {
book.intro = Jsoup.parse(metadata.descriptions[0]).text()
}
val refs = eBook.tableOfContents.tocReferences
if (refs == null || refs.isEmpty()) {
val spineReferences = eBook.spine.spineReferences
var i = 0
val size = spineReferences.size
while (i < size) {
val resource =
spineReferences[i].resource
var title = resource.title
if (TextUtils.isEmpty(title)) {
try {
val doc =
Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title")
if (elements.size > 0) {
title = elements[0].text()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
val chapter = BookChapter()
chapter.index = i
chapter.bookUrl = book.bookUrl
chapter.url = resource.href
if (i == 0 && title.isEmpty()) {
chapter.title = "封面"
} else {
chapter.title = title
}
chapterList.add(chapter)
i++
}
} else {
parseMenu(chapterList, refs, 0)
for (i in chapterList.indices) {
chapterList[i].index = i
}
}
}
book.latestChapterTitle = chapterList.lastOrNull()?.title
book.totalChapterNum = chapterList.size
return chapterList
}
private fun parseMenu(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?,
level: Int
) {
if (refs == null) return
for (ref in refs) {
if (ref.resource != null) {
val chapter = BookChapter()
chapter.bookUrl = book.bookUrl
chapter.title = ref.title
chapter.url = ref.completeHref
chapterList.add(chapter)
}
if (ref.children != null && ref.children.isNotEmpty()) {
parseMenu(chapterList, ref.children, level + 1)
}
}
}
}

@ -0,0 +1,227 @@
package io.legado.app.model.localBook
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.text.TextUtils
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.utils.FileUtils
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFilesDir
import me.ag2s.epublib.domain.EpubBook
import me.ag2s.epublib.epub.EpubReader
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
import java.util.zip.ZipFile
class EpubFile(var book: Book) {
companion object {
private var eFile: EpubFile? = null
@Synchronized
private fun getEFile(book: Book): EpubFile {
BookHelp.getEpubFile(book)
if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) {
eFile = EpubFile(book)
return eFile!!
}
eFile?.book = book
return eFile!!
}
@Synchronized
fun getChapterList(book: Book): ArrayList<BookChapter> {
return getEFile(book).getChapterList()
}
@Synchronized
fun getContent(book: Book, chapter: BookChapter): String? {
return getEFile(book).getContent(chapter)
}
@Synchronized
fun getImage(
book: Book,
href: String
): InputStream? {
return getEFile(book).getImage(href)
}
@Synchronized
fun upBookInfo(book: Book) {
return getEFile(book).upBookInfo()
}
}
private var mCharset: Charset = Charset.defaultCharset()
private var epubBook: EpubBook? = null
get() {
if (field != null) {
return field
}
field = readEpub()
return field
}
init {
try {
epubBook?.let {
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
appCtx.externalFilesDir,
"covers",
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
)
}
if (!File(book.coverUrl!!).exists()) {
/*部分书籍DRM处理后,封面获取异常,待优化*/
it.coverImage?.inputStream?.use { input ->
val cover = BitmapFactory.decodeStream(input)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/
private fun readEpub(): EpubBook? {
try {
val file = BookHelp.getEpubFile(book)
//通过懒加载读取epub
return EpubReader().readEpubLazy(ZipFile(file), "utf-8")
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
private fun getContent(chapter: BookChapter): String? {
/*获取当前章节文本*/
return getChildChapter(chapter, chapter.url)
}
private fun getChildChapter(chapter: BookChapter, href: String): String? {
epubBook?.let {
val body = Jsoup.parse(String(it.resources.getByHref(href).data, mCharset)).body()
if (chapter.url == href) {
val startFragmentId = chapter.startFragmentId
val endFragmentId = chapter.endFragmentId
/*一些书籍依靠href索引的resource会包含多个章节,需要依靠fragmentId来截取到当前章节的内容*/
/*注:这里较大增加了内容加载的时间,所以首次获取内容后可存储到本地cache,减少重复加载*/
if (!startFragmentId.isNullOrBlank())
body.getElementById(startFragmentId)?.previousElementSiblings()?.remove()
if (!endFragmentId.isNullOrBlank() && endFragmentId != startFragmentId)
body.getElementById(endFragmentId)?.nextElementSiblings()?.remove()
}
/*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/
var tag = Book.hTag
if (book.getDelTag(tag)) {
body.getElementsByTag("h1")?.remove()
body.getElementsByTag("h2")?.remove()
body.getElementsByTag("h3")?.remove()
body.getElementsByTag("h4")?.remove()
body.getElementsByTag("h5")?.remove()
body.getElementsByTag("h6")?.remove()
//body.getElementsMatchingOwnText(chapter.title)?.remove()
}
/*选择去除正文中的img标签,目前图片支持效果待优化*/
tag = Book.imgTag
if (book.getDelTag(tag)) {
body.getElementsByTag("img")?.remove()
}
val elements = body.children()
elements.select("script").remove()
elements.select("style").remove()
/*选择去除正文中的ruby标签,目前注释支持效果待优化*/
tag = Book.rubyTag
var html = elements.outerHtml()
if (book.getDelTag(tag)) {
html = html.replace("<ruby>\\s?([\\u4e00-\\u9fa5])\\s?.*?</ruby>".toRegex(), "$1")
}
return HtmlFormatter.formatKeepImg(html)
}
return null
}
private fun getImage(href: String): InputStream? {
val abHref = href.replace("../", "")
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
private fun upBookInfo() {
if (epubBook == null) {
eFile = null
book.intro = "书籍导入异常"
} else {
val metadata = epubBook!!.metadata
book.name = metadata.firstTitle
if (book.name.isEmpty()) {
book.name = book.originName.replace(".epub", "")
}
if (metadata.authors.size > 0) {
val author =
metadata.authors[0].toString().replace("^, |, $".toRegex(), "")
book.author = author
}
if (metadata.descriptions.size > 0) {
book.intro = Jsoup.parse(metadata.descriptions[0]).text()
}
}
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource ->
var title = resource.title
if (TextUtils.isEmpty(title)) {
try {
val doc =
Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title")
if (elements != null && elements.size > 0) {
title = elements[0].text()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
val chapter = BookChapter()
chapter.index = index
chapter.bookUrl = book.bookUrl
chapter.url = resource.href
if (index == 0 && title.isEmpty()) {
chapter.title = "封面"
} else {
chapter.title = title
}
chapterList.add(chapter)
}
book.latestChapterTitle = chapterList.lastOrNull()?.title
book.totalChapterNum = chapterList.size
return chapterList
}
}

@ -9,6 +9,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.util.regex.Pattern
object LocalBook { object LocalBook {
@ -22,7 +23,7 @@ object LocalBook {
fun getChapterList(book: Book): ArrayList<BookChapter> { fun getChapterList(book: Book): ArrayList<BookChapter> {
return if (book.isEpub()) { return if (book.isEpub()) {
EPUBFile.getChapterList(book) EpubFile.getChapterList(book)
} else { } else {
AnalyzeTxtFile().analyze(book) AnalyzeTxtFile().analyze(book)
} }
@ -30,7 +31,7 @@ object LocalBook {
fun getContext(book: Book, chapter: BookChapter): String? { fun getContext(book: Book, chapter: BookChapter): String? {
return if (book.isEpub()) { return if (book.isEpub()) {
EPUBFile.getContent(book, chapter) EpubFile.getContent(book, chapter)
} else { } else {
AnalyzeTxtFile.getContent(book, chapter) AnalyzeTxtFile.getContent(book, chapter)
} }
@ -55,23 +56,66 @@ object LocalBook {
path = uri.path!! path = uri.path!!
File(path).name File(path).name
} }
val str = fileName.substringBeforeLast(".")
val authorIndex = str.indexOf("作者") val name: String
var name: String val author: String
var author: String
if (authorIndex == -1) { if (("" in fileName && "" in fileName)
name = str || "作者" in fileName
author = "" || (fileName.contains(" by ", true))
) {
//匹配(知轩藏书常用格式) 《书名》其它信息作者:作者名.txt
val m1 = Pattern
.compile("《(.*?)》.*?作者:(.*?)\\.txt")
.matcher(fileName)
//匹配 书名 by 作者名.txt
val m2 = Pattern
.compile("txt\\.(.*?) yb (.*?)$")
.matcher(fileName.reversed())
if (m1.find()) {
name = m1.group(1) ?: fileName.replace(".txt", "")
author = m1.group(2) ?: ""
BookHelp.formatBookAuthor(author)
} else if (m2.find()) {
var temp = m2.group(2)
name = if (temp==null||temp == "") {
fileName.replace(".txt", "")
} else {
temp.reversed()
}
temp = m2.group(1) ?: ""
author = temp.reversed()
BookHelp.formatBookAuthor(author)
} else {
val st = fileName.indexOf("")
val e = fileName.indexOf("")
name = if (e > st && st != -1) {
fileName.substring(st + 1, e)
} else {
fileName.replace(".txt", "")
}
val s = fileName.indexOf("作者")
author = if (s != -1 && s + 2 < fileName.length) {
fileName.substring(s + 2).replace(".txt", "")
} else {
""
}
BookHelp.formatBookAuthor(author)
}
} else { } else {
name = str.substring(0, authorIndex)
author = str.substring(authorIndex) name = fileName.replace(".txt", "")
author = BookHelp.formatBookAuthor(author) author = ""
}
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
} }
val book = Book( val book = Book(
bookUrl = path, bookUrl = path,
name = name, name = name,
@ -83,6 +127,7 @@ object LocalBook {
"${MD5Utils.md5Encode16(path)}.jpg" "${MD5Utils.md5Encode16(path)}.jpg"
) )
) )
if (book.isEpub()) EpubFile.upBookInfo(book)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
return book return book
} }
@ -93,6 +138,13 @@ object LocalBook {
val bookFile = FileUtils.getFile(cacheFolder, book.originName) val bookFile = FileUtils.getFile(cacheFolder, book.originName)
bookFile.delete() bookFile.delete()
} }
if(book.isEpub()){
val bookFile=BookHelp.getEpubFile(book).parentFile
if (bookFile!=null&&bookFile.exists()){
FileUtils.delete(bookFile,true)
}
}
if (deleteOriginal) { if (deleteOriginal) {
if (book.bookUrl.isContentScheme()) { if (book.bookUrl.isContentScheme()) {

@ -3,6 +3,7 @@ package io.legado.app.model.rss
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.RuleData import io.legado.app.model.analyzeRule.RuleData
@ -38,7 +39,7 @@ object Rss {
scope: CoroutineScope, scope: CoroutineScope,
rssArticle: RssArticle, rssArticle: RssArticle,
ruleContent: String, ruleContent: String,
rssSource: RssSource?, rssSource: RssSource,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<String> { ): Coroutine<String> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
@ -46,9 +47,11 @@ object Rss {
rssArticle.link, rssArticle.link,
baseUrl = rssArticle.origin, baseUrl = rssArticle.origin,
ruleData = rssArticle, ruleData = rssArticle,
headerMapF = rssSource?.getHeaderMap() headerMapF = rssSource.getHeaderMap()
) )
val body = analyzeUrl.getStrResponse(rssArticle.origin).body val body = analyzeUrl.getStrResponse(rssArticle.origin).body
Debug.log(rssSource.sourceUrl, "≡获取成功:${rssSource.sourceUrl}")
Debug.log(rssSource.sourceUrl, body, state = 20)
val analyzeRule = AnalyzeRule(rssArticle) val analyzeRule = AnalyzeRule(rssArticle)
analyzeRule.setContent(body) analyzeRule.setContent(body)
.setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)) .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link))

@ -31,6 +31,7 @@ object RssParserByRule {
) )
} }
Debug.log(sourceUrl, "≡获取成功:$sourceUrl") Debug.log(sourceUrl, "≡获取成功:$sourceUrl")
Debug.log(sourceUrl, body, state = 10)
var ruleArticles = rssSource.ruleArticles var ruleArticles = rssSource.ruleArticles
if (ruleArticles.isNullOrBlank()) { if (ruleArticles.isNullOrBlank()) {
Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析") Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析")
@ -39,6 +40,7 @@ object RssParserByRule {
val articleList = mutableListOf<RssArticle>() val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule(ruleData) val analyzeRule = AnalyzeRule(ruleData)
analyzeRule.setContent(body).setBaseUrl(sortUrl) analyzeRule.setContent(body).setBaseUrl(sortUrl)
analyzeRule.setRedirectUrl(sortUrl)
var reverse = false var reverse = false
if (ruleArticles.startsWith("-")) { if (ruleArticles.startsWith("-")) {
reverse = true reverse = true
@ -49,7 +51,7 @@ object RssParserByRule {
Debug.log(sourceUrl, "└列表大小:${collections.size}") Debug.log(sourceUrl, "└列表大小:${collections.size}")
if (!rssSource.ruleNextPage.isNullOrEmpty()) { if (!rssSource.ruleNextPage.isNullOrEmpty()) {
Debug.log(sourceUrl, "┌获取下一页链接") Debug.log(sourceUrl, "┌获取下一页链接")
if (rssSource.ruleNextPage!!.toUpperCase(Locale.getDefault()) == "PAGE") { if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == "PAGE") {
nextUrl = sortUrl nextUrl = sortUrl
} else { } else {
nextUrl = analyzeRule.getString(rssSource.ruleNextPage) nextUrl = analyzeRule.getString(rssSource.ruleNextPage)

@ -6,16 +6,16 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.TocRule import io.legado.app.data.entities.rule.TocRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object BookChapterList { object BookChapterList {
@ -24,151 +24,83 @@ object BookChapterList {
book: Book, book: Book,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String baseUrl: String,
): List<BookChapter> = suspendCancellableCoroutine { block -> redirectUrl: String
kotlin.runCatching { ): List<BookChapter> {
val chapterList = ArrayList<BookChapter>() val chapterList = ArrayList<BookChapter>()
body ?: throw Exception( body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
Debug.log(bookSource.bookSourceUrl, body, state = 30)
val tocRule = bookSource.getTocRule()
val nextUrlList = arrayListOf(baseUrl)
var reverse = false
var listRule = tocRule.chapterList ?: ""
if (listRule.startsWith("-")) {
reverse = true
listRule = listRule.substring(1)
}
if (listRule.startsWith("+")) {
listRule = listRule.substring(1)
}
var chapterData =
analyzeChapterList(
scope, book, baseUrl, redirectUrl, body,
tocRule, listRule, bookSource, log = true
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") chapterData.chapterList?.let {
chapterList.addAll(it)
val tocRule = bookSource.getTocRule() }
val nextUrlList = arrayListOf(baseUrl) when (chapterData.nextUrl.size) {
var reverse = false 0 -> Unit
var listRule = tocRule.chapterList ?: "" 1 -> {
if (listRule.startsWith("-")) { var nextUrl = chapterData.nextUrl[0]
reverse = true while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
listRule = listRule.substring(1) nextUrlList.add(nextUrl)
} AnalyzeUrl(
if (listRule.startsWith("+")) { ruleUrl = nextUrl,
listRule = listRule.substring(1) book = book,
} headerMapF = bookSource.getHeaderMap()
var chapterData = ).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
analyzeChapterList( chapterData = analyzeChapterList(
scope, book, baseUrl, body, tocRule, listRule, bookSource, log = true scope, book, nextUrl, nextUrl,
) nextBody, tocRule, listRule, bookSource
chapterData.chapterList?.let { )
chapterList.addAll(it) nextUrl = chapterData.nextUrl.firstOrNull() ?: ""
} chapterData.chapterList?.let {
when (chapterData.nextUrl.size) { chapterList.addAll(it)
0 -> {
block.resume(finish(book, chapterList, reverse))
}
1 -> {
Coroutine.async(scope = scope) {
var nextUrl = chapterData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
nextUrlList.add(nextUrl)
AnalyzeUrl(
ruleUrl = nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
chapterData = analyzeChapterList(
this, book, nextUrl, nextBody, tocRule, listRule, bookSource
)
nextUrl = chapterData.nextUrl.firstOrNull() ?: ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
}
} }
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
block.resume(finish(book, chapterList, reverse))
}.onError {
block.resumeWithException(it)
} }
} }
else -> { Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
val chapterDataList = arrayListOf<ChapterData<String>>() }
for (item in chapterData.nextUrl) { else -> {
if (!nextUrlList.contains(item)) { Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${chapterData.nextUrl.size}")
val data = ChapterData(nextUrl = item) withContext(IO) {
chapterDataList.add(data) val asyncArray = Array(chapterData.nextUrl.size) {
nextUrlList.add(item) async(IO) {
} val urlStr = chapterData.nextUrl[it]
} val analyzeUrl = AnalyzeUrl(
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") ruleUrl = urlStr,
for (item in chapterDataList) { book = book,
downloadToc( headerMapF = bookSource.getHeaderMap()
scope, item, book, bookSource, )
tocRule, listRule, chapterList, chapterDataList, val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
{ analyzeChapterList(
block.resume(finish(book, chapterList, reverse)) this, book, urlStr, res.url,
} res.body!!, tocRule, listRule, bookSource, false
) { ).chapterList
block.cancel(it)
} }
} }
} asyncArray.forEach { coroutine ->
} coroutine.await()?.let {
}.onFailure {
block.cancel(it)
}
}
private fun downloadToc(
scope: CoroutineScope,
chapterData: ChapterData<String>,
book: Book,
bookSource: BookSource,
tocRule: TocRule,
listRule: String,
chapterList: ArrayList<BookChapter>,
chapterDataList: ArrayList<ChapterData<String>>,
onFinish: () -> Unit,
onError: (error: Throwable) -> Unit
) {
Coroutine.async(scope = scope) {
val nextBody = AnalyzeUrl(
ruleUrl = chapterData.nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body
?: throw Exception("${chapterData.nextUrl}, 下载失败")
val nextChapterData = analyzeChapterList(
this, book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource, false
)
synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish(
chapterDataList,
chapterData,
nextChapterData.chapterList
)
if (isFinished) {
chapterDataList.forEach { item ->
item.chapterList?.let {
chapterList.addAll(it) chapterList.addAll(it)
} }
} }
onFinish()
} }
} }
}.onError {
onError.invoke(it)
} }
}
private fun addChapterListIsFinish(
chapterDataList: ArrayList<ChapterData<String>>,
chapterData: ChapterData<String>,
chapterList: List<BookChapter>?
): Boolean {
chapterData.chapterList = chapterList
chapterDataList.forEach {
if (it.chapterList == null) {
return false
}
}
return true
}
private fun finish(
book: Book,
chapterList: ArrayList<BookChapter>,
reverse: Boolean
): ArrayList<BookChapter> {
//去重 //去重
if (!reverse) { if (!reverse) {
chapterList.reverse() chapterList.reverse()
@ -195,6 +127,7 @@ object BookChapterList {
scope: CoroutineScope, scope: CoroutineScope,
book: Book, book: Book,
baseUrl: String, baseUrl: String,
redirectUrl: String,
body: String, body: String,
tocRule: TocRule, tocRule: TocRule,
listRule: String, listRule: String,
@ -204,7 +137,13 @@ object BookChapterList {
): ChapterData<List<String>> { ): ChapterData<List<String>> {
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(redirectUrl)
//获取目录列表
val chapterList = arrayListOf<BookChapter>() val chapterList = arrayListOf<BookChapter>()
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
//获取下一页链接
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
val nextTocRule = tocRule.nextTocUrl val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) { if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
@ -222,11 +161,9 @@ object BookChapterList {
log log
) )
} }
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
scope.ensureActive() scope.ensureActive()
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log)
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
@ -251,11 +188,12 @@ object BookChapterList {
chapterList.add(bookChapter) chapterList.add(bookChapter)
} }
} }
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) Debug.log(bookSource.bookSourceUrl, "└解析目录列表完成", log)
Debug.log(bookSource.bookSourceUrl, "┌首章名称", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log)
Debug.log(bookSource.bookSourceUrl, "获取首章链接", log) Debug.log(bookSource.bookSourceUrl, "┌首章链接", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log)
Debug.log(bookSource.bookSourceUrl, "获取首章信息", log) Debug.log(bookSource.bookSourceUrl, "┌首章信息", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log)
} }
return ChapterData(chapterList, nextUrlList) return ChapterData(chapterList, nextUrlList)

@ -10,9 +10,12 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
@ -26,40 +29,48 @@ object BookContent {
bookChapter: BookChapter, bookChapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String, baseUrl: String,
nextChapterUrlF: String? = null redirectUrl: String,
nextChapterUrl: String? = null
): String { ): String {
body ?: throw Exception( body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
Debug.log(bookSource.bookSourceUrl, body, state = 40)
val mNextChapterUrl = if (!nextChapterUrl.isNullOrEmpty()) {
nextChapterUrl
} else {
appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url
}
val content = StringBuilder() val content = StringBuilder()
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule() val contentRule = bookSource.getContentRule()
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = mNextChapterUrl
scope.ensureActive()
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
) )
content.append(contentData.content).append("\n") content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
nextChapterUrlF
else
appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
if (!nextChapterUrl.isNullOrEmpty() if (!mNextChapterUrl.isNullOrEmpty()
&& NetworkUtils.getAbsoluteURL(baseUrl, nextUrl) && NetworkUtils.getAbsoluteURL(baseUrl, nextUrl)
== NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl) == NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl)
) break ) break
nextUrlList.add(nextUrl) nextUrlList.add(nextUrl)
AnalyzeUrl( scope.ensureActive()
val res = AnalyzeUrl(
ruleUrl = nextUrl, ruleUrl = nextUrl,
book = book, book = book,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody -> ).getStrResponse(bookSource.bookSourceUrl)
res.body?.let { nextBody ->
contentData = analyzeContent( contentData = analyzeContent(
book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false book, nextUrl, res.url, nextBody, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
) )
nextUrl = nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
@ -68,31 +79,31 @@ object BookContent {
} }
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) { } else if (contentData.nextUrl.size > 1) {
val contentDataList = arrayListOf<ContentData<String>>() Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${contentData.nextUrl.size}")
for (item in contentData.nextUrl) { withContext(IO) {
if (!nextUrlList.contains(item)) val asyncArray = Array(contentData.nextUrl.size) {
contentDataList.add(ContentData(nextUrl = item)) async(IO) {
} val urlStr = contentData.nextUrl[it]
for (item in contentDataList) { val analyzeUrl = AnalyzeUrl(
withContext(scope.coroutineContext) { ruleUrl = urlStr,
AnalyzeUrl( book = book,
ruleUrl = item.nextUrl, headerMapF = bookSource.getHeaderMap()
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let {
contentData = analyzeContent(
book, item.nextUrl, it, contentRule, bookChapter, bookSource, false
) )
item.content = contentData.content val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
analyzeContent(
book, urlStr, res.url, res.body!!, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
).content
} }
} }
} asyncArray.forEach { coroutine ->
for (item in contentDataList) { scope.ensureActive()
content.append(item.content).append("\n") content.append(coroutine.await()).append("\n")
}
} }
} }
content.deleteCharAt(content.length - 1) content.deleteCharAt(content.length - 1)
var contentStr = content.toString().htmlFormat() var contentStr = content.toString()
val replaceRegex = contentRule.replaceRegex val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) { if (!replaceRegex.isNullOrEmpty()) {
contentStr = analyzeRule.getString(replaceRegex, value = contentStr) contentStr = analyzeRule.getString(replaceRegex, value = contentStr)
@ -111,16 +122,24 @@ object BookContent {
private fun analyzeContent( private fun analyzeContent(
book: Book, book: Book,
baseUrl: String, baseUrl: String,
redirectUrl: String,
body: String, body: String,
contentRule: ContentRule, contentRule: ContentRule,
chapter: BookChapter, chapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
nextChapterUrl: String?,
printLog: Boolean = true printLog: Boolean = true
): ContentData<List<String>> { ): ContentData<List<String>> {
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body, baseUrl)
val rUrl = analyzeRule.setRedirectUrl(redirectUrl)
analyzeRule.nextChapterUrl = nextChapterUrl
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
analyzeRule.chapter = chapter analyzeRule.chapter = chapter
//获取正文
var content = analyzeRule.getString(contentRule.content)
content = HtmlFormatter.formatKeepImg(content, rUrl)
//获取下一页链接
val nextUrlRule = contentRule.nextContentUrl val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) { if (!nextUrlRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog) Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog)
@ -129,7 +148,6 @@ object BookContent {
} }
Debug.log(bookSource.bookSourceUrl, "" + nextUrlList.joinToString(""), printLog) Debug.log(bookSource.bookSourceUrl, "" + nextUrlList.joinToString(""), printLog)
} }
val content = analyzeRule.getString(contentRule.content)
return ContentData(content, nextUrlList) return ContentData(content, nextUrlList)
} }
} }

@ -6,9 +6,9 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import splitties.init.appCtx import splitties.init.appCtx
@ -22,15 +22,31 @@ object BookInfo {
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String, baseUrl: String,
redirectUrl: String,
canReName: Boolean, canReName: Boolean,
) { ) {
body ?: throw Exception( body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val infoRule = bookSource.getBookInfoRule() Debug.log(bookSource.bookSourceUrl, body, state = 20)
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(redirectUrl)
analyzeBookInfo(scope, book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName)
}
fun analyzeBookInfo(
scope: CoroutineScope,
book: Book,
body: String,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
baseUrl: String,
redirectUrl: String,
canReName: Boolean,
) {
val infoRule = bookSource.getBookInfoRule()
infoRule.init?.let { infoRule.init?.let {
if (it.isNotBlank()) { if (it.isNotBlank()) {
scope.ensureActive() scope.ensureActive()
@ -78,13 +94,13 @@ object BookInfo {
scope.ensureActive() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
analyzeRule.getString(infoRule.intro).let { analyzeRule.getString(infoRule.intro).let {
if (it.isNotEmpty()) book.intro = it.htmlFormat() if (it.isNotEmpty()) book.intro = HtmlFormatter.format(it)
} }
Debug.log(bookSource.bookSourceUrl, "${book.intro}") Debug.log(bookSource.bookSourceUrl, "${book.intro}")
scope.ensureActive() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
analyzeRule.getString(infoRule.coverUrl).let { analyzeRule.getString(infoRule.coverUrl).let {
if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(redirectUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
scope.ensureActive() scope.ensureActive()

@ -1,6 +1,7 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.BookListRule import io.legado.app.data.entities.rule.BookListRule
@ -8,9 +9,9 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import splitties.init.appCtx import splitties.init.appCtx
@ -35,13 +36,15 @@ object BookList {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
Debug.log(bookSource.bookSourceUrl, body, state = 10)
val analyzeRule = AnalyzeRule(variableBook) val analyzeRule = AnalyzeRule(variableBook)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
bookSource.bookUrlPattern?.let { bookSource.bookUrlPattern?.let {
scope.ensureActive() scope.ensureActive()
if (baseUrl.matches(it.toRegex())) { if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) getInfoItem(scope, body, analyzeRule, bookSource, baseUrl, variableBook.variable)
?.let { searchBook -> ?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
@ -69,7 +72,7 @@ object BookList {
scope.ensureActive() scope.ensureActive()
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) getInfoItem(scope, body, analyzeRule, bookSource, baseUrl, variableBook.variable)
?.let { searchBook -> ?.let { searchBook ->
searchBook.infoHtml = body searchBook.infoHtml = body
bookList.add(searchBook) bookList.add(searchBook)
@ -118,57 +121,31 @@ object BookList {
@Throws(Exception::class) @Throws(Exception::class)
private fun getInfoItem( private fun getInfoItem(
scope: CoroutineScope, scope: CoroutineScope,
body: String,
analyzeRule: AnalyzeRule, analyzeRule: AnalyzeRule,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String, baseUrl: String,
variable: String? variable: String?
): SearchBook? { ): SearchBook? {
val searchBook = SearchBook(variable = variable) val book = Book(variable = variable)
searchBook.bookUrl = baseUrl book.bookUrl = baseUrl
searchBook.origin = bookSource.bookSourceUrl book.origin = bookSource.bookSourceUrl
searchBook.originName = bookSource.bookSourceName book.originName = bookSource.bookSourceName
searchBook.originOrder = bookSource.customOrder book.originOrder = bookSource.customOrder
searchBook.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
analyzeRule.book = searchBook analyzeRule.book = book
with(bookSource.getBookInfoRule()) { BookInfo.analyzeBookInfo(
init?.let { scope,
if (it.isNotEmpty()) { book,
scope.ensureActive() body,
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") analyzeRule,
analyzeRule.setContent(analyzeRule.getElement(it)) bookSource,
} baseUrl,
} baseUrl,
scope.ensureActive() false
Debug.log(bookSource.bookSourceUrl, "┌获取书名") )
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(name)) if (book.name.isNotBlank()) {
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}") return book.toSearchBook()
if (searchBook.name.isNotEmpty()) {
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = wordCountFormat(analyzeRule.getString(wordCount))
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
return searchBook
}
} }
return null return null
} }
@ -221,7 +198,7 @@ object BookList {
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
scope.ensureActive() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat() searchBook.intro = HtmlFormatter.format(analyzeRule.getString(ruleIntro))
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log)
scope.ensureActive() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)

@ -127,6 +127,7 @@ class WebBook(val bookSource: BookSource) {
book.infoHtml, book.infoHtml,
bookSource, bookSource,
book.bookUrl, book.bookUrl,
book.bookUrl,
canReName canReName
) )
} else { } else {
@ -136,7 +137,15 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap(), headerMapF = bookSource.getHeaderMap(),
book = book book = book
).getStrResponse(bookSource.bookSourceUrl) ).getStrResponse(bookSource.bookSourceUrl)
BookInfo.analyzeBookInfo(scope, book, res.body, bookSource, book.bookUrl, canReName) BookInfo.analyzeBookInfo(
scope,
book,
res.body,
bookSource,
book.bookUrl,
res.url,
canReName
)
} }
return book return book
} }
@ -160,7 +169,14 @@ class WebBook(val bookSource: BookSource) {
): List<BookChapter> { ): List<BookChapter> {
book.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
BookChapterList.analyzeChapterList(scope, book, book.tocHtml, bookSource, book.tocUrl) BookChapterList.analyzeChapterList(
scope,
book,
book.tocHtml,
bookSource,
book.tocUrl,
book.tocUrl
)
} else { } else {
val res = AnalyzeUrl( val res = AnalyzeUrl(
book = book, book = book,
@ -168,7 +184,14 @@ class WebBook(val bookSource: BookSource) {
baseUrl = book.bookUrl, baseUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl) ).getStrResponse(bookSource.bookSourceUrl)
BookChapterList.analyzeChapterList(scope, book, res.body, bookSource, book.tocUrl) BookChapterList.analyzeChapterList(
scope,
book,
res.body,
bookSource,
book.tocUrl,
res.url
)
} }
} }
@ -205,6 +228,7 @@ class WebBook(val bookSource: BookSource) {
bookChapter, bookChapter,
bookSource, bookSource,
bookChapter.getAbsoluteURL(), bookChapter.getAbsoluteURL(),
bookChapter.getAbsoluteURL(),
nextChapterUrl nextChapterUrl
) )
} else { } else {
@ -226,6 +250,7 @@ class WebBook(val bookSource: BookSource) {
bookChapter, bookChapter,
bookSource, bookSource,
bookChapter.getAbsoluteURL(), bookChapter.getAbsoluteURL(),
res.url,
nextChapterUrl nextChapterUrl
) )
} }

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

Loading…
Cancel
Save