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. 54
      app/build.gradle
  5. 48
      app/src/main/AndroidManifest.xml
  6. 162
      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:
branches:
- master
# tags:
# - '3.*'
# pull_request:
# branches:
# - master
# watch:
# types: [started]
# schedule:
@ -20,21 +15,24 @@ jobs:
build:
runs-on: ubuntu-latest
env:
# 登录蓝奏云后查看cookie
# LANZOU_ID : woozooo.com -> Cookie -> ylogin
# LANZOU_PSD : pc.woozooo.com -> Cookie -> phpdisk_info
LANZOU_ID: ${{ secrets.LANZOU_ID }}
LANZOU_PSD: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹名(需手动在蓝奏云创建)
LANZOU_FOLDER: '阅读3测试版'
# 登录蓝奏云后在控制台运行document.cookie
ylogin: ${{ secrets.LANZOU_ID }}
phpdisk_info: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_FOLDER_ID: '2670621'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v2
- name: Set Up JDK
uses: actions/setup-java@v1
- uses: actions/cache@v2
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
run: |
echo "清空18PlusList.txt"
@ -56,35 +54,27 @@ jobs:
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease
./gradlew assembleAppRelease --build-cache --parallel
- name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado 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
if: ${{ env.LANZOU_PSD }}
run: python $GITHUB_WORKSPACE/.github/lzy.py
env:
UPLOAD_FOLDER: ${{ github.workspace }}/app/build/outputs/apk/app/release/
if: ${{ env.ylogin }}
run: |
path="$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
## 界面
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B4.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg)
![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg)
## 其它
其它网友做的IOS版本: https://github.com/kaich/Yuedu

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

@ -173,7 +173,7 @@
android:launchMode="singleTask" />
<!-- 授权界面 -->
<activity
android:name=".help.permission.PermissionActivity"
android:name="io.legado.app.lib.permission.PermissionActivity"
android:theme="@style/Activity.Permission" />
<!-- 二维码扫描 -->
<activity
@ -286,7 +286,7 @@
android:launchMode="singleTop" />
<!-- 书源登录 -->
<activity
android:name=".ui.login.SourceLogin"
android:name=".ui.login.SourceLoginActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 阅读记录 -->
@ -294,10 +294,17 @@
android:name=".ui.about.ReadRecordActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 选择文件 -->
<activity
android:name=".ui.document.FilePickerActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme.Transparent" />
<!-- 文字处理 -->
<activity
android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label">
android:label="@string/receiving_shared_label"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
@ -366,16 +373,47 @@
android:name=".ui.association.FileAssociationActivity"
android:launchMode="singleTop"
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>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="app" />
<data android:scheme="content" />
<data android:scheme="file" />
<!-- text -->
<data android:mimeType="text/plain" />
<!-- 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>
</activity>

@ -1,147 +1,237 @@
[
{
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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": "度小鹿②",
"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": "度小鹿③",
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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}"
"id": -89,
"name": "温柔女声",
"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,
"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}"
"id": -88,
"name": "甜美女声①",
"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,
"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}"
"name": "甜美女声",
"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,
"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}"
"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=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,
"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}"
},
{
"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,
"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,
"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,
"name": "阿里云语音",
"url": "<js>/*播音人Aiting可改其他https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/speaker.json,详见https://ai.aliyun.com/nls/tts*/;eval(''+java.ajax('https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/ttsDemo.js'));ttsDemo(speakText,speakSpeed,'Aiting')</js>"
}
]
]

