Merge pull request #3 from gedoor/master

更新
pull/885/head
guibiaoguo 4 years ago committed by GitHub
commit 618c155e3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      .github/scripts/lzy.sh
  2. 62
      .github/workflows/legado.yml
  3. 25
      app/build.gradle
  4. 37
      app/google-services.json
  5. 7
      app/src/main/AndroidManifest.xml
  6. 202
      app/src/main/assets/18PlusList.txt
  7. 9
      app/src/main/assets/defaultData/rssSources.json
  8. 13
      app/src/main/assets/help/appHelp.md
  9. 6
      app/src/main/assets/help/replaceRuleHelp.md
  10. 2
      app/src/main/assets/help/ruleHelp.md
  11. 74
      app/src/main/assets/updateLog.md
  12. 6
      app/src/main/assets/web/index.html
  13. 29
      app/src/main/assets/web/index.js
  14. 4
      app/src/main/assets/web/new/index.html
  15. 0
      app/src/main/assets/web/new/js/about.4300b5ad.js
  16. 1
      app/src/main/assets/web/new/js/about~detail.1caf6ef5.js
  17. 1
      app/src/main/assets/web/new/js/about~detail.c1b29cbc.js
  18. 1
      app/src/main/assets/web/new/js/app.acf50eba.js
  19. 1
      app/src/main/assets/web/new/js/app.d60dad33.js
  20. 33
      app/src/main/assets/web/new/js/chunk-vendors.060eee33.js
  21. 33
      app/src/main/assets/web/new/js/chunk-vendors.1042cf7f.js
  22. 1
      app/src/main/assets/web/new/js/detail.4d0609e7.js
  23. 1
      app/src/main/assets/web/new/js/detail.6a5cc86c.js
  24. 30
      app/src/main/assets/web/new/precache-manifest.591d552208e3dedf905ccfa69870ce16.js
  25. 2
      app/src/main/assets/web/new/service-worker.js
  26. 66
      app/src/main/java/io/legado/app/App.kt
  27. 21
      app/src/main/java/io/legado/app/api/controller/BookshelfController.kt
  28. 12
      app/src/main/java/io/legado/app/api/controller/SourceController.kt
  29. 6
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  30. 29
      app/src/main/java/io/legado/app/base/BaseViewModel.kt
  31. 7
      app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt
  32. 7
      app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt
  33. 11
      app/src/main/java/io/legado/app/constant/AppConst.kt
  34. 30
      app/src/main/java/io/legado/app/constant/Const.kt
  35. 7
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  36. 12
      app/src/main/java/io/legado/app/constant/Theme.kt
  37. 39
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  38. 2
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  39. 32
      app/src/main/java/io/legado/app/data/dao/EpubChapterDao.kt
  40. 45
      app/src/main/java/io/legado/app/data/entities/Book.kt
  41. 2
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  42. 18
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  43. 23
      app/src/main/java/io/legado/app/data/entities/EpubChapter.kt
  44. 31
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  45. 140
      app/src/main/java/io/legado/app/help/AppConfig.kt
  46. 45
      app/src/main/java/io/legado/app/help/BookHelp.kt
  47. 11
      app/src/main/java/io/legado/app/help/CacheManager.kt
  48. 79
      app/src/main/java/io/legado/app/help/ContentHelp.kt
  49. 16
      app/src/main/java/io/legado/app/help/ContentProcessor.kt
  50. 69
      app/src/main/java/io/legado/app/help/CrashHandler.kt
  51. 23
      app/src/main/java/io/legado/app/help/DefaultData.kt
  52. 6
      app/src/main/java/io/legado/app/help/IntentHelp.kt
  53. 61
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  54. 24
      app/src/main/java/io/legado/app/help/LauncherIconHelp.kt
  55. 12
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  56. 42
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  57. 4
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  58. 15
      app/src/main/java/io/legado/app/help/SourceHelp.kt
  59. 45
      app/src/main/java/io/legado/app/help/ThemeConfig.kt
  60. 4
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  61. 10
      app/src/main/java/io/legado/app/help/http/AjaxWebView.kt
  62. 10
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  63. 4
      app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt
  64. 40
      app/src/main/java/io/legado/app/help/permission/Request.kt
  65. 35
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  66. 28
      app/src/main/java/io/legado/app/help/storage/BookWebDav.kt
  67. 36
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  68. 4
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  69. 88
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  70. 34
      app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt
  71. 62
      app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt
  72. 10
      app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt
  73. 4
      app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt
  74. 8
      app/src/main/java/io/legado/app/lib/dialogs/Dialogs.kt
  75. 2
      app/src/main/java/io/legado/app/lib/dialogs/Selectors.kt
  76. 8
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  77. 10
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  78. 8
      app/src/main/java/io/legado/app/model/Debug.kt
  79. 20
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  80. 24
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  81. 600
      app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java
  82. 633
      app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.kt
  83. 9
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  84. 243
      app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt
  85. 20
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  86. 5
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  87. 34
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  88. 49
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  89. 23
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  90. 76
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  91. 9
      app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt
  92. 29
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  93. 47
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  94. 34
      app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt
  95. 17
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  96. 4
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  97. 15
      app/src/main/java/io/legado/app/service/CacheBookService.kt
  98. 30
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  99. 6
      app/src/main/java/io/legado/app/service/DownloadService.kt
  100. 81
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,60 @@
#!/usr/bin/env bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
# --------------------------------------------------------------
# 系统: ALL
# 项目: 蓝奏云上传文件
# 版本: 1.0.3
# 作者: XIU2
# 官网: https://shell.xiu2.xyz
# 项目: https://github.com/XIU2/Shell
# --------------------------------------------------------------
USERNAME="admin" # 蓝奏云用户名
COOKIE_PHPDISK_INFO="$LANZOU_PSD" # Cookie 中 phpdisk_info 的值
COOKIE_YLOGIN="$LANZOU_ID" # Cookie 中 ylogin 的值
UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.7.3670.199 Safari/537.36"
HEADER_CHECK_LOGIN="User-Agent: ${UA}
Referer: https://up.woozooo.com/mydisk.php?item=files&action=index&u=${USERNAME}
Accept-Language: zh-CN,zh;q=0.9"
URL_ACCOUNT="https://pc.woozooo.com/account.php"
URL_UPLOAD="https://up.woozooo.com/fileup.php"
INFO="[信息]" && ERROR="[错误]" && TIP="[注意]"
# 检查是否已登录
_CHECK_LOGIN() {
if [[ "${COOKIE_PHPDISK_INFO}" = "" || "${COOKIE_PHPDISK_INFO}" = "XXX" ]]; then
_NOTICE "ERROR" "请指定 Cookie 中 phpdisk_info 的值!"
fi
if [[ "${COOKIE_YLOGIN}" = "" || "${COOKIE_YLOGIN}" = "XXX" ]]; then
_NOTICE "ERROR" "请指定 Cookie 中 ylogin 的值!"
fi
HTML_CHECK_LOGIN=$(curl -s --http1.1 -b "ylogin=${COOKIE_YLOGIN};phpdisk_info=${COOKIE_PHPDISK_INFO}" -H "${HEADER_CHECK_LOGIN}" "${URL_ACCOUNT}"|grep "登录")
[[ ! -z "${HTML_CHECK_LOGIN}" ]] && _NOTICE "ERROR" "Cookie 已失效,请更新!"
}
# 上传文件
_UPLOAD() {
[[ $(du "${NAME_FILE}"|awk '{print $1}') -gt 100000000 ]] && _NOTICE "ERROR" "${NAME}文件大于 100MB!"
HTML_UPLOAD=$(curl --connect-timeout 120 -m 5000 --retry 2 -s -b "ylogin=${COOKIE_YLOGIN};phpdisk_info=${COOKIE_PHPDISK_INFO}" -H "${URL_UPLOAD}" -F "task=1" -F "id=WU_FILE_0" -F "folder_id=${FOLDER_ID}" -F "name=${NAME}" -F "upload_file=@${NAME_FILE}" "${URL_UPLOAD}"|grep '\\u4e0a\\u4f20\\u6210\\u529f')
[[ -z "${HTML_UPLOAD}" ]] && _NOTICE "ERROR" "${NAME}文件上传失败!"
echo -e "${INFO} 文件上传成功![$(date -u -d '+8 hour' '+%Y-%m-%d %H:%M:%S')]"
}
NAME="$1" # 文件名
NAME_FILE="$2" # 文件路径
FOLDER_ID="$3" # 上传文件夹ID
if [[ -z "${NAME}" ]]; then
echo -e "${ERROR} 未指定文件名!" && exit 1
elif [[ -z "${NAME_FILE}" ]]; then
echo -e "${ERROR} 未指定文件路径!" && exit 1
elif [[ -z "${FOLDER_ID}" ]]; then
echo -e "${ERROR} 未指定上传文件夹ID!" && exit 1
fi
_CHECK_LOGIN
_UPLOAD

@ -6,11 +6,6 @@ on:
push: push:
branches: branches:
- master - master
# tags:
# - '3.*'
# pull_request:
# branches:
# - master
# watch: # watch:
# types: [started] # types: [started]
# schedule: # schedule:
@ -25,16 +20,21 @@ jobs:
# LANZOU_PSD : pc.woozooo.com -> Cookie -> phpdisk_info # LANZOU_PSD : pc.woozooo.com -> Cookie -> phpdisk_info
LANZOU_ID: ${{ secrets.LANZOU_ID }} LANZOU_ID: ${{ secrets.LANZOU_ID }}
LANZOU_PSD: ${{ secrets.LANZOU_PSD }} LANZOU_PSD: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹名(需手动在蓝奏云创建 # 蓝奏云里的文件夹ID(阅读3测试版:2670621
LANZOU_FOLDER: '阅读3测试版' LANZOU_FOLDER_ID: '2670621'
# 是否上传到artifact # 是否上传到artifact
UPLOAD_ARTIFACT: 'true' UPLOAD_ARTIFACT: 'true'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set Up JDK - uses: actions/cache@v2
uses: actions/setup-java@v1
with: with:
java-version: 14 path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt - name: Clear 18PlusList.txt
run: | run: |
echo "清空18PlusList.txt" echo "清空18PlusList.txt"
@ -56,35 +56,31 @@ jobs:
run: | run: |
echo "开始进行release构建" echo "开始进行release构建"
chmod +x gradlew chmod +x gradlew
./gradlew assembleAppRelease ./gradlew assembleAppRelease --build-cache --parallel
- name: Upload App To Artifact - name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }} if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: legado apk name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk
- name: Set Up Python 3
if: ${{ env.LANZOU_PSD }}
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Dependence Install
if: ${{ env.LANZOU_PSD }}
run: |
pip install requests
pip install lanzou-api
- name: Download Script
if: ${{ env.LANZOU_PSD }}
shell: python
run: |
import requests,os
res = requests.get('https://raw.githubusercontent.com/Celeter/build/master/.github/scripts/API_lanzou.py')
with open(os.environ["FILE_PATH"], 'wb') as f:
f.write(res.content)
env:
FILE_PATH: ${{ github.workspace }}/.github/lzy.py
- name: Upload App To Lanzou - name: Upload App To Lanzou
if: ${{ env.LANZOU_PSD }} if: ${{ env.LANZOU_PSD }}
run: python $GITHUB_WORKSPACE/.github/lzy.py 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 "文件:$file"
break
fi
done
bash $GITHUB_WORKSPACE/.github/scripts/lzy.sh "$file" \
"$GITHUB_WORKSPACE/app/build/outputs/apk/app/release/$file" "${{ env.LANZOU_FOLDER_ID }}"
echo "分享链接: https://kunfei.lanzous.com/b0f810h4b"
env: env:
UPLOAD_FOLDER: ${{ github.workspace }}/app/build/outputs/apk/app/release/ LANZOU_ID: ${{ env.LANZOU_ID }}
LANZOU_PSD: ${{ env.LANZOU_PSD }}

@ -125,10 +125,12 @@ dependencies {
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.0' implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.0'
//androidX //androidX
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.media:media:1.2.1' 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.preference:preference-ktx:1.1.1' 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.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
@ -136,6 +138,18 @@ dependencies {
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.6'
//media
def media2_version = "1.1.2"
implementation "androidx.media2:media2-session:$media2_version"
//implementation "androidx.media2:media2-player:$media2_version"
//implementation 'com.google.android.exoplayer:exoplayer:2.13.0'
//Splitties
def splitties_version = '2.1.1'
implementation("com.louiscad.splitties:splitties-appctx:$splitties_version")
implementation("com.louiscad.splitties:splitties-systemservices:$splitties_version")
implementation("com.louiscad.splitties:splitties-views:$splitties_version")
//lifecycle //lifecycle
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@ -150,11 +164,6 @@ dependencies {
//paging //paging
implementation 'androidx.paging:paging-runtime-ktx:2.1.2' implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
//anko
def anko_version = '0.10.8'
implementation "org.jetbrains.anko:anko-sdk27:$anko_version"
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
//liveEventBus //liveEventBus
implementation 'com.jeremyliao:live-event-bus-x:1.5.7' implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
@ -185,7 +194,7 @@ dependencies {
implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
// //
implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7' implementation 'com.king.zxing:zxing-lite:2.0.2'
// //
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'

@ -108,6 +108,43 @@
] ]
} }
} }
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7",
"android_client_info": {
"package_name": "io.legado.play.debug"
}
},
"oauth_client": [
{
"client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.legado.play.debug",
"certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b"
}
},
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
} }
], ],
"configuration_version": "1" "configuration_version": "1"

@ -297,7 +297,8 @@
<!-- 文字处理 --> <!-- 文字处理 -->
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"
android:label="@string/receiving_shared_label"> android:label="@string/receiving_shared_label"
android:theme="@style/AppTheme.Transparent">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" /> <action android:name="android.intent.action.PROCESS_TEXT" />
@ -418,6 +419,10 @@
<intent> <intent>
<action android:name="android.intent.action.TTS_SERVICE" /> <action android:name="android.intent.action.TTS_SERVICE" />
</intent> </intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries> </queries>
</manifest> </manifest>

@ -80,3 +80,205 @@ cm5neHM=
OTl3ZW5rdQ== OTl3ZW5rdQ==
bGFvc2lqaXhz bGFvc2lqaXhz
ZnVzaHV6aGFpMQ== ZnVzaHV6aGFpMQ==
cG8xOA==
czUyMTc=
c2FuaGFveHM=
NTJrc2h1
NDhyeA==
ZWNub3ZlbA==
bGllaHVvenc=
eGlhb3FpYW5nd3g=
NTJrc2h1
NDh3eA==
NTJrc2h1
MDB1aQ==
MDFieg==
c2h1YmFvMQ==
ZG54aWFvc2h1b2E=
am5zaHViYQ==
MThzaHV3dQ==
bGV4cw==
MzM1eHM=
dXB1
ZnVndW9kdQ==
ODB0eHQ=
YWFyZWFk
eWlkdWR1MQ==
YmFuemh1d2FuZw==
cWloYW9xaWhhbw==
OHhpYW54cw==
amluamlzaHV3dQ==
d21wOA==
ZXl1c2h1d3U=
NTB4c2Y=
aGF4d3g1
cG93YW5qdWFu
d2luMTBjaXR5
eWV5ZXhzdw==
bXlzaHVnZQ==
eGlhbmd0eHN3
Y3Vpd2VpanV4
MzY2eHN3
aHVheXVld2Vua3U=
eW91ZGlhbmxlbg==
c291Nzg=
bGFucm91Mg==
cXFib29r
eW91d3V4cw==
cnVpbGlzYWxl
MzY1bXd3
ZnV3ZW5o
bGVzYmw=
YXd1Ym9vaw==
bGl5dXhpYW5nMjAyMA==
OTJwb3Bv
ZnVzaHV0dWFu
ODhkYW5tZWk=
ZG14cw==
eXVsaW56aGFueWU=
M2hlYmFv
eGd1YWd1YXhz
ZGl5aWJhbnpodTY=
aXJlYWR4cw==
c2h1YmFvOTY=
ZGl5aWJhbnpodTU1NQ==
c2Fuaml1enc=
N3Fpbmc3
NjZsZXdlbg==
a3l4czU=
MjIyMjJ4cw==
c2hhb3NodWdl
amlsaW41NQ==
bWt4czY=
amluc2h1bG91
eGlhbndhbmdz
eWlkdWR1
cWR0eHQ=
MTZib29rMQ==
am1zaHV3dQ==
MzY2eHN3
ZHliejk=
c2hvdWRhOA==
ZnlxMTg=
eWlzaHVn
eXV6aGFpd3VsYQ==
MTFiYW56aHU=
MTIzeGlhb3FpYW5n
ZGl5aWJhbnpodTk=
ZGl5aWJhbnpodQ==
MzY2eHN3
ODdzaHV3dQ==
NnF3eA==
emhlbmh1bnhpYW9zaHVv
bG9uZ3Rlbmc1Mg==
eGlueGluZ3hpYW5nemhpZmE=
ZHliejk=
ZHVvemhla2Fu
MTIzeGlhb3FpYW5n
MzM1eHM=
am1zaHV3dQ==
c2hhb3NodWdl
bGF3ZW54cw==
cnVzaHV3dQ==
MzY2eHN3
NTB4c2Y=
bGV3ZW41NQ==
aGFpdGFuZzEyMw==
aGViYW81MjA=
bHVvcWl1enc=
c3NzeHN3
c2h1c2h1d3V4cw==
cm5neHM=
cWR4aWFvc2h1bw==
dHl1ZQ==
Y2hlNDM=
bG9uZ3Rlbmcy
amZ5eHNo
aGV0dTI=
bGFvc2lqaXhz
bG9uZ3Rlbmd4cw==
bGllaHVvenc=
c2h1YmFvYW4=
eHNodW9zaHVv
NTIxZGFubWVp
YmFuemh1MjI=
cWtzaHU=
eWZ4aWFvc2h1b2U=
a3lnc28=
c2h1bG91YmE=
NXRucw==
N3Fpbmc3
bWlhb2R1NQ==
eXVzaHV3ZW4=
YWFyZWFk
cXRzaHU=
MTdzaHV3dQ==
c2h1YmFvMnM=
YnowMDE=
ZGFtb2dl
MTMxdGI=
aXhpYW9z
bXlzaHVnZQ==
OXhpYW53ZW4=
ZHVvemhla2Fu
MTIwdw==
c2h1c2h1d3U1MjA=
c2h1YmFvMnM=
YWd4c3c=
OTR4c3c=
cG8xOA==
eWFvY2hpeHM=
eGlhb3FpYW5neHM=
Ym9va2Js
c2Fuaml1eHM=
d29kZXNodWJhbw==
em9uZ2NhaXhpYW9zaHVvMg==
OWI4OTEzOTRkZjVi
MThub3ZlbA==
YWFib29r
YjF0eHQ=
eXVjYWl6dw==
Yzl0eHQ=
ZGl5aWJhbnpodTU1NQ==
MzBtYw==
eGlueXVzaHV3dQ==
c2h1YmFvd2FuZzEyMw==
YWd4cw==
YmlxdWdlbmw=
c2hpcWlzaHV3dQ==
c2lsdWtl
ZGl5aWJhbnpodTg=
ZGl5aWJhbnpodTk=
aGV0dW54cw==
OTl3ZW5rdQ==
aGFpdGFuZ3NodXd1
OTd5ZA==
eXV6aGFpd3UxMQ==
Y3Vpd2VpanV4cw==
Y2JpcXU=
NTIxZGFubWVp
c2h1YmFvMzM=
c2FuaGFvMQ==
dGlhbm1lbmd3ZW5rdQ==
eXVzaHV3dTUyMA==
c2h1YmFvMjIy
c2h1YmFvd2FuZzEyMw==
eXVib29r
Y2JpcXU=
MWxld2Vu
MTV4c3c=
eG5jd3h3
c2h1YmFvd2FuZzEyMw==
c2FuaGFveHM=
eXV3YW5nc2hl
YmlxdXRz
bGFtZWl4cw==
eGJhbnpodQ==
cWR4aWFvc2h1bw==
bWh0bGE=
OTl3ZW5rdQ==
eGlhb3FpYW5nNTIw
dGlhbm1lbmd3ZW5rdQ==
YWlmdXNodQ==
bWlhb2R1NQ==
bWlmZW5neHM=

@ -17,5 +17,14 @@
"sourceName": "阅读下载", "sourceName": "阅读下载",
"sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases", "sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases",
"style": "" "style": ""
},
{
"customOrder": 1,
"enableJs": true,
"enabled": true,
"singleUrl": true,
"sourceIcon": "http:\/\/mmbiz.qpic.cn\/mmbiz_png\/hpfMV8hEuL2eS6vnCxvTzoOiaCAibV6exBzJWq9xMic9xDg3YXAick87tsfafic0icRwkQ5ibV0bJ84JtSuxhPuEDVquA\/0?wx_fmt=png",
"sourceName": "小说拾遗",
"sourceUrl": "snssdk1128:\/\/user\/profile\/562564899806367"
} }
] ]

@ -13,6 +13,9 @@
3. “勐”、“收十”、“猫腻”、“发动机”、“沃尔沃”等词无法替换净化,如何解决? 3. “勐”、“收十”、“猫腻”、“发动机”、“沃尔沃”等词无法替换净化,如何解决?
* 请在 界面 - 简/繁 中关闭简繁互转功能. * 请在 界面 - 简/繁 中关闭简繁互转功能.
4. 漫画源看书显示乱码,如何解决?
* 异次元和阅读是两个不同的软件,**两个软件的源并不通用**,请导入阅读的支持的漫画源!
## 书源相关 ## 书源相关
1. 如何导入本地书源文件? 1. 如何导入本地书源文件?
@ -121,6 +124,16 @@
11. 看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办? 11. 看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办?
* 在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。 * 在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。
12. 为什么每一章的最后一页,阅读的文字和横线背景总是对不齐?
* 请在 设置-文字底部对齐 选项中关闭底部对齐,再调整排版。
13. 漫画源或图片章节只能看到第一页,如何解决?
* 请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的 **⁝** 按钮,在弹出的菜单中,选择 **翻页动画(本书)**,将翻页动画更改为 **滚动**
14. 阅读图片章节、漫画或epub插图时,图片被缩放到一页中,以至无法看清,如何处理?
* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考Q13中的方案将翻页动画更改为**滚动**,再点击书籍界面的章节标题进入 编辑书源 界面,在 正文-图片样式 中填入 *full*,保存更改,刷新当前章节即可。
## 替换净化相关 ## 替换净化相关
1. 替换净化是什么? 1. 替换净化是什么?
* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 * 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。

@ -0,0 +1,6 @@
# 替换管理界面帮助
* 替换规则是用来替换正文内容的一种规则
* 菜单可以新建和导入规则
* 可以拖动排序
* 可以选择操作

@ -45,7 +45,7 @@ src 内容,源码
上述js变量与函数中,一些js的对象属性用的频率较高,在此列举。方便写源的时候快速翻阅。 上述js变量与函数中,一些js的对象属性用的频率较高,在此列举。方便写源的时候快速翻阅。
### book对象的可用属性 ### book对象的可用属性
> 使用方法: 在js中或{{}}中使用book.属性的方式即可获取.如在正文内容后加上 ##{{book.name+"正文卷"+title}} 可以净化 书名+正文卷+章节名称(如 我是大明星正卷第二章我爸是豪门总裁) 这一类的字符. > 使用方法: 在js中或{{}}中使用book.属性的方式即可获取.如在正文内容后加上 ##{{book.name+"正文卷"+title}} 可以净化 书名+正文卷+章节名称(如 我是大明星正卷第二章我爸是豪门总裁) 这一类的字符.
``` ```
bookUrl // 详情页Url(本地书源存储完整文件路径) bookUrl // 详情页Url(本地书源存储完整文件路径)
tocUrl // 目录页Url (toc=table of Contents) tocUrl // 目录页Url (toc=table of Contents)

@ -3,8 +3,57 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**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
* 订阅添加夜间模式,需启用js,还不是很完善
* 优化源导入界面
**2021/02/03**
* 排版导出文件名修改为配置名称
* 取消在线朗读下载文件检测,会导致朗读中断
* 修复其它一些bug
**2021/01/30**
* 优化阅读记录界面
* 自定义分组可以隐藏,删除按钮移到编辑对话框
* 修复其它一些bug
**2021/01/23** **2021/01/23**
* 优化书源校验,从搜索到正文全部校验 * 优化书源校验,从搜索到正文全部校验
* play版可以设置背景图片
* 添加几个js方法,见io.legado.app.help.JsExtensions
**2021/01/20** **2021/01/20**
* 更新在线朗读 * 更新在线朗读
@ -262,27 +311,4 @@ getTxtInFolder(unzipPath: String): String
**2020/09/10** **2020/09/10**
* 修复自动换源的bug * 修复自动换源的bug
* 修复保存主题的bug * 修复保存主题的bug
* 书源排序,分享,注释优化 by [h11128](https://github.com/h11128) * 书源排序,分享,注释优化 by [h11128](https://github.com/h11128)
**2020/09/09**
* 修复主题导入导出bug
* 优化分屏模式状态栏
* 书源基本属性增加“书源注释”
* 页眉页脚跟随背景
* 主题导入导出
* 订阅源和替换规则添加滑动选择
* 修复排版配置导入导出
* 订阅界面添加下载文件功能
* 优化翻页
* EInk模式独立背景
* 阅读排版配置导入导出,包括背景和字体,支持网络导入
* 修复替换中的回车消失的bug
* 所有内容恢复htmlFormat, 在想其它办法解决丢失一些内容的问题
* 图片(漫画)支持导出
* 搜索url支持put,get,js里使用java.put,java.get
* 正文合并后替换规则支持所有规则写法,包括js
* 导入书源列表添加全不选
* 详情页菜单添加清理缓存,清理当前书籍缓存
* 修复滑动选择,选择数量不更新的bug
* 字体跟随背景,每个背景对应一个字体
* 优化图片下载

@ -290,10 +290,10 @@
<textarea rows="1" id="customOrder" class="base" title="customOrder" <textarea rows="1" id="customOrder" class="base" title="customOrder"
placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea> placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
</div> </div>
<div> <div style="display:none;">
<div>更新时间:</div> <div>更新时间:</div>
<textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime" <textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime"
placeholder="整数: 0~N (可选,默认0) | 暂未使用"></textarea> placeholder="毫秒级时间戳 (自动生成) | 请勿手动填写"></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -386,4 +386,4 @@
<script type="text/javascript" src="index.js"></script> <script type="text/javascript" src="index.js"></script>
</body> </body>
</html> </html>

@ -36,27 +36,22 @@ function Container() {
// 搜索规则 // 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = ''); $$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
//ruleJson.ruleSearch = JSON.stringify(searchJson);
ruleJson.ruleSearch = searchJson; ruleJson.ruleSearch = searchJson;
// 发现规则 // 发现规则
$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = ''); $$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
//ruleJson.ruleExplore = JSON.stringify(exploreJson);
ruleJson.ruleExplore = exploreJson; ruleJson.ruleExplore = exploreJson;
// 详情页规则 // 详情页规则
$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = ''); $$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
//ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
ruleJson.ruleBookInfo = bookInfoJson; ruleJson.ruleBookInfo = bookInfoJson;
// 目录规则 // 目录规则
$$('.rules .ruleToc').forEach(item => tocJson[item.title] = ''); $$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
//ruleJson.ruleToc = JSON.stringify(tocJson);
ruleJson.ruleToc = tocJson; ruleJson.ruleToc = tocJson;
// 正文规则 // 正文规则
$$('.rules .ruleContent').forEach(item => contentJson[item.title] = ''); $$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
//ruleJson.ruleContent = JSON.stringify(contentJson);
ruleJson.ruleContent = contentJson; ruleJson.ruleContent = contentJson;
return ruleJson; return ruleJson;
@ -116,55 +111,45 @@ function rule2json() {
// 转换搜索规则 // 转换搜索规则
let searchJson = {}; let searchJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => { Object.keys(RuleJSON.ruleSearch).forEach(key => {
if ($('#' + 'ruleSearch_' + key).value) if ($('#' + 'ruleSearch_' + key).value)
searchJson[key] = $('#' + 'ruleSearch_' + key).value; searchJson[key] = $('#' + 'ruleSearch_' + key).value;
}); });
//RuleJSON.ruleSearch = JSON.stringify(searchJson);
RuleJSON.ruleSearch = searchJson; RuleJSON.ruleSearch = searchJson;
// 转换发现规则 // 转换发现规则
let exploreJson = {}; let exploreJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
Object.keys(RuleJSON.ruleExplore).forEach(key => { Object.keys(RuleJSON.ruleExplore).forEach(key => {
if ($('#' + 'ruleExplore_' + key).value) if ($('#' + 'ruleExplore_' + key).value)
exploreJson[key] = $('#' + 'ruleExplore_' + key).value; exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
}); });
//RuleJSON.ruleExplore = JSON.stringify(exploreJson);
RuleJSON.ruleExplore = exploreJson; RuleJSON.ruleExplore = exploreJson;
// 转换详情页规则 // 转换详情页规则
let bookInfoJson = {}; let bookInfoJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
Object.keys(RuleJSON.ruleBookInfo).forEach(key => { Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
if ($('#' + 'ruleBookInfo_' + key).value) if ($('#' + 'ruleBookInfo_' + key).value)
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value; bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
}); });
//RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
RuleJSON.ruleBookInfo = bookInfoJson; RuleJSON.ruleBookInfo = bookInfoJson;
// 转换目录规则 // 转换目录规则
let tocJson = {}; let tocJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
Object.keys(RuleJSON.ruleToc).forEach(key => { Object.keys(RuleJSON.ruleToc).forEach(key => {
if ($('#' + 'ruleToc_' + key).value) if ($('#' + 'ruleToc_' + key).value)
tocJson[key] = $('#' + 'ruleToc_' + key).value; tocJson[key] = $('#' + 'ruleToc_' + key).value;
}); });
//RuleJSON.ruleToc = JSON.stringify(tocJson);
RuleJSON.ruleToc = tocJson; RuleJSON.ruleToc = tocJson;
// 转换正文规则 // 转换正文规则
let contentJson = {}; let contentJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
Object.keys(RuleJSON.ruleContent).forEach(key => { Object.keys(RuleJSON.ruleContent).forEach(key => {
if ($('#' + 'ruleContent_' + key).value) if ($('#' + 'ruleContent_' + key).value)
contentJson[key] = $('#' + 'ruleContent_' + key).value; contentJson[key] = $('#' + 'ruleContent_' + key).value;
}); });
//RuleJSON.ruleContent = JSON.stringify(contentJson);
RuleJSON.ruleContent = contentJson; RuleJSON.ruleContent = contentJson;
RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime); RuleJSON.lastUpdateTime = new Date().getTime();
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder); RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.weight); RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.weight);
@ -193,9 +178,7 @@ function json2rule(RuleEditor) {
// 转换搜索规则 // 转换搜索规则
if (RuleEditor.ruleSearch) { if (RuleEditor.ruleSearch) {
//let searchJson = JSON.parse(RuleEditor.ruleSearch);
let searchJson = RuleEditor.ruleSearch; let searchJson = RuleEditor.ruleSearch;
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => { Object.keys(RuleJSON.ruleSearch).forEach(key => {
$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : ''; $('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
}); });
@ -203,8 +186,6 @@ function json2rule(RuleEditor) {
// 转换发现规则 // 转换发现规则
if (RuleEditor.ruleExplore) { if (RuleEditor.ruleExplore) {
//let exploreJson = JSON.parse(RuleEditor.ruleExplore);
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
let exploreJson = RuleEditor.ruleExplore; let exploreJson = RuleEditor.ruleExplore;
Object.keys(RuleJSON.ruleExplore).forEach(key => { Object.keys(RuleJSON.ruleExplore).forEach(key => {
$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : ''; $('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
@ -213,8 +194,6 @@ function json2rule(RuleEditor) {
// 转换详情页规则 // 转换详情页规则
if (RuleEditor.ruleBookInfo) { if (RuleEditor.ruleBookInfo) {
//let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
let bookInfoJson = RuleEditor.ruleBookInfo; let bookInfoJson = RuleEditor.ruleBookInfo;
Object.keys(RuleJSON.ruleBookInfo).forEach(key => { Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : ''; $('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
@ -223,8 +202,6 @@ function json2rule(RuleEditor) {
// 转换目录规则 // 转换目录规则
if (RuleEditor.ruleToc) { if (RuleEditor.ruleToc) {
//let tocJson = JSON.parse(RuleEditor.ruleToc);
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
let tocJson = RuleEditor.ruleToc; let tocJson = RuleEditor.ruleToc;
Object.keys(RuleJSON.ruleToc).forEach(key => { Object.keys(RuleJSON.ruleToc).forEach(key => {
$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : ''; $('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
@ -233,8 +210,6 @@ function json2rule(RuleEditor) {
// 转换正文规则 // 转换正文规则
if (RuleEditor.ruleContent) { if (RuleEditor.ruleContent) {
//let contentJson = JSON.parse(RuleEditor.ruleContent);
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
let contentJson = RuleEditor.ruleContent; let contentJson = RuleEditor.ruleContent;
Object.keys(RuleJSON.ruleContent).forEach(key => { Object.keys(RuleJSON.ruleContent).forEach(key => {
$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : ''; $('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
@ -538,4 +513,4 @@ $('.tab3>.titlebar').addEventListener('click', e => {
break; break;
default: default:
} }
}); });

@ -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.b4324411.css" rel="prefetch"><link href="js/about.4300b5ad.js" rel="prefetch"><link href="js/about~detail.c1b29cbc.js" rel="prefetch"><link href="js/detail.4d0609e7.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.acf50eba.js" rel="preload" as="script"><link href="js/chunk-vendors.1042cf7f.js" rel="preload" as="script"><link href="css/chunk-vendors.ad4ff18f.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="yd-web-tool"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><style>body::-webkit-scrollbar {
display: none; display: none;
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.060eee33.js"></script><script src="js/app.d60dad33.js"></script></body></html> }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.1042cf7f.js"></script><script src="js/app.acf50eba.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,18 +1,18 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([ self.__precacheManifest = (self.__precacheManifest || []).concat([
{ {
"revision": "414c7503fa98c3f2e328", "revision": "12386c7dce7d463af4ed",
"url": "css/about.dbe575e1.css" "url": "css/about.dbe575e1.css"
}, },
{ {
"revision": "13925596cfa342cbb55d", "revision": "d761b0dee902b1105e91",
"url": "css/app.e4c919b7.css" "url": "css/app.e4c919b7.css"
}, },
{ {
"revision": "d53f06b957b78ddb78b7", "revision": "16ab934214be8df372e3",
"url": "css/chunk-vendors.ad4ff18f.css" "url": "css/chunk-vendors.ad4ff18f.css"
}, },
{ {
"revision": "0607c99f49945c348397", "revision": "4083fbb292940cf804ca",
"url": "css/detail.b4324411.css" "url": "css/detail.b4324411.css"
}, },
{ {
@ -40,28 +40,28 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "img/noCover.b5c48bc1.jpeg" "url": "img/noCover.b5c48bc1.jpeg"
}, },
{ {
"revision": "8ff165c058a9618dc10065e65133a4e7", "revision": "bc7efb33b2dbae4d4578e3657e73de4b",
"url": "index.html" "url": "index.html"
}, },
{ {
"revision": "414c7503fa98c3f2e328", "revision": "12386c7dce7d463af4ed",
"url": "js/about.d8b0dc91.js" "url": "js/about.4300b5ad.js"
}, },
{ {
"revision": "2c81bd893f3a92f018d8", "revision": "2828d7869f38c2483b33",
"url": "js/about~detail.1caf6ef5.js" "url": "js/about~detail.c1b29cbc.js"
}, },
{ {
"revision": "13925596cfa342cbb55d", "revision": "d761b0dee902b1105e91",
"url": "js/app.d60dad33.js" "url": "js/app.acf50eba.js"
}, },
{ {
"revision": "d53f06b957b78ddb78b7", "revision": "16ab934214be8df372e3",
"url": "js/chunk-vendors.060eee33.js" "url": "js/chunk-vendors.1042cf7f.js"
}, },
{ {
"revision": "0607c99f49945c348397", "revision": "4083fbb292940cf804ca",
"url": "js/detail.6a5cc86c.js" "url": "js/detail.4d0609e7.js"
}, },
{ {
"revision": "b46d04eb43bc31ca0f9f95121646440d", "revision": "b46d04eb43bc31ca0f9f95121646440d",

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

@ -5,58 +5,33 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.channelIdDownload import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.constant.EventBus import io.legado.app.help.ActivityHelp
import io.legado.app.data.AppDatabase import io.legado.app.help.AppConfig
import io.legado.app.help.* import io.legado.app.help.CrashHandler
import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.utils.LanguageUtils import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.postEvent import io.legado.app.utils.defaultSharedPreferences
import org.jetbrains.anko.defaultSharedPreferences
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
@Suppress("DEPRECATION")
class App : MultiDexApplication() { class App : MultiDexApplication() {
companion object {
@JvmStatic
lateinit var INSTANCE: App
private set
@JvmStatic
lateinit var db: AppDatabase
private set
lateinit var androidId: String
var versionCode = 0
var versionName = ""
var navigationBarHeight = 0
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
INSTANCE = this
androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID)
CrashHandler(this) CrashHandler(this)
LanguageUtils.setConfiguration(this) LanguageUtils.setConfiguration(this)
db = AppDatabase.createDatabase(INSTANCE)
RxHttp.init(HttpHelper.client, BuildConfig.DEBUG) RxHttp.init(HttpHelper.client, BuildConfig.DEBUG)
RxHttp.setOnParamAssembly { RxHttp.setOnParamAssembly {
it.addHeader(AppConst.UA_NAME, AppConfig.userAgent) it.addHeader(AppConst.UA_NAME, AppConfig.userAgent)
} }
packageManager.getPackageInfo(packageName, 0)?.let {
versionCode = it.versionCode
versionName = it.versionName
}
createNotificationChannels() createNotificationChannels()
applyDayNight() applyDayNight(this)
LiveEventBus.config() LiveEventBus.config()
.supportBroadcast(this) .supportBroadcast(this)
.lifecycleObserverAlwaysActive(true) .lifecycleObserverAlwaysActive(true)
@ -69,27 +44,10 @@ class App : MultiDexApplication() {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) { when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES, Configuration.UI_MODE_NIGHT_YES,
Configuration.UI_MODE_NIGHT_NO -> applyDayNight() Configuration.UI_MODE_NIGHT_NO -> applyDayNight(this)
} }
} }
fun applyDayNight() {
ReadBookConfig.upBg()
ThemeConfig.applyTheme(this)
initNightMode()
postEvent(EventBus.RECREATE, "")
}
private fun initNightMode() {
val targetMode =
if (AppConfig.isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(targetMode)
}
/** /**
* 创建通知ID * 创建通知ID
*/ */
@ -99,7 +57,7 @@ class App : MultiDexApplication() {
val downloadChannel = NotificationChannel( val downloadChannel = NotificationChannel(
channelIdDownload, channelIdDownload,
getString(R.string.action_download), getString(R.string.action_download),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_DEFAULT
).apply { ).apply {
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
@ -109,7 +67,7 @@ class App : MultiDexApplication() {
val readAloudChannel = NotificationChannel( val readAloudChannel = NotificationChannel(
channelIdReadAloud, channelIdReadAloud,
getString(R.string.read_aloud), getString(R.string.read_aloud),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_DEFAULT
).apply { ).apply {
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
@ -119,7 +77,7 @@ class App : MultiDexApplication() {
val webChannel = NotificationChannel( val webChannel = NotificationChannel(
channelIdWeb, channelIdWeb,
getString(R.string.web_service), getString(R.string.web_service),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_DEFAULT
).apply { ).apply {
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
@ -131,4 +89,8 @@ class App : MultiDexApplication() {
} }
} }
companion object {
var navigationBarHeight = 0
}
} }

@ -1,8 +1,8 @@
package io.legado.app.api.controller package io.legado.app.api.controller
import io.legado.app.App
import io.legado.app.api.ReturnData import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
@ -12,17 +12,18 @@ import io.legado.app.utils.cnCompare
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import splitties.init.appCtx
object BookshelfController { object BookshelfController {
val bookshelf: ReturnData val bookshelf: ReturnData
get() { get() {
val books = App.db.bookDao.all val books = appDb.bookDao.all
val returnData = ReturnData() val returnData = ReturnData()
return if (books.isEmpty()) { return if (books.isEmpty()) {
returnData.setErrorMsg("还没有添加小说") returnData.setErrorMsg("还没有添加小说")
} else { } else {
val data = when (App.INSTANCE.getPrefInt(PreferKey.bookshelfSort)) { val data = when (appCtx.getPrefInt(PreferKey.bookshelfSort)) {
1 -> books.sortedByDescending { it.latestChapterTime } 1 -> books.sortedByDescending { it.latestChapterTime }
2 -> books.sortedWith { o1, o2 -> 2 -> books.sortedWith { o1, o2 ->
o1.name.cnCompare(o2.name) o1.name.cnCompare(o2.name)
@ -40,7 +41,7 @@ object BookshelfController {
if (bookUrl.isNullOrEmpty()) { if (bookUrl.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
} }
val chapterList = App.db.bookChapterDao.getChapterList(bookUrl) val chapterList = appDb.bookChapterDao.getChapterList(bookUrl)
return returnData.setData(chapterList) return returnData.setData(chapterList)
} }
@ -54,8 +55,8 @@ object BookshelfController {
if (index == null) { if (index == null) {
return returnData.setErrorMsg("参数index不能为空, 请指定目录序号") return returnData.setErrorMsg("参数index不能为空, 请指定目录序号")
} }
val book = App.db.bookDao.getBook(bookUrl) val book = appDb.bookDao.getBook(bookUrl)
val chapter = App.db.bookChapterDao.getChapter(bookUrl, index) val chapter = appDb.bookChapterDao.getChapter(bookUrl, index)
if (book == null || chapter == null) { if (book == null || chapter == null) {
returnData.setErrorMsg("未找到") returnData.setErrorMsg("未找到")
} else { } else {
@ -64,7 +65,7 @@ object BookshelfController {
saveBookReadIndex(book, index) saveBookReadIndex(book, index)
returnData.setData(content) returnData.setData(content)
} else { } else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { source -> appDb.bookSourceDao.getBookSource(book.origin)?.let { source ->
runBlocking { runBlocking {
WebBook(source).getContentAwait(this, book, chapter) WebBook(source).getContentAwait(this, book, chapter)
}.let { }.let {
@ -81,7 +82,7 @@ object BookshelfController {
val book = GSON.fromJsonObject<Book>(postData) val book = GSON.fromJsonObject<Book>(postData)
val returnData = ReturnData() val returnData = ReturnData()
if (book != null) { if (book != null) {
App.db.bookDao.insert(book) appDb.bookDao.insert(book)
if (ReadBook.book?.bookUrl == book.bookUrl) { if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.book = book ReadBook.book = book
ReadBook.durChapterIndex = book.durChapterIndex ReadBook.durChapterIndex = book.durChapterIndex
@ -95,10 +96,10 @@ object BookshelfController {
if (index > book.durChapterIndex) { if (index > book.durChapterIndex) {
book.durChapterIndex = index book.durChapterIndex = index
book.durChapterTime = System.currentTimeMillis() book.durChapterTime = System.currentTimeMillis()
App.db.bookChapterDao.getChapter(book.bookUrl, index)?.let { appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
book.durChapterTitle = it.title book.durChapterTitle = it.title
} }
App.db.bookDao.update(book) appDb.bookDao.update(book)
if (ReadBook.book?.bookUrl == book.bookUrl) { if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.book = book ReadBook.book = book
ReadBook.durChapterIndex = index ReadBook.durChapterIndex = index

@ -2,8 +2,8 @@ package io.legado.app.api.controller
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App
import io.legado.app.api.ReturnData import io.legado.app.api.ReturnData
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonArray
@ -14,7 +14,7 @@ object SourceController {
val sources: ReturnData val sources: ReturnData
get() { get() {
val bookSources = App.db.bookSourceDao.all val bookSources = appDb.bookSourceDao.all
val returnData = ReturnData() val returnData = ReturnData()
return if (bookSources.isEmpty()) { return if (bookSources.isEmpty()) {
returnData.setErrorMsg("设备书源列表为空") returnData.setErrorMsg("设备书源列表为空")
@ -29,7 +29,7 @@ object SourceController {
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) { if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
returnData.setErrorMsg("书源名称和URL不能为空") returnData.setErrorMsg("书源名称和URL不能为空")
} else { } else {
App.db.bookSourceDao.insert(bookSource) appDb.bookSourceDao.insert(bookSource)
returnData.setData("") returnData.setData("")
} }
} else { } else {
@ -50,7 +50,7 @@ object SourceController {
if (bookSource.bookSourceName.isBlank() || bookSource.bookSourceUrl.isBlank()) { if (bookSource.bookSourceName.isBlank() || bookSource.bookSourceUrl.isBlank()) {
continue continue
} }
App.db.bookSourceDao.insert(bookSource) appDb.bookSourceDao.insert(bookSource)
okSources.add(bookSource) okSources.add(bookSource)
} }
} }
@ -64,7 +64,7 @@ object SourceController {
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书源地址") return returnData.setErrorMsg("参数url不能为空,请指定书源地址")
} }
val bookSource = App.db.bookSourceDao.getBookSource(url) val bookSource = appDb.bookSourceDao.getBookSource(url)
?: return returnData.setErrorMsg("未找到书源,请检查书源地址") ?: return returnData.setErrorMsg("未找到书源,请检查书源地址")
return returnData.setData(bookSource) return returnData.setData(bookSource)
} }
@ -73,7 +73,7 @@ object SourceController {
kotlin.runCatching { kotlin.runCatching {
GSON.fromJsonArray<BookSource>(postData)?.let { GSON.fromJsonArray<BookSource>(postData)?.let {
it.forEach { source -> it.forEach { source ->
App.db.bookSourceDao.delete(source) appDb.bookSourceDao.delete(source)
} }
} }
} }

@ -16,6 +16,7 @@ import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ThemeConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
@ -151,6 +152,11 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView) ATH.applyBackgroundTint(window.decorView)
} }
} }
if (AppConfig.isGooglePlay) {
ThemeConfig.getBgImage(this)?.let {
window.decorView.background = it
}
}
} }
private fun setupSystemBar() { private fun setupSystemBar() {

@ -6,15 +6,13 @@ import androidx.annotation.CallSuper
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import io.legado.app.App import io.legado.app.App
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@Suppress("unused") @Suppress("unused")
open class BaseViewModel(application: Application) : AndroidViewModel(application), open class BaseViewModel(application: Application) : AndroidViewModel(application),
CoroutineScope by MainScope(), CoroutineScope by MainScope() {
AnkoLogger {
val context: Context by lazy { this.getApplication<App>() } val context: Context by lazy { this.getApplication<App>() }
@ -40,27 +38,12 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio
cancel() cancel()
} }
open fun toast(message: Int) { open fun toastOnUi(message: Int) {
launch { context.toastOnUi(message)
context.toast(message)
}
} }
open fun toast(message: CharSequence?) { open fun toastOnUi(message: CharSequence?) {
launch { context.toastOnUi(message ?: toString())
context.toast(message ?: toString())
}
} }
open fun longToast(message: Int) {
launch {
context.toast(message)
}
}
open fun longToast(message: CharSequence?) {
launch {
context.toast(message ?: toString())
}
}
} }

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import splitties.views.onLongClick
/** /**
* Created by Invincible on 2017/12/15. * Created by Invincible on 2017/12/15.
@ -130,10 +131,10 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
} }
if (itemLongClickListener != null) { if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener { holder.itemView.onLongClick {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it) ?: true itemLongClickListener?.invoke(holder, it)
} ?: true }
} }
} }

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import splitties.views.onLongClick
import java.util.* import java.util.*
/** /**
@ -351,10 +352,10 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
if (itemLongClickListener != null) { if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener { holder.itemView.onLongClick {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it) ?: true itemLongClickListener?.invoke(holder, it)
} ?: true }
} }
} }

@ -1,8 +1,8 @@
package io.legado.app.constant package io.legado.app.constant
import android.annotation.SuppressLint import android.annotation.SuppressLint
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import splitties.init.appCtx
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.script.ScriptEngine import javax.script.ScriptEngine
import javax.script.ScriptEngineManager import javax.script.ScriptEngineManager
@ -68,5 +68,12 @@ object AppConst {
"androidx.appcompat.view.menu.ListMenuItemView" "androidx.appcompat.view.menu.ListMenuItemView"
) )
val sysElevation = App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt() val sysElevation = appCtx.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
val darkWebViewJs by lazy {
"""
document.body.style.backgroundColor = "#222222";
document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a';
""".trimIndent()
}
} }

@ -0,0 +1,30 @@
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
}
val charsets =
arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII")
data class AppInfo(
var versionCode: Long = 0L,
var versionName: String = ""
)

@ -39,7 +39,7 @@ object PreferKey {
const val webDavAccount = "web_dav_account" const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password" const val webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir" const val webDavCreateDir = "webDavCreateDir"
const val webDavCacheBackup = "webDavCacheBackup" const val exportToWebDav = "webDavCacheBackup"
const val changeSourceLoadToc = "changeSourceLoadToc" const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo" const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType" const val chineseConverterType = "chineseConverterType"
@ -65,6 +65,11 @@ object PreferKey {
const val screenOrientation = "screenOrientation" const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress" const val syncBookProgress = "syncBookProgress"
const val preDownload = "preDownload" const val preDownload = "preDownload"
const val autoRefresh = "auto_refresh"
const val defaultToRead = "defaultToRead"
const val exportCharset = "exportCharset"
const val exportUseReplace = "exportUseReplace"
const val useZhLayout = "useZhLayout"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -4,16 +4,18 @@ import io.legado.app.help.AppConfig
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils
enum class Theme { enum class Theme {
Dark, Light, Auto, Transparent; Dark, Light, Auto, Transparent, EInk;
companion object { companion object {
fun getTheme() = fun getTheme() = when {
if (AppConfig.isNightTheme) Dark AppConfig.isEInkMode -> EInk
else Light AppConfig.isNightTheme -> Dark
else -> Light
}
fun getTheme(backgroundColor: Int) = fun getTheme(backgroundColor: Int) =
if (ColorUtils.isColorLight(backgroundColor)) Light if (ColorUtils.isColorLight(backgroundColor)) Light
else Dark else Dark
} }
} }

@ -6,20 +6,24 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.androidId
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import splitties.init.appCtx
import java.util.* import java.util.*
val appDb by lazy {
AppDatabase.createDatabase(appCtx)
}
@Database( @Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class], RuleSub::class, EpubChapter::class],
version = 28, version = 30,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -36,11 +40,12 @@ abstract class AppDatabase : RoomDatabase() {
abstract val rssArticleDao: RssArticleDao abstract val rssArticleDao: RssArticleDao
abstract val rssStarDao: RssStarDao abstract val rssStarDao: RssStarDao
abstract val cookieDao: CookieDao abstract val cookieDao: CookieDao
abstract val txtTocRule: TxtTocRuleDao abstract val txtTocRuleDao: TxtTocRuleDao
abstract val readRecordDao: ReadRecordDao abstract val readRecordDao: ReadRecordDao
abstract val httpTTSDao: HttpTTSDao abstract val httpTTSDao: HttpTTSDao
abstract val cacheDao: CacheDao abstract val cacheDao: CacheDao
abstract val ruleSubDao: RuleSubDao abstract val ruleSubDao: RuleSubDao
abstract val epubChapterDao: EpubChapterDao
companion object { companion object {
@ -54,7 +59,7 @@ abstract class AppDatabase : RoomDatabase() {
migration_14_15, migration_15_17, migration_17_18, migration_18_19, migration_14_15, migration_15_17, migration_17_18, migration_18_19,
migration_19_20, migration_20_21, migration_21_22, migration_22_23, migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25, migration_25_26, migration_26_27, migration_23_24, migration_24_25, migration_25_26, migration_26_27,
migration_27_28 migration_27_28, migration_28_29, migration_29_30
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -151,7 +156,7 @@ abstract class AppDatabase : RoomDatabase() {
"""CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, """CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL,
PRIMARY KEY(`androidId`, `bookName`))""" PRIMARY KEY(`androidId`, `bookName`))"""
) )
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord") database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord") database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord") database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
} }
@ -251,6 +256,28 @@ abstract class AppDatabase : RoomDatabase() {
database.execSQL("ALTER TABLE rssStars ADD variable TEXT") database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
} }
} }
private val migration_28_29 = object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
}
}
private val migration_29_30 = object : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `epubChapters`
(`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT,
PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
"""
)
database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
}
}
} }
} }

@ -19,7 +19,7 @@ interface BookGroupDao {
@Query( @Query(
""" """
SELECT * FROM book_groups where groupId >= 0 SELECT * FROM book_groups where (groupId >= 0 and show > 0)
or (groupId = -4 and show > 0 and (select count(bookUrl) from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0) > 0) or (groupId = -4 and show > 0 and (select count(bookUrl) from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0) > 0)
or (groupId = -3 and show > 0 and (select count(bookUrl) from books where type = ${BookType.audio}) > 0) or (groupId = -3 and show > 0 and (select count(bookUrl) from books where type = ${BookType.audio}) > 0)
or (groupId = -2 and show > 0 and (select count(bookUrl) from books where origin = '${BookType.local}') > 0) or (groupId = -2 and show > 0 and (select count(bookUrl) from books where origin = '${BookType.local}') > 0)

@ -0,0 +1,32 @@
package io.legado.app.data.dao
import androidx.room.*
import io.legado.app.data.entities.EpubChapter
@Dao
interface EpubChapterDao {
@get:Query("select * from epubChapters")
val all: List<EpubChapter>
@Query("select count(*) from epubChapters Where bookUrl = :bookUrl")
fun getCnt(bookUrl: String): Int
@Query("select * from epubChapters Where bookUrl = :bookUrl and parentHref = :parentHref ")
fun get(bookUrl: String, parentHref: String): List<EpubChapter>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg chapter: EpubChapter)
@Query("delete from epubChapters")
fun clear()
@Query("delete from epubChapters Where bookUrl = :bookUrl")
fun deleteByName(bookUrl: String)
@Delete
fun delete(vararg chapter: EpubChapter)
@Update
fun update(vararg chapter: EpubChapter)
}

@ -2,9 +2,9 @@ package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import androidx.room.* import androidx.room.*
import io.legado.app.App
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
@ -146,6 +146,31 @@ data class Book(
config().pageAnim = pageAnim config().pageAnim = pageAnim
} }
fun getImageStyle(): String? {
return config().imageStyle
}
fun setImageStyle(imageStyle: String?) {
config().imageStyle = imageStyle
}
fun getDelParagraph(): Int {
return config().delParagraph
}
fun setDelParagraph(num: Int) {
config().delParagraph = num
}
fun setDelTag(tag: Long) {
config().delTag =
if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag
}
fun getDelTag(tag: Long): Boolean {
return config().delTag and tag == tag
}
fun getFolderName(): String { fun getFolderName(): String {
//防止书名过长,只取9位 //防止书名过长,只取9位
var folderName = name.replace(AppPattern.fileNameRegex, "") var folderName = name.replace(AppPattern.fileNameRegex, "")
@ -182,14 +207,14 @@ data class Book(
newBook.canUpdate = canUpdate newBook.canUpdate = canUpdate
newBook.readConfig = readConfig newBook.readConfig = readConfig
delete() delete()
App.db.bookDao.insert(newBook) appDb.bookDao.insert(newBook)
} }
fun delete() { fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) { if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null ReadBook.book = null
} }
App.db.bookDao.delete(this) appDb.bookDao.delete(this)
} }
fun upInfoFromOld(oldBook: Book?) { fun upInfoFromOld(oldBook: Book?) {
@ -207,11 +232,23 @@ data class Book(
} }
} }
companion object {
const val hTag = 2L
const val rubyTag = 4L
const val imgTag = 8L
const val imgStyleDefault = "DEFAULT"
const val imgStyleFull = "FULL"
const val imgStyleText = "TEXT"
}
@Parcelize @Parcelize
data class ReadConfig( data class ReadConfig(
var pageAnim: Int = -1, var pageAnim: Int = -1,
var reSegment: Boolean = false, var reSegment: Boolean = false,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则 var imageStyle: String? = null,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则
var delParagraph: Int = 0,//去除段首
var delTag: Long = 0L,//去除标签
) : Parcelable ) : Parcelable
class Converters { class Converters {

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

@ -3,7 +3,6 @@ package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import android.text.TextUtils import android.text.TextUtils
import androidx.room.* import androidx.room.*
import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.rule.* import io.legado.app.data.entities.rule.*
@ -16,6 +15,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import splitties.init.appCtx
import javax.script.SimpleBindings import javax.script.SimpleBindings
@Parcelize @Parcelize
@ -105,8 +105,10 @@ data class BookSource(
var a = urlRule var a = urlRule
if (a.isNotBlank()) { if (a.isNotBlank()) {
kotlin.runCatching { kotlin.runCatching {
if (urlRule.startsWith("<js>", false)) { if (urlRule.startsWith("<js>", false)
val aCache = ACache.get(App.INSTANCE, "explore") || urlRule.startsWith("@js", false)
) {
val aCache = ACache.get(appCtx, "explore")
a = aCache.getAsString(bookSourceUrl) ?: "" a = aCache.getAsString(bookSourceUrl) ?: ""
if (a.isBlank()) { if (a.isBlank()) {
val bindings = SimpleBindings() val bindings = SimpleBindings()
@ -114,10 +116,12 @@ data class BookSource(
bindings["java"] = this bindings["java"] = this
bindings["cookie"] = CookieStore bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager bindings["cache"] = CacheManager
a = AppConst.SCRIPT_ENGINE.eval( val jsStr = if (urlRule.startsWith("@")) {
urlRule.substring(4, urlRule.lastIndexOf("<")), urlRule.substring(3)
bindings } else {
).toString() urlRule.substring(4, urlRule.lastIndexOf("<"))
}
a = AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString()
aCache.put(bookSourceUrl, a) aCache.put(bookSourceUrl, a)
} }
} }

@ -0,0 +1,23 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@Entity(
tableName = "epubChapters",
primaryKeys = ["bookUrl", "href"],
indices = [(Index(value = ["bookUrl"], unique = false)),
(Index(value = ["bookUrl", "href"], unique = true))],
foreignKeys = [(ForeignKey(
entity = Book::class,
parentColumns = ["bookUrl"],
childColumns = ["bookUrl"],
onDelete = ForeignKey.CASCADE
))]
)
data class EpubChapter(
var bookUrl: String = "",
var href: String = "",
var parentHref: String? = null,
)

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

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

@ -1,8 +1,8 @@
package io.legado.app.help package io.legado.app.help
import io.legado.app.App
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
@ -11,6 +11,7 @@ import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.apache.commons.text.similarity.JaccardSimilarity import org.apache.commons.text.similarity.JaccardSimilarity
import splitties.init.appCtx
import java.io.File import java.io.File
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import java.util.regex.Matcher import java.util.regex.Matcher
@ -22,7 +23,7 @@ import kotlin.math.min
object BookHelp { object BookHelp {
private const val cacheFolderName = "book_cache" private const val cacheFolderName = "book_cache"
private const val cacheImageFolderName = "images" private const val cacheImageFolderName = "images"
private val downloadDir: File = App.INSTANCE.externalFilesDir private val downloadDir: File = appCtx.externalFilesDir
private val downloadImages = CopyOnWriteArraySet<String>() private val downloadImages = CopyOnWriteArraySet<String>()
fun clearCache() { fun clearCache() {
@ -42,7 +43,7 @@ object BookHelp {
fun clearRemovedCache() { fun clearRemovedCache() {
Coroutine.async { Coroutine.async {
val bookFolderNames = arrayListOf<String>() val bookFolderNames = arrayListOf<String>()
App.db.bookDao.all.forEach { appDb.bookDao.all.forEach {
bookFolderNames.add(it.getFolderName()) bookFolderNames.add(it.getFolderName())
} }
val file = FileUtils.getFile(downloadDir, cacheFolderName) val file = FileUtils.getFile(downloadDir, cacheFolderName)
@ -147,7 +148,7 @@ object BookHelp {
fun getChapterFiles(book: Book): List<String> { fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>() val fileNameList = arrayListOf<String>()
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return fileNameList return fileNameList
} }
FileUtils.createFolderIfNotExist( FileUtils.createFolderIfNotExist(
@ -161,7 +162,7 @@ object BookHelp {
// 检测该章节是否下载 // 检测该章节是否下载
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
return if (book.isLocalBook()) { return if (book.isLocalTxt()) {
true true
} else { } else {
FileUtils.exists( FileUtils.exists(
@ -174,8 +175,19 @@ object BookHelp {
} }
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return LocalBook.getContext(book, bookChapter) return LocalBook.getContext(book, bookChapter)
} else if (book.isEpub() && !hasContent(book, bookChapter)) {
val string = LocalBook.getContext(book, bookChapter)
string?.let {
FileUtils.createFileIfNotExist(
downloadDir,
cacheFolderName,
book.getFolderName(),
bookChapter.getFileName(),
).writeText(it)
}
return string
} else { } else {
val file = FileUtils.getFile( val file = FileUtils.getFile(
downloadDir, downloadDir,
@ -190,8 +202,27 @@ object BookHelp {
return null return null
} }
fun reverseContent(book: Book, bookChapter: BookChapter) {
if (!book.isLocalBook()) {
val file = FileUtils.getFile(
downloadDir,
cacheFolderName,
book.getFolderName(),
bookChapter.getFileName()
)
if (file.exists()) {
val text = file.readText()
val stringBuilder = StringBuilder()
text.toStringArray().forEach {
stringBuilder.insert(0, it)
}
file.writeText(stringBuilder.toString())
}
}
}
fun delContent(book: Book, bookChapter: BookChapter) { fun delContent(book: Book, bookChapter: BookChapter) {
if (book.isLocalBook()) { if (book.isLocalTxt()) {
return return
} else { } else {
FileUtils.createFileIfNotExist( FileUtils.createFileIfNotExist(

@ -1,9 +1,10 @@
package io.legado.app.help package io.legado.app.help
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.Cache import io.legado.app.data.entities.Cache
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import splitties.init.appCtx
@Suppress("unused") @Suppress("unused")
object CacheManager { object CacheManager {
@ -19,16 +20,16 @@ object CacheManager {
if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000
when (value) { when (value) {
is QueryTTF -> queryTTFMap[key] = Pair(deadline, value) is QueryTTF -> queryTTFMap[key] = Pair(deadline, value)
is ByteArray -> ACache.get(App.INSTANCE).put(key, value, saveTime) is ByteArray -> ACache.get(appCtx).put(key, value, saveTime)
else -> { else -> {
val cache = Cache(key, value.toString(), deadline) val cache = Cache(key, value.toString(), deadline)
App.db.cacheDao.insert(cache) appDb.cacheDao.insert(cache)
} }
} }
} }
fun get(key: String): String? { fun get(key: String): String? {
return App.db.cacheDao.get(key, System.currentTimeMillis()) return appDb.cacheDao.get(key, System.currentTimeMillis())
} }
fun getInt(key: String): Int? { fun getInt(key: String): Int? {
@ -48,7 +49,7 @@ object CacheManager {
} }
fun getByteArray(key: String): ByteArray? { fun getByteArray(key: String): ByteArray? {
return ACache.get(App.INSTANCE).getAsBinary(key) return ACache.get(appCtx).getAsBinary(key)
} }
fun getQueryTTF(key: String): QueryTTF? { fun getQueryTTF(key: String): QueryTTF? {

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

@ -1,12 +1,13 @@
package io.legado.app.help package io.legado.app.help
import com.hankcs.hanlp.HanLP import com.hankcs.hanlp.HanLP
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast import splitties.init.appCtx
class ContentProcessor(private val bookName: String, private val bookOrigin: String) { class ContentProcessor(private val bookName: String, private val bookOrigin: String) {
@ -19,17 +20,18 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
@Synchronized @Synchronized
fun upReplaceRules() { fun upReplaceRules() {
replaceRules.clear() replaceRules.clear()
replaceRules.addAll(App.db.replaceRuleDao.findEnabledByScope(bookName, bookOrigin)) replaceRules.addAll(appDb.replaceRuleDao.findEnabledByScope(bookName, bookOrigin))
} }
suspend fun getContent( suspend fun getContent(
book: Book, book: Book,
title: String, //已经经过简繁转换 title: String, //已经经过简繁转换
content: String, content: String,
isRead: Boolean = true isRead: Boolean = true,
useReplace: Boolean = book.getUseReplaceRule()
): List<String> { ): List<String> {
var content1 = content var content1 = content
if (book.getUseReplaceRule()) { if (useReplace) {
replaceRules.forEach { item -> replaceRules.forEach { item ->
if (item.pattern.isNotEmpty()) { if (item.pattern.isNotEmpty()) {
try { try {
@ -40,7 +42,7 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
App.INSTANCE.toast("${item.name}替换出错") appCtx.toastOnUi("${item.name}替换出错")
} }
} }
} }
@ -57,7 +59,7 @@ class ContentProcessor(private val bookName: String, private val bookOrigin: Str
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
App.INSTANCE.toast("简繁转换出错") appCtx.toastOnUi("简繁转换出错")
} }
} }
} }

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

@ -1,11 +1,12 @@
package io.legado.app.help package io.legado.app.help
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.data.entities.TxtTocRule import io.legado.app.data.entities.TxtTocRule
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonArray
import splitties.init.appCtx
import java.io.File import java.io.File
object DefaultData { object DefaultData {
@ -16,7 +17,7 @@ object DefaultData {
val httpTTS by lazy { val httpTTS by lazy {
val json = val json =
String( String(
App.INSTANCE.assets.open("defaultData${File.separator}$httpTtsFileName") appCtx.assets.open("defaultData${File.separator}$httpTtsFileName")
.readBytes() .readBytes()
) )
GSON.fromJsonArray<HttpTTS>(json)!! GSON.fromJsonArray<HttpTTS>(json)!!
@ -24,7 +25,7 @@ object DefaultData {
val readConfigs by lazy { val readConfigs by lazy {
val json = String( val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}") appCtx.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}")
.readBytes() .readBytes()
) )
GSON.fromJsonArray<ReadBookConfig.Config>(json)!! GSON.fromJsonArray<ReadBookConfig.Config>(json)!!
@ -32,7 +33,7 @@ object DefaultData {
val txtTocRules by lazy { val txtTocRules by lazy {
val json = String( val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}$txtTocRuleFileName") appCtx.assets.open("defaultData${File.separator}$txtTocRuleFileName")
.readBytes() .readBytes()
) )
GSON.fromJsonArray<TxtTocRule>(json)!! GSON.fromJsonArray<TxtTocRule>(json)!!
@ -40,7 +41,7 @@ object DefaultData {
val themeConfigs by lazy { val themeConfigs by lazy {
val json = String( val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}") appCtx.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}")
.readBytes() .readBytes()
) )
GSON.fromJsonArray<ThemeConfig.Config>(json)!! GSON.fromJsonArray<ThemeConfig.Config>(json)!!
@ -48,23 +49,23 @@ object DefaultData {
val rssSources by lazy { val rssSources by lazy {
val json = String( val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}rssSources.json") appCtx.assets.open("defaultData${File.separator}rssSources.json")
.readBytes() .readBytes()
) )
GSON.fromJsonArray<RssSource>(json)!! GSON.fromJsonArray<RssSource>(json)!!
} }
fun importDefaultHttpTTS() { fun importDefaultHttpTTS() {
App.db.httpTTSDao.deleteDefault() appDb.httpTTSDao.deleteDefault()
App.db.httpTTSDao.insert(*httpTTS.toTypedArray()) appDb.httpTTSDao.insert(*httpTTS.toTypedArray())
} }
fun importDefaultTocRules() { fun importDefaultTocRules() {
App.db.txtTocRule.deleteDefault() appDb.txtTocRuleDao.deleteDefault()
App.db.txtTocRule.insert(*txtTocRules.toTypedArray()) appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray())
} }
fun importDefaultRssSources() { fun importDefaultRssSources() {
App.db.rssSourceDao.insert(*rssSources.toTypedArray()) appDb.rssSourceDao.insert(*rssSources.toTypedArray())
} }
} }

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import io.legado.app.R import io.legado.app.R
import org.jetbrains.anko.toast import io.legado.app.utils.toastOnUi
@Suppress("unused") @Suppress("unused")
object IntentHelp { object IntentHelp {
@ -19,7 +19,7 @@ object IntentHelp {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent) context.startActivity(intent)
}.onFailure { }.onFailure {
context.toast(R.string.tip_cannot_jump_setting_page) context.toastOnUi(R.string.tip_cannot_jump_setting_page)
} }
} }
@ -30,7 +30,7 @@ object IntentHelp {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent) context.startActivity(intent)
}.onFailure { }.onFailure {
context.toast("无法打开设置") context.toastOnUi("无法打开设置")
} }
} }

@ -3,7 +3,6 @@ package io.legado.app.help
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.App
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.help.http.CookieStore import io.legado.app.help.http.CookieStore
import io.legado.app.help.http.SSLHelper import io.legado.app.help.http.SSLHelper
@ -16,9 +15,9 @@ import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray import rxhttp.wrapper.param.toByteArray
import splitties.init.appCtx
import java.io.File import java.io.File
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.Charset
import java.util.* import java.util.*
@Keep @Keep
@ -63,8 +62,8 @@ interface JsExtensions {
fun downloadFile(content: String, url: String): String { fun downloadFile(content: String, url: String): String {
val type = AnalyzeUrl(url).type ?: return "" val type = AnalyzeUrl(url).type ?: return ""
val zipPath = FileUtils.getPath( val zipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}" "${MD5Utils.md5Encode16(url)}.${type}"
) )
FileUtils.deleteFile(zipPath) FileUtils.deleteFile(zipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath) val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -82,8 +81,8 @@ interface JsExtensions {
fun unzipFile(zipPath: String): String { fun unzipFile(zipPath: String): String {
if (zipPath.isEmpty()) return "" if (zipPath.isEmpty()) return ""
val unzipPath = FileUtils.getPath( val unzipPath = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
FileUtils.getNameExcludeExtension(zipPath) FileUtils.getNameExcludeExtension(zipPath)
) )
FileUtils.deleteFile(unzipPath) FileUtils.deleteFile(unzipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath) val zipFile = FileUtils.createFileIfNotExist(zipPath)
@ -104,8 +103,8 @@ interface JsExtensions {
if (it != null) { if (it != null) {
for (f in it) { for (f in it) {
val charsetName = EncodingDetect.getEncode(f) val charsetName = EncodingDetect.getEncode(f)
contents.append(String(f.readBytes(), Charset.forName(charsetName))) contents.append(String(f.readBytes(), charset(charsetName)))
.append("\n") .append("\n")
} }
contents.deleteCharAt(contents.length - 1) contents.deleteCharAt(contents.length - 1)
} }
@ -119,12 +118,12 @@ interface JsExtensions {
*/ */
fun get(urlStr: String, headers: Map<String, String>): Connection.Response { fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr) return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true) .ignoreContentType(true)
.followRedirects(false) .followRedirects(false)
.headers(headers) .headers(headers)
.method(Connection.Method.GET) .method(Connection.Method.GET)
.execute() .execute()
} }
/** /**
@ -132,13 +131,13 @@ interface JsExtensions {
*/ */
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response { fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
return Jsoup.connect(urlStr) return Jsoup.connect(urlStr)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
.ignoreContentType(true) .ignoreContentType(true)
.followRedirects(false) .followRedirects(false)
.requestBody(body) .requestBody(body)
.headers(headers) .headers(headers)
.method(Connection.Method.POST) .method(Connection.Method.POST)
.execute() .execute()
} }
/** /**
@ -234,10 +233,20 @@ interface JsExtensions {
/** /**
* 读取本地文件 * 读取本地文件
*/ */
fun readFile(path: String): ByteArray? { fun readFile(path: String): ByteArray {
return File(path).readBytes() return File(path).readBytes()
} }
fun readTxtFile(path: String): String {
val f = File(path)
val charsetName = EncodingDetect.getEncode(f)
return String(f.readBytes(), charset(charsetName))
}
fun readTxtFile(path: String, charsetName: String): String {
return String(File(path).readBytes(), charset(charsetName))
}
/** /**
* 解析字体,返回字体解析类 * 解析字体,返回字体解析类
*/ */
@ -264,7 +273,7 @@ interface JsExtensions {
} }
return@runBlocking x return@runBlocking x
} }
str.isContentScheme() -> Uri.parse(str).readBytes(App.INSTANCE) str.isContentScheme() -> Uri.parse(str).readBytes(appCtx)
str.startsWith("/storage") -> File(str).readBytes() str.startsWith("/storage") -> File(str).readBytes()
else -> base64DecodeToByteArray(str) else -> base64DecodeToByteArray(str)
} }
@ -275,9 +284,9 @@ interface JsExtensions {
} }
fun replaceFont( fun replaceFont(
text: String, text: String,
font1: QueryTTF?, font1: QueryTTF?,
font2: QueryTTF? font2: QueryTTF?
): String { ): String {
if (font1 == null || font2 == null) return text if (font1 == null || font2 == null) return text
val contentArray = text.toCharArray() val contentArray = text.toCharArray()

@ -3,30 +3,30 @@ package io.legado.app.help
import android.content.ComponentName import android.content.ComponentName
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.welcome.* import io.legado.app.ui.welcome.*
import org.jetbrains.anko.toast import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
/** /**
* Created by GKF on 2018/2/27. * Created by GKF on 2018/2/27.
* 更换图标 * 更换图标
*/ */
object LauncherIconHelp { object LauncherIconHelp {
private val packageManager: PackageManager = App.INSTANCE.packageManager private val packageManager: PackageManager = appCtx.packageManager
private val componentNames = arrayListOf( private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name), ComponentName(appCtx, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::class.java.name), ComponentName(appCtx, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name), ComponentName(appCtx, Launcher3::class.java.name),
ComponentName(App.INSTANCE, Launcher4::class.java.name), ComponentName(appCtx, Launcher4::class.java.name),
ComponentName(App.INSTANCE, Launcher5::class.java.name), ComponentName(appCtx, Launcher5::class.java.name),
ComponentName(App.INSTANCE, Launcher6::class.java.name) ComponentName(appCtx, Launcher6::class.java.name)
) )
fun changeIcon(icon: String?) { fun changeIcon(icon: String?) {
if (icon.isNullOrEmpty()) return if (icon.isNullOrEmpty()) return
if (Build.VERSION.SDK_INT < 26) { if (Build.VERSION.SDK_INT < 26) {
App.INSTANCE.toast(R.string.change_icon_error) appCtx.toastOnUi(R.string.change_icon_error)
return return
} }
var hasEnabled = false var hasEnabled = false
@ -50,13 +50,13 @@ object LauncherIconHelp {
} }
if (hasEnabled) { if (hasEnabled) {
packageManager.setComponentEnabledSetting( packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), ComponentName(appCtx, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP PackageManager.DONT_KILL_APP
) )
} else { } else {
packageManager.setComponentEnabledSetting( packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), ComponentName(appCtx, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP PackageManager.DONT_KILL_APP
) )

@ -2,19 +2,19 @@ package io.legado.app.help
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import io.legado.app.App import splitties.init.appCtx
object LocalConfig { object LocalConfig {
private const val versionCodeKey = "versionCode" private const val versionCodeKey = "appVersionCode"
private val localConfig = private val localConfig =
App.INSTANCE.getSharedPreferences("local", Context.MODE_PRIVATE) appCtx.getSharedPreferences("local", Context.MODE_PRIVATE)
var versionCode var versionCode
get() = localConfig.getInt(versionCodeKey, 0) get() = localConfig.getLong(versionCodeKey, 0)
set(value) { set(value) {
localConfig.edit { localConfig.edit {
putInt(versionCodeKey, value) putLong(versionCodeKey, value)
} }
} }
@ -71,5 +71,5 @@ object LocalConfig {
get() = !isLastVersion(1, "txtTocRuleVersion") get() = !isLastVersion(1, "txtTocRuleVersion")
val hasUpRssSources: Boolean val hasUpRssSources: Boolean
get() = !isLastVersion(1, "rssSourceVersion") get() = !isLastVersion(2, "rssSourceVersion")
} }

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

@ -1,11 +1,11 @@
package io.legado.app.help package io.legado.app.help
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import splitties.init.appCtx
object ReadTipConfig { object ReadTipConfig {
val tips by lazy { val tips by lazy {
App.INSTANCE.resources.getStringArray(R.array.read_tip).toList() appCtx.resources.getStringArray(R.array.read_tip).toList()
} }
val headerModes by lazy { val headerModes by lazy {
linkedMapOf(0 to "状态栏显示时隐藏", 1 to "显示", 2 to "隐藏") linkedMapOf(0 to "状态栏显示时隐藏", 1 to "显示", 2 to "隐藏")

@ -2,20 +2,21 @@ package io.legado.app.help
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.utils.EncoderUtils import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import org.jetbrains.anko.toast import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
object SourceHelp { object SourceHelp {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val list18Plus by lazy { private val list18Plus by lazy {
try { try {
return@lazy String(App.INSTANCE.assets.open("18PlusList.txt").readBytes()) return@lazy String(appCtx.assets.open("18PlusList.txt").readBytes())
.splitNotBlank("\n") .splitNotBlank("\n")
} catch (e: Exception) { } catch (e: Exception) {
return@lazy arrayOf<String>() return@lazy arrayOf<String>()
@ -26,10 +27,10 @@ object SourceHelp {
rssSources.forEach { rssSource -> rssSources.forEach { rssSource ->
if (is18Plus(rssSource.sourceUrl)) { if (is18Plus(rssSource.sourceUrl)) {
handler.post { handler.post {
App.INSTANCE.toast("${rssSource.sourceName}是18+网址,禁止导入.") appCtx.toastOnUi("${rssSource.sourceName}是18+网址,禁止导入.")
} }
} else { } else {
App.db.rssSourceDao.insert(rssSource) appDb.rssSourceDao.insert(rssSource)
} }
} }
} }
@ -38,10 +39,10 @@ object SourceHelp {
bookSources.forEach { bookSource -> bookSources.forEach { bookSource ->
if (is18Plus(bookSource.bookSourceUrl)) { if (is18Plus(bookSource.bookSourceUrl)) {
handler.post { handler.post {
App.INSTANCE.toast("${bookSource.bookSourceName}是18+网址,禁止导入.") appCtx.toastOnUi("${bookSource.bookSourceName}是18+网址,禁止导入.")
} }
} else { } else {
App.db.bookSourceDao.insert(bookSource) appDb.bookSourceDao.insert(bookSource)
} }
} }
} }

@ -2,29 +2,58 @@ package io.legado.app.help
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.App import androidx.appcompat.app.AppCompatDelegate
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File import java.io.File
object ThemeConfig { object ThemeConfig {
const val configFileName = "themeConfig.json" const val configFileName = "themeConfig.json"
val configFilePath = FileUtils.getPath(App.INSTANCE.filesDir, configFileName) val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
val configList: ArrayList<Config> by lazy { val configList: ArrayList<Config> by lazy {
val cList = getConfigs() ?: DefaultData.themeConfigs val cList = getConfigs() ?: DefaultData.themeConfigs
ArrayList(cList) ArrayList(cList)
} }
fun upConfig() { fun applyDayNight(context: Context) {
getConfigs()?.let { ReadBookConfig.upBg()
it.forEach { config -> applyTheme(context)
addConfig(config) initNightMode()
postEvent(EventBus.RECREATE, "")
}
private fun initNightMode() {
val targetMode =
if (AppConfig.isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
} }
AppCompatDelegate.setDefaultNightMode(targetMode)
}
fun getBgImage(context: Context): Drawable? {
val bgPath = when (Theme.getTheme()) {
Theme.Light -> context.getPrefString(PreferKey.bgImage)
Theme.Dark -> context.getPrefString(PreferKey.bgImageN)
else -> null
}
if (bgPath.isNullOrBlank()) return null
return BitmapDrawable.createFromPath(bgPath)
}
fun upConfig() {
getConfigs()?.forEach { config ->
addConfig(config)
} }
} }
@ -88,8 +117,7 @@ object ThemeConfig {
context.putPrefInt(PreferKey.cBBackground, bBackground) context.putPrefInt(PreferKey.cBBackground, bBackground)
} }
AppConfig.isNightTheme = config.isNightTheme AppConfig.isNightTheme = config.isNightTheme
App.INSTANCE.applyDayNight() applyDayNight(context)
postEvent(EventBus.RECREATE, "")
} }
fun saveDayTheme(context: Context, name: String) { fun saveDayTheme(context: Context, name: String) {
@ -204,7 +232,6 @@ object ThemeConfig {
var primaryColor: String, var primaryColor: String,
var accentColor: String, var accentColor: String,
var backgroundColor: String, var backgroundColor: String,
var backgroundImage: String? = null,
var bottomBackground: String var bottomBackground: String
) )

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

@ -9,19 +9,15 @@ import android.webkit.CookieManager
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
import splitties.init.appCtx
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class AjaxWebView { class AjaxWebView {
var callback: Callback? = null var callback: Callback? = null
private var mHandler: AjaxHandler private var mHandler: AjaxHandler = AjaxHandler(this)
init {
mHandler = AjaxHandler(this)
}
class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) { class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) {
@ -51,7 +47,7 @@ class AjaxWebView {
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView { fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView {
val webView = WebView(App.INSTANCE) val webView = WebView(appCtx)
val settings = webView.settings val settings = webView.settings
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.domStorageEnabled = true settings.domStorageEnabled = true

@ -3,7 +3,7 @@
package io.legado.app.help.http package io.legado.app.help.http
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.Cookie import io.legado.app.data.entities.Cookie
import io.legado.app.help.http.api.CookieManager import io.legado.app.help.http.api.CookieManager
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
@ -12,7 +12,7 @@ object CookieStore : CookieManager {
override fun setCookie(url: String, cookie: String?) { override fun setCookie(url: String, cookie: String?) {
val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "") val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "")
App.db.cookieDao.insert(cookieBean) appDb.cookieDao.insert(cookieBean)
} }
override fun replaceCookie(url: String, cookie: String) { override fun replaceCookie(url: String, cookie: String) {
@ -31,12 +31,12 @@ object CookieStore : CookieManager {
} }
override fun getCookie(url: String): String { override fun getCookie(url: String): String {
val cookieBean = App.db.cookieDao.get(NetworkUtils.getSubDomain(url)) val cookieBean = appDb.cookieDao.get(NetworkUtils.getSubDomain(url))
return cookieBean?.cookie ?: "" return cookieBean?.cookie ?: ""
} }
override fun removeCookie(url: String) { override fun removeCookie(url: String) {
App.db.cookieDao.delete(NetworkUtils.getSubDomain(url)) appDb.cookieDao.delete(NetworkUtils.getSubDomain(url))
} }
override fun cookieToMap(cookie: String): MutableMap<String, String> { override fun cookieToMap(cookie: String): MutableMap<String, String> {
@ -77,7 +77,7 @@ object CookieStore : CookieManager {
} }
fun clear() { fun clear() {
App.db.cookieDao.deleteOkHttp() appDb.cookieDao.deleteOkHttp()
} }
} }

@ -8,7 +8,7 @@ import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import io.legado.app.R import io.legado.app.R
import org.jetbrains.anko.toast import io.legado.app.utils.toastOnUi
class PermissionActivity : AppCompatActivity() { class PermissionActivity : AppCompatActivity() {
@ -32,7 +32,7 @@ class PermissionActivity : AppCompatActivity() {
settingIntent.data = Uri.fromParts("package", packageName, null) settingIntent.data = Uri.fromParts("package", packageName, null)
startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING) startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING)
} catch (e: Exception) { } catch (e: Exception) {
toast(R.string.tip_cannot_jump_setting_page) toastOnUi(R.string.tip_cannot_jump_setting_page)
finish() finish()
} }

@ -9,7 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.legado.app.R import io.legado.app.R
import org.jetbrains.anko.startActivity import io.legado.app.utils.startActivity
import java.util.* import java.util.*
internal class Request : OnRequestPermissionsResultCallback { internal class Request : OnRequestPermissionsResultCallback {
@ -77,20 +77,26 @@ internal class Request : OnRequestPermissionsResultCallback {
if (deniedPermissions == null) { if (deniedPermissions == null) {
onPermissionsGranted(requestCode) onPermissionsGranted(requestCode)
} else { } else {
val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } showSettingDialog(rationale) {
onPermissionsDenied(
requestCode,
deniedPermissions
)
}
} else { } else {
onPermissionsDenied(requestCode, deniedPermissions) onPermissionsDenied(requestCode, deniedPermissions)
} }
} }
} else { } else {
if (deniedPermissions != null) { if (deniedPermissions != null) {
source?.context?.startActivity<PermissionActivity>( source?.context?.startActivity<PermissionActivity> {
PermissionActivity.KEY_INPUT_REQUEST_TYPE to TYPE_REQUEST_PERMISSION, putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION)
PermissionActivity.KEY_INPUT_PERMISSIONS_CODE to requestCode, putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode)
PermissionActivity.KEY_INPUT_PERMISSIONS to deniedPermissions putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
) }
} else { } else {
onPermissionsGranted(requestCode) onPermissionsGranted(requestCode)
} }
@ -132,9 +138,12 @@ internal class Request : OnRequestPermissionsResultCallback {
.setTitle(R.string.dialog_title) .setTitle(R.string.dialog_title)
.setMessage(rationale) .setMessage(rationale)
.setPositiveButton(R.string.dialog_setting) { _, _ -> .setPositiveButton(R.string.dialog_setting) { _, _ ->
it.startActivity<PermissionActivity>( it.startActivity<PermissionActivity> {
PermissionActivity.KEY_INPUT_REQUEST_TYPE to TYPE_REQUEST_SETTING putExtra(
) PermissionActivity.KEY_INPUT_REQUEST_TYPE,
TYPE_REQUEST_SETTING
)
}
} }
.setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() } .setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() }
.show() .show()
@ -160,10 +169,15 @@ internal class Request : OnRequestPermissionsResultCallback {
RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions) RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions)
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
val deniedPermissions = getDeniedPermissions(permissions) val deniedPermissions = getDeniedPermissions(permissions)
if (deniedPermissions != null) { if (deniedPermissions != null) {
val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) }
} else { } else {

@ -3,8 +3,8 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
@ -12,7 +12,7 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences import splitties.init.appCtx
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit
object Backup { object Backup {
val backupPath: String by lazy { val backupPath: String by lazy {
FileUtils.getFile(App.INSTANCE.filesDir, "backup").absolutePath FileUtils.getFile(appCtx.filesDir, "backup").absolutePath
} }
val backupFileNames by lazy { val backupFileNames by lazy {
@ -57,32 +57,33 @@ object Backup {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
FileUtils.deleteFile(backupPath) FileUtils.deleteFile(backupPath)
writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath) writeListToJson(appDb.bookDao.all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) writeListToJson(appDb.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) writeListToJson(appDb.bookGroupDao.all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) writeListToJson(appDb.bookSourceDao.all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao.all, "rssSources.json", backupPath) writeListToJson(appDb.rssSourceDao.all, "rssSources.json", backupPath)
writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) writeListToJson(appDb.rssStarDao.all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) writeListToJson(appDb.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath)
writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath) writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath)
writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath) writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath)
writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath) writeListToJson(appDb.txtTocRuleDao.all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) writeListToJson(appDb.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath)
GSON.toJson(ReadBookConfig.configList).let { GSON.toJson(ReadBookConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
.writeText(it) .writeText(it)
} }
GSON.toJson(ReadBookConfig.shareConfig).let { GSON.toJson(ReadBookConfig.shareConfig).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
.writeText(it)
} }
GSON.toJson(ThemeConfig.configList).let { GSON.toJson(ThemeConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName)
.writeText(it) .writeText(it)
} }
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> Preferences.getSharedPreferences(appCtx, backupPath, "config")?.let { sp ->
val edit = sp.edit() val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map { appCtx.defaultSharedPreferences.all.map {
when (val value = it.value) { when (val value = it.value) {
is Int -> edit.putInt(it.key, value) is Int -> edit.putInt(it.key, value)
is Boolean -> edit.putBoolean(it.key, value) is Boolean -> edit.putBoolean(it.key, value)

@ -3,7 +3,6 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -16,7 +15,7 @@ import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast import splitties.init.appCtx
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -28,20 +27,20 @@ object BookWebDav {
private val rootWebDavUrl: String private val rootWebDavUrl: String
get() { get() {
var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl) var url = appCtx.getPrefString(PreferKey.webDavUrl)
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
url = defaultWebDavUrl url = defaultWebDavUrl
} }
if (!url.endsWith("/")) url = "${url}/" if (!url.endsWith("/")) url = "${url}/"
if (App.INSTANCE.getPrefBoolean(PreferKey.webDavCreateDir, true)) { if (appCtx.getPrefBoolean(PreferKey.webDavCreateDir, true)) {
url = "${url}legado/" url = "${url}legado/"
} }
return url return url
} }
suspend fun initWebDav(): Boolean { private suspend fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount) val account = appCtx.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword) val password = appCtx.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password) HttpAuth.auth = HttpAuth.Auth(account, password)
WebDav(rootWebDavUrl).makeAsDir() WebDav(rootWebDavUrl).makeAsDir()
@ -80,7 +79,7 @@ object BookWebDav {
Coroutine.async { Coroutine.async {
restoreWebDav(names[index]) restoreWebDav(names[index])
}.onError { }.onError {
App.INSTANCE.toast("WebDavError:${it.localizedMessage}") appCtx.toastOnUi("WebDavError:${it.localizedMessage}")
} }
} }
} }
@ -118,28 +117,25 @@ object BookWebDav {
} }
} catch (e: Exception) { } catch (e: Exception) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
App.INSTANCE.toast("WebDav\n${e.localizedMessage}") appCtx.toastOnUi("WebDav\n${e.localizedMessage}")
} }
} }
} }
suspend fun exportWebDav(path: String, fileName: String) { suspend fun exportWebDav(byteArray: ByteArray, fileName: String) {
try { try {
if (initWebDav()) { if (initWebDav()) {
// 默认导出到legado文件夹下exports目录 // 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/" val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话 // 在legado文件夹创建exports目录,如果不存在的话
WebDav(exportsWebDavUrl).makeAsDir() WebDav(exportsWebDavUrl).makeAsDir()
val file = File("${path}${File.separator}${fileName}")
// 如果导出的本地文件存在,开始上传 // 如果导出的本地文件存在,开始上传
if (file.exists()) { val putUrl = exportsWebDavUrl + fileName
val putUrl = exportsWebDavUrl + fileName WebDav(putUrl).upload(byteArray)
WebDav(putUrl).upload("${path}${File.separator}${fileName}")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
App.INSTANCE.toast("WebDav导出\n${e.localizedMessage}") appCtx.toastOnUi("WebDav导出\n${e.localizedMessage}")
} }
} }
} }

@ -3,12 +3,12 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import org.jetbrains.anko.toast import io.legado.app.utils.toastOnUi
import java.io.File import java.io.File
object ImportOldData { object ImportOldData {
@ -21,28 +21,28 @@ object ImportOldData {
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json -> DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}") context.toastOnUi("成功导入书籍${importCount}")
} }
}.onFailure { }.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}") context.toastOnUi("导入书籍失败\n${it.localizedMessage}")
} }
"myBookSource.json" -> "myBookSource.json" ->
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json -> DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldSource(json) val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}") context.toastOnUi("成功导入书源${importCount}")
} }
}.onFailure { }.onFailure {
context.toast("导入源失败\n${it.localizedMessage}") context.toastOnUi("导入源失败\n${it.localizedMessage}")
} }
"myBookReplaceRule.json" -> "myBookReplaceRule.json" ->
kotlin.runCatching { kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json -> DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}") context.toastOnUi("成功导入替换规则${importCount}")
} }
}.onFailure { }.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}") context.toastOnUi("导入替换规则失败\n${it.localizedMessage}")
} }
} }
} }
@ -54,9 +54,9 @@ object ImportOldData {
FileUtils.createFileIfNotExist(file, "myBookShelf.json") FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText() val json = shelfFile.readText()
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}") context.toastOnUi("成功导入书籍${importCount}")
}.onFailure { }.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}") context.toastOnUi("导入书籍失败\n${it.localizedMessage}")
} }
kotlin.runCatching {// Book source kotlin.runCatching {// Book source
@ -64,9 +64,9 @@ object ImportOldData {
FileUtils.getFile(file, "myBookSource.json") FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText() val json = sourceFile.readText()
val importCount = importOldSource(json) val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}") context.toastOnUi("成功导入书源${importCount}")
}.onFailure { }.onFailure {
context.toast("导入源失败\n${it.localizedMessage}") context.toastOnUi("导入源失败\n${it.localizedMessage}")
} }
kotlin.runCatching {// Replace rules kotlin.runCatching {// Replace rules
@ -74,12 +74,12 @@ object ImportOldData {
if (ruleFile.exists()) { if (ruleFile.exists()) {
val json = ruleFile.readText() val json = ruleFile.readText()
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}") context.toastOnUi("成功导入替换规则${importCount}")
} else { } else {
context.toast("未找到替换规则") context.toastOnUi("未找到替换规则")
} }
}.onFailure { }.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}") context.toastOnUi("导入替换规则失败\n${it.localizedMessage}")
} }
} }
} }
@ -87,7 +87,7 @@ object ImportOldData {
private fun importOldBookshelf(json: String): Int { private fun importOldBookshelf(json: String): Int {
val books = OldBook.toNewBook(json) val books = OldBook.toNewBook(json)
App.db.bookDao.insert(*books.toTypedArray()) appDb.bookDao.insert(*books.toTypedArray())
return books.size return books.size
} }
@ -100,13 +100,13 @@ object ImportOldData {
bookSources.add(it) bookSources.add(it)
} }
} }
App.db.bookSourceDao.insert(*bookSources.toTypedArray()) appDb.bookSourceDao.insert(*bookSources.toTypedArray())
return bookSources.size return bookSources.size
} }
private fun importOldReplaceRule(json: String): Int { private fun importOldReplaceRule(json: String): Int {
val rules = OldReplace.jsonToReplaceRules(json) val rules = OldReplace.jsonToReplaceRules(json)
App.db.replaceRuleDao.insert(*rules.toTypedArray()) appDb.replaceRuleDao.insert(*rules.toTypedArray())
return rules.size return rules.size
} }
} }

@ -1,8 +1,8 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.util.Log import android.util.Log
import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.utils.readBool import io.legado.app.utils.readBool
import io.legado.app.utils.readInt import io.legado.app.utils.readInt
@ -14,7 +14,7 @@ object OldBook {
fun toNewBook(json: String): List<Book> { fun toNewBook(json: String): List<Book> {
val books = mutableListOf<Book>() val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao.allBookUrls.toSet() val existingBooks = appDb.bookDao.allBookUrls.toSet()
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)
val book = Book() val book = Book()

@ -1,5 +1,7 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -7,28 +9,29 @@ import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext import com.jayway.jsonpath.ParseContext
import io.legado.app.App
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey 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.data.entities.*
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences import splitties.init.appCtx
import org.jetbrains.anko.toast import splitties.systemservices.alarmManager
import java.io.File import java.io.File
import kotlin.system.exitProcess
object Restore { object Restore {
private val ignoreConfigPath = FileUtils.getPath(App.INSTANCE.filesDir, "restoreIgnore.json") private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json")
val ignoreConfig: HashMap<String, Boolean> by lazy { val ignoreConfig: HashMap<String, Boolean> by lazy {
val file = FileUtils.createFileIfNotExist(ignoreConfigPath) val file = FileUtils.createFileIfNotExist(ignoreConfigPath)
val json = file.readText() val json = file.readText()
@ -46,11 +49,11 @@ object Restore {
//忽略标题 //忽略标题
val ignoreTitle = arrayOf( val ignoreTitle = arrayOf(
App.INSTANCE.getString(R.string.read_config), appCtx.getString(R.string.read_config),
App.INSTANCE.getString(R.string.theme_mode), appCtx.getString(R.string.theme_mode),
App.INSTANCE.getString(R.string.bookshelf_layout), appCtx.getString(R.string.bookshelf_layout),
App.INSTANCE.getString(R.string.show_rss), appCtx.getString(R.string.show_rss),
App.INSTANCE.getString(R.string.thread_count) appCtx.getString(R.string.thread_count)
) )
//默认忽略keys //默认忽略keys
@ -111,48 +114,48 @@ object Restore {
suspend fun restoreDatabase(path: String = Backup.backupPath) { suspend fun restoreDatabase(path: String = Backup.backupPath) {
withContext(IO) { withContext(IO) {
fileToListT<Book>(path, "bookshelf.json")?.let { fileToListT<Book>(path, "bookshelf.json")?.let {
App.db.bookDao.insert(*it.toTypedArray()) appDb.bookDao.insert(*it.toTypedArray())
} }
fileToListT<Bookmark>(path, "bookmark.json")?.let { fileToListT<Bookmark>(path, "bookmark.json")?.let {
App.db.bookmarkDao.insert(*it.toTypedArray()) appDb.bookmarkDao.insert(*it.toTypedArray())
} }
fileToListT<BookGroup>(path, "bookGroup.json")?.let { fileToListT<BookGroup>(path, "bookGroup.json")?.let {
App.db.bookGroupDao.insert(*it.toTypedArray()) appDb.bookGroupDao.insert(*it.toTypedArray())
} }
fileToListT<BookSource>(path, "bookSource.json")?.let { fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao.insert(*it.toTypedArray()) appDb.bookSourceDao.insert(*it.toTypedArray())
} }
fileToListT<RssSource>(path, "rssSources.json")?.let { fileToListT<RssSource>(path, "rssSources.json")?.let {
App.db.rssSourceDao.insert(*it.toTypedArray()) appDb.rssSourceDao.insert(*it.toTypedArray())
} }
fileToListT<RssStar>(path, "rssStar.json")?.let { fileToListT<RssStar>(path, "rssStar.json")?.let {
App.db.rssStarDao.insert(*it.toTypedArray()) appDb.rssStarDao.insert(*it.toTypedArray())
} }
fileToListT<ReplaceRule>(path, "replaceRule.json")?.let { fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
App.db.replaceRuleDao.insert(*it.toTypedArray()) appDb.replaceRuleDao.insert(*it.toTypedArray())
} }
fileToListT<SearchKeyword>(path, "searchHistory.json")?.let { fileToListT<SearchKeyword>(path, "searchHistory.json")?.let {
App.db.searchKeywordDao.insert(*it.toTypedArray()) appDb.searchKeywordDao.insert(*it.toTypedArray())
} }
fileToListT<RuleSub>(path, "sourceSub.json")?.let { fileToListT<RuleSub>(path, "sourceSub.json")?.let {
App.db.ruleSubDao.insert(*it.toTypedArray()) appDb.ruleSubDao.insert(*it.toTypedArray())
} }
fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let { fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let {
App.db.txtTocRule.insert(*it.toTypedArray()) appDb.txtTocRuleDao.insert(*it.toTypedArray())
} }
fileToListT<HttpTTS>(path, DefaultData.httpTtsFileName)?.let { fileToListT<HttpTTS>(path, DefaultData.httpTtsFileName)?.let {
App.db.httpTTSDao.insert(*it.toTypedArray()) appDb.httpTTSDao.insert(*it.toTypedArray())
} }
fileToListT<ReadRecord>(path, "readRecord.json")?.let { fileToListT<ReadRecord>(path, "readRecord.json")?.let {
it.forEach { readRecord -> it.forEach { readRecord ->
//判断是不是本机记录 //判断是不是本机记录
if (readRecord.androidId != App.androidId) { if (readRecord.androidId != androidId) {
App.db.readRecordDao.insert(readRecord) appDb.readRecordDao.insert(readRecord)
} else { } else {
val time = App.db.readRecordDao val time = appDb.readRecordDao
.getReadTime(readRecord.androidId, readRecord.bookName) .getReadTime(readRecord.androidId, readRecord.bookName)
if (time == null || time < readRecord.readTime) { if (time == null || time < readRecord.readTime) {
App.db.readRecordDao.insert(readRecord) appDb.readRecordDao.insert(readRecord)
} }
} }
} }
@ -197,8 +200,8 @@ object Restore {
e.printStackTrace() e.printStackTrace()
} }
} }
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.all?.let { map -> Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->
val edit = App.INSTANCE.defaultSharedPreferences.edit() val edit = appCtx.defaultSharedPreferences.edit()
map.forEach { map.forEach {
if (keyIsNotIgnore(it.key)) { if (keyIsNotIgnore(it.key)) {
when (val value = it.value) { when (val value = it.value) {
@ -207,30 +210,31 @@ object Restore {
is Long -> edit.putLong(it.key, value) is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value) is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value) is String -> edit.putString(it.key, value)
else -> Unit
} }
} }
} }
edit.apply() edit.apply()
} }
ReadBookConfig.apply { ReadBookConfig.apply {
styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout)
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46) autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46)
} }
ChapterProvider.upStyle()
ReadBook.loadContent(resetPageOffset = false)
} }
appCtx.toastOnUi(R.string.restore_success)
withContext(Main) { withContext(Main) {
App.INSTANCE.toast(R.string.restore_success) delay(100)
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon))
}
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)
} }
LanguageUtils.setConfiguration(App.INSTANCE)
App.INSTANCE.applyDayNight()
postEvent(EventBus.SHOW_RSS, "")
} }
} }