@ -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",
"bgStrNight": "#000000",
"textColor": "#000000",
@ -10,6 +61,7 @@
"darkStatusIconNight": false
},
{
"name": "预设2",
"bgStr": "#DDC090",
"bgStrNight": "#3C3F43",
"textColor": "#3E3422",
@ -20,6 +72,7 @@
"darkStatusIconNight": false
},
{
"name": "预设3",
"bgStr": "#C2D8AA",
"bgStrNight": "#3C3F43",
"textColor": "#596C44",
@ -30,6 +83,7 @@
"darkStatusIconNight": false
},
{
"name": "预设4",
"bgStr": "#DBB8E2",
"bgStrNight": "#3C3F43",
"textColor": "#68516C",
@ -40,6 +94,7 @@
"darkStatusIconNight": false
},
{
"name": "预设5",
"bgStr": "#ABCEE0",
"bgStrNight": "#3C3F43",
"textColor": "#3D4C54",

@ -1,9 +1,18 @@
[
{
"articleStyle": 0,
"customOrder": 1,
"enableJs": 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,
"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}",
@ -17,5 +26,14 @@
"sourceName": "阅读下载",
"sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases",
"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)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**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**
* 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低
* 修复保存书源时不更新书源时间的bug

@ -152,7 +152,7 @@ function rule2json() {
RuleJSON.lastUpdateTime = new Date().getTime();
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
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.enabledExplore = RuleJSON.enabledExplore == '' || String(RuleJSON.enabledExplore).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
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;
}</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([
{
"revision": "414c7503fa98c3f2e328",
"revision": "a4809f9e102ca093c5e0",
"url": "css/about.dbe575e1.css"
},
{
"revision": "13925596cfa342cbb55d",
"revision": "92b120ee6d7003919840",
"url": "css/app.e4c919b7.css"
},
{
"revision": "d53f06b957b78ddb78b7",
"revision": "d14a57da901d79a81631",
"url": "css/chunk-vendors.ad4ff18f.css"
},
{
"revision": "0607c99f49945c348397",
"url": "css/detail.b4324411.css"
"revision": "329d93821b9c998393c1",
"url": "css/detail.79c91c30.css"
},
{
"revision": "535877f50039c0cb49a6196a5b7517cd",
@ -40,28 +40,28 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "img/noCover.b5c48bc1.jpeg"
},
{
"revision": "8ff165c058a9618dc10065e65133a4e7",
"revision": "6b1059341e07bf6fefd98e31613ba6da",
"url": "index.html"
},
{
"revision": "414c7503fa98c3f2e328",
"url": "js/about.d8b0dc91.js"
"revision": "a4809f9e102ca093c5e0",
"url": "js/about.32f651f9.js"
},
{
"revision": "2c81bd893f3a92f018d8",
"url": "js/about~detail.1caf6ef5.js"
"revision": "2828d7869f38c2483b33",
"url": "js/about~detail.c1b29cbc.js"
},
{
"revision": "13925596cfa342cbb55d",
"url": "js/app.d60dad33.js"
"revision": "92b120ee6d7003919840",
"url": "js/app.3f3311a5.js"
},
{
"revision": "d53f06b957b78ddb78b7",
"url": "js/chunk-vendors.060eee33.js"
"revision": "d14a57da901d79a81631",
"url": "js/chunk-vendors.305d5536.js"
},
{
"revision": "0607c99f49945c348397",
"url": "js/detail.6a5cc86c.js"
"revision": "329d93821b9c998393c1",
"url": "js/detail.0cc184f2.js"
},
{
"revision": "b46d04eb43bc31ca0f9f95121646440d",

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

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

@ -14,6 +14,7 @@ import androidx.viewbinding.ViewBinding
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.help.AppConfig
import io.legado.app.help.ThemeConfig
@ -32,7 +33,8 @@ abstract class BaseActivity<VB : ViewBinding>(
val fullScreen: Boolean = true,
private val theme: 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(),
CoroutineScope by MainScope() {
@ -66,6 +68,26 @@ abstract class BaseActivity<VB : ViewBinding>(
}
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()
initTheme()
super.onCreate(savedInstanceState)
@ -152,9 +174,13 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView)
}
}
if (AppConfig.isGooglePlay) {
if (imageBg) {
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.show(parentFragmentManager, dialogFragmentTag)
}

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

@ -1,6 +1,7 @@
package io.legado.app.constant
import android.annotation.SuppressLint
import android.provider.Settings
import io.legado.app.R
import splitties.init.appCtx
import java.text.SimpleDateFormat
@ -76,4 +77,30 @@ object AppConst {
document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a';
""".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 CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone"
const val REPLACE_RULE_SAVE = "replaceRuleSave"
const val TIP_COLOR = "tipColor"
}

@ -4,6 +4,8 @@ object PreferKey {
const val language = "language"
const val themeMode = "themeMode"
const val userAgent = "userAgent"
const val showUnread = "showUnread"
const val useDefaultCover = "useDefaultCover"
const val hideStatusBar = "hideStatusBar"
const val clickActionTL = "clickActionTopLeft"
const val clickActionTC = "clickActionTopCenter"
@ -39,7 +41,9 @@ object PreferKey {
const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password"
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 changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType"
@ -64,7 +68,14 @@ object PreferKey {
const val importKeepName = "importKeepName"
const val screenOrientation = "screenOrientation"
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 cAccent = "colorAccent"

@ -7,9 +7,10 @@ import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
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.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx
import java.util.*
@ -23,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class],
version = 28,
version = 32,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -40,7 +41,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val rssArticleDao: RssArticleDao
abstract val rssStarDao: RssStarDao
abstract val cookieDao: CookieDao
abstract val txtTocRule: TxtTocRuleDao
abstract val txtTocRuleDao: TxtTocRuleDao
abstract val readRecordDao: ReadRecordDao
abstract val httpTTSDao: HttpTTSDao
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_19_20, migration_20_21, migration_21_22, migration_22_23,
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()
.addCallback(dbCallback)
@ -87,6 +89,13 @@ abstract class AppDatabase : RoomDatabase() {
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
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")
}
}
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
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.*
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) <> ''")
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 || '%'")
fun getByGroup(group: String): List<BookSource>

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

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

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

@ -1,6 +1,5 @@
package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@ -10,12 +9,6 @@ import io.legado.app.data.entities.SearchBook
@Dao
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")
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
from searchBooks as t1 inner join book_sources as t2
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
"""
)

@ -107,6 +107,8 @@ data class Book(
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
@ -146,6 +148,31 @@ data class Book(
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 {
//防止书名过长,只取9位
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
data class ReadConfig(
var pageAnim: Int = -1,
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
class Converters {

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

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

@ -2,9 +2,9 @@ package io.legado.app.data.entities
import androidx.room.Entity
@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"])
@Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"])
data class ReadRecord(
var androidId: String = "",
var deviceId: String = "",
var bookName: String = "",
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.JsExtensions
import io.legado.app.help.http.CookieStore
import io.legado.app.utils.ACache
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.parcelize.Parcelize
import splitties.init.appCtx
import java.util.*
import javax.script.SimpleBindings
@ -23,6 +25,7 @@ data class RssSource(
var sourceName: String = "",
var sourceIcon: String = "",
var sourceGroup: String? = null,
var sourceComment: String? = null,
var enabled: Boolean = true,
var sortUrl: String? = null,
var singleUrl: Boolean = false,
@ -104,9 +107,30 @@ data class RssSource(
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
}
fun sortUrls(): LinkedHashMap<String, String> =
linkedMapOf<String, String>().apply {
sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c ->
fun sortUrls(): LinkedHashMap<String, String> = linkedMapOf<String, String>().apply {
kotlin.runCatching {
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("::")
if (d.size > 1)
this[d[0]] = d[1]
@ -115,4 +139,5 @@ data class RssSource(
this[""] = sourceUrl
}
}
}
}

@ -2,7 +2,6 @@ package io.legado.app.help
import android.content.Context
import android.content.SharedPreferences
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.utils.*
@ -10,44 +9,44 @@ import splitties.init.appCtx
@Suppress("MemberVisibilityCanBePrivate")
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
private val context get() = appCtx
val isGooglePlay = context.channel == "google"
val isGooglePlay = appCtx.channel == "google"
var userAgent: String = getPrefUserAgent()
var replaceEnableDefault = context.getPrefBoolean(PreferKey.replaceEnableDefault, true)
var isEInkMode = context.getPrefString(PreferKey.themeMode) == "3"
var clickActionTL = context.getPrefInt(PreferKey.clickActionTL, 2)
var clickActionTC = context.getPrefInt(PreferKey.clickActionTC, 2)
var clickActionTR = context.getPrefInt(PreferKey.clickActionTR, 1)
var clickActionML = context.getPrefInt(PreferKey.clickActionML, 2)
var clickActionMC = context.getPrefInt(PreferKey.clickActionMC, 0)
var clickActionMR = context.getPrefInt(PreferKey.clickActionMR, 1)
var clickActionBL = context.getPrefInt(PreferKey.clickActionBL, 2)
var clickActionBC = context.getPrefInt(PreferKey.clickActionBC, 1)
var clickActionBR = context.getPrefInt(PreferKey.clickActionBR, 1)
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
var clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2)
var clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1)
var clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2)
var clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0)
var clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1)
var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2)
var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1)
var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
PreferKey.themeMode -> isEInkMode = context.getPrefString(PreferKey.themeMode) == "3"
PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
PreferKey.clickActionTL -> clickActionTL =
context.getPrefInt(PreferKey.clickActionTL, 2)
appCtx.getPrefInt(PreferKey.clickActionTL, 2)
PreferKey.clickActionTC -> clickActionTC =
context.getPrefInt(PreferKey.clickActionTC, 2)
appCtx.getPrefInt(PreferKey.clickActionTC, 2)
PreferKey.clickActionTR -> clickActionTR =
context.getPrefInt(PreferKey.clickActionTR, 2)
appCtx.getPrefInt(PreferKey.clickActionTR, 2)
PreferKey.clickActionML -> clickActionML =
context.getPrefInt(PreferKey.clickActionML, 2)
appCtx.getPrefInt(PreferKey.clickActionML, 2)
PreferKey.clickActionMC -> clickActionMC =
context.getPrefInt(PreferKey.clickActionMC, 2)
appCtx.getPrefInt(PreferKey.clickActionMC, 2)
PreferKey.clickActionMR -> clickActionMR =
context.getPrefInt(PreferKey.clickActionMR, 2)
appCtx.getPrefInt(PreferKey.clickActionMR, 2)
PreferKey.clickActionBL -> clickActionBL =
context.getPrefInt(PreferKey.clickActionBL, 2)
appCtx.getPrefInt(PreferKey.clickActionBL, 2)
PreferKey.clickActionBC -> clickActionBC =
context.getPrefInt(PreferKey.clickActionBC, 2)
appCtx.getPrefInt(PreferKey.clickActionBC, 2)
PreferKey.clickActionBR -> clickActionBR =
context.getPrefInt(PreferKey.clickActionBR, 2)
appCtx.getPrefInt(PreferKey.clickActionBR, 2)
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()
}
}
@ -62,97 +61,153 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
}
var isNightTheme: Boolean
get() = isNightTheme(context)
get() = isNightTheme(appCtx)
set(value) {
if (isNightTheme != value) {
if (value) {
context.putPrefString(PreferKey.themeMode, "2")
appCtx.putPrefString(PreferKey.themeMode, "2")
} 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
get() = context.getPrefBoolean(PreferKey.transparentStatusBar, true)
get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
val immNavigationBar: Boolean
get() = context.getPrefBoolean(PreferKey.immNavigationBar, true)
get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
val screenOrientation: String?
get() = context.getPrefString(PreferKey.screenOrientation)
get() = appCtx.getPrefString(PreferKey.screenOrientation)
var backupPath: String?
get() = context.getPrefString(PreferKey.backupPath)
get() = appCtx.getPrefString(PreferKey.backupPath)
set(value) {
if (value.isNullOrEmpty()) {
context.removePref(PreferKey.backupPath)
appCtx.removePref(PreferKey.backupPath)
} else {
context.putPrefString(PreferKey.backupPath, value)
appCtx.putPrefString(PreferKey.backupPath, value)
}
}
val isShowRSS: Boolean
get() = context.getPrefBoolean(PreferKey.showRss, true)
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
val autoRefreshBook: Boolean
get() = context.getPrefBoolean(R.string.pk_auto_refresh)
get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
var threadCount: Int
get() = context.getPrefInt(PreferKey.threadCount, 16)
get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
set(value) {
context.putPrefInt(PreferKey.threadCount, value)
appCtx.putPrefInt(PreferKey.threadCount, value)
}
var importBookPath: String?
get() = context.getPrefString("importBookPath")
get() = appCtx.getPrefString("importBookPath")
set(value) {
if (value == null) {
context.removePref("importBookPath")
appCtx.removePref("importBookPath")
} else {
context.putPrefString("importBookPath", value)
appCtx.putPrefString("importBookPath", value)
}
}
var ttsSpeechRate: Int
get() = context.getPrefInt(PreferKey.ttsSpeechRate, 5)
get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, 5)
set(value) {
context.putPrefInt(PreferKey.ttsSpeechRate, value)
appCtx.putPrefInt(PreferKey.ttsSpeechRate, value)
}
var chineseConverterType: Int
get() = context.getPrefInt(PreferKey.chineseConverterType)
get() = appCtx.getPrefInt(PreferKey.chineseConverterType)
set(value) {
context.putPrefInt(PreferKey.chineseConverterType, value)
appCtx.putPrefInt(PreferKey.chineseConverterType, value)
}
var systemTypefaces: Int
get() = context.getPrefInt(PreferKey.systemTypefaces)
get() = appCtx.getPrefInt(PreferKey.systemTypefaces)
set(value) {
context.putPrefInt(PreferKey.systemTypefaces, value)
appCtx.putPrefInt(PreferKey.systemTypefaces, value)
}
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) {
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
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 {
val ua = context.getPrefString(PreferKey.userAgent)
val ua = appCtx.getPrefString(PreferKey.userAgent)
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"
}

@ -1,5 +1,6 @@
package io.legado.app.help
import android.net.Uri
import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
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) {
if (content.isEmpty()) return
//保存文本
@ -77,31 +100,6 @@ object BookHelp {
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) {
while (downloadImages.contains(src)) {
delay(100)
@ -138,7 +136,7 @@ object BookHelp {
)
}
private fun getImageSuffix(src: String): String {
fun getImageSuffix(src: String): String {
var suffix = src.substringAfterLast(".").substringBefore(",")
if (suffix.length > 5) {
suffix = ".jpg"
@ -148,7 +146,7 @@ object BookHelp {
fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>()
if (book.isLocalBook()) {
if (book.isLocalTxt()) {
return fileNameList
}
FileUtils.createFolderIfNotExist(
@ -162,7 +160,7 @@ object BookHelp {
// 检测该章节是否下载
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
return if (book.isLocalBook()) {
return if (book.isLocalTxt()) {
true
} else {
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? {
if (book.isLocalBook()) {
if (book.isLocalTxt()) {
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 {
val file = FileUtils.getFile(
downloadDir,
@ -191,8 +218,27 @@ object BookHelp {
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) {
if (book.isLocalBook()) {
if (book.isLocalTxt()) {
return
} else {
FileUtils.createFileIfNotExist(

@ -17,22 +17,8 @@ object ContentHelp {
*/
fun reSegment(content: String, chapterName: String): String {
var content1 = content
val content2: String
val chapterNameLength = chapterName.trim { it <= ' ' }.length
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
val dict = makeDict(content1)
var p = content1
.replace("&quot;".toRegex(), "")
.replace("[::]['\"‘”“]+".toRegex(), ":“")
.replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "\n")
@ -315,14 +301,6 @@ object ContentHelp {
if (loop2Mod1 < 0 && loop2Mod2 > 0) {
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
i++
}
@ -441,28 +419,6 @@ object ContentHelp {
insN = ArrayList(HashSet(insN))
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复制、插入引号和换行
// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
// ins_n 插入换行。数组的值表示插入换行符的位置
@ -626,37 +582,6 @@ object ContentHelp {
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)对引号内容进行矫正并分句。
完整引用说话内容在反引号内侧有断句标点例如

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

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

@ -4,17 +4,16 @@ import android.net.Uri
import android.util.Base64
import androidx.annotation.Keep
import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.help.http.CookieStore
import io.legado.app.help.http.SSLHelper
import io.legado.app.help.http.*
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.jsoup.Connection
import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import splitties.init.appCtx
import java.io.File
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>
*/
@ -62,8 +80,8 @@ interface JsExtensions {
fun downloadFile(content: String, url: String): String {
val type = AnalyzeUrl(url).type ?: return ""
val zipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}"
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}"
)
FileUtils.deleteFile(zipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -81,8 +99,8 @@ interface JsExtensions {
fun unzipFile(zipPath: String): String {
if (zipPath.isEmpty()) return ""
val unzipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
FileUtils.getNameExcludeExtension(zipPath)
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
FileUtils.getNameExcludeExtension(zipPath)
)
FileUtils.deleteFile(unzipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -104,7 +122,7 @@ interface JsExtensions {
for (f in it) {
val charsetName = EncodingDetect.getEncode(f)
contents.append(String(f.readBytes(), charset(charsetName)))
.append("\n")
.append("\n")
}
contents.deleteCharAt(contents.length - 1)
}
@ -118,12 +136,12 @@ interface JsExtensions {
*/
fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true)
.followRedirects(false)
.headers(headers)
.method(Connection.Method.GET)
.execute()
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true)
.followRedirects(false)
.headers(headers)
.method(Connection.Method.GET)
.execute()
}
/**
@ -131,13 +149,13 @@ interface JsExtensions {
*/
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true)
.followRedirects(false)
.requestBody(body)
.headers(headers)
.method(Connection.Method.POST)
.execute()
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true)
.followRedirects(false)
.requestBody(body)
.headers(headers)
.method(Connection.Method.POST)
.execute()
}
/**
@ -227,7 +245,7 @@ interface JsExtensions {
}
fun htmlFormat(str: String): String {
return str.htmlFormat()
return HtmlFormatter.formatKeepImg(str)
}
/**
@ -257,6 +275,10 @@ interface JsExtensions {
return null
}
/**
* 返回字体解析类
* @param str 支持url,本地文件,base64,自动判断,自动缓存
*/
fun queryTTF(str: String?): QueryTTF? {
str ?: return null
val key = md5Encode16(str)
@ -266,7 +288,7 @@ interface JsExtensions {
str.isAbsUrl() -> runBlocking {
var x = CacheManager.getByteArray(key)
if (x == null) {
x = RxHttp.get(str).toByteArray().await()
x = okHttpClient.newCall { url(str) }.bytes()
x.let {
CacheManager.put(key, it)
}
@ -283,15 +305,20 @@ interface JsExtensions {
return qTTF
}
/**
* @param text 包含错误字体的内容
* @param font1 错误的字体
* @param font2 正确的字体
*/
fun replaceFont(
text: String,
font1: QueryTTF?,
font2: QueryTTF?
text: String,
font1: QueryTTF?,
font2: QueryTTF?
): String {
if (font1 == null || font2 == null) return text
val contentArray = text.toCharArray()
contentArray.forEachIndexed { index, s ->
val oldCode = s.toInt()
val oldCode = s.code
if (font1.inLimit(s)) {
val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode))
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)
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 =
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
get() = localConfig.getLong(versionCodeKey, 0)
set(value) {
@ -46,30 +73,4 @@ object LocalConfig {
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
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import io.legado.app.R
object MediaHelp {
@ -32,20 +32,16 @@ object MediaHelp {
or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED)
fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequest? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val mPlaybackAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(mPlaybackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build()
} else {
null
}
fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequestCompat? {
val mPlaybackAttributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.build()
return AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setAudioAttributes(mPlaybackAttributes)
//.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build()
}
/**
@ -53,21 +49,11 @@ object MediaHelp {
*/
fun requestFocus(
audioManager: AudioManager,
listener: AudioManager.OnAudioFocusChangeListener,
focusRequest: AudioFocusRequest?
focusRequest: AudioFocusRequestCompat?
): Boolean {
val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest?.let {
audioManager.requestAudioFocus(focusRequest)
} ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
listener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
val request: Int =
focusRequest?.let { AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) }
?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED
return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}

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

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

@ -1,90 +1,38 @@
package io.legado.app.help.http
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.Proxy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@Suppress("unused")
object HttpHelper {
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()
}
private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
ConcurrentHashMap()
}
/**
* 缓存代理okHttp
*/
fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) {
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
}
val okHttpClient: OkHttpClient by lazy {
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
private fun getHeaderInterceptor(): Interceptor {
return Interceptor { chain ->
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)
.addInterceptor(Interceptor { chain ->
val request = chain.request()
.newBuilder()
.addHeader("Keep-Alive", "300")
@ -92,28 +40,72 @@ object HttpHelper {
.addHeader("Cache-Control", "no-cache")
.build()
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 =
suspendCancellableCoroutine { block ->
val webView = AjaxWebView()
block.invokeOnCancellation {
webView.destroyWebView()
}
webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) {
suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse =
suspendCancellableCoroutine { block ->
val webView = AjaxWebView()
block.invokeOnCancellation {
webView.destroyWebView()
}
webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) {
if (!block.isCompleted)
block.resume(response)
}
if (!block.isCompleted)
block.resume(response)
}
override fun onError(error: Throwable) {
if (!block.isCompleted)
block.cancel(error)
}
override fun onError(error: Throwable) {
if (!block.isCompleted)
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.searchKeywordDao.all, "searchHistory.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)
GSON.toJson(ReadBookConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)

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

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

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

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

@ -1,9 +1,8 @@
package io.legado.app.help.permission
package io.legado.app.lib.permission
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import java.util.*
@Suppress("unused")
class PermissionsCompat private constructor() {
@ -14,17 +13,6 @@ class PermissionsCompat private constructor() {
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 {
private val request: Request
@ -41,24 +29,19 @@ class PermissionsCompat private constructor() {
return this
}
fun requestCode(requestCode: Int): Builder {
request.setRequestCode(requestCode)
return this
}
fun onGranted(callback: (requestCode: Int) -> Unit): Builder {
fun onGranted(callback: () -> Unit): Builder {
request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {
override fun onPermissionsGranted(requestCode: Int) {
callback(requestCode)
override fun onPermissionsGranted() {
callback()
}
})
return this
}
fun onDenied(callback: (requestCode: Int, deniedPermissions: Array<String>) -> Unit): Builder {
fun onDenied(callback: (deniedPermissions: Array<String>) -> Unit): Builder {
request.setOnDeniedCallback(object : OnPermissionsDeniedCallback {
override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) {
callback(requestCode, deniedPermissions)
override fun onPermissionsDenied(deniedPermissions: Array<String>) {
callback(deniedPermissions)
}
})
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.os.Build
import androidx.annotation.StringRes
@ -12,6 +11,7 @@ import io.legado.app.R
import io.legado.app.utils.startActivity
import java.util.*
@Suppress("MemberVisibilityCanBePrivate")
internal class Request : OnRequestPermissionsResultCallback {
internal val requestTime: Long
@ -46,10 +46,6 @@ internal class Request : OnRequestPermissionsResultCallback {
this.permissions?.addAll(listOf(*permissions))
}
fun setRequestCode(requestCode: Int) {
this.requestCode = requestCode
}
fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) {
grantedCallback = callback
}
@ -75,19 +71,16 @@ internal class Request : OnRequestPermissionsResultCallback {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (deniedPermissions == null) {
onPermissionsGranted(requestCode)
onPermissionsGranted()
} else {
val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) {
showSettingDialog(rationale) {
onPermissionsDenied(
requestCode,
deniedPermissions
)
onPermissionsDenied(deniedPermissions)
}
} else {
onPermissionsDenied(requestCode, deniedPermissions)
onPermissionsDenied(deniedPermissions)
}
}
} else {
@ -98,7 +91,7 @@ internal class Request : OnRequestPermissionsResultCallback {
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
}
} else {
onPermissionsGranted(requestCode)
onPermissionsGranted()
}
}
}
@ -151,26 +144,25 @@ internal class Request : OnRequestPermissionsResultCallback {
}
}
private fun onPermissionsGranted(requestCode: Int) {
private fun onPermissionsGranted() {
try {
grantedCallback?.onPermissionsGranted(requestCode)
grantedCallback?.onPermissionsGranted()
} 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 {
deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions)
deniedCallback?.onPermissionsDenied(deniedPermissions)
} catch (ignore: Exception) {
}
RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions)
RequestPlugins.sResultCallback?.onPermissionsDenied(deniedPermissions)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
@ -179,21 +171,21 @@ internal class Request : OnRequestPermissionsResultCallback {
val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) {
showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) }
showSettingDialog(rationale) { onPermissionsDenied(deniedPermissions) }
} else {
onPermissionsDenied(requestCode, deniedPermissions)
onPermissionsDenied(deniedPermissions)
}
} else {
onPermissionsGranted(requestCode)
onPermissionsGranted()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
override fun onSettingActivityResult() {
val deniedPermissions = deniedPermissions
if (deniedPermissions == null) {
onPermissionsGranted(this.requestCode)
onPermissionsGranted()
} 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.Looper
@ -58,11 +58,11 @@ internal object RequestManager : OnPermissionsResultCallback {
}
}
override fun onPermissionsGranted(requestCode: Int) {
override fun onPermissionsGranted() {
startNextRequest()
}
override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) {
override fun onPermissionsDenied(deniedPermissions: Array<String>) {
startNextRequest()
}

@ -1,4 +1,4 @@
package io.legado.app.help.permission
package io.legado.app.lib.permission
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.Intent

@ -16,7 +16,7 @@ import android.widget.ScrollView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.R
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 {
val clazz = ViewPager::class.java
val clazz = ViewPager2::class.java
for (name in arrayOf("mLeftEdge", "mRightEdge")) {
val field = clazz.getDeclaredField(name)
field.isAccessible = true
@ -280,7 +280,7 @@ object ATH {
fun applyEdgeEffectColor(view: View?) {
when (view) {
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))
}
}

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

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

@ -8,7 +8,7 @@ import io.legado.app.data.entities.RssSource
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.rss.Rss
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.msg
import kotlinx.coroutines.CoroutineScope
@ -36,7 +36,7 @@ object Debug {
if (debugSource != sourceUrl || callback == null || !print) return
var printMsg = msg ?: ""
if (isHtml) {
printMsg = printMsg.htmlFormat()
printMsg = HtmlFormatter.format(msg)
}
if (showTime) {
printMsg =
@ -186,6 +186,12 @@ object Debug {
}
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, "︾开始解析详情页")
val info = webBook.getBookInfo(scope, book)
.onSuccess {
@ -206,7 +212,7 @@ object Debug {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
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)
} else {
log(debugSource, "︽目录列表为空", state = -1)

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

@ -27,9 +27,10 @@ import kotlin.collections.HashMap
class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
var book: BaseBook? = null
var chapter: BookChapter? = null
private var content: Any? = null
private var baseUrl: String? = null
private var baseURL: URL? = null
var nextChapterUrl: String? = null
var content: Any? = null
var baseUrl: String? = null
var redirectUrl: URL? = null
private var isJSON: Boolean = false
private var isRegex: Boolean = false
@ -62,15 +63,17 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
fun setBaseUrl(baseUrl: String?): AnalyzeRule {
baseUrl?.let {
this.baseUrl = baseUrl
kotlin.runCatching {
baseURL = URL(baseUrl.substringBefore(","))
}.onFailure {
it.printStackTrace()
}
}
return this
}
fun setRedirectUrl(url: String): URL? {
kotlin.runCatching {
redirectUrl = URL(url.split(AnalyzeUrl.splitUrlRegex, 1)[0])
}
return redirectUrl
}
/**
* 获取XPath解析类
*/
@ -169,7 +172,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
val urlList = ArrayList<String>()
if (result is 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)) {
urlList.add(absoluteURL)
}
@ -238,7 +241,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
return if (str.isBlank()) {
baseUrl ?: ""
} else {
NetworkUtils.getAbsoluteURL(baseURL, str)
NetworkUtils.getAbsoluteURL(redirectUrl, str)
}
}
return str
@ -646,6 +649,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
bindings["chapter"] = chapter
bindings["title"] = chapter?.title
bindings["src"] = content
bindings["nextChapterUrl"] = nextChapterUrl
return runBlocking {
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.http.*
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.util.*
import java.util.regex.Pattern
@ -58,6 +55,7 @@ class AnalyzeUrl(
private var charset: String? = null
private var method = RequestMethod.GET
private var proxy: String? = null
private var retry: Int = 0
init {
baseUrl = baseUrl.split(splitUrlRegex, 1)[0]
@ -200,6 +198,7 @@ class AnalyzeUrl(
option.js?.let {
evalJS(it)
}
retry = option.retry
}
}
headerMap[UA_NAME] ?: let {
@ -305,62 +304,43 @@ class AnalyzeUrl(
params.sourceRegex = sourceRegex
params.postData = body?.toByteArray()
params.tag = tag
return HttpHelper.ajax(params)
return getWebViewSrc(params)
}
return when (method) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} else {
RxHttp.postJson(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toStrResponse().await()
return getProxyClient(proxy).newCallStrResponse(retry) {
removeHeader(UA_NAME)
addHeaders(headerMap)
when (method) {
RequestMethod.POST -> {
url(url)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
} else {
postJson(body)
}
}
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 {
setCookie(tag)
return when (method) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
} else {
RxHttp.postJson(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toByteArray().await()
@Suppress("BlockingMethodInNonBlockingContext")
return getProxyClient(proxy).newCall(retry) {
removeHeader(UA_NAME)
addHeaders(headerMap)
when (method) {
RequestMethod.POST -> {
url(url)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
} else {
postJson(body)
}
}
else -> get(url, fieldMap, true)
}
else -> RxHttp.get(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
}
}.bytes()
}
private fun setCookie(tag: String?) {
@ -393,7 +373,8 @@ class AnalyzeUrl(
val headers: Any?,
val body: Any?,
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.Map;
@SuppressWarnings({"FieldCanBeLocal", "StatementWithEmptyBody", "unused"})
public class QueryTTF {
private static class Header {
public int majorVersion;
@ -84,7 +85,7 @@ public class QueryTTF {
public int maxComponentDepth;
}
private class CmapLayout {
private static class CmapLayout {
public int version;
public int numTables;
public List<CmapRecord> records = new LinkedList<>();
@ -146,19 +147,19 @@ public class QueryTTF {
}
private static class ByteArrayReader {
public int Index;
public byte[] Buffer;
public int index;
public byte[] buffer;
public ByteArrayReader(byte[] buffer, int index) {
Buffer = buffer;
Index = index;
this.buffer = buffer;
this.index = index;
}
public long ReadUIntX(long len) {
long result = 0;
for (long i = 0; i < len; ++i) {
result <<= 8;
result |= Buffer[Index++] & 0xFF;
result |= buffer[index++] & 0xFF;
}
return result;
}
@ -186,17 +187,17 @@ public class QueryTTF {
public String ReadStrings(int len, Charset charset) {
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);
}
public byte GetByte() {
return Buffer[Index++];
return buffer[index++];
}
public byte[] GetBytes(int len) {
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;
}
@ -213,16 +214,17 @@ public class QueryTTF {
}
}
private final ByteArrayReader FontReader;
private final ByteArrayReader fontReader;
private final Header fileHeader = new Header();
private final List<Directory> Directorys = new LinkedList<>();
private final NameLayout Name = new NameLayout();
private final List<Directory> directorys = new LinkedList<>();
private final NameLayout name = new NameLayout();
private final HeadLayout head = new HeadLayout();
private final MaxpLayout maxp = new MaxpLayout();
private final List<Integer> loca = new LinkedList<>();
private final CmapLayout Cmap = new CmapLayout();
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(0, 4),
Pair.of(3, 1),
@ -231,10 +233,10 @@ public class QueryTTF {
Pair.of(0, 1)
};
public final Map<Integer, String> CodeToGlyph = new HashMap<>();
public final Map<String, Integer> GlyphToCode = new HashMap<>();
private int LimitMix = 0;
private int LimitMax = 0;
public final Map<Integer, String> codeToGlyph = new HashMap<>();
public final Map<String, Integer> glyphToCode = new HashMap<>();
private int limitMix = 0;
private int limitMax = 0;
/**
* 构造函数
@ -242,162 +244,162 @@ public class QueryTTF {
* @param buffer 传入TTF字体二进制数组
*/
public QueryTTF(byte[] buffer) {
FontReader = new ByteArrayReader(buffer, 0);
fontReader = new ByteArrayReader(buffer, 0);
// 获取文件头
fileHeader.majorVersion = FontReader.ReadUInt16();
fileHeader.minorVersion = FontReader.ReadUInt16();
fileHeader.numOfTables = FontReader.ReadUInt16();
fileHeader.searchRange = FontReader.ReadUInt16();
fileHeader.entrySelector = FontReader.ReadUInt16();
fileHeader.rangeShift = FontReader.ReadUInt16();
fileHeader.majorVersion = fontReader.ReadUInt16();
fileHeader.minorVersion = fontReader.ReadUInt16();
fileHeader.numOfTables = fontReader.ReadUInt16();
fileHeader.searchRange = fontReader.ReadUInt16();
fileHeader.entrySelector = fontReader.ReadUInt16();
fileHeader.rangeShift = fontReader.ReadUInt16();
// 获取目录
for (int i = 0; i < fileHeader.numOfTables; ++i) {
Directory d = new Directory();
d.tag = FontReader.ReadStrings(4, StandardCharsets.US_ASCII);
d.checkSum = FontReader.ReadUInt32();
d.offset = FontReader.ReadUInt32();
d.length = FontReader.ReadUInt32();
Directorys.add(d);
d.tag = fontReader.ReadStrings(4, StandardCharsets.US_ASCII);
d.checkSum = fontReader.ReadUInt32();
d.offset = fontReader.ReadUInt32();
d.length = fontReader.ReadUInt32();
directorys.add(d);
}
// 解析表 name (字体信息,包含版权、名称、作者等...)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("name")) {
FontReader.Index = Temp.offset;
Name.format = FontReader.ReadUInt16();
Name.count = FontReader.ReadUInt16();
Name.stringOffset = FontReader.ReadUInt16();
for (int i = 0; i < Name.count; ++i) {
fontReader.index = Temp.offset;
name.format = fontReader.ReadUInt16();
name.count = fontReader.ReadUInt16();
name.stringOffset = fontReader.ReadUInt16();
for (int i = 0; i < name.count; ++i) {
NameRecord record = new NameRecord();
record.platformID = FontReader.ReadUInt16();
record.encodingID = FontReader.ReadUInt16();
record.languageID = FontReader.ReadUInt16();
record.nameID = FontReader.ReadUInt16();
record.length = FontReader.ReadUInt16();
record.offset = FontReader.ReadUInt16();
Name.records.add(record);
record.platformID = fontReader.ReadUInt16();
record.encodingID = fontReader.ReadUInt16();
record.languageID = fontReader.ReadUInt16();
record.nameID = fontReader.ReadUInt16();
record.length = fontReader.ReadUInt16();
record.offset = fontReader.ReadUInt16();
name.records.add(record);
}
}
}
// 解析表 head (获取 head.indexToLocFormat)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("head")) {
FontReader.Index = Temp.offset;
head.majorVersion = FontReader.ReadUInt16();
head.minorVersion = FontReader.ReadUInt16();
head.fontRevision = FontReader.ReadUInt32();
head.checkSumAdjustment = FontReader.ReadUInt32();
head.magicNumber = FontReader.ReadUInt32();
head.flags = FontReader.ReadUInt16();
head.unitsPerEm = FontReader.ReadUInt16();
head.created = FontReader.ReadUInt64();
head.modified = FontReader.ReadUInt64();
head.xMin = FontReader.ReadInt16();
head.yMin = FontReader.ReadInt16();
head.xMax = FontReader.ReadInt16();
head.yMax = FontReader.ReadInt16();
head.macStyle = FontReader.ReadUInt16();
head.lowestRecPPEM = FontReader.ReadUInt16();
head.fontDirectionHint = FontReader.ReadInt16();
head.indexToLocFormat = FontReader.ReadInt16();
head.glyphDataFormat = FontReader.ReadInt16();
fontReader.index = Temp.offset;
head.majorVersion = fontReader.ReadUInt16();
head.minorVersion = fontReader.ReadUInt16();
head.fontRevision = fontReader.ReadUInt32();
head.checkSumAdjustment = fontReader.ReadUInt32();
head.magicNumber = fontReader.ReadUInt32();
head.flags = fontReader.ReadUInt16();
head.unitsPerEm = fontReader.ReadUInt16();
head.created = fontReader.ReadUInt64();
head.modified = fontReader.ReadUInt64();
head.xMin = fontReader.ReadInt16();
head.yMin = fontReader.ReadInt16();
head.xMax = fontReader.ReadInt16();
head.yMax = fontReader.ReadInt16();
head.macStyle = fontReader.ReadUInt16();
head.lowestRecPPEM = fontReader.ReadUInt16();
head.fontDirectionHint = fontReader.ReadInt16();
head.indexToLocFormat = fontReader.ReadInt16();
head.glyphDataFormat = fontReader.ReadInt16();
}
}
// 解析表 maxp (获取 maxp.numGlyphs)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("maxp")) {
FontReader.Index = Temp.offset;
maxp.majorVersion = FontReader.ReadUInt16();
maxp.minorVersion = FontReader.ReadUInt16();
maxp.numGlyphs = FontReader.ReadUInt16();
maxp.maxPoints = FontReader.ReadUInt16();
maxp.maxContours = FontReader.ReadUInt16();
maxp.maxCompositePoints = FontReader.ReadUInt16();
maxp.maxCompositeContours = FontReader.ReadUInt16();
maxp.maxZones = FontReader.ReadUInt16();
maxp.maxTwilightPoints = FontReader.ReadUInt16();
maxp.maxStorage = FontReader.ReadUInt16();
maxp.maxFunctionDefs = FontReader.ReadUInt16();
maxp.maxInstructionDefs = FontReader.ReadUInt16();
maxp.maxStackElements = FontReader.ReadUInt16();
maxp.maxSizeOfInstructions = FontReader.ReadUInt16();
maxp.maxComponentElements = FontReader.ReadUInt16();
maxp.maxComponentDepth = FontReader.ReadUInt16();
fontReader.index = Temp.offset;
maxp.majorVersion = fontReader.ReadUInt16();
maxp.minorVersion = fontReader.ReadUInt16();
maxp.numGlyphs = fontReader.ReadUInt16();
maxp.maxPoints = fontReader.ReadUInt16();
maxp.maxContours = fontReader.ReadUInt16();
maxp.maxCompositePoints = fontReader.ReadUInt16();
maxp.maxCompositeContours = fontReader.ReadUInt16();
maxp.maxZones = fontReader.ReadUInt16();
maxp.maxTwilightPoints = fontReader.ReadUInt16();
maxp.maxStorage = fontReader.ReadUInt16();
maxp.maxFunctionDefs = fontReader.ReadUInt16();
maxp.maxInstructionDefs = fontReader.ReadUInt16();
maxp.maxStackElements = fontReader.ReadUInt16();
maxp.maxSizeOfInstructions = fontReader.ReadUInt16();
maxp.maxComponentElements = fontReader.ReadUInt16();
maxp.maxComponentDepth = fontReader.ReadUInt16();
}
}
// 解析表 loca (轮廓数据偏移地址表)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("loca")) {
FontReader.Index = Temp.offset;
fontReader.index = Temp.offset;
int offset = head.indexToLocFormat == 0 ? 2 : 4;
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编码轮廓索引对照表)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("cmap")) {
FontReader.Index = Temp.offset;
Cmap.version = FontReader.ReadUInt16();
Cmap.numTables = FontReader.ReadUInt16();
fontReader.index = Temp.offset;
Cmap.version = fontReader.ReadUInt16();
Cmap.numTables = fontReader.ReadUInt16();
for (int i = 0; i < Cmap.numTables; ++i) {
CmapRecord record = new CmapRecord();
record.platformID = FontReader.ReadUInt16();
record.encodingID = FontReader.ReadUInt16();
record.offset = FontReader.ReadUInt32();
record.platformID = fontReader.ReadUInt16();
record.encodingID = fontReader.ReadUInt16();
record.offset = fontReader.ReadUInt32();
Cmap.records.add(record);
}
for (int i = 0; i < Cmap.numTables; ++i) {
int fmtOffset = Cmap.records.get(i).offset;
FontReader.Index = Temp.offset + fmtOffset;
int EndIndex = FontReader.Index;
fontReader.index = Temp.offset + fmtOffset;
int EndIndex = fontReader.index;
int format = FontReader.ReadUInt16();
int format = fontReader.ReadUInt16();
if (Cmap.tables.containsKey(fmtOffset)) continue;
if (format == 0) {
CmapFormat f = new CmapFormat();
f.format = format;
f.length = FontReader.ReadUInt16();
f.language = FontReader.ReadUInt16();
f.glyphIdArray = FontReader.GetBytes(f.length - 6);
f.length = fontReader.ReadUInt16();
f.language = fontReader.ReadUInt16();
f.glyphIdArray = fontReader.GetBytes(f.length - 6);
Cmap.tables.put(fmtOffset, f);
} else if (format == 4) {
CmapFormat4 f = new CmapFormat4();
f.format = format;
f.length = FontReader.ReadUInt16();
f.language = FontReader.ReadUInt16();
f.segCountX2 = FontReader.ReadUInt16();
f.length = fontReader.ReadUInt16();
f.language = fontReader.ReadUInt16();
f.segCountX2 = fontReader.ReadUInt16();
int segCount = f.segCountX2 >> 1;
f.searchRange = FontReader.ReadUInt16();
f.entrySelector = FontReader.ReadUInt16();
f.rangeShift = FontReader.ReadUInt16();
f.endCode = FontReader.GetUInt16Array(segCount);
f.reservedPad = FontReader.ReadUInt16();
f.startCode = FontReader.GetUInt16Array(segCount);
f.idDelta = FontReader.GetInt16Array(segCount);
f.idRangeOffset = FontReader.GetUInt16Array(segCount);
f.glyphIdArray = FontReader.GetUInt16Array((EndIndex + f.length - FontReader.Index) >> 1);
f.searchRange = fontReader.ReadUInt16();
f.entrySelector = fontReader.ReadUInt16();
f.rangeShift = fontReader.ReadUInt16();
f.endCode = fontReader.GetUInt16Array(segCount);
f.reservedPad = fontReader.ReadUInt16();
f.startCode = fontReader.GetUInt16Array(segCount);
f.idDelta = fontReader.GetInt16Array(segCount);
f.idRangeOffset = fontReader.GetUInt16Array(segCount);
f.glyphIdArray = fontReader.GetUInt16Array((EndIndex + f.length - fontReader.index) >> 1);
Cmap.tables.put(fmtOffset, f);
} else if (format == 6) {
CmapFormat6 f = new CmapFormat6();
f.format = format;
f.length = FontReader.ReadUInt16();
f.language = FontReader.ReadUInt16();
f.firstCode = FontReader.ReadUInt16();
f.entryCount = FontReader.ReadUInt16();
f.glyphIdArray = FontReader.GetUInt16Array(f.entryCount);
f.length = fontReader.ReadUInt16();
f.language = fontReader.ReadUInt16();
f.firstCode = fontReader.ReadUInt16();
f.entryCount = fontReader.ReadUInt16();
f.glyphIdArray = fontReader.GetUInt16Array(f.entryCount);
Cmap.tables.put(fmtOffset, f);
} else if (format == 12) {
CmapFormat12 f = new CmapFormat12();
f.format = format;
f.reserved = FontReader.ReadUInt16();
f.length = FontReader.ReadUInt32();
f.language = FontReader.ReadUInt32();
f.numGroups = FontReader.ReadUInt32();
f.reserved = fontReader.ReadUInt16();
f.length = fontReader.ReadUInt32();
f.language = fontReader.ReadUInt32();
f.numGroups = fontReader.ReadUInt32();
f.groups = new ArrayList<>(f.numGroups);
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);
}
@ -405,30 +407,30 @@ public class QueryTTF {
}
}
// 解析表 glyf (字体轮廓数据表)
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (Temp.tag.equals("glyf")) {
FontReader.Index = Temp.offset;
fontReader.index = Temp.offset;
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) {
GlyfLayout g = new GlyfLayout();
g.numberOfContours = numberOfContours;
g.xMin = FontReader.ReadInt16();
g.yMin = FontReader.ReadInt16();
g.xMax = FontReader.ReadInt16();
g.yMax = FontReader.ReadInt16();
g.endPtsOfContours = FontReader.GetUInt16Array(numberOfContours);
g.instructionLength = FontReader.ReadUInt16();
g.instructions = FontReader.GetBytes(g.instructionLength);
g.xMin = fontReader.ReadInt16();
g.yMin = fontReader.ReadInt16();
g.xMax = fontReader.ReadInt16();
g.yMax = fontReader.ReadInt16();
g.endPtsOfContours = fontReader.GetUInt16Array(numberOfContours);
g.instructionLength = fontReader.ReadUInt16();
g.instructions = fontReader.GetBytes(g.instructionLength);
int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1;
// 获取轮廓点描述标志
g.flags = new byte[flagLength];
for (int n = 0; n < flagLength; ++n) {
g.flags[n] = FontReader.GetByte();
g.flags[n] = fontReader.GetByte();
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];
}
}
@ -438,9 +440,9 @@ public class QueryTTF {
for (int n = 0; n < flagLength; ++n) {
short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1);
if ((g.flags[n] & 0x02) != 0) {
g.xCoordinates[n] = (short) (same * FontReader.ReadUInt8());
g.xCoordinates[n] = (short) (same * fontReader.ReadUInt8());
} else {
g.xCoordinates[n] = same == 1 ? (short) 0 : FontReader.ReadInt16();
g.xCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16();
}
}
// 获取轮廓点描述y轴相对值
@ -448,9 +450,9 @@ public class QueryTTF {
for (int n = 0; n < flagLength; ++n) {
short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1);
if ((g.flags[n] & 0x04) != 0) {
g.yCoordinates[n] = (short) (same * FontReader.ReadUInt8());
g.yCoordinates[n] = (short) (same * fontReader.ReadUInt8());
} 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).yCoordinates) sb.append(b);
String val = sb.toString();
if (LimitMix == 0) LimitMix = key;
LimitMax = key;
CodeToGlyph.put(key, val);
if (GlyphToCode.containsKey(val)) continue;
GlyphToCode.put(val, key);
if (limitMix == 0) limitMix = key;
limitMax = key;
codeToGlyph.put(key, val);
if (glyphToCode.containsKey(val)) continue;
glyphToCode.put(val, key);
}
}
@ -492,15 +494,15 @@ public class QueryTTF {
* @return 返回查询结果字符串
*/
public String getNameById(int nameId) {
for (Directory Temp : Directorys) {
for (Directory Temp : directorys) {
if (!Temp.tag.equals("name")) continue;
FontReader.Index = Temp.offset;
fontReader.index = Temp.offset;
break;
}
for (NameRecord record : Name.records) {
for (NameRecord record : name.records) {
if (record.nameID != nameId) continue;
FontReader.Index += Name.stringOffset + record.offset;
return FontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE);
fontReader.index += name.stringOffset + record.offset;
return fontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE);
}
return "error";
}
@ -514,7 +516,7 @@ public class QueryTTF {
private int getGlyfIndex(int code) {
if (code == 0) return 0;
int fmtKey = 0;
for (@SuppressWarnings("unchecked") Pair<Integer, Integer> item : pps) {
for (Pair<Integer, Integer> item : pps) {
for (CmapRecord record : Cmap.records) {
if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) {
fmtKey = record.offset;
@ -527,6 +529,7 @@ public class QueryTTF {
int glyfID = 0;
CmapFormat table = Cmap.tables.get(fmtKey);
assert table != null;
int fmt = table.format;
if (fmt == 0) {
if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF;
@ -575,7 +578,7 @@ public class QueryTTF {
* @return 返回bool查询结果
*/
public boolean inLimit(char code) {
return (LimitMix <= code) && (code < LimitMax);
return (limitMix <= code) && (code < limitMax);
}
/**
@ -585,7 +588,7 @@ public class QueryTTF {
* @return 返回轮廓数组的String值
*/
public String getGlyfByCode(int key) {
return CodeToGlyph.getOrDefault(key, "");
return codeToGlyph.getOrDefault(key, "");
}
/**
@ -595,6 +598,7 @@ public class QueryTTF {
* @return 返回Unicode十进制值
*/
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> {
var rules = appDb.txtTocRule.enabled
var rules = appDb.txtTocRuleDao.enabled
if (rules.isEmpty()) {
rules = DefaultData.txtTocRules.apply {
appDb.txtTocRule.insert(*this.toTypedArray())
appDb.txtTocRuleDao.insert(*this.toTypedArray())
}.filter {
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 splitties.init.appCtx
import java.io.File
import java.util.regex.Pattern
object LocalBook {
@ -22,7 +23,7 @@ object LocalBook {
fun getChapterList(book: Book): ArrayList<BookChapter> {
return if (book.isEpub()) {
EPUBFile.getChapterList(book)
EpubFile.getChapterList(book)
} else {
AnalyzeTxtFile().analyze(book)
}
@ -30,7 +31,7 @@ object LocalBook {
fun getContext(book: Book, chapter: BookChapter): String? {
return if (book.isEpub()) {
EPUBFile.getContent(book, chapter)
EpubFile.getContent(book, chapter)
} else {
AnalyzeTxtFile.getContent(book, chapter)
}
@ -55,23 +56,66 @@ object LocalBook {
path = uri.path!!
File(path).name
}
val str = fileName.substringBeforeLast(".")
val authorIndex = str.indexOf("作者")
var name: String
var author: String
if (authorIndex == -1) {
name = str
author = ""
val name: String
val author: String
if (("" in fileName && "" in fileName)
|| "作者" in fileName
|| (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 {
name = str.substring(0, authorIndex)
author = str.substring(authorIndex)
author = BookHelp.formatBookAuthor(author)
}
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
name = fileName.replace(".txt", "")
author = ""
}
val book = Book(
bookUrl = path,
name = name,
@ -83,6 +127,7 @@ object LocalBook {
"${MD5Utils.md5Encode16(path)}.jpg"
)
)
if (book.isEpub()) EpubFile.upBookInfo(book)
appDb.bookDao.insert(book)
return book
}
@ -93,6 +138,13 @@ object LocalBook {
val bookFile = FileUtils.getFile(cacheFolder, book.originName)
bookFile.delete()
}
if(book.isEpub()){
val bookFile=BookHelp.getEpubFile(book).parentFile
if (bookFile!=null&&bookFile.exists()){
FileUtils.delete(bookFile,true)
}
}
if (deleteOriginal) {
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.RssSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.RuleData
@ -38,7 +39,7 @@ object Rss {
scope: CoroutineScope,
rssArticle: RssArticle,
ruleContent: String,
rssSource: RssSource?,
rssSource: RssSource,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
@ -46,9 +47,11 @@ object Rss {
rssArticle.link,
baseUrl = rssArticle.origin,
ruleData = rssArticle,
headerMapF = rssSource?.getHeaderMap()
headerMapF = rssSource.getHeaderMap()
)
val body = analyzeUrl.getStrResponse(rssArticle.origin).body
Debug.log(rssSource.sourceUrl, "≡获取成功:${rssSource.sourceUrl}")
Debug.log(rssSource.sourceUrl, body, state = 20)
val analyzeRule = AnalyzeRule(rssArticle)
analyzeRule.setContent(body)
.setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link))

@ -31,6 +31,7 @@ object RssParserByRule {
)
}
Debug.log(sourceUrl, "≡获取成功:$sourceUrl")
Debug.log(sourceUrl, body, state = 10)
var ruleArticles = rssSource.ruleArticles
if (ruleArticles.isNullOrBlank()) {
Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析")
@ -39,6 +40,7 @@ object RssParserByRule {
val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule(ruleData)
analyzeRule.setContent(body).setBaseUrl(sortUrl)
analyzeRule.setRedirectUrl(sortUrl)
var reverse = false
if (ruleArticles.startsWith("-")) {
reverse = true
@ -49,7 +51,7 @@ object RssParserByRule {
Debug.log(sourceUrl, "└列表大小:${collections.size}")
if (!rssSource.ruleNextPage.isNullOrEmpty()) {
Debug.log(sourceUrl, "┌获取下一页链接")
if (rssSource.ruleNextPage!!.toUpperCase(Locale.getDefault()) == "PAGE") {
if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == "PAGE") {
nextUrl = sortUrl
} else {
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.BookSource
import io.legado.app.data.entities.rule.TocRule
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object BookChapterList {
@ -24,151 +24,83 @@ object BookChapterList {
book: Book,
body: String?,
bookSource: BookSource,
baseUrl: String
): List<BookChapter> = suspendCancellableCoroutine { block ->
kotlin.runCatching {
val chapterList = ArrayList<BookChapter>()
body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, baseUrl)
baseUrl: String,
redirectUrl: String
): List<BookChapter> {
val chapterList = ArrayList<BookChapter>()
body ?: throw Exception(
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}")
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, body, tocRule, listRule, bookSource, log = true
)
chapterData.chapterList?.let {
chapterList.addAll(it)
}
when (chapterData.nextUrl.size) {
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)
}
}
chapterData.chapterList?.let {
chapterList.addAll(it)
}
when (chapterData.nextUrl.size) {
0 -> Unit
1 -> {
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(
scope, book, nextUrl, 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 -> {
val chapterDataList = arrayListOf<ChapterData<String>>()
for (item in chapterData.nextUrl) {
if (!nextUrlList.contains(item)) {
val data = ChapterData(nextUrl = item)
chapterDataList.add(data)
nextUrlList.add(item)
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
for (item in chapterDataList) {
downloadToc(
scope, item, book, bookSource,
tocRule, listRule, chapterList, chapterDataList,
{
block.resume(finish(book, chapterList, reverse))
}
) {
block.cancel(it)
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
}
else -> {
Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${chapterData.nextUrl.size}")
withContext(IO) {
val asyncArray = Array(chapterData.nextUrl.size) {
async(IO) {
val urlStr = chapterData.nextUrl[it]
val analyzeUrl = AnalyzeUrl(
ruleUrl = urlStr,
book = book,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
analyzeChapterList(
this, book, urlStr, res.url,
res.body!!, tocRule, listRule, bookSource, false
).chapterList
}
}
}
}
}.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 {
asyncArray.forEach { coroutine ->
coroutine.await()?.let {
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) {
chapterList.reverse()
@ -195,6 +127,7 @@ object BookChapterList {
scope: CoroutineScope,
book: Book,
baseUrl: String,
redirectUrl: String,
body: String,
tocRule: TocRule,
listRule: String,
@ -204,7 +137,13 @@ object BookChapterList {
): ChapterData<List<String>> {
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(redirectUrl)
//获取目录列表
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 nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
@ -222,11 +161,9 @@ object BookChapterList {
log
)
}
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
scope.ensureActive()
if (elements.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log)
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
@ -251,11 +188,12 @@ object BookChapterList {
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, "获取首章链接", log)
Debug.log(bookSource.bookSourceUrl, "┌首章链接", 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)
}
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.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import splitties.init.appCtx
@ -26,40 +29,48 @@ object BookContent {
bookChapter: BookChapter,
bookSource: BookSource,
baseUrl: String,
nextChapterUrlF: String? = null
redirectUrl: String,
nextChapterUrl: String? = null
): String {
body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, 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 nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule()
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = mNextChapterUrl
scope.ensureActive()
var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
)
content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) {
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)) {
if (!nextChapterUrl.isNullOrEmpty()
if (!mNextChapterUrl.isNullOrEmpty()
&& NetworkUtils.getAbsoluteURL(baseUrl, nextUrl)
== NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl)
== NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl)
) break
nextUrlList.add(nextUrl)
AnalyzeUrl(
scope.ensureActive()
val res = AnalyzeUrl(
ruleUrl = nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
).getStrResponse(bookSource.bookSourceUrl)
res.body?.let { nextBody ->
contentData = analyzeContent(
book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false
book, nextUrl, res.url, nextBody, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
)
nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
@ -68,31 +79,31 @@ object BookContent {
}
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) {
val contentDataList = arrayListOf<ContentData<String>>()
for (item in contentData.nextUrl) {
if (!nextUrlList.contains(item))
contentDataList.add(ContentData(nextUrl = item))
}
for (item in contentDataList) {
withContext(scope.coroutineContext) {
AnalyzeUrl(
ruleUrl = item.nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let {
contentData = analyzeContent(
book, item.nextUrl, it, contentRule, bookChapter, bookSource, false
Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${contentData.nextUrl.size}")
withContext(IO) {
val asyncArray = Array(contentData.nextUrl.size) {
async(IO) {
val urlStr = contentData.nextUrl[it]
val analyzeUrl = AnalyzeUrl(
ruleUrl = urlStr,
book = book,
headerMapF = bookSource.getHeaderMap()
)
item.content = contentData.content
val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
analyzeContent(
book, urlStr, res.url, res.body!!, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
).content
}
}
}
for (item in contentDataList) {
content.append(item.content).append("\n")
asyncArray.forEach { coroutine ->
scope.ensureActive()
content.append(coroutine.await()).append("\n")
}
}
}
content.deleteCharAt(content.length - 1)
var contentStr = content.toString().htmlFormat()
var contentStr = content.toString()
val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) {
contentStr = analyzeRule.getString(replaceRegex, value = contentStr)
@ -111,16 +122,24 @@ object BookContent {
private fun analyzeContent(
book: Book,
baseUrl: String,
redirectUrl: String,
body: String,
contentRule: ContentRule,
chapter: BookChapter,
bookSource: BookSource,
nextChapterUrl: String?,
printLog: Boolean = true
): ContentData<List<String>> {
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>()
analyzeRule.chapter = chapter
//获取正文
var content = analyzeRule.getString(contentRule.content)
content = HtmlFormatter.formatKeepImg(content, rUrl)
//获取下一页链接
val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog)
@ -129,7 +148,6 @@ object BookContent {
}
Debug.log(bookSource.bookSourceUrl, "" + nextUrlList.joinToString(""), printLog)
}
val content = analyzeRule.getString(contentRule.content)
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.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import splitties.init.appCtx
@ -22,15 +22,31 @@ object BookInfo {
body: String?,
bookSource: BookSource,
baseUrl: String,
redirectUrl: String,
canReName: Boolean,
) {
body ?: throw Exception(
appCtx.getString(R.string.error_get_web_content, baseUrl)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val infoRule = bookSource.getBookInfoRule()
Debug.log(bookSource.bookSourceUrl, body, state = 20)
val analyzeRule = AnalyzeRule(book)
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 {
if (it.isNotBlank()) {
scope.ensureActive()
@ -78,13 +94,13 @@ object BookInfo {
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
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}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
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}")
scope.ensureActive()

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

@ -127,6 +127,7 @@ class WebBook(val bookSource: BookSource) {
book.infoHtml,
bookSource,
book.bookUrl,
book.bookUrl,
canReName
)
} else {
@ -136,7 +137,15 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap(),
book = book
).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
}
@ -160,7 +169,14 @@ class WebBook(val bookSource: BookSource) {
): List<BookChapter> {
book.type = bookSource.bookSourceType
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 {
val res = AnalyzeUrl(
book = book,
@ -168,7 +184,14 @@ class WebBook(val bookSource: BookSource) {
baseUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap()
).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,
bookSource,
bookChapter.getAbsoluteURL(),
bookChapter.getAbsoluteURL(),
nextChapterUrl
)
} else {
@ -226,6 +250,7 @@ class WebBook(val bookSource: BookSource) {
bookChapter,
bookSource,
bookChapter.getAbsoluteURL(),
res.url,
nextChapterUrl
)
}

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

Loading…
Cancel
Save