@ -11,40 +11,28 @@ import android.view.View
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.legado.app.R import io.legado.app.R
import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER
import kotlin.DeprecationLevel.ERROR
@SuppressLint("SupportAnnotationUsage") @SuppressLint("SupportAnnotationUsage")
interface AlertBuilder<out D : DialogInterface> { interface AlertBuilder<out D : DialogInterface> {
val ctx: Context val ctx: Context
var title: CharSequence fun setTitle(title: CharSequence)
@Deprecated(NO_GETTER, level = ERROR) get
var titleResource: Int fun setTitle(titleResource: Int)
@Deprecated(NO_GETTER, level = ERROR) get
var message: CharSequence fun setMessage(message: CharSequence)
@Deprecated(NO_GETTER, level = ERROR) get
var messageResource: Int fun setMessage(messageResource: Int)
@Deprecated(NO_GETTER, level = ERROR) get
var icon: Drawable fun setIcon(icon: Drawable)
@Deprecated(NO_GETTER, level = ERROR) get
@setparam:DrawableRes fun setIcon(@DrawableRes iconResource: Int)
var iconResource: Int
@Deprecated(NO_GETTER, level = ERROR) get
var customTitle: View fun setCustomTitle(customTitle: View)
@Deprecated(NO_GETTER, level = ERROR) get
var customView: View fun setCustomView(customView: View)
@Deprecated(NO_GETTER, level = ERROR) get
var isCancelable: Boolean fun setCancelable(isCancelable: Boolean)
@Deprecated(NO_GETTER, level = ERROR) get
fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null)
fun positiveButton( fun positiveButton(
@ -97,11 +85,11 @@ interface AlertBuilder<out D : DialogInterface> {
fun customTitle(view: () -> View) { fun customTitle(view: () -> View) {
customTitle = view() setCustomTitle(view())
} }
fun customView(view: () -> View) { fun customView(view: () -> View) {
customView = view() setCustomView(view())
} }
fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =

@ -7,48 +7,45 @@ import android.view.KeyEvent
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import org.jetbrains.anko.internals.AnkoInternals
import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER
import kotlin.DeprecationLevel.ERROR
internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<AlertDialog> { internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<AlertDialog> {
private val builder = AlertDialog.Builder(ctx) private val builder = AlertDialog.Builder(ctx)
override var title: CharSequence override fun setTitle(title: CharSequence) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setTitle(title)
set(value) { builder.setTitle(value) } }
override var titleResource: Int override fun setTitle(titleResource: Int) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setTitle(titleResource)
set(value) { builder.setTitle(value) } }
override var message: CharSequence override fun setMessage(message: CharSequence) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setMessage(message)
set(value) { builder.setMessage(value) } }
override var messageResource: Int override fun setMessage(messageResource: Int) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setMessage(messageResource)
set(value) { builder.setMessage(value) } }
override var icon: Drawable override fun setIcon(icon: Drawable) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setIcon(icon)
set(value) { builder.setIcon(value) } }
override var iconResource: Int override fun setIcon(iconResource: Int) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setIcon(iconResource)
set(value) { builder.setIcon(value) } }
override var customTitle: View override fun setCustomTitle(customTitle: View) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setCustomTitle(customTitle)
set(value) { builder.setCustomTitle(value) } }
override var customView: View override fun setCustomView(customView: View) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setView(customView)
set(value) { builder.setView(value) } }
override var isCancelable: Boolean override fun setCancelable(isCancelable: Boolean) {
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() builder.setCancelable(isCancelable)
set(value) { builder.setCancelable(value) } }
override fun onCancelled(handler: (DialogInterface) -> Unit) { override fun onCancelled(handler: (DialogInterface) -> Unit) {
builder.setOnCancelListener(handler) builder.setOnCancelListener(handler)
@ -58,7 +55,10 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
builder.setOnKeyListener(handler) builder.setOnKeyListener(handler)
} }
override fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { override fun positiveButton(
buttonText: String,
onClicked: ((dialog: DialogInterface) -> Unit)?
) {
builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) }
} }

@ -7,7 +7,6 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.jetbrains.anko.AnkoContext
inline fun Fragment.alert( inline fun Fragment.alert(
title: CharSequence? = null, title: CharSequence? = null,
@ -22,10 +21,10 @@ fun Context.alert(
): AlertBuilder<AlertDialog> { ): AlertBuilder<AlertDialog> {
return AndroidAlertBuilder(this).apply { return AndroidAlertBuilder(this).apply {
if (title != null) { if (title != null) {
this.title = title this.setTitle(title)
} }
if (message != null) { if (message != null) {
this.message = message this.setMessage(message)
} }
if (init != null) init() if (init != null) init()
} }
@ -44,17 +43,16 @@ fun Context.alert(
): AlertBuilder<AlertDialog> { ): AlertBuilder<AlertDialog> {
return AndroidAlertBuilder(this).apply { return AndroidAlertBuilder(this).apply {
if (titleResource != null) { if (titleResource != null) {
this.titleResource = titleResource this.setTitle(titleResource)
} }
if (messageResource != null) { if (messageResource != null) {
this.messageResource = messageResource this.setMessage(messageResource)
} }
if (init != null) init() if (init != null) init()
} }
} }
inline fun AnkoContext<*>.alert(noinline init: AlertBuilder<DialogInterface>.() -> Unit) = ctx.alert(init)
inline fun Fragment.alert(noinline init: AlertBuilder<DialogInterface>.() -> Unit) = requireContext().alert(init) inline fun Fragment.alert(noinline init: AlertBuilder<DialogInterface>.() -> Unit) = requireContext().alert(init)
fun Context.alert(init: AlertBuilder<AlertDialog>.() -> Unit): AlertBuilder<AlertDialog> = fun Context.alert(init: AlertBuilder<AlertDialog>.() -> Unit): AlertBuilder<AlertDialog> =

@ -35,7 +35,7 @@ fun Context.selector(
) { ) {
with(AndroidAlertBuilder(this)) { with(AndroidAlertBuilder(this)) {
if (title != null) { if (title != null) {
this.title = title this.setTitle(title)
} }
items(items, onClick) items(items, onClick)
show() show()
@ -49,7 +49,7 @@ fun Context.selector(
) { ) {
with(AndroidAlertBuilder(this)) { with(AndroidAlertBuilder(this)) {
if (titleSource != null) { if (titleSource != null) {
this.title = getString(titleSource) this.setTitle(titleSource)
} }
items(items, onClick) items(items, onClick)
show() show()

@ -23,10 +23,10 @@ fun <D : DialogInterface> Context.alert(
): AlertBuilder<D> { ): AlertBuilder<D> {
return factory(this).apply { return factory(this).apply {
if (title != null) { if (title != null) {
this.title = title this.setTitle(title)
} }
if (message != null) { if (message != null) {
this.message = message this.setMessage(message)
} }
if (init != null) init() if (init != null) init()
} }
@ -47,10 +47,10 @@ fun <D : DialogInterface> Context.alert(
): AlertBuilder<D> { ): AlertBuilder<D> {
return factory(this).apply { return factory(this).apply {
if (titleResource != null) { if (titleResource != null) {
this.titleResource = titleResource this.setTitle(titleResource)
} }
if (messageResource != null) { if (messageResource != null) {
this.messageResource = messageResource this.setMessage(messageResource)
} }
if (init != null) init() if (init != null) init()
} }

@ -21,7 +21,7 @@ fun <D : DialogInterface> Context.selector(
) { ) {
with(factory(this)) { with(factory(this)) {
if (title != null) { if (title != null) {
this.title = title this.setTitle(title)
} }
items(items, onClick) items(items, onClick)
show() show()

@ -18,14 +18,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils
import io.legado.app.utils.dp import io.legado.app.utils.dp
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import org.jetbrains.anko.backgroundColor import splitties.init.appCtx
/** /**
* @author Karim Abou Zeid (kabouzeid) * @author Karim Abou Zeid (kabouzeid)
@ -272,7 +270,7 @@ object ATH {
fun applyBackgroundTint(view: View?) { fun applyBackgroundTint(view: View?) {
view?.apply { view?.apply {
if (background == null) { if (background == null) {
backgroundColor = context.backgroundColor setBackgroundColor(context.backgroundColor)
} else { } else {
setBackgroundTint(this, context.backgroundColor) setBackgroundTint(this, context.backgroundColor)
} }
@ -290,7 +288,7 @@ object ATH {
fun getDialogBackground(): GradientDrawable { fun getDialogBackground(): GradientDrawable {
val background = GradientDrawable() val background = GradientDrawable()
background.cornerRadius = 3F.dp background.cornerRadius = 3F.dp
background.setColor(App.INSTANCE.backgroundColor) background.setColor(appCtx.backgroundColor)
return background return background
} }

@ -9,9 +9,9 @@ import androidx.annotation.CheckResult
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils
import splitties.init.appCtx
/** /**
* @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)
@ -200,7 +200,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun primaryColor(context: Context = App.INSTANCE): Int { fun primaryColor(context: Context = appCtx): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR, ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64")) ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
@ -218,7 +218,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun accentColor(context: Context = App.INSTANCE): Int { fun accentColor(context: Context = appCtx): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_ACCENT_COLOR, ThemeStorePrefKeys.KEY_ACCENT_COLOR,
ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238")) ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))
@ -292,7 +292,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun backgroundColor(context: Context = App.INSTANCE): Int { fun backgroundColor(context: Context = appCtx): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, ThemeStorePrefKeys.KEY_BACKGROUND_COLOR,
ATHUtils.resolveColor(context, android.R.attr.colorBackground) ATHUtils.resolveColor(context, android.R.attr.colorBackground)
@ -314,7 +314,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
@CheckResult @CheckResult
@ColorInt @ColorInt
fun bottomBackground(context: Context = App.INSTANCE): Int { fun bottomBackground(context: Context = appCtx): Int {
return prefs(context).getInt( return prefs(context).getInt(
ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND,
ATHUtils.resolveColor(context, android.R.attr.colorBackground) ATHUtils.resolveColor(context, android.R.attr.colorBackground)

@ -186,6 +186,12 @@ object Debug {
} }
private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) { private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
if (book.tocUrl.isNotBlank()) {
log(debugSource, "目录url不为空,详情页已解析")
log(debugSource, showTime = false)
tocDebug(scope, webBook, book)
return
}
log(debugSource, "︾开始解析详情页") log(debugSource, "︾开始解析详情页")
val info = webBook.getBookInfo(scope, book) val info = webBook.getBookInfo(scope, book)
.onSuccess { .onSuccess {
@ -206,7 +212,7 @@ object Debug {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成") log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null val nextChapterUrl = it.getOrNull(1)?.url
contentDebug(scope, webBook, book, it[0], nextChapterUrl) contentDebug(scope, webBook, book, it[0], nextChapterUrl)
} else { } else {
log(debugSource, "︽目录列表为空", state = -1) log(debugSource, "︽目录列表为空", state = -1)

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

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

@ -0,0 +1,600 @@
package io.legado.app.model.analyzeRule;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class QueryTTF {
private static class Header {
public int majorVersion;
public int minorVersion;
public int numOfTables;
public int searchRange;
public int entrySelector;
public int rangeShift;
}
private static class Directory {
public String tag; // table name
public int checkSum; // Check sum
public int offset; // Offset from beginning of file
public int length; // length of the table in bytes
}
private static class NameLayout {
public int format;
public int count;
public int stringOffset;
public List<NameRecord> records = new LinkedList<>();
}
private static class NameRecord {
public int platformID; // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom>
public int encodingID; // 编码标识符
public int languageID; // 语言标识符
public int nameID; // 名称标识符
public int length; // 名称字符串的长度
public int offset; // 名称字符串相对于stringOffset的字节偏移量
}
private static class HeadLayout {
public int majorVersion;
public int minorVersion;
public int fontRevision;
public int checkSumAdjustment;
public int magicNumber;
public int flags;
public int unitsPerEm;
public long created;
public long modified;
public short xMin;
public short yMin;
public short xMax;
public short yMax;
public int macStyle;
public int lowestRecPPEM;
public short fontDirectionHint;
public short indexToLocFormat; // <0:loca是2字节数组, 1:loca是4字节数组>
public short glyphDataFormat;
}
private static class MaxpLayout {
public int majorVersion;
public int minorVersion;
public int numGlyphs; // 字体中的字形数量
public int maxPoints;
public int maxContours;
public int maxCompositePoints;
public int maxCompositeContours;
public int maxZones;
public int maxTwilightPoints;
public int maxStorage;
public int maxFunctionDefs;
public int maxInstructionDefs;
public int maxStackElements;
public int maxSizeOfInstructions;
public int maxComponentElements;
public int maxComponentDepth;
}
private class CmapLayout {
public int version;
public int numTables;
public List<CmapRecord> records = new LinkedList<>();
public Map<Integer, CmapFormat> tables = new HashMap<>();
}
private static class CmapRecord {
public int platformID;
public int encodingID;
public int offset;
}
private static class CmapFormat {
public int format;
public int length;
public int language;
public byte[] glyphIdArray;
}
private static class CmapFormat4 extends CmapFormat {
public int segCountX2;
public int searchRange;
public int entrySelector;
public int rangeShift;
public int[] endCode;
public int reservedPad;
public int[] startCode;
public short[] idDelta;
public int[] idRangeOffset;
public int[] glyphIdArray;
}
private static class CmapFormat6 extends CmapFormat {
public int firstCode;
public int entryCount;
public int[] glyphIdArray;
}
private static class CmapFormat12 extends CmapFormat {
public int reserved;
public int length;
public int language;
public int numGroups;
public List<Triple<Integer, Integer, Integer>> groups;
}
private static class GlyfLayout {
public short numberOfContours; // 非负值为简单字型,负值为符合字型
public short xMin;
public short yMin;
public short xMax;
public short yMax;
public int[] endPtsOfContours; // length=numberOfContours
public int instructionLength;
public byte[] instructions; // length=instructionLength
public byte[] flags;
public short[] xCoordinates; // length = flags.length
public short[] yCoordinates; // length = flags.length
}
private static class ByteArrayReader {
public int Index;
public byte[] Buffer;
public ByteArrayReader(byte[] buffer, int index) {
Buffer = buffer;
Index = index;
}
public long ReadUIntX(long len) {
long result = 0;
for (long i = 0; i < len; ++i) {
result <<= 8;
result |= Buffer[Index++] & 0xFF;
}
return result;
}
public long ReadUInt64() {
return ReadUIntX(8);
}
public int ReadUInt32() {
return (int) ReadUIntX(4);
}
public int ReadUInt16() {
return (int) ReadUIntX(2);
}
public short ReadInt16() {
return (short) ReadUIntX(2);
}
public short ReadUInt8() {
return (short) ReadUIntX(1);
}
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++];
return new String(result, charset);
}
public byte GetByte() {
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++];
return result;
}
public int[] GetUInt16Array(int len) {
int[] result = len > 0 ? new int[len] : null;
for (int i = 0; i < len; ++i) result[i] = ReadUInt16();
return result;
}
public short[] GetInt16Array(int len) {
short[] result = len > 0 ? new short[len] : null;
for (int i = 0; i < len; ++i) result[i] = ReadInt16();
return result;
}
}
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 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[]{
Pair.of(3, 10),
Pair.of(0, 4),
Pair.of(3, 1),
Pair.of(1, 0),
Pair.of(0, 3),
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;
/**
* 构造函数
*
* @param buffer 传入TTF字体二进制数组
*/
public QueryTTF(byte[] buffer) {
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();
// 获取目录
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);
}
// 解析表 name (字体信息,包含版权、名称、作者等...)
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) {
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);
}
}
}
// 解析表 head (获取 head.indexToLocFormat)
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();
}
}
// 解析表 maxp (获取 maxp.numGlyphs)
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();
}
}
// 解析表 loca (轮廓数据偏移地址表)
for (Directory Temp : Directorys) {
if (Temp.tag.equals("loca")) {
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());
}
}
}
// 解析表 cmap (Unicode编码轮廓索引对照表)
for (Directory Temp : Directorys) {
if (Temp.tag.equals("cmap")) {
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();
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;
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);
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();
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);
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);
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.groups = new ArrayList<>(f.numGroups);
for (int n = 0; n < f.numGroups; ++n) {
f.groups.add(Triple.of(FontReader.ReadUInt32(), FontReader.ReadUInt32(), FontReader.ReadUInt32()));
}
Cmap.tables.put(fmtOffset, f);
}
}
}
}
// 解析表 glyf (字体轮廓数据表)
for (Directory Temp : Directorys) {
if (Temp.tag.equals("glyf")) {
FontReader.Index = Temp.offset;
for (int i = 0; i < maxp.numGlyphs; ++i) {
FontReader.Index = Temp.offset + loca.get(i);
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);
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();
if ((g.flags[n] & 0x08) != 0x00) {
for (int m = FontReader.ReadUInt8(); m > 0; --m) {
g.flags[++n] = g.flags[n - 1];
}
}
}
// 获取轮廓点描述x轴相对值
g.xCoordinates = new short[flagLength];
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());
} else {
g.xCoordinates[n] = same == 1 ? (short) 0 : FontReader.ReadInt16();
}
}
// 获取轮廓点描述y轴相对值
g.yCoordinates = new short[flagLength];
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());
} else {
g.yCoordinates[n] = same == 1 ? (short) 0 : FontReader.ReadInt16();
}
}
// 相对坐标转绝对坐标
// for (int n = 1; n < flagLength; ++n) {
// xCoordinates[n] += xCoordinates[n - 1];
// yCoordinates[n] += yCoordinates[n - 1];
// }
glyf.add(g);
} else {
// 复合字体暂未使用
}
}
}
}
// 建立Unicode&Glyph双向表
for (int key = 0; key < 130000; ++key) {
if (key == 0xFF) key = 0x3400;
int gid = getGlyfIndex(key);
if (gid == 0) continue;
StringBuilder sb = new StringBuilder();
// 字型数据转String,方便存HashMap
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);
}
}
/**
* 获取字体信息 (1=字体名称)
*
* @param nameId 传入十进制字体信息索引
* @return 返回查询结果字符串
*/
public String getNameById(int nameId) {
for (Directory Temp : Directorys) {
if (!Temp.tag.equals("name")) continue;
FontReader.Index = Temp.offset;
break;
}
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);
}
return "error";
}
/**
* 使用Unicode值查找轮廓索引
*
* @param code 传入Unicode十进制值
* @return 返回十进制轮廓索引
*/
private int getGlyfIndex(int code) {
if (code == 0) return 0;
int fmtKey = 0;
for (@SuppressWarnings("unchecked") Pair<Integer, Integer> item : pps) {
for (CmapRecord record : Cmap.records) {
if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) {
fmtKey = record.offset;
break;
}
}
if (fmtKey > 0) break;
}
if (fmtKey == 0) return 0;
int glyfID = 0;
CmapFormat table = Cmap.tables.get(fmtKey);
int fmt = table.format;
if (fmt == 0) {
if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF;
} else if (fmt == 4) {
CmapFormat4 tab = (CmapFormat4) table;
if (code > tab.endCode[tab.endCode.length - 1]) return 0;
// 二分法查找数值索引
int start = 0, middle, end = tab.endCode.length - 1;
while (start + 1 < end) {
middle = (start + end) / 2;
if (tab.endCode[middle] <= code) start = middle;
else end = middle;
}
if (tab.endCode[start] < code) ++start;
if (code < tab.startCode[start]) return 0;
if (tab.idRangeOffset[start] != 0) {
glyfID = tab.glyphIdArray[code - tab.startCode[start] + (tab.idRangeOffset[start] >> 1) - (tab.idRangeOffset.length - start)];
} else glyfID = code + tab.idDelta[start];
glyfID &= 0xFFFF;
} else if (fmt == 6) {
CmapFormat6 tab = (CmapFormat6) table;
int index = code - tab.firstCode;
if (index < 0 || index >= tab.glyphIdArray.length) glyfID = 0;
else glyfID = tab.glyphIdArray[index];
} else if (fmt == 12) {
CmapFormat12 tab = (CmapFormat12) table;
if (code > tab.groups.get(tab.numGroups - 1).getMiddle()) return 0;
// 二分法查找数值索引
int start = 0, middle, end = tab.numGroups - 1;
while (start + 1 < end) {
middle = (start + end) / 2;
if (tab.groups.get(middle).getLeft() <= code) start = middle;
else end = middle;
}
if (tab.groups.get(start).getLeft() <= code && code <= tab.groups.get(start).getMiddle()) {
glyfID = tab.groups.get(start).getRight() + code - tab.groups.get(start).getLeft();
}
}
return glyfID;
}
/**
* 判断Unicode值是否在字体范围内
*
* @param code 传入Unicode十进制值
* @return 返回bool查询结果
*/
public boolean inLimit(char code) {
return (LimitMix <= code) && (code < LimitMax);
}
/**
* 使用Unicode值获取轮廓数据
*
* @param key 传入Unicode十进制值
* @return 返回轮廓数组的String值
*/
public String getGlyfByCode(int key) {
return CodeToGlyph.getOrDefault(key, "");
}
/**
* 使用轮廓数据获取Unicode值
*
* @param val 传入轮廓数组的String值
* @return 返回Unicode十进制值
*/
public int getCodeByGlyf(String val) {
return GlyphToCode.getOrDefault(val, 0);
}
}

@ -1,633 +0,0 @@
package io.legado.app.model.analyzeRule
import org.apache.commons.lang3.tuple.Pair
import org.apache.commons.lang3.tuple.Triple
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.experimental.and
class QueryTTF(buffer: ByteArray) {
private class Header(
var majorVersion: Int = 0,
var minorVersion: Int = 0,
var numOfTables: Int = 0,
var searchRange: Int = 0,
var entrySelector: Int = 0,
var rangeShift: Int = 0,
)
private class Directory {
lateinit var tag: String // table name
var checkSum: Int = 0 // Check sum
var offset: Int = 0 // Offset from beginning of file
var length: Int = 0 // length of the table in bytes
}
private class NameLayout(
var format: Int = 0,
var count: Int = 0,
var stringOffset: Int = 0,
var records: MutableList<NameRecord> = LinkedList(),
)
private class NameRecord(
var platformID: Int = 0, // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom>
var encodingID: Int = 0, // 编码标识符
var languageID: Int = 0, // 语言标识符
var nameID: Int = 0, // 名称标识符
var length: Int = 0,// 名称字符串的长度
var offset: Int = 0, // 名称字符串相对于stringOffset的字节偏移量
)
private class HeadLayout(
var majorVersion: Int = 0,
var minorVersion: Int = 0,
var fontRevision: Int = 0,
var checkSumAdjustment: Int = 0,
var magicNumber: Int = 0,
var flags: Int = 0,
var unitsPerEm: Int = 0,
var created: Long = 0,
var modified: Long = 0,
var xMin: Short = 0,
var yMin: Short = 0,
var xMax: Short = 0,
var yMax: Short = 0,
var macStyle: Int = 0,
var lowestRecPPEM: Int = 0,
var fontDirectionHint: Short = 0,
var indexToLocFormat: Short = 0, // <0:loca是2字节数组, 1:loca是4字节数组>
var glyphDataFormat: Short = 0,
)
private class MaxpLayout(
var majorVersion: Int = 0,
var minorVersion: Int = 0,
var numGlyphs: Int = 0, // 字体中的字形数量
var maxPoints: Int = 0,
var maxContours: Int = 0,
var maxCompositePoints: Int = 0,
var maxCompositeContours: Int = 0,
var maxZones: Int = 0,
var maxTwilightPoints: Int = 0,
var maxStorage: Int = 0,
var maxFunctionDefs: Int = 0,
var maxInstructionDefs: Int = 0,
var maxStackElements: Int = 0,
var maxSizeOfInstructions: Int = 0,
var maxComponentElements: Int = 0,
var maxComponentDepth: Int = 0,
)
private class CmapLayout(
var version: Int = 0,
var numTables: Int = 0,
var records: MutableList<CmapRecord> = LinkedList(),
var tables: MutableMap<Int, CmapFormat> = HashMap(),
)
private class CmapRecord(
var platformID: Int = 0,
var encodingID: Int = 0,
var offset: Int = 0,
)
private open class CmapFormat {
var format: Int = 0
open var length: Int = 0
open var language: Int = 0
lateinit var glyphIdArray: IntArray
}
private class CmapFormat4 : CmapFormat() {
var segCountX2: Int = 0
var searchRange: Int = 0
var entrySelector: Int = 0
var rangeShift: Int = 0
lateinit var endCode: IntArray
var reservedPad: Int = 0
lateinit var startCode: IntArray
lateinit var idDelta: ShortArray
lateinit var idRangeOffset: IntArray
// override lateinit var glyphIdArray: IntArray
}
private class CmapFormat6(
var firstCode: Int = 0,
var entryCount: Int = 0,
) : CmapFormat()
private class CmapFormat12(
var reserved: Int = 0,
override var length: Int = 0,
override var language: Int = 0,
var numGroups: Int = 0,
var groups: MutableList<Triple<Int, Int, Int>>
) : CmapFormat()
private class GlyfLayout(
var numberOfContours: Short = 0, // 非负值为简单字型,负值为符合字型
var xMin: Short = 0,
var yMin: Short = 0,
var xMax: Short = 0,
var yMax: Short = 0,
var instructionLength: Int = 0
) {
lateinit var endPtsOfContours: IntArray // length=numberOfContours
lateinit var instructions: IntArray // length=instructionLength
lateinit var flags: ByteArray
lateinit var xCoordinates: ShortArray // length = flags.length
lateinit var yCoordinates: ShortArray // length = flags.length
}
private class ByteArrayReader(var buffer: ByteArray, var index: Int) {
fun readUIntX(len: Long): Long {
var result: Long = 0
for (i in 0 until len) {
result = result shl 8
result = result or ((buffer[index++] and 0xFF.toByte()).toLong())
}
return result
}
fun readUInt64(): Long = readUIntX(8)
fun readUInt32(): Int = readUIntX(4).toInt()
fun readUInt16(): Int = readUIntX(2).toInt()
fun readInt16(): Short = readUIntX(2).toShort()
fun readUInt8(): Short = readUIntX(1).toShort()
fun readStrings(len: Int, charset: Charset): String {
val result = ByteArray(len)
for (i in 0 until len) {
result[i] = buffer[index++]
}
return String(result, charset)
}
fun getByte() = buffer[index++]
fun getBytes(len: Int): IntArray {
val result = IntArray(len)
for (i in 0 until len) {
result[i] = buffer[index++].toInt()
}
return result
}
fun getUInt16Array(len: Int): IntArray {
val result = IntArray(len)
for (i in 0 until len) {
result[i] = readUInt16()
}
return result
}
fun getInt16Array(len: Int): ShortArray {
val result = ShortArray(len)
for (i in 0 until len) {
result[i] = readInt16()
}
return result
}
}
private val fontReader: ByteArrayReader = ByteArrayReader(buffer, 0)
private val directories: MutableList<Directory> = LinkedList()
private val name = NameLayout()
private val cmap = CmapLayout()
private val pps: Array<Pair<Int, Int>> = arrayOf(
Pair.of(3, 10),
Pair.of(0, 4),
Pair.of(3, 1),
Pair.of(1, 0),
Pair.of(0, 3),
Pair.of(0, 1)
)
private val codeToGlyph: MutableMap<Int, String> = HashMap()
private val glyphToCode: MutableMap<String, Int> = HashMap()
private var limitMix = 0
private var limitMax = 0
/**
* 获取字体信息 (1=字体名称)
*
* @param nameId 传入十进制字体信息索引
* @return 返回查询结果字符串
*/
fun getNameById(nameId: Int): String {
for (temp in directories) {
if ("name" != temp.tag) {
continue
}
fontReader.index = temp.offset
break
}
for (record in name.records) {
if (record.nameID != nameId) {
continue
}
fontReader.index += name.stringOffset + record.offset
return fontReader.readStrings(record.length, if (record.platformID == 1) StandardCharsets.UTF_8 else StandardCharsets.UTF_16BE)
}
return "error"
}
/**
* 使用Unicode值查找轮廓索引
*
* @param code 传入Unicode十进制值
* @return 返回十进制轮廓索引
*/
private fun getGlyfIndex(code: Int): Int {
if (code == 0) {
return 0
}
var fmtKey = 0
for (item in pps) {
for (record in cmap.records) {
if (item.left == record.platformID && item.right == record.encodingID) {
fmtKey = record.offset
break
}
}
if (fmtKey > 0) {
break
}
}
if (fmtKey == 0) {
return 0
}
var glyfID = 0
val table = cmap.tables[fmtKey]
when (table?.format) {
0 -> {
if (code < table.glyphIdArray.size) {
glyfID = table.glyphIdArray[code] and 0xFF
}
}
4 -> {
val tab = table as CmapFormat4
if (code > tab.endCode[tab.endCode.size - 1]) {
return 0
}
// 二分法查找数值索引
var start = 0
var middle: Int
var end = tab.endCode.size - 1
while (start + 1 < end) {
middle = (start + end) / 2
if (tab.endCode[middle] <= code) {
start = middle
} else {
end = middle
}
}
if (tab.endCode[start] < code) {
++start
}
if (code < tab.startCode[start]) {
return 0
}
glyfID = if (tab.idRangeOffset[start] != 0) {
tab.glyphIdArray[code - tab.startCode[start] + (tab.idRangeOffset[start] shr 1) - (tab.idRangeOffset.size - start)]
} else {
code + tab.idDelta[start]
}
glyfID = glyfID and 0xFFFF
}
6 -> {
val tab = table as CmapFormat6
val index = code - tab.firstCode
glyfID = if (index < 0 || index >= tab.glyphIdArray.size) {
0
} else {
tab.glyphIdArray[index]
}
}
12 -> {
val tab = table as CmapFormat12
if (code > tab.groups[tab.numGroups - 1].middle) {
return 0
}
// 二分法查找数值索引
var start = 0
var middle: Int
var end = tab.numGroups - 1
while (start + 1 < end) {
middle = (start + end) / 2
if (tab.groups[middle].left <= code) {
start = middle
} else {
end = middle
}
}
if (tab.groups[start].left <= code && code <= tab.groups[start].middle) {
glyfID = tab.groups[start].right + code - tab.groups[start].left
}
}
}
return glyfID
}
/**
* 判断Unicode值是否在字体范围内
* @param code 传入Unicode十进制值
* @return 返回bool查询结果
*/
fun inLimit(code: Char): Boolean = code.toInt() in limitMix until limitMax
/**
* 使用Unicode值获取轮廓数据
*
* @param key 传入Unicode十进制值
* @return 返回轮廓数组的String值
*/
fun getGlyfByCode(key: Int): String = codeToGlyph.getOrDefault(key, "")
/**
* 使用轮廓数据获取Unicode值
*
* @param val 传入轮廓数组的String值
* @return 返回Unicode十进制值
*/
fun getCodeByGlyf(`val`: String): Int = glyphToCode.getOrDefault(`val`, 0)
/**
* 构造函数 buffer 传入TTF字体二进制数组
*/
init {
// 获取文件头
val fileHeader = Header()
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 (i in 0 until fileHeader.numOfTables) {
val d = Directory()
d.tag = fontReader.readStrings(4, StandardCharsets.US_ASCII)
d.checkSum = fontReader.readUInt32()
d.offset = fontReader.readUInt32()
d.length = fontReader.readUInt32()
directories.add(d)
}
// 解析表 name (字体信息,包含版权、名称、作者等...)
for (temp in directories) {
if ("name" == temp.tag) {
fontReader.index = temp.offset
name.format = fontReader.readUInt16()
name.count = fontReader.readUInt16()
name.stringOffset = fontReader.readUInt16()
for (i in 0 until name.count) {
val record = 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)
}
}
}
// 解析表 head (获取 head.indexToLocFormat)
val head = HeadLayout()
for (temp in directories) {
if ("head" == temp.tag) {
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)
val maxp = MaxpLayout()
for (temp in directories) {
if ("maxp" == temp.tag) {
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 (轮廓数据偏移地址表)
val loca: MutableList<Int> = LinkedList()
for (temp in directories) {
if ("loca" == temp.tag) {
fontReader.index = temp.offset
val offset = if (head.indexToLocFormat == 0.toShort()) 2 else 4
var i: Long = 0
while (i < temp.length) {
loca.add(if (offset == 2) fontReader.readUInt16() shl 1 else fontReader.readUInt32())
i += offset
}
}
}
// 解析表 cmap (Unicode编码轮廓索引对照表)
for (temp in directories) {
if ("cmap" == temp.tag) {
fontReader.index = temp.offset
cmap.version = fontReader.readUInt16()
cmap.numTables = fontReader.readUInt16()
for (i in 0 until cmap.numTables) {
val record = CmapRecord()
record.platformID = fontReader.readUInt16()
record.encodingID = fontReader.readUInt16()
record.offset = fontReader.readUInt32()
cmap.records.add(record)
}
for (i in 0 until cmap.numTables) {
val fmtOffset = cmap.records[i].offset
fontReader.index = temp.offset + fmtOffset
val endIndex = fontReader.index
val format = fontReader.readUInt16()
if (cmap.tables.containsKey(fmtOffset)) {
continue
}
when (format) {
0 -> {
val f = CmapFormat()
f.format = format
f.length = fontReader.readUInt16()
f.language = fontReader.readUInt16()
f.glyphIdArray = fontReader.getBytes(f.length - 6)
cmap.tables[fmtOffset] = f
}
4 -> {
val f = CmapFormat4()
f.format = format
f.length = fontReader.readUInt16()
f.language = fontReader.readUInt16()
f.segCountX2 = fontReader.readUInt16()
val segCount = f.segCountX2 shr 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 shr 1)
cmap.tables[fmtOffset] = f
}
6 -> {
val f = 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)
cmap.tables[fmtOffset] = f
}
12 -> {
val f = CmapFormat12(
reserved = fontReader.readUInt16(),
length = fontReader.readUInt32(),
language = fontReader.readUInt32(),
numGroups = fontReader.readUInt32(),
groups = ArrayList(fontReader.readUInt32())
)
f.format = format
for (n in 0 until f.numGroups) {
(f.groups as ArrayList<Triple<Int, Int, Int>>).add(Triple.of(fontReader.readUInt32(), fontReader.readUInt32(), fontReader.readUInt32()))
}
cmap.tables[fmtOffset] = f
}
}
}
}
}
// 解析表 glyf (字体轮廓数据表)
val glyf: MutableList<GlyfLayout> = LinkedList()
for (temp in directories) {
if ("glyf" == temp.tag) {
fontReader.index = temp.offset
for (i in 0 until maxp.numGlyphs) {
fontReader.index = temp.offset + loca[i]
val numberOfContours = fontReader.readInt16()
if (numberOfContours > 0) {
val g = 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.toInt())
g.instructionLength = fontReader.readUInt16()
g.instructions = fontReader.getBytes(g.instructionLength)
val flagLength = g.endPtsOfContours[g.endPtsOfContours.size - 1] + 1
// 获取轮廓点描述标志
g.flags = ByteArray(flagLength).apply {
var n = 0
while (n < flagLength) {
this[n] = fontReader.getByte()
if ((this[n] and 0x08) != 0x00.toByte()) {
for (m in fontReader.readUInt8() downTo 1) {
this[++n] = this[n - 1]
}
}
++n
}
}
// 获取轮廓点描述x轴相对值
g.xCoordinates = ShortArray(flagLength)
for (n in 0 until flagLength) {
val same = (if (g.flags[n] and 0x10 != 0.toByte()) 1 else -1).toShort()
if (g.flags[n] and 0x02 != 0.toByte()) {
g.xCoordinates[n] = (same * fontReader.readUInt8()).toShort()
} else {
g.xCoordinates[n] = if (same.toInt() == 1) 0.toShort() else fontReader.readInt16()
}
}
// 获取轮廓点描述y轴相对值
g.yCoordinates = ShortArray(flagLength)
for (n in 0 until flagLength) {
val same = (if (g.flags[n] and 0x20 != 0.toByte()) 1 else -1).toShort()
if (g.flags[n] and 0x04 != 0.toByte()) {
g.yCoordinates[n] = (same * fontReader.readUInt8()).toShort()
} else {
g.yCoordinates[n] = if (same.toInt() == 1) 0.toShort() else fontReader.readInt16()
}
}
// 相对坐标转绝对坐标
// for (int n = 1; n < flagLength; ++n) {
// xCoordinates[n] += xCoordinates[n - 1];
// yCoordinates[n] += yCoordinates[n - 1];
// }
glyf.add(g)
} //else情况:复合字体暂未使用
}
}
}
// 建立Unicode&Glyph双向表
var key = 0
while (key < 130000) {
if (key == 0xFF) {
key = 0x3400
}
val gid = getGlyfIndex(key)
if (gid == 0) {
++key
continue
}
val sb = StringBuilder()
// 字型数据转String,方便存HashMap
for (b in glyf[gid].xCoordinates) {
sb.append(b.toInt())
}
for (b in glyf[gid].yCoordinates) {
sb.append(b.toInt())
}
val `val` = sb.toString()
if (limitMix == 0) {
limitMix = key
}
limitMax = key
codeToGlyph[key] = `val`
if (glyphToCode.containsKey(`val`)) {
++key
continue
}
glyphToCode[`val`] = key
++key
}
}
}

@ -1,12 +1,13 @@
package io.legado.app.model.localBook package io.legado.app.model.localBook
import android.net.Uri import android.net.Uri
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.TxtTocRule import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.charset.Charset import java.nio.charset.Charset
@ -266,7 +267,7 @@ class AnalyzeTxtFile {
val bookFile = FileUtils.getFile(LocalBook.cacheFolder, book.originName) val bookFile = FileUtils.getFile(LocalBook.cacheFolder, book.originName)
if (!bookFile.exists()) { if (!bookFile.exists()) {
bookFile.createNewFile() bookFile.createNewFile()
DocumentUtils.readBytes(App.INSTANCE, uri)?.let { DocumentUtils.readBytes(appCtx, uri)?.let {
bookFile.writeBytes(it) bookFile.writeBytes(it)
} }
} }
@ -276,10 +277,10 @@ class AnalyzeTxtFile {
} }
private fun getTocRules(): List<TxtTocRule> { private fun getTocRules(): List<TxtTocRule> {
var rules = App.db.txtTocRule.enabled var rules = appDb.txtTocRuleDao.enabled
if (rules.isEmpty()) { if (rules.isEmpty()) {
rules = DefaultData.txtTocRules.apply { rules = DefaultData.txtTocRules.apply {
App.db.txtTocRule.insert(*this.toTypedArray()) appDb.txtTocRuleDao.insert(*this.toTypedArray())
}.filter { }.filter {
it.enable it.enable
} }

@ -4,13 +4,20 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.EpubChapter
import io.legado.app.utils.* import io.legado.app.utils.*
import net.sf.jazzlib.ZipEntry
import net.sf.jazzlib.ZipInputStream
import nl.siegmann.epublib.domain.Book import nl.siegmann.epublib.domain.Book
import nl.siegmann.epublib.domain.Resources
import nl.siegmann.epublib.domain.TOCReference import nl.siegmann.epublib.domain.TOCReference
import nl.siegmann.epublib.epub.EpubReader import nl.siegmann.epublib.epub.EpubReader
import nl.siegmann.epublib.service.MediatypeService
import nl.siegmann.epublib.util.ResourceUtil
import org.jsoup.Jsoup import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@ -18,7 +25,7 @@ import java.io.InputStream
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.* import java.util.*
class EPUBFile(val book: io.legado.app.data.entities.Book) { class EPUBFile(var book: io.legado.app.data.entities.Book) {
companion object { companion object {
private var eFile: EPUBFile? = null private var eFile: EPUBFile? = null
@ -29,6 +36,7 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
eFile = EPUBFile(book) eFile = EPUBFile(book)
return eFile!! return eFile!!
} }
eFile?.book = book
return eFile!! return eFile!!
} }
@ -49,6 +57,11 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
): InputStream? { ): InputStream? {
return getEFile(book).getImage(href) return getEFile(book).getImage(href)
} }
@Synchronized
fun upBookInfo(book: io.legado.app.data.entities.Book) {
return getEFile(book).upBookInfo()
}
} }
private var epubBook: Book? = null private var epubBook: Book? = null
@ -56,28 +69,30 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
init { init {
try { try {
val epubReader = EpubReader()
val inputStream = if (book.bookUrl.isContentScheme()) { val inputStream = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl) val uri = Uri.parse(book.bookUrl)
App.INSTANCE.contentResolver.openInputStream(uri) appCtx.contentResolver.openInputStream(uri)
} else { } else {
File(book.bookUrl).inputStream() File(book.bookUrl).inputStream()
} }
epubBook = epubReader.readEpub(inputStream) epubBook = readEpub(inputStream)
if (book.coverUrl.isNullOrEmpty()) { if (epubBook != null) {
book.coverUrl = FileUtils.getPath( if (book.coverUrl.isNullOrEmpty()) {
App.INSTANCE.externalFilesDir, book.coverUrl = FileUtils.getPath(
"covers", appCtx.externalFilesDir,
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg" "covers",
) "${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
} )
if (!File(book.coverUrl!!).exists()) { }
epubBook!!.coverImage?.inputStream?.use { if (!File(book.coverUrl!!).exists()) {
val cover = BitmapFactory.decodeStream(it) /*部分书籍DRM处理后,封面获取异常,待优化*/
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) epubBook!!.coverImage?.inputStream?.use {
cover.compress(Bitmap.CompressFormat.JPEG, 90, out) val cover = BitmapFactory.decodeStream(it)
out.flush() val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
out.close() cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -85,14 +100,88 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
} }
} }
/*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/
private fun readEpub(input: InputStream?): Book? {
if (input == null) return null
try {
val inZip = ZipInputStream(input)
var zipEntry: ZipEntry?
val resources = Resources()
do {
zipEntry = inZip.nextEntry
if ((zipEntry == null) || zipEntry.isDirectory || zipEntry == ZipEntry("<error>")) continue
val resource = ResourceUtil.createResource(zipEntry, inZip)
if (resource.mediaType == MediatypeService.XHTML) resource.inputEncoding = "UTF-8"
if (zipEntry.name.endsWith(".opf")) {
/*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
val newS = String(resource.data).replace(
"\\smlns=\"http://www.idpf.org/2007/opf\"".toRegex(),
" xmlns=\"http://www.idpf.org/2007/opf\""
)
resource.data = newS.toByteArray()
}
resources.add(resource)
} while (zipEntry != null)
if (resources.size() > 0) return EpubReader().readEpub(resources)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
private fun getContent(chapter: BookChapter): String? { private fun getContent(chapter: BookChapter): String? {
epubBook?.let { eBook -> /*获取当前章节文本*/
val resource = eBook.resources.getByHref(chapter.url) var string = getChildChapter(chapter, chapter.url)
val doc = Jsoup.parse(String(resource.data, mCharset)) val childContends = appDb.epubChapterDao.get(book.bookUrl, chapter.url)
val elements = doc.body().children() childContends.forEach {
string += "\n" + getChildChapter(chapter, it.href)
}
return string
}
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 = io.legado.app.data.entities.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 = io.legado.app.data.entities.Book.imgTag
if (book.getDelTag(tag)) {
body.getElementsByTag("img")?.remove()
}
val elements = body.children()
elements.select("script").remove() elements.select("script").remove()
elements.select("style").remove() elements.select("style").remove()
return elements.outerHtml().htmlFormat() /*选择去除正文中的ruby标签,目前注释支持效果待优化*/
tag = io.legado.app.data.entities.Book.rubyTag
var html = elements.outerHtml()
if (book.getDelTag(tag)) {
html = html.replace("<ruby>\\s?([\\u4e00-\\u9fa5])\\s?.*?</ruby>".toRegex(), "$1")
}
return html.htmlFormat()
} }
return null return null
} }
@ -102,11 +191,13 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
return epubBook?.resources?.getByHref(abHref)?.inputStream return epubBook?.resources?.getByHref(abHref)?.inputStream
} }
private fun getChapterList(): ArrayList<BookChapter> { private fun upBookInfo() {
val chapterList = ArrayList<BookChapter>() if (epubBook == null) {
epubBook?.let { eBook -> eFile = null
val metadata = eBook.metadata book.intro = "书籍导入异常"
book.name = metadata.firstTitle } else {
val metadata = epubBook!!.metadata
book.name = book.originName
if (metadata.authors.size > 0) { if (metadata.authors.size > 0) {
val author = val author =
metadata.authors[0].toString().replace("^, |, $".toRegex(), "") metadata.authors[0].toString().replace("^, |, $".toRegex(), "")
@ -115,7 +206,12 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
if (metadata.descriptions.size > 0) { if (metadata.descriptions.size > 0) {
book.intro = Jsoup.parse(metadata.descriptions[0]).text() book.intro = Jsoup.parse(metadata.descriptions[0]).text()
} }
}
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook ->
val refs = eBook.tableOfContents.tocReferences val refs = eBook.tableOfContents.tocReferences
if (refs == null || refs.isEmpty()) { if (refs == null || refs.isEmpty()) {
val spineReferences = eBook.spine.spineReferences val spineReferences = eBook.spine.spineReferences
@ -130,7 +226,7 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
val doc = val doc =
Jsoup.parse(String(resource.data, mCharset)) Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title") val elements = doc.getElementsByTag("title")
if (elements.size > 0) { if (elements != null && elements.size > 0) {
title = elements[0].text() title = elements[0].text()
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -150,10 +246,12 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
i++ i++
} }
} else { } else {
parseFirstPage(chapterList, refs)
parseMenu(chapterList, refs, 0) parseMenu(chapterList, refs, 0)
for (i in chapterList.indices) { for (i in chapterList.indices) {
chapterList[i].index = i chapterList[i].index = i
} }
getChildChapter(chapterList)
} }
} }
book.latestChapterTitle = chapterList.lastOrNull()?.title book.latestChapterTitle = chapterList.lastOrNull()?.title
@ -161,19 +259,100 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
return chapterList return chapterList
} }
/*获取当前章节的子章节。部分书籍一个章节包含多个html文件,(一些精排书籍,每一章节正文前的标题、标题封面、引言等都会有独立html)*/
/*需在读取常规章节列表后调用,遍历书籍全内容,根据href检索原不包含在章节内的html归属父章节*/
private fun getChildChapter(chapterList: ArrayList<BookChapter>) {
epubBook?.let {
val contents = it.contents
val chapters = ArrayList<EpubChapter>()
if (contents != null) {
var i = 0
var j = 0
var parentHref: String? = null
while (i < contents.size) {
val content = contents[i]
if (j < chapterList.size && content.href == chapterList[j].url) {
parentHref = content.href
j++
} else if (!parentHref.isNullOrBlank() && content.mediaType.toString()
.contains("htm")
) {
val epubChapter = EpubChapter()
epubChapter.bookUrl = book.bookUrl
epubChapter.href = content.href
epubChapter.parentHref = parentHref
chapters.add(epubChapter)
}
i++
}
}
appDb.epubChapterDao.deleteByName(book.bookUrl)
if (chapters.size > 0) appDb.epubChapterDao.insert(*chapters.toTypedArray())
}
}
/*获取书籍起始页内容。部分书籍第一章之前存在封面,引言,扉页等内容*/
/*tile获取不同书籍风格杂乱,格式化处理待优化*/
private var durIndex = 0
private fun parseFirstPage(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?
) {
val contents = epubBook?.contents
if (epubBook == null || contents == null || refs == null) return
var i = 0
durIndex = 0
while (i < contents.size) {
val content = contents[i]
if (!content.mediaType.toString().contains("htm")) continue
/*检索到第一章href停止*/
if (refs[0].completeHref == content.href) break
val chapter = BookChapter()
var title = content.title
if (TextUtils.isEmpty(title)) {
val elements = Jsoup.parse(
String(
epubBook!!.resources.getByHref(content.href).data,
mCharset
)
).getElementsByTag("title")
title =
if (elements != null && elements.size > 0) elements[0].text() else "--卷首--"
}
chapter.bookUrl = book.bookUrl
chapter.title = title
chapter.url = content.href
chapter.startFragmentId =
if (content.href.substringAfter("#") == content.href) null
else content.href.substringAfter("#")
if (durIndex > 0) {
val preIndex = durIndex - 1
chapterList[preIndex].endFragmentId = chapter.startFragmentId
}
chapterList.add(chapter)
durIndex++
i++
}
}
private fun parseMenu( private fun parseMenu(
chapterList: ArrayList<BookChapter>, chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?, refs: List<TOCReference>?,
level: Int level: Int
) { ) {
if (refs == null) return refs?.forEach { ref ->
for (ref in refs) {
if (ref.resource != null) { if (ref.resource != null) {
val chapter = BookChapter() val chapter = BookChapter()
chapter.bookUrl = book.bookUrl chapter.bookUrl = book.bookUrl
chapter.title = ref.title chapter.title = ref.title
chapter.url = ref.completeHref chapter.url = ref.completeHref
chapter.startFragmentId = ref.fragmentId
if (durIndex > 0) {
val preIndex = durIndex - 1
chapterList[preIndex].endFragmentId = chapter.startFragmentId
}
chapterList.add(chapter) chapterList.add(chapter)
durIndex++
} }
if (ref.children != null && ref.children.isNotEmpty()) { if (ref.children != null && ref.children.isNotEmpty()) {
parseMenu(chapterList, ref.children, level + 1) parseMenu(chapterList, ref.children, level + 1)

@ -2,20 +2,21 @@ package io.legado.app.model.localBook
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File import java.io.File
object LocalBook { object LocalBook {
private const val folderName = "bookTxt" private const val folderName = "bookTxt"
val cacheFolder: File by lazy { val cacheFolder: File by lazy {
val rootFile = App.INSTANCE.getExternalFilesDir(null) val rootFile = appCtx.getExternalFilesDir(null)
?: App.INSTANCE.externalCacheDir ?: appCtx.externalCacheDir
?: App.INSTANCE.cacheDir ?: appCtx.cacheDir
FileUtils.createFolderIfNotExist(rootFile, folderName) FileUtils.createFolderIfNotExist(rootFile, folderName)
} }
@ -39,12 +40,12 @@ object LocalBook {
val path: String val path: String
val fileName = if (uri.isContentScheme()) { val fileName = if (uri.isContentScheme()) {
path = uri.toString() path = uri.toString()
val doc = DocumentFile.fromSingleUri(App.INSTANCE, uri) val doc = DocumentFile.fromSingleUri(appCtx, uri)
doc?.let { doc?.let {
val bookFile = FileUtils.getFile(cacheFolder, it.name!!) val bookFile = FileUtils.getFile(cacheFolder, it.name!!)
if (!bookFile.exists()) { if (!bookFile.exists()) {
bookFile.createNewFile() bookFile.createNewFile()
doc.readBytes(App.INSTANCE)?.let { bytes -> doc.readBytes(appCtx)?.let { bytes ->
bookFile.writeBytes(bytes) bookFile.writeBytes(bytes)
} }
} }
@ -77,12 +78,13 @@ object LocalBook {
author = author, author = author,
originName = fileName, originName = fileName,
coverUrl = FileUtils.getPath( coverUrl = FileUtils.getPath(
App.INSTANCE.externalFilesDir, appCtx.externalFilesDir,
"covers", "covers",
"${MD5Utils.md5Encode16(path)}.jpg" "${MD5Utils.md5Encode16(path)}.jpg"
) )
) )
App.db.bookDao.insert(book) if (book.isEpub()) EPUBFile.upBookInfo(book)
appDb.bookDao.insert(book)
return book return book
} }
@ -96,7 +98,7 @@ object LocalBook {
if (deleteOriginal) { if (deleteOriginal) {
if (book.bookUrl.isContentScheme()) { if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl) val uri = Uri.parse(book.bookUrl)
DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete() DocumentFile.fromSingleUri(appCtx, uri)?.delete()
} else { } else {
FileUtils.deleteFile(book.bookUrl) FileUtils.deleteFile(book.bookUrl)
} }

@ -1,7 +1,6 @@
package io.legado.app.model.rss package io.legado.app.model.rss
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
@ -10,6 +9,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.RuleDataInterface import io.legado.app.model.analyzeRule.RuleDataInterface
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import splitties.init.appCtx
import java.util.* import java.util.*
@Keep @Keep
@ -27,7 +27,7 @@ object RssParserByRule {
var nextUrl: String? = null var nextUrl: String? = null
if (body.isNullOrBlank()) { if (body.isNullOrBlank()) {
throw Exception( throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, rssSource.sourceUrl) appCtx.getString(R.string.error_get_web_content, rssSource.sourceUrl)
) )
} }
Debug.log(sourceUrl, "≡获取成功:$sourceUrl") Debug.log(sourceUrl, "≡获取成功:$sourceUrl")
@ -39,6 +39,7 @@ object RssParserByRule {
val articleList = mutableListOf<RssArticle>() val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule(ruleData) val analyzeRule = AnalyzeRule(ruleData)
analyzeRule.setContent(body).setBaseUrl(sortUrl) analyzeRule.setContent(body).setBaseUrl(sortUrl)
analyzeRule.setRedirectUrl(sortUrl)
var reverse = false var reverse = false
if (ruleArticles.startsWith("-")) { if (ruleArticles.startsWith("-")) {
reverse = true reverse = true

@ -1,7 +1,6 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -14,6 +13,7 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.init.appCtx
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -24,12 +24,13 @@ object BookChapterList {
book: Book, book: Book,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String baseUrl: String,
redirectUrl: String
): List<BookChapter> = suspendCancellableCoroutine { block -> ): List<BookChapter> = suspendCancellableCoroutine { block ->
kotlin.runCatching { kotlin.runCatching {
val chapterList = ArrayList<BookChapter>() val chapterList = ArrayList<BookChapter>()
body ?: throw Exception( body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
@ -46,7 +47,8 @@ object BookChapterList {
} }
var chapterData = var chapterData =
analyzeChapterList( analyzeChapterList(
scope, book, baseUrl, body, tocRule, listRule, bookSource, log = true scope, book, baseUrl, redirectUrl, body,
tocRule, listRule, bookSource, log = true
) )
chapterData.chapterList?.let { chapterData.chapterList?.let {
chapterList.addAll(it) chapterList.addAll(it)
@ -66,7 +68,8 @@ object BookChapterList {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody -> ).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
chapterData = analyzeChapterList( chapterData = analyzeChapterList(
this, book, nextUrl, nextBody, tocRule, listRule, bookSource this, book, nextUrl, nextUrl,
nextBody, tocRule, listRule, bookSource
) )
nextUrl = chapterData.nextUrl.firstOrNull() ?: "" nextUrl = chapterData.nextUrl.firstOrNull() ?: ""
chapterData.chapterList?.let { chapterData.chapterList?.let {
@ -128,7 +131,8 @@ object BookChapterList {
).getStrResponse(bookSource.bookSourceUrl).body ).getStrResponse(bookSource.bookSourceUrl).body
?: throw Exception("${chapterData.nextUrl}, 下载失败") ?: throw Exception("${chapterData.nextUrl}, 下载失败")
val nextChapterData = analyzeChapterList( val nextChapterData = analyzeChapterList(
this, book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource, false this, book, chapterData.nextUrl, chapterData.nextUrl,
nextBody, tocRule, listRule, bookSource, false
) )
synchronized(chapterDataList) { synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish( val isFinished = addChapterListIsFinish(
@ -195,6 +199,7 @@ object BookChapterList {
scope: CoroutineScope, scope: CoroutineScope,
book: Book, book: Book,
baseUrl: String, baseUrl: String,
redirectUrl: String,
body: String, body: String,
tocRule: TocRule, tocRule: TocRule,
listRule: String, listRule: String,
@ -204,7 +209,13 @@ object BookChapterList {
): ChapterData<List<String>> { ): ChapterData<List<String>> {
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(redirectUrl)
//获取目录列表
val chapterList = arrayListOf<BookChapter>() val chapterList = arrayListOf<BookChapter>()
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
//获取下一页链接
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
val nextTocRule = tocRule.nextTocUrl val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) { if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
@ -222,11 +233,9 @@ object BookChapterList {
log log
) )
} }
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
scope.ensureActive() scope.ensureActive()
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log)
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
@ -251,11 +260,12 @@ object BookChapterList {
chapterList.add(bookChapter) chapterList.add(bookChapter)
} }
} }
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) Debug.log(bookSource.bookSourceUrl, "└解析目录列表完成", log)
Debug.log(bookSource.bookSourceUrl, "┌首章名称", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].title}", log)
Debug.log(bookSource.bookSourceUrl, "获取首章链接", log) Debug.log(bookSource.bookSourceUrl, "┌首章链接", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].url}", log)
Debug.log(bookSource.bookSourceUrl, "获取首章信息", log) Debug.log(bookSource.bookSourceUrl, "┌首章信息", log)
Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log) Debug.log(bookSource.bookSourceUrl, "${chapterList[0].tag}", log)
} }
return ChapterData(chapterList, nextUrlList) return ChapterData(chapterList, nextUrlList)

@ -1,7 +1,7 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -14,6 +14,7 @@ import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx
object BookContent { object BookContent {
@ -25,31 +26,34 @@ object BookContent {
bookChapter: BookChapter, bookChapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String, baseUrl: String,
nextChapterUrlF: String? = null nextChapterUrl: String? = null
): String { ): String {
body ?: throw Exception( body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val mNextChapterUrl = if (!nextChapterUrl.isNullOrEmpty()) {
nextChapterUrl
} else {
appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url
}
val content = StringBuilder() val content = StringBuilder()
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule() val contentRule = bookSource.getContentRule()
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = mNextChapterUrl
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, body, contentRule, bookChapter, bookSource book, baseUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
) )
content.append(contentData.content).append("\n") content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
nextChapterUrlF
else
App.db.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
if (!nextChapterUrl.isNullOrEmpty() if (!mNextChapterUrl.isNullOrEmpty()
&& NetworkUtils.getAbsoluteURL(baseUrl, nextUrl) && NetworkUtils.getAbsoluteURL(baseUrl, nextUrl)
== NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl) == NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl)
) break ) break
nextUrlList.add(nextUrl) nextUrlList.add(nextUrl)
AnalyzeUrl( AnalyzeUrl(
@ -58,7 +62,14 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody -> ).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
contentData = analyzeContent( contentData = analyzeContent(
book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false book,
nextUrl,
nextBody,
contentRule,
bookChapter,
bookSource,
mNextChapterUrl,
false
) )
nextUrl = nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
@ -80,7 +91,14 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl).body?.let { ).getStrResponse(bookSource.bookSourceUrl).body?.let {
contentData = analyzeContent( contentData = analyzeContent(
book, item.nextUrl, it, contentRule, bookChapter, bookSource, false book,
item.nextUrl,
it,
contentRule,
bookChapter,
bookSource,
mNextChapterUrl,
false
) )
item.content = contentData.content item.content = contentData.content
} }
@ -114,12 +132,18 @@ object BookContent {
contentRule: ContentRule, contentRule: ContentRule,
chapter: BookChapter, chapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
nextChapterUrl: String?,
printLog: Boolean = true printLog: Boolean = true
): ContentData<List<String>> { ): ContentData<List<String>> {
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = nextChapterUrl
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
analyzeRule.chapter = chapter analyzeRule.chapter = chapter
//获取正文
val content = analyzeRule.getString(contentRule.content)
//获取下一页链接
val nextUrlRule = contentRule.nextContentUrl val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) { if (!nextUrlRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog) Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog)
@ -128,7 +152,6 @@ object BookContent {
} }
Debug.log(bookSource.bookSourceUrl, "" + nextUrlList.joinToString(""), printLog) Debug.log(bookSource.bookSourceUrl, "" + nextUrlList.joinToString(""), printLog)
} }
val content = analyzeRule.getString(contentRule.content)
return ContentData(content, nextUrlList) return ContentData(content, nextUrlList)
} }
} }

@ -1,6 +1,5 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -12,6 +11,7 @@ import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import splitties.init.appCtx
object BookInfo { object BookInfo {
@ -22,15 +22,30 @@ object BookInfo {
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String, baseUrl: String,
redirectUrl: String,
canReName: Boolean, canReName: Boolean,
) { ) {
body ?: throw Exception( body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) appCtx.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val infoRule = bookSource.getBookInfoRule()
val analyzeRule = AnalyzeRule(book) val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
analyzeRule.setRedirectUrl(redirectUrl)
analyzeBookInfo(scope, book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName)
}
fun analyzeBookInfo(
scope: CoroutineScope,
book: Book,
body: String,
analyzeRule: AnalyzeRule,
bookSource: BookSource,
baseUrl: String,
redirectUrl: String,
canReName: Boolean,
) {
val infoRule = bookSource.getBookInfoRule()
infoRule.init?.let { infoRule.init?.let {
if (it.isNotBlank()) { if (it.isNotBlank()) {
scope.ensureActive() scope.ensureActive()
@ -84,7 +99,7 @@ object BookInfo {
scope.ensureActive() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
analyzeRule.getString(infoRule.coverUrl).let { analyzeRule.getString(infoRule.coverUrl).let {
if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(redirectUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
scope.ensureActive() scope.ensureActive()

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

@ -1,6 +1,6 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import io.legado.app.App import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import splitties.init.appCtx
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
@ -44,12 +45,12 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C
initSearchPool() initSearchPool()
mSearchId = searchId mSearchId = searchId
searchPage = 1 searchPage = 1
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: "" val searchGroup = appCtx.getPrefString("searchGroup") ?: ""
bookSourceList.clear() bookSourceList.clear()
if (searchGroup.isBlank()) { if (searchGroup.isBlank()) {
bookSourceList.addAll(App.db.bookSourceDao.allEnabled) bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
} else { } else {
bookSourceList.addAll(App.db.bookSourceDao.getEnabledByGroup(searchGroup)) bookSourceList.addAll(appDb.bookSourceDao.getEnabledByGroup(searchGroup))
} }
} else { } else {
searchPage++ searchPage++

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

@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.KeyEvent import android.view.KeyEvent
import io.legado.app.App
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book import io.legado.app.data.appDb
import io.legado.app.help.ActivityHelp import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.service.AudioPlayService import io.legado.app.service.AudioPlayService
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
@ -15,13 +15,7 @@ import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.main.MainActivity import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** /**
@ -31,6 +25,12 @@ import kotlinx.coroutines.withContext
class MediaButtonReceiver : BroadcastReceiver() { class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (handleIntent(context, intent) && isOrderedBroadcast) {
abortBroadcast()
}
}
companion object { companion object {
fun handleIntent(context: Context, intent: Intent): Boolean { fun handleIntent(context: Context, intent: Intent): Boolean {
@ -55,7 +55,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
return true return true
} }
private fun readAloud(context: Context) { fun readAloud(context: Context, isMediaKey: Boolean = true) {
when { when {
BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) { BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) {
ReadAloud.pause(context) ReadAloud.pause(context)
@ -73,34 +73,23 @@ class MediaButtonReceiver : BroadcastReceiver() {
postEvent(EventBus.MEDIA_BUTTON, true) postEvent(EventBus.MEDIA_BUTTON, true)
ActivityHelp.isExist(AudioPlayActivity::class.java) -> ActivityHelp.isExist(AudioPlayActivity::class.java) ->
postEvent(EventBus.MEDIA_BUTTON, true) postEvent(EventBus.MEDIA_BUTTON, true)
else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) { else -> if (AppConfig.mediaButtonOnExit || !isMediaKey) {
GlobalScope.launch(Main) { appDb.bookDao.lastReadBook?.let {
val lastBook: Book? = withContext(IO) { if (!ActivityHelp.isExist(MainActivity::class.java)) {
App.db.bookDao.lastReadBook Intent(context, MainActivity::class.java).let {
}
lastBook?.let {
if (!ActivityHelp.isExist(MainActivity::class.java)) {
Intent(context, MainActivity::class.java).let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(it)
}
}
Intent(context, ReadBookActivity::class.java).let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
it.putExtra("readAloud", true)
context.startActivity(it) context.startActivity(it)
} }
} }
Intent(context, ReadBookActivity::class.java).let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
it.putExtra("readAloud", true)
context.startActivity(it)
}
} }
} }
} }
} }
} }
override fun onReceive(context: Context, intent: Intent) {
if (handleIntent(context, intent) && isOrderedBroadcast) {
abortBroadcast()
}
}
} }

@ -6,7 +6,8 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.main.MainActivity import io.legado.app.ui.main.MainActivity
import org.jetbrains.anko.startActivity import io.legado.app.utils.startActivity
import splitties.init.appCtx
class SharedReceiverActivity : AppCompatActivity() { class SharedReceiverActivity : AppCompatActivity() {
@ -19,21 +20,30 @@ class SharedReceiverActivity : AppCompatActivity() {
} }
private fun initIntent() { private fun initIntent() {
if (Intent.ACTION_SEND == intent.action && intent.type == receivingType) { when {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { Intent.ACTION_SEND == intent.action && intent.type == receivingType -> {
if (openUrl(it)) { intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
startActivity<SearchActivity>(Pair("key", it)) if (openUrl(it)) {
startActivity<SearchActivity> {
putExtra("key", it)
}
}
} }
} }
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Intent.ACTION_PROCESS_TEXT == intent.action && Intent.ACTION_PROCESS_TEXT == intent.action
&& intent.type == receivingType && intent.type == receivingType -> {
) { intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let {
intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let { if (openUrl(it)) {
if (openUrl(it)) { startActivity<SearchActivity> {
startActivity<SearchActivity>(Pair("key", it)) putExtra("key", it)
}
}
} }
} }
intent.getStringExtra("action") == "readAloud" -> {
MediaButtonReceiver.readAloud(appCtx, false)
}
} }
} }

@ -16,13 +16,13 @@ import android.os.Looper
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
@ -31,10 +31,11 @@ import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast import splitties.init.appCtx
class AudioPlayService : BaseService(), class AudioPlayService : BaseService(),
@ -133,7 +134,7 @@ class AudioPlayService : BaseService(),
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
launch { launch {
toast("$url ${it.localizedMessage}") toastOnUi("$url ${it.localizedMessage}")
stopSelf() stopSelf()
} }
} }
@ -219,7 +220,7 @@ class AudioPlayService : BaseService(),
if (!mediaPlayer.isPlaying) { if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP) postEvent(EventBus.AUDIO_STATE, Status.STOP)
launch { toast("error: $what $extra $url") } launch { toastOnUi("error: $what $extra $url") }
} }
return true return true
} }
@ -274,7 +275,7 @@ class AudioPlayService : BaseService(),
.onSuccess { content -> .onSuccess { content ->
if (content.isEmpty()) { if (content.isEmpty()) {
withContext(Main) { withContext(Main) {
toast("未获取到资源链接") toastOnUi("未获取到资源链接")
} }
} else { } else {
contentLoadFinish(chapter, content) contentLoadFinish(chapter, content)
@ -286,7 +287,7 @@ class AudioPlayService : BaseService(),
} }
} else { } else {
removeLoading(chapter.index) removeLoading(chapter.index)
toast("book or source is null") toastOnUi("book or source is null")
} }
} }
} }
@ -321,7 +322,7 @@ class AudioPlayService : BaseService(),
execute { execute {
AudioPlay.book?.let { AudioPlay.book?.let {
AudioPlay.durChapterPos = mediaPlayer.currentPosition AudioPlay.durChapterPos = mediaPlayer.currentPosition
App.db.bookDao.upProgress(it.bookUrl, AudioPlay.durChapterPos) appDb.bookDao.upProgress(it.bookUrl, AudioPlay.durChapterPos)
} }
} }
} }
@ -370,7 +371,7 @@ class AudioPlayService : BaseService(),
Intent( Intent(
Intent.ACTION_MEDIA_BUTTON, Intent.ACTION_MEDIA_BUTTON,
null, null,
App.INSTANCE, appCtx,
MediaButtonReceiver::class.java MediaButtonReceiver::class.java
), ),
PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT

@ -14,7 +14,6 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.* import io.legado.app.constant.*
@ -27,6 +26,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import splitties.init.appCtx
abstract class BaseReadAloudService : BaseService(), abstract class BaseReadAloudService : BaseService(),
AudioManager.OnAudioFocusChangeListener { AudioManager.OnAudioFocusChangeListener {
@ -233,7 +233,7 @@ abstract class BaseReadAloudService : BaseService(),
Intent( Intent(
Intent.ACTION_MEDIA_BUTTON, Intent.ACTION_MEDIA_BUTTON,
null, null,
App.INSTANCE, appCtx,
MediaButtonReceiver::class.java MediaButtonReceiver::class.java
), ),
PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT

@ -4,12 +4,12 @@ import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -20,9 +20,10 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.CacheBook import io.legado.app.service.help.CacheBook
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import org.jetbrains.anko.toast import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -43,7 +44,7 @@ class CacheBookService : BaseService() {
@Volatile @Volatile
private var downloadingCount = 0 private var downloadingCount = 0
private var notificationContent = App.INSTANCE.getString(R.string.starting_download) private var notificationContent = appCtx.getString(R.string.starting_download)
private val notificationBuilder by lazy { private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
@ -95,7 +96,7 @@ class CacheBookService : BaseService() {
synchronized(this) { synchronized(this) {
book = bookMap[bookUrl] book = bookMap[bookUrl]
if (book == null) { if (book == null) {
book = App.db.bookDao.getBook(bookUrl) book = appDb.bookDao.getBook(bookUrl)
if (book == null) { if (book == null) {
removeDownload(bookUrl) removeDownload(bookUrl)
} }
@ -111,7 +112,7 @@ class CacheBookService : BaseService() {
synchronized(this) { synchronized(this) {
webBook = webBookMap[origin] webBook = webBookMap[origin]
if (webBook == null) { if (webBook == null) {
App.db.bookSourceDao.getBookSource(origin)?.let { appDb.bookSourceDao.getBookSource(origin)?.let {
webBook = WebBook(it) webBook = WebBook(it)
} }
if (webBook == null) { if (webBook == null) {
@ -128,12 +129,12 @@ class CacheBookService : BaseService() {
if (downloadMap.containsKey(bookUrl)) { if (downloadMap.containsKey(bookUrl)) {
notificationContent = getString(R.string.already_in_download) notificationContent = getString(R.string.already_in_download)
upNotification() upNotification()
toast(notificationContent) toastOnUi(notificationContent)
return return
} }
downloadCount[bookUrl] = DownloadCount() downloadCount[bookUrl] = DownloadCount()
execute { execute {
App.db.bookChapterDao.getChapterList(bookUrl, start, end).let { appDb.bookChapterDao.getChapterList(bookUrl, start, end).let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val chapters = CopyOnWriteArraySet<BookChapter>() val chapters = CopyOnWriteArraySet<BookChapter>()
chapters.addAll(it) chapters.addAll(it)

@ -2,12 +2,12 @@ package io.legado.app.service
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
@ -16,8 +16,8 @@ import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.CheckSource import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
@ -70,7 +70,7 @@ class CheckSourceService : BaseService() {
private fun check(ids: List<String>) { private fun check(ids: List<String>) {
if (allIds.isNotEmpty()) { if (allIds.isNotEmpty()) {
toast("已有书源在校验,等完成后再试") toastOnUi("已有书源在校验,等完成后再试")
return return
} }
tasks.clear() tasks.clear()
@ -97,7 +97,7 @@ class CheckSourceService : BaseService() {
execute { execute {
if (index < allIds.size) { if (index < allIds.size) {
val sourceUrl = allIds[index] val sourceUrl = allIds[index]
App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source -> appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
check(source) check(source)
} ?: onNext(sourceUrl, "") } ?: onNext(sourceUrl, "")
} }
@ -107,22 +107,36 @@ class CheckSourceService : BaseService() {
fun check(source: BookSource) { fun check(source: BookSource) {
execute(context = searchCoroutine) { execute(context = searchCoroutine) {
val webBook = WebBook(source) val webBook = WebBook(source)
val books = webBook.searchBookAwait(this, CheckSource.keyword) var books = webBook.searchBookAwait(this, CheckSource.keyword)
if (books.isEmpty()) {
val exs = source.getExploreKinds()
if (exs.isEmpty()) {
throw Exception("搜索内容为空并且没有发现")
}
var url: String? = null
for (ex in exs) {
url = ex.url
if (!url.isNullOrBlank()) {
break
}
}
books = webBook.exploreBookAwait(this, url!!)
}
val book = webBook.getBookInfoAwait(this, books.first().toBook()) val book = webBook.getBookInfoAwait(this, books.first().toBook())
val toc = webBook.getChapterListAwait(this, book) val toc = webBook.getChapterListAwait(this, book)
val content = webBook.getContentAwait(this, book, toc.first()) val content = webBook.getContentAwait(this, book, toc.first())
if (content.isBlank()) { if (content.isBlank()) {
throw Exception("正文内容为空") throw Exception("正文内容为空")
} }
}.timeout(60000L) }.timeout(180000L)
.onError { .onError {
source.addGroup("失效") source.addGroup("失效")
source.bookSourceComment = source.bookSourceComment =
"error:${it.localizedMessage}\n${source.bookSourceComment}" "error:${it.localizedMessage}\n${source.bookSourceComment}"
App.db.bookSourceDao.update(source) appDb.bookSourceDao.update(source)
}.onSuccess { }.onSuccess {
source.removeGroup("失效") source.removeGroup("失效")
App.db.bookSourceDao.update(source) appDb.bookSourceDao.update(source)
}.onFinally { }.onFinally {
onNext(source.bookSourceUrl, source.bookSourceName) onNext(source.bookSourceUrl, source.bookSourceName)
} }

@ -20,8 +20,8 @@ import io.legado.app.constant.IntentAction
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.utils.RealPathUtil import io.legado.app.utils.RealPathUtil
import io.legado.app.utils.msg import io.legado.app.utils.msg
import org.jetbrains.anko.downloadManager import io.legado.app.utils.toastOnUi
import org.jetbrains.anko.toast import splitties.systemservices.downloadManager
import java.io.File import java.io.File
@ -149,7 +149,7 @@ class DownloadService : BaseService() {
try { try {
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
toast(e.msg) toastOnUi(e.msg)
} }
} }
} }

@ -3,6 +3,7 @@ package io.legado.app.service
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.media.MediaPlayer import android.media.MediaPlayer
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
@ -10,26 +11,23 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.FileUtils import io.legado.app.utils.*
import io.legado.app.utils.LogUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.postEvent
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import org.jetbrains.anko.collections.forEachWithIndex
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.* import java.util.*
class HttpReadAloudService : BaseReadAloudService(), class HttpReadAloudService : BaseReadAloudService(),
MediaPlayer.OnPreparedListener, MediaPlayer.OnPreparedListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener { MediaPlayer.OnCompletionListener {
private val mediaPlayer = MediaPlayer() private val player by lazy { MediaPlayer() }
private lateinit var ttsFolder: String private lateinit var ttsFolder: String
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
private var playingIndex = -1 private var playingIndex = -1
@ -37,9 +35,9 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS" ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS"
mediaPlayer.setOnErrorListener(this) player.setOnErrorListener(this)
mediaPlayer.setOnPreparedListener(this) player.setOnPreparedListener(this)
mediaPlayer.setOnCompletionListener(this) player.setOnCompletionListener(this)
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
@ -50,11 +48,11 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
task?.cancel() task?.cancel()
mediaPlayer.release() player.release()
} }
override fun newReadAloud(dataKey: String?, play: Boolean) { override fun newReadAloud(dataKey: String?, play: Boolean) {
mediaPlayer.reset() player.reset()
playingIndex = -1 playingIndex = -1
super.newReadAloud(dataKey, play) super.newReadAloud(dataKey, play)
} }
@ -81,7 +79,7 @@ class HttpReadAloudService : BaseReadAloudService(),
task = execute { task = execute {
removeCacheFile() removeCacheFile()
ReadAloud.httpTTS?.let { ReadAloud.httpTTS?.let {
contentList.forEachWithIndex { index, item -> contentList.forEachIndexed { index, item ->
if (isActive) { if (isActive) {
val fileName = val fileName =
md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item) md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item)
@ -100,27 +98,37 @@ class HttpReadAloudService : BaseReadAloudService(),
try { try {
createSpeakCacheFile(fileName) createSpeakCacheFile(fileName)
AnalyzeUrl( AnalyzeUrl(
it.url, it.url,
speakText = item, speakText = item,
speakSpeed = AppConfig.ttsSpeechRate speakSpeed = AppConfig.ttsSpeechRate
).getByteArray().let { bytes -> ).getByteArray().let { bytes ->
ensureActive() ensureActive()
val file = getSpeakFileAsMd5(fileName)
val file = getSpeakFileAsMd5IfNotExist(fileName)
//val file = getSpeakFile(index) //val file = getSpeakFile(index)
file.writeBytes(bytes) file.writeBytes(bytes)
removeSpeakCacheFile(fileName) removeSpeakCacheFile(fileName)
val fis = FileInputStream(file)
if (index == nowSpeak) { if (index == nowSpeak) {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
val fis = FileInputStream(file)
playAudio(fis.fd) playAudio(fis.fd)
} }
} }
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
removeSpeakCacheFile(fileName) removeSpeakCacheFile(fileName)
// delay(2000) toastOnUi("tts接口超时,尝试重新获取")
// downloadAudio() downloadAudio()
} catch (e: ConnectException) { } catch (e: ConnectException) {
removeSpeakCacheFile(fileName) removeSpeakCacheFile(fileName)
toastOnUi("网络错误")
} catch (e: IOException) {
val file = getSpeakFileAsMd5(fileName)
if (file.exists()) {
FileUtils.deleteFile(file.absolutePath)
}
toastOnUi("tts文件解析错误")
} catch (e: Exception) { } catch (e: Exception) {
removeSpeakCacheFile(fileName) removeSpeakCacheFile(fileName)
} }
@ -135,9 +143,9 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun playAudio(fd: FileDescriptor) { private fun playAudio(fd: FileDescriptor) {
if (playingIndex != nowSpeak && requestFocus()) { if (playingIndex != nowSpeak && requestFocus()) {
try { try {
mediaPlayer.reset() player.reset()
mediaPlayer.setDataSource(fd) player.setDataSource(fd)
mediaPlayer.prepareAsync() player.prepareAsync()
playingIndex = nowSpeak playingIndex = nowSpeak
postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1)
} catch (e: Exception) { } catch (e: Exception) {
@ -152,20 +160,23 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun hasSpeakFile(name: String) = private fun hasSpeakFile(name: String) =
FileUtils.exist("${speakFilePath()}$name.mp3") FileUtils.exist("${speakFilePath()}$name.mp3")
private fun hasSpeakCacheFile(name: String) = private fun hasSpeakCacheFile(name: String) =
FileUtils.exist("${speakFilePath()}$name.mp3.cache") FileUtils.exist("${speakFilePath()}$name.mp3.cache")
private fun createSpeakCacheFile(name: String): File = private fun createSpeakCacheFile(name: String): File =
FileUtils.createFileWithReplace("${speakFilePath()}$name.mp3.cache") FileUtils.createFileWithReplace("${speakFilePath()}$name.mp3.cache")
private fun removeSpeakCacheFile(name: String) { private fun removeSpeakCacheFile(name: String) {
FileUtils.delete("${speakFilePath()}$name.mp3.cache") FileUtils.delete("${speakFilePath()}$name.mp3.cache")
} }
private fun getSpeakFileAsMd5(name: String): File = private fun getSpeakFileAsMd5(name: String): File =
FileUtils.createFileIfNotExist("${speakFilePath()}$name.mp3") FileUtils.getFile(File(speakFilePath()), "$name.mp3")
private fun getSpeakFileAsMd5IfNotExist(name: String): File =
FileUtils.createFileIfNotExist("${speakFilePath()}$name.mp3")
private fun removeCacheFile() { private fun removeCacheFile() {
FileUtils.listDirsAndFiles(speakFilePath())?.forEach { FileUtils.listDirsAndFiles(speakFilePath())?.forEach {
@ -174,7 +185,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
if (Regex(""".+\.mp3$""").matches(it.name)) { //mp3缓存文件 if (Regex(""".+\.mp3$""").matches(it.name)) { //mp3缓存文件
val reg = val reg =
"""^${MD5Utils.md5Encode16(textChapter!!.title)}_[a-z0-9]{16}\.mp3$""".toRegex() """^${MD5Utils.md5Encode16(textChapter!!.title)}_[a-z0-9]{16}\.mp3$""".toRegex()
if (!reg.matches(it.name)) { if (!reg.matches(it.name)) {
FileUtils.deleteFile(it.absolutePath) FileUtils.deleteFile(it.absolutePath)
} }
@ -189,7 +200,7 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun pauseReadAloud(pause: Boolean) { override fun pauseReadAloud(pause: Boolean) {
super.pauseReadAloud(pause) super.pauseReadAloud(pause)
mediaPlayer.pause() player.pause()
} }
override fun resumeReadAloud() { override fun resumeReadAloud() {
@ -197,7 +208,7 @@ class HttpReadAloudService : BaseReadAloudService(),
if (playingIndex == -1) { if (playingIndex == -1) {
play() play()
} else { } else {
mediaPlayer.start() player.start()
} }
} }
@ -206,7 +217,7 @@ class HttpReadAloudService : BaseReadAloudService(),
*/ */
override fun upSpeechRate(reset: Boolean) { override fun upSpeechRate(reset: Boolean) {
task?.cancel() task?.cancel()
mediaPlayer.stop() player.stop()
playingIndex = -1 playingIndex = -1
downloadAudio() downloadAudio()
} }
@ -216,7 +227,7 @@ class HttpReadAloudService : BaseReadAloudService(),
*/ */
override fun prevP() { override fun prevP() {
if (nowSpeak > 0) { if (nowSpeak > 0) {
mediaPlayer.stop() player.stop()
nowSpeak-- nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1) readAloudNumber -= contentList[nowSpeak].length.minus(1)
play() play()
@ -228,7 +239,7 @@ class HttpReadAloudService : BaseReadAloudService(),
*/ */
override fun nextP() { override fun nextP() {
if (nowSpeak < contentList.size - 1) { if (nowSpeak < contentList.size - 1) {
mediaPlayer.stop() player.stop()
readAloudNumber += contentList[nowSpeak].length.plus(1) readAloudNumber += contentList[nowSpeak].length.plus(1)
nowSpeak++ nowSpeak++
play() play()

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

Loading…
Cancel
Save