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:
branches:
- master
# tags:
# - '3.*'
# pull_request:
# branches:
# - master
# watch:
# types: [started]
# schedule:
@ -25,16 +20,21 @@ jobs:
# LANZOU_PSD : pc.woozooo.com -> Cookie -> phpdisk_info
LANZOU_ID: ${{ secrets.LANZOU_ID }}
LANZOU_PSD: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹名(需手动在蓝奏云创建
LANZOU_FOLDER: '阅读3测试版'
# 蓝奏云里的文件夹ID(阅读3测试版:2670621
LANZOU_FOLDER_ID: '2670621'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v2
- name: Set Up JDK
uses: actions/setup-java@v1
- uses: actions/cache@v2
with:
java-version: 14
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt
run: |
echo "清空18PlusList.txt"
@ -56,35 +56,31 @@ jobs:
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease
./gradlew assembleAppRelease --build-cache --parallel
- name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk
- name: Set Up Python 3
if: ${{ env.LANZOU_PSD }}
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Dependence Install
if: ${{ env.LANZOU_PSD }}
run: |
pip install requests
pip install lanzou-api
- name: Download Script
if: ${{ env.LANZOU_PSD }}
shell: python
run: |
import requests,os
res = requests.get('https://raw.githubusercontent.com/Celeter/build/master/.github/scripts/API_lanzou.py')
with open(os.environ["FILE_PATH"], 'wb') as f:
f.write(res.content)
env:
FILE_PATH: ${{ github.workspace }}/.github/lzy.py
- name: Upload App To Lanzou
if: ${{ env.LANZOU_PSD }}
run: python $GITHUB_WORKSPACE/.github/lzy.py
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:
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'
//androidX
implementation 'androidx.core:core-ktx:1.3.2'
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.collection:collection-ktx:1.1.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
@ -136,6 +138,18 @@ dependencies {
implementation 'com.google.android:flexbox:1.1.0'
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
def lifecycle_version = '2.2.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@ -150,11 +164,6 @@ dependencies {
//paging
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
implementation 'com.jeremyliao:live-event-bus-x:1.5.7'
@ -185,7 +194,7 @@ dependencies {
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'

@ -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"

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

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

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

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

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

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

@ -36,27 +36,22 @@ function Container() {
// 搜索规则
$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
//ruleJson.ruleSearch = JSON.stringify(searchJson);
ruleJson.ruleSearch = searchJson;
// 发现规则
$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
//ruleJson.ruleExplore = JSON.stringify(exploreJson);
ruleJson.ruleExplore = exploreJson;
// 详情页规则
$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
//ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson);
ruleJson.ruleBookInfo = bookInfoJson;
// 目录规则
$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
//ruleJson.ruleToc = JSON.stringify(tocJson);
ruleJson.ruleToc = tocJson;
// 正文规则
$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
//ruleJson.ruleContent = JSON.stringify(contentJson);
ruleJson.ruleContent = contentJson;
return ruleJson;
@ -116,55 +111,45 @@ function rule2json() {
// 转换搜索规则
let searchJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
if ($('#' + 'ruleSearch_' + key).value)
searchJson[key] = $('#' + 'ruleSearch_' + key).value;
});
//RuleJSON.ruleSearch = JSON.stringify(searchJson);
RuleJSON.ruleSearch = searchJson;
// 转换发现规则
let exploreJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
Object.keys(RuleJSON.ruleExplore).forEach(key => {
if ($('#' + 'ruleExplore_' + key).value)
exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
});
//RuleJSON.ruleExplore = JSON.stringify(exploreJson);
RuleJSON.ruleExplore = exploreJson;
// 转换详情页规则
let bookInfoJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
if ($('#' + 'ruleBookInfo_' + key).value)
bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
});
//RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson);
RuleJSON.ruleBookInfo = bookInfoJson;
// 转换目录规则
let tocJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
Object.keys(RuleJSON.ruleToc).forEach(key => {
if ($('#' + 'ruleToc_' + key).value)
tocJson[key] = $('#' + 'ruleToc_' + key).value;
});
//RuleJSON.ruleToc = JSON.stringify(tocJson);
RuleJSON.ruleToc = tocJson;
// 转换正文规则
let contentJson = {};
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
Object.keys(RuleJSON.ruleContent).forEach(key => {
if ($('#' + 'ruleContent_' + key).value)
contentJson[key] = $('#' + 'ruleContent_' + key).value;
});
//RuleJSON.ruleContent = JSON.stringify(contentJson);
RuleJSON.ruleContent = contentJson;
RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime);
RuleJSON.lastUpdateTime = new Date().getTime();
RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.weight);
@ -193,9 +178,7 @@ function json2rule(RuleEditor) {
// 转换搜索规则
if (RuleEditor.ruleSearch) {
//let searchJson = JSON.parse(RuleEditor.ruleSearch);
let searchJson = RuleEditor.ruleSearch;
//Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => {
Object.keys(RuleJSON.ruleSearch).forEach(key => {
$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
});
@ -203,8 +186,6 @@ function json2rule(RuleEditor) {
// 转换发现规则
if (RuleEditor.ruleExplore) {
//let exploreJson = JSON.parse(RuleEditor.ruleExplore);
//Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => {
let exploreJson = RuleEditor.ruleExplore;
Object.keys(RuleJSON.ruleExplore).forEach(key => {
$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
@ -213,8 +194,6 @@ function json2rule(RuleEditor) {
// 转换详情页规则
if (RuleEditor.ruleBookInfo) {
//let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo);
//Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => {
let bookInfoJson = RuleEditor.ruleBookInfo;
Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
@ -223,8 +202,6 @@ function json2rule(RuleEditor) {
// 转换目录规则
if (RuleEditor.ruleToc) {
//let tocJson = JSON.parse(RuleEditor.ruleToc);
//Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => {
let tocJson = RuleEditor.ruleToc;
Object.keys(RuleJSON.ruleToc).forEach(key => {
$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
@ -233,8 +210,6 @@ function json2rule(RuleEditor) {
// 转换正文规则
if (RuleEditor.ruleContent) {
//let contentJson = JSON.parse(RuleEditor.ruleContent);
//Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => {
let contentJson = RuleEditor.ruleContent;
Object.keys(RuleJSON.ruleContent).forEach(key => {
$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
@ -538,4 +513,4 @@ $('.tab3>.titlebar').addEventListener('click', e => {
break;
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;
}</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([
{
"revision": "414c7503fa98c3f2e328",
"revision": "12386c7dce7d463af4ed",
"url": "css/about.dbe575e1.css"
},
{
"revision": "13925596cfa342cbb55d",
"revision": "d761b0dee902b1105e91",
"url": "css/app.e4c919b7.css"
},
{
"revision": "d53f06b957b78ddb78b7",
"revision": "16ab934214be8df372e3",
"url": "css/chunk-vendors.ad4ff18f.css"
},
{
"revision": "0607c99f49945c348397",
"revision": "4083fbb292940cf804ca",
"url": "css/detail.b4324411.css"
},
{
@ -40,28 +40,28 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "img/noCover.b5c48bc1.jpeg"
},
{
"revision": "8ff165c058a9618dc10065e65133a4e7",
"revision": "bc7efb33b2dbae4d4578e3657e73de4b",
"url": "index.html"
},
{
"revision": "414c7503fa98c3f2e328",
"url": "js/about.d8b0dc91.js"
"revision": "12386c7dce7d463af4ed",
"url": "js/about.4300b5ad.js"
},
{
"revision": "2c81bd893f3a92f018d8",
"url": "js/about~detail.1caf6ef5.js"
"revision": "2828d7869f38c2483b33",
"url": "js/about~detail.c1b29cbc.js"
},
{
"revision": "13925596cfa342cbb55d",
"url": "js/app.d60dad33.js"
"revision": "d761b0dee902b1105e91",
"url": "js/app.acf50eba.js"
},
{
"revision": "d53f06b957b78ddb78b7",
"url": "js/chunk-vendors.060eee33.js"
"revision": "16ab934214be8df372e3",
"url": "js/chunk-vendors.1042cf7f.js"
},
{
"revision": "0607c99f49945c348397",
"url": "js/detail.6a5cc86c.js"
"revision": "4083fbb292940cf804ca",
"url": "js/detail.4d0609e7.js"
},
{
"revision": "b46d04eb43bc31ca0f9f95121646440d",

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

@ -5,58 +5,33 @@ import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.constant.EventBus
import io.legado.app.data.AppDatabase
import io.legado.app.help.*
import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler
import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.HttpHelper
import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.postEvent
import org.jetbrains.anko.defaultSharedPreferences
import io.legado.app.utils.defaultSharedPreferences
import rxhttp.wrapper.param.RxHttp
@Suppress("DEPRECATION")
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() {
super.onCreate()
INSTANCE = this
androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID)
CrashHandler(this)
LanguageUtils.setConfiguration(this)
db = AppDatabase.createDatabase(INSTANCE)
RxHttp.init(HttpHelper.client, BuildConfig.DEBUG)
RxHttp.setOnParamAssembly {
it.addHeader(AppConst.UA_NAME, AppConfig.userAgent)
}
packageManager.getPackageInfo(packageName, 0)?.let {
versionCode = it.versionCode
versionName = it.versionName
}
createNotificationChannels()
applyDayNight()
applyDayNight(this)
LiveEventBus.config()
.supportBroadcast(this)
.lifecycleObserverAlwaysActive(true)
@ -69,27 +44,10 @@ class App : MultiDexApplication() {
super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES,
Configuration.UI_MODE_NIGHT_NO -> applyDayNight()
Configuration.UI_MODE_NIGHT_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
*/
@ -99,7 +57,7 @@ class App : MultiDexApplication() {
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.action_download),
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(false)
enableVibration(false)
@ -109,7 +67,7 @@ class App : MultiDexApplication() {
val readAloudChannel = NotificationChannel(
channelIdReadAloud,
getString(R.string.read_aloud),
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(false)
enableVibration(false)
@ -119,7 +77,7 @@ class App : MultiDexApplication() {
val webChannel = NotificationChannel(
channelIdWeb,
getString(R.string.web_service),
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(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
import io.legado.app.App
import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
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.getPrefInt
import kotlinx.coroutines.runBlocking
import splitties.init.appCtx
object BookshelfController {
val bookshelf: ReturnData
get() {
val books = App.db.bookDao.all
val books = appDb.bookDao.all
val returnData = ReturnData()
return if (books.isEmpty()) {
returnData.setErrorMsg("还没有添加小说")
} else {
val data = when (App.INSTANCE.getPrefInt(PreferKey.bookshelfSort)) {
val data = when (appCtx.getPrefInt(PreferKey.bookshelfSort)) {
1 -> books.sortedByDescending { it.latestChapterTime }
2 -> books.sortedWith { o1, o2 ->
o1.name.cnCompare(o2.name)
@ -40,7 +41,7 @@ object BookshelfController {
if (bookUrl.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
}
val chapterList = App.db.bookChapterDao.getChapterList(bookUrl)
val chapterList = appDb.bookChapterDao.getChapterList(bookUrl)
return returnData.setData(chapterList)
}
@ -54,8 +55,8 @@ object BookshelfController {
if (index == null) {
return returnData.setErrorMsg("参数index不能为空, 请指定目录序号")
}
val book = App.db.bookDao.getBook(bookUrl)
val chapter = App.db.bookChapterDao.getChapter(bookUrl, index)
val book = appDb.bookDao.getBook(bookUrl)
val chapter = appDb.bookChapterDao.getChapter(bookUrl, index)
if (book == null || chapter == null) {
returnData.setErrorMsg("未找到")
} else {
@ -64,7 +65,7 @@ object BookshelfController {
saveBookReadIndex(book, index)
returnData.setData(content)
} else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { source ->
appDb.bookSourceDao.getBookSource(book.origin)?.let { source ->
runBlocking {
WebBook(source).getContentAwait(this, book, chapter)
}.let {
@ -81,7 +82,7 @@ object BookshelfController {
val book = GSON.fromJsonObject<Book>(postData)
val returnData = ReturnData()
if (book != null) {
App.db.bookDao.insert(book)
appDb.bookDao.insert(book)
if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.book = book
ReadBook.durChapterIndex = book.durChapterIndex
@ -95,10 +96,10 @@ object BookshelfController {
if (index > book.durChapterIndex) {
book.durChapterIndex = index
book.durChapterTime = System.currentTimeMillis()
App.db.bookChapterDao.getChapter(book.bookUrl, index)?.let {
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
book.durChapterTitle = it.title
}
App.db.bookDao.update(book)
appDb.bookDao.update(book)
if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.book = book
ReadBook.durChapterIndex = index

@ -2,8 +2,8 @@ package io.legado.app.api.controller
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.api.ReturnData
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
@ -14,7 +14,7 @@ object SourceController {
val sources: ReturnData
get() {
val bookSources = App.db.bookSourceDao.all
val bookSources = appDb.bookSourceDao.all
val returnData = ReturnData()
return if (bookSources.isEmpty()) {
returnData.setErrorMsg("设备书源列表为空")
@ -29,7 +29,7 @@ object SourceController {
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
returnData.setErrorMsg("书源名称和URL不能为空")
} else {
App.db.bookSourceDao.insert(bookSource)
appDb.bookSourceDao.insert(bookSource)
returnData.setData("")
}
} else {
@ -50,7 +50,7 @@ object SourceController {
if (bookSource.bookSourceName.isBlank() || bookSource.bookSourceUrl.isBlank()) {
continue
}
App.db.bookSourceDao.insert(bookSource)
appDb.bookSourceDao.insert(bookSource)
okSources.add(bookSource)
}
}
@ -64,7 +64,7 @@ object SourceController {
if (url.isNullOrEmpty()) {
return returnData.setErrorMsg("参数url不能为空,请指定书源地址")
}
val bookSource = App.db.bookSourceDao.getBookSource(url)
val bookSource = appDb.bookSourceDao.getBookSource(url)
?: return returnData.setErrorMsg("未找到书源,请检查书源地址")
return returnData.setData(bookSource)
}
@ -73,7 +73,7 @@ object SourceController {
kotlin.runCatching {
GSON.fromJsonArray<BookSource>(postData)?.let {
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.Theme
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.ThemeStore
import io.legado.app.lib.theme.backgroundColor
@ -151,6 +152,11 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView)
}
}
if (AppConfig.isGooglePlay) {
ThemeConfig.getBgImage(this)?.let {
window.decorView.background = it
}
}
}
private fun setupSystemBar() {

@ -6,15 +6,13 @@ import androidx.annotation.CallSuper
import androidx.lifecycle.AndroidViewModel
import io.legado.app.App
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.*
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
@Suppress("unused")
open class BaseViewModel(application: Application) : AndroidViewModel(application),
CoroutineScope by MainScope(),
AnkoLogger {
CoroutineScope by MainScope() {
val context: Context by lazy { this.getApplication<App>() }
@ -40,27 +38,12 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio
cancel()
}
open fun toast(message: Int) {
launch {
context.toast(message)
}
open fun toastOnUi(message: Int) {
context.toastOnUi(message)
}
open fun toast(message: CharSequence?) {
launch {
context.toast(message ?: toString())
}
open fun toastOnUi(message: CharSequence?) {
context.toastOnUi(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.RecyclerView
import androidx.viewbinding.ViewBinding
import splitties.views.onLongClick
/**
* Created by Invincible on 2017/12/15.
@ -130,10 +131,10 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
}
if (itemLongClickListener != null) {
holder.itemView.setOnLongClickListener {
holder.itemView.onLongClick {
getItem(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it) ?: true
} ?: true
itemLongClickListener?.invoke(holder, it)
}
}
}

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

@ -1,8 +1,8 @@
package io.legado.app.constant
import android.annotation.SuppressLint
import io.legado.app.App
import io.legado.app.R
import splitties.init.appCtx
import java.text.SimpleDateFormat
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
@ -68,5 +68,12 @@ object AppConst {
"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 webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir"
const val webDavCacheBackup = "webDavCacheBackup"
const val exportToWebDav = "webDavCacheBackup"
const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType"
@ -65,6 +65,11 @@ object PreferKey {
const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress"
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 cAccent = "colorAccent"

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

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

@ -19,7 +19,7 @@ interface BookGroupDao {
@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 = -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)

@ -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 androidx.room.*
import io.legado.app.App
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.help.AppConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON
@ -146,6 +146,31 @@ data class Book(
config().pageAnim = pageAnim
}
fun getImageStyle(): String? {
return config().imageStyle
}
fun setImageStyle(imageStyle: String?) {
config().imageStyle = imageStyle
}
fun getDelParagraph(): Int {
return config().delParagraph
}
fun setDelParagraph(num: Int) {
config().delParagraph = num
}
fun setDelTag(tag: Long) {
config().delTag =
if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag
}
fun getDelTag(tag: Long): Boolean {
return config().delTag and tag == tag
}
fun getFolderName(): String {
//防止书名过长,只取9位
var folderName = name.replace(AppPattern.fileNameRegex, "")
@ -182,14 +207,14 @@ data class Book(
newBook.canUpdate = canUpdate
newBook.readConfig = readConfig
delete()
App.db.bookDao.insert(newBook)
appDb.bookDao.insert(newBook)
}
fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null
}
App.db.bookDao.delete(this)
appDb.bookDao.delete(this)
}
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
data class ReadConfig(
var pageAnim: Int = -1,
var reSegment: Boolean = false,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则
var imageStyle: String? = null,
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则
var delParagraph: Int = 0,//去除段首
var delTag: Long = 0L,//去除标签
) : Parcelable
class Converters {

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

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

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

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

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

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

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

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

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

@ -1,11 +1,12 @@
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.RssSource
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import splitties.init.appCtx
import java.io.File
object DefaultData {
@ -16,7 +17,7 @@ object DefaultData {
val httpTTS by lazy {
val json =
String(
App.INSTANCE.assets.open("defaultData${File.separator}$httpTtsFileName")
appCtx.assets.open("defaultData${File.separator}$httpTtsFileName")
.readBytes()
)
GSON.fromJsonArray<HttpTTS>(json)!!
@ -24,7 +25,7 @@ object DefaultData {
val readConfigs by lazy {
val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}")
appCtx.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}")
.readBytes()
)
GSON.fromJsonArray<ReadBookConfig.Config>(json)!!
@ -32,7 +33,7 @@ object DefaultData {
val txtTocRules by lazy {
val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}$txtTocRuleFileName")
appCtx.assets.open("defaultData${File.separator}$txtTocRuleFileName")
.readBytes()
)
GSON.fromJsonArray<TxtTocRule>(json)!!
@ -40,7 +41,7 @@ object DefaultData {
val themeConfigs by lazy {
val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}")
appCtx.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}")
.readBytes()
)
GSON.fromJsonArray<ThemeConfig.Config>(json)!!
@ -48,23 +49,23 @@ object DefaultData {
val rssSources by lazy {
val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}rssSources.json")
appCtx.assets.open("defaultData${File.separator}rssSources.json")
.readBytes()
)
GSON.fromJsonArray<RssSource>(json)!!
}
fun importDefaultHttpTTS() {
App.db.httpTTSDao.deleteDefault()
App.db.httpTTSDao.insert(*httpTTS.toTypedArray())
appDb.httpTTSDao.deleteDefault()
appDb.httpTTSDao.insert(*httpTTS.toTypedArray())
}
fun importDefaultTocRules() {
App.db.txtTocRule.deleteDefault()
App.db.txtTocRule.insert(*txtTocRules.toTypedArray())
appDb.txtTocRuleDao.deleteDefault()
appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray())
}
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.os.Bundle
import io.legado.app.R
import org.jetbrains.anko.toast
import io.legado.app.utils.toastOnUi
@Suppress("unused")
object IntentHelp {
@ -19,7 +19,7 @@ object IntentHelp {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}.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
context.startActivity(intent)
}.onFailure {
context.toast("无法打开设置")
context.toastOnUi("无法打开设置")
}
}

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

@ -3,30 +3,30 @@ package io.legado.app.help
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import io.legado.app.App
import io.legado.app.R
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.
* 更换图标
*/
object LauncherIconHelp {
private val packageManager: PackageManager = App.INSTANCE.packageManager
private val packageManager: PackageManager = appCtx.packageManager
private val componentNames = arrayListOf(
ComponentName(App.INSTANCE, Launcher1::class.java.name),
ComponentName(App.INSTANCE, Launcher2::class.java.name),
ComponentName(App.INSTANCE, Launcher3::class.java.name),
ComponentName(App.INSTANCE, Launcher4::class.java.name),
ComponentName(App.INSTANCE, Launcher5::class.java.name),
ComponentName(App.INSTANCE, Launcher6::class.java.name)
ComponentName(appCtx, Launcher1::class.java.name),
ComponentName(appCtx, Launcher2::class.java.name),
ComponentName(appCtx, Launcher3::class.java.name),
ComponentName(appCtx, Launcher4::class.java.name),
ComponentName(appCtx, Launcher5::class.java.name),
ComponentName(appCtx, Launcher6::class.java.name)
)
fun changeIcon(icon: String?) {
if (icon.isNullOrEmpty()) return
if (Build.VERSION.SDK_INT < 26) {
App.INSTANCE.toast(R.string.change_icon_error)
appCtx.toastOnUi(R.string.change_icon_error)
return
}
var hasEnabled = false
@ -50,13 +50,13 @@ object LauncherIconHelp {
}
if (hasEnabled) {
packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name),
ComponentName(appCtx, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
} else {
packageManager.setComponentEnabledSetting(
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name),
ComponentName(appCtx, WelcomeActivity::class.java.name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)

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

@ -1,11 +1,11 @@
package io.legado.app.help
import io.legado.app.App
import io.legado.app.R
import splitties.init.appCtx
object ReadTipConfig {
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 {
linkedMapOf(0 to "状态栏显示时隐藏", 1 to "显示", 2 to "隐藏")

@ -2,20 +2,21 @@ package io.legado.app.help
import android.os.Handler
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.RssSource
import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.splitNotBlank
import org.jetbrains.anko.toast
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
object SourceHelp {
private val handler = Handler(Looper.getMainLooper())
private val list18Plus by lazy {
try {
return@lazy String(App.INSTANCE.assets.open("18PlusList.txt").readBytes())
return@lazy String(appCtx.assets.open("18PlusList.txt").readBytes())
.splitNotBlank("\n")
} catch (e: Exception) {
return@lazy arrayOf<String>()
@ -26,10 +27,10 @@ object SourceHelp {
rssSources.forEach { rssSource ->
if (is18Plus(rssSource.sourceUrl)) {
handler.post {
App.INSTANCE.toast("${rssSource.sourceName}是18+网址,禁止导入.")
appCtx.toastOnUi("${rssSource.sourceName}是18+网址,禁止导入.")
}
} else {
App.db.rssSourceDao.insert(rssSource)
appDb.rssSourceDao.insert(rssSource)
}
}
}
@ -38,10 +39,10 @@ object SourceHelp {
bookSources.forEach { bookSource ->
if (is18Plus(bookSource.bookSourceUrl)) {
handler.post {
App.INSTANCE.toast("${bookSource.bookSourceName}是18+网址,禁止导入.")
appCtx.toastOnUi("${bookSource.bookSourceName}是18+网址,禁止导入.")
}
} 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.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.annotation.Keep
import io.legado.app.App
import androidx.appcompat.app.AppCompatDelegate
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File
object ThemeConfig {
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 cList = getConfigs() ?: DefaultData.themeConfigs
ArrayList(cList)
}
fun upConfig() {
getConfigs()?.let {
it.forEach { config ->
addConfig(config)
fun applyDayNight(context: Context) {
ReadBookConfig.upBg()
applyTheme(context)
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)
}
AppConfig.isNightTheme = config.isNightTheme
App.INSTANCE.applyDayNight()
postEvent(EventBus.RECREATE, "")
applyDayNight(context)
}
fun saveDayTheme(context: Context, name: String) {
@ -204,7 +232,6 @@ object ThemeConfig {
var primaryColor: String,
var accentColor: String,
var backgroundColor: String,
var backgroundImage: String? = null,
var bottomBackground: String
)

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

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

@ -3,7 +3,7 @@
package io.legado.app.help.http
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.help.http.api.CookieManager
import io.legado.app.utils.NetworkUtils
@ -12,7 +12,7 @@ object CookieStore : CookieManager {
override fun setCookie(url: String, cookie: String?) {
val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "")
App.db.cookieDao.insert(cookieBean)
appDb.cookieDao.insert(cookieBean)
}
override fun replaceCookie(url: String, cookie: String) {
@ -31,12 +31,12 @@ object CookieStore : CookieManager {
}
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 ?: ""
}
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> {
@ -77,7 +77,7 @@ object CookieStore : CookieManager {
}
fun clear() {
App.db.cookieDao.deleteOkHttp()
appDb.cookieDao.deleteOkHttp()
}
}

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

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

@ -3,8 +3,8 @@ package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.help.DefaultData
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ThemeConfig
@ -12,7 +12,7 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences
import splitties.init.appCtx
import java.io.File
import java.util.concurrent.TimeUnit
@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit
object Backup {
val backupPath: String by lazy {
FileUtils.getFile(App.INSTANCE.filesDir, "backup").absolutePath
FileUtils.getFile(appCtx.filesDir, "backup").absolutePath
}
val backupFileNames by lazy {
@ -57,32 +57,33 @@ object Backup {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) {
FileUtils.deleteFile(backupPath)
writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao.all, "rssSources.json", backupPath)
writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath)
writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath)
writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath)
writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath)
writeListToJson(appDb.bookDao.all, "bookshelf.json", backupPath)
writeListToJson(appDb.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(appDb.bookGroupDao.all, "bookGroup.json", backupPath)
writeListToJson(appDb.bookSourceDao.all, "bookSource.json", backupPath)
writeListToJson(appDb.rssSourceDao.all, "rssSources.json", backupPath)
writeListToJson(appDb.rssStarDao.all, "rssStar.json", backupPath)
writeListToJson(appDb.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath)
writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath)
writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath)
writeListToJson(appDb.txtTocRuleDao.all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(appDb.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath)
GSON.toJson(ReadBookConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
.writeText(it)
}
GSON.toJson(ReadBookConfig.shareConfig).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
.writeText(it)
}
GSON.toJson(ThemeConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName)
.writeText(it)
}
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
Preferences.getSharedPreferences(appCtx, backupPath, "config")?.let { sp ->
val edit = sp.edit()
App.INSTANCE.defaultSharedPreferences.all.map {
appCtx.defaultSharedPreferences.all.map {
when (val value = it.value) {
is Int -> edit.putInt(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.os.Handler
import android.os.Looper
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.PreferKey
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.Main
import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast
import splitties.init.appCtx
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@ -28,20 +27,20 @@ object BookWebDav {
private val rootWebDavUrl: String
get() {
var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl)
var url = appCtx.getPrefString(PreferKey.webDavUrl)
if (url.isNullOrEmpty()) {
url = defaultWebDavUrl
}
if (!url.endsWith("/")) url = "${url}/"
if (App.INSTANCE.getPrefBoolean(PreferKey.webDavCreateDir, true)) {
if (appCtx.getPrefBoolean(PreferKey.webDavCreateDir, true)) {
url = "${url}legado/"
}
return url
}
suspend fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
private suspend fun initWebDav(): Boolean {
val account = appCtx.getPrefString(PreferKey.webDavAccount)
val password = appCtx.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password)
WebDav(rootWebDavUrl).makeAsDir()
@ -80,7 +79,7 @@ object BookWebDav {
Coroutine.async {
restoreWebDav(names[index])
}.onError {
App.INSTANCE.toast("WebDavError:${it.localizedMessage}")
appCtx.toastOnUi("WebDavError:${it.localizedMessage}")
}
}
}
@ -118,28 +117,25 @@ object BookWebDav {
}
} catch (e: Exception) {
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 {
if (initWebDav()) {
// 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话
WebDav(exportsWebDavUrl).makeAsDir()
val file = File("${path}${File.separator}${fileName}")
// 如果导出的本地文件存在,开始上传
if (file.exists()) {
val putUrl = exportsWebDavUrl + fileName
WebDav(putUrl).upload("${path}${File.separator}${fileName}")
}
val putUrl = exportsWebDavUrl + fileName
WebDav(putUrl).upload(byteArray)
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
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.net.Uri
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.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.isContentScheme
import org.jetbrains.anko.toast
import io.legado.app.utils.toastOnUi
import java.io.File
object ImportOldData {
@ -21,28 +21,28 @@ object ImportOldData {
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}")
context.toastOnUi("成功导入书籍${importCount}")
}
}.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}")
context.toastOnUi("导入书籍失败\n${it.localizedMessage}")
}
"myBookSource.json" ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}")
context.toastOnUi("成功导入书源${importCount}")
}
}.onFailure {
context.toast("导入源失败\n${it.localizedMessage}")
context.toastOnUi("导入源失败\n${it.localizedMessage}")
}
"myBookReplaceRule.json" ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}")
context.toastOnUi("成功导入替换规则${importCount}")
}
}.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}")
context.toastOnUi("导入替换规则失败\n${it.localizedMessage}")
}
}
}
@ -54,9 +54,9 @@ object ImportOldData {
FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}")
context.toastOnUi("成功导入书籍${importCount}")
}.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}")
context.toastOnUi("导入书籍失败\n${it.localizedMessage}")
}
kotlin.runCatching {// Book source
@ -64,9 +64,9 @@ object ImportOldData {
FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}")
context.toastOnUi("成功导入书源${importCount}")
}.onFailure {
context.toast("导入源失败\n${it.localizedMessage}")
context.toastOnUi("导入源失败\n${it.localizedMessage}")
}
kotlin.runCatching {// Replace rules
@ -74,12 +74,12 @@ object ImportOldData {
if (ruleFile.exists()) {
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}")
context.toastOnUi("成功导入替换规则${importCount}")
} else {
context.toast("未找到替换规则")
context.toastOnUi("未找到替换规则")
}
}.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}")
context.toastOnUi("导入替换规则失败\n${it.localizedMessage}")
}
}
}
@ -87,7 +87,7 @@ object ImportOldData {
private fun importOldBookshelf(json: String): Int {
val books = OldBook.toNewBook(json)
App.db.bookDao.insert(*books.toTypedArray())
appDb.bookDao.insert(*books.toTypedArray())
return books.size
}
@ -100,13 +100,13 @@ object ImportOldData {
bookSources.add(it)
}
}
App.db.bookSourceDao.insert(*bookSources.toTypedArray())
appDb.bookSourceDao.insert(*bookSources.toTypedArray())
return bookSources.size
}
private fun importOldReplaceRule(json: String): Int {
val rules = OldReplace.jsonToReplaceRules(json)
App.db.replaceRuleDao.insert(*rules.toTypedArray())
appDb.replaceRuleDao.insert(*rules.toTypedArray())
return rules.size
}
}

@ -1,8 +1,8 @@
package io.legado.app.help.storage
import android.util.Log
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.utils.readBool
import io.legado.app.utils.readInt
@ -14,7 +14,7 @@ object OldBook {
fun toNewBook(json: String): List<Book> {
val books = mutableListOf<Book>()
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) {
val jsonItem = Restore.jsonPath.parse(item)
val book = Book()

@ -1,5 +1,7 @@
package io.legado.app.help.storage
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
@ -7,28 +9,29 @@ import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext
import io.legado.app.App
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.constant.androidId
import io.legado.app.data.appDb
import io.legado.app.data.entities.*
import io.legado.app.help.DefaultData
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ThemeConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.toast
import splitties.init.appCtx
import splitties.systemservices.alarmManager
import java.io.File
import kotlin.system.exitProcess
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 file = FileUtils.createFileIfNotExist(ignoreConfigPath)
val json = file.readText()
@ -46,11 +49,11 @@ object Restore {
//忽略标题
val ignoreTitle = arrayOf(
App.INSTANCE.getString(R.string.read_config),
App.INSTANCE.getString(R.string.theme_mode),
App.INSTANCE.getString(R.string.bookshelf_layout),
App.INSTANCE.getString(R.string.show_rss),
App.INSTANCE.getString(R.string.thread_count)
appCtx.getString(R.string.read_config),
appCtx.getString(R.string.theme_mode),
appCtx.getString(R.string.bookshelf_layout),
appCtx.getString(R.string.show_rss),
appCtx.getString(R.string.thread_count)
)
//默认忽略keys
@ -111,48 +114,48 @@ object Restore {
suspend fun restoreDatabase(path: String = Backup.backupPath) {
withContext(IO) {
fileToListT<Book>(path, "bookshelf.json")?.let {
App.db.bookDao.insert(*it.toTypedArray())
appDb.bookDao.insert(*it.toTypedArray())
}
fileToListT<Bookmark>(path, "bookmark.json")?.let {
App.db.bookmarkDao.insert(*it.toTypedArray())
appDb.bookmarkDao.insert(*it.toTypedArray())
}
fileToListT<BookGroup>(path, "bookGroup.json")?.let {
App.db.bookGroupDao.insert(*it.toTypedArray())
appDb.bookGroupDao.insert(*it.toTypedArray())
}
fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao.insert(*it.toTypedArray())
appDb.bookSourceDao.insert(*it.toTypedArray())
}
fileToListT<RssSource>(path, "rssSources.json")?.let {
App.db.rssSourceDao.insert(*it.toTypedArray())
appDb.rssSourceDao.insert(*it.toTypedArray())
}
fileToListT<RssStar>(path, "rssStar.json")?.let {
App.db.rssStarDao.insert(*it.toTypedArray())
appDb.rssStarDao.insert(*it.toTypedArray())
}
fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
App.db.replaceRuleDao.insert(*it.toTypedArray())
appDb.replaceRuleDao.insert(*it.toTypedArray())
}
fileToListT<SearchKeyword>(path, "searchHistory.json")?.let {
App.db.searchKeywordDao.insert(*it.toTypedArray())
appDb.searchKeywordDao.insert(*it.toTypedArray())
}
fileToListT<RuleSub>(path, "sourceSub.json")?.let {
App.db.ruleSubDao.insert(*it.toTypedArray())
appDb.ruleSubDao.insert(*it.toTypedArray())
}
fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let {
App.db.txtTocRule.insert(*it.toTypedArray())
appDb.txtTocRuleDao.insert(*it.toTypedArray())
}
fileToListT<HttpTTS>(path, DefaultData.httpTtsFileName)?.let {
App.db.httpTTSDao.insert(*it.toTypedArray())
appDb.httpTTSDao.insert(*it.toTypedArray())
}
fileToListT<ReadRecord>(path, "readRecord.json")?.let {
it.forEach { readRecord ->
//判断是不是本机记录
if (readRecord.androidId != App.androidId) {
App.db.readRecordDao.insert(readRecord)
if (readRecord.androidId != androidId) {
appDb.readRecordDao.insert(readRecord)
} else {
val time = App.db.readRecordDao
val time = appDb.readRecordDao
.getReadTime(readRecord.androidId, readRecord.bookName)
if (time == null || time < readRecord.readTime) {
App.db.readRecordDao.insert(readRecord)
appDb.readRecordDao.insert(readRecord)
}
}
}
@ -197,8 +200,8 @@ object Restore {
e.printStackTrace()
}
}
Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.all?.let { map ->
val edit = App.INSTANCE.defaultSharedPreferences.edit()
Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->
val edit = appCtx.defaultSharedPreferences.edit()
map.forEach {
if (keyIsNotIgnore(it.key)) {
when (val value = it.value) {
@ -207,30 +210,31 @@ object Restore {
is Long -> edit.putLong(it.key, value)
is Float -> edit.putFloat(it.key, value)
is String -> edit.putString(it.key, value)
else -> Unit
}
}
}
edit.apply()
}
ReadBookConfig.apply {
styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect)
shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout)
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46)
styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect)
shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout)
hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46)
}
ChapterProvider.upStyle()
ReadBook.loadContent(resetPageOffset = false)
}
appCtx.toastOnUi(R.string.restore_success)
withContext(Main) {
App.INSTANCE.toast(R.string.restore_success)
delay(100)
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.StringRes
import io.legado.app.R
import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER
import kotlin.DeprecationLevel.ERROR
@SuppressLint("SupportAnnotationUsage")
interface AlertBuilder<out D : DialogInterface> {
val ctx: Context
var title: CharSequence
@Deprecated(NO_GETTER, level = ERROR) get
fun setTitle(title: CharSequence)
var titleResource: Int
@Deprecated(NO_GETTER, level = ERROR) get
fun setTitle(titleResource: Int)
var message: CharSequence
@Deprecated(NO_GETTER, level = ERROR) get
fun setMessage(message: CharSequence)
var messageResource: Int
@Deprecated(NO_GETTER, level = ERROR) get
fun setMessage(messageResource: Int)
var icon: Drawable
@Deprecated(NO_GETTER, level = ERROR) get
fun setIcon(icon: Drawable)
@setparam:DrawableRes
var iconResource: Int
@Deprecated(NO_GETTER, level = ERROR) get
fun setIcon(@DrawableRes iconResource: Int)
var customTitle: View
@Deprecated(NO_GETTER, level = ERROR) get
fun setCustomTitle(customTitle: View)
var customView: View
@Deprecated(NO_GETTER, level = ERROR) get
fun setCustomView(customView: View)
var isCancelable: Boolean
@Deprecated(NO_GETTER, level = ERROR) get
fun setCancelable(isCancelable: Boolean)
fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null)
fun positiveButton(
@ -97,11 +85,11 @@ interface AlertBuilder<out D : DialogInterface> {
fun customTitle(view: () -> View) {
customTitle = view()
setCustomTitle(view())
}
fun customView(view: () -> View) {
customView = view()
setCustomView(view())
}
fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =

@ -7,48 +7,45 @@ import android.view.KeyEvent
import android.view.View
import androidx.appcompat.app.AlertDialog
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> {
private val builder = AlertDialog.Builder(ctx)
override var title: CharSequence
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setTitle(value) }
override fun setTitle(title: CharSequence) {
builder.setTitle(title)
}
override var titleResource: Int
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setTitle(value) }
override fun setTitle(titleResource: Int) {
builder.setTitle(titleResource)
}
override var message: CharSequence
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setMessage(value) }
override fun setMessage(message: CharSequence) {
builder.setMessage(message)
}
override var messageResource: Int
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setMessage(value) }
override fun setMessage(messageResource: Int) {
builder.setMessage(messageResource)
}
override var icon: Drawable
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setIcon(value) }
override fun setIcon(icon: Drawable) {
builder.setIcon(icon)
}
override var iconResource: Int
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setIcon(value) }
override fun setIcon(iconResource: Int) {
builder.setIcon(iconResource)
}
override var customTitle: View
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setCustomTitle(value) }
override fun setCustomTitle(customTitle: View) {
builder.setCustomTitle(customTitle)
}
override var customView: View
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setView(value) }
override fun setCustomView(customView: View) {
builder.setView(customView)
}
override var isCancelable: Boolean
@Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter()
set(value) { builder.setCancelable(value) }
override fun setCancelable(isCancelable: Boolean) {
builder.setCancelable(isCancelable)
}
override fun onCancelled(handler: (DialogInterface) -> Unit) {
builder.setOnCancelListener(handler)
@ -58,7 +55,10 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
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) }
}

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

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

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

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

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

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

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

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

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

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

@ -4,13 +4,20 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
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.EpubChapter
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.Resources
import nl.siegmann.epublib.domain.TOCReference
import nl.siegmann.epublib.epub.EpubReader
import nl.siegmann.epublib.service.MediatypeService
import nl.siegmann.epublib.util.ResourceUtil
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -18,7 +25,7 @@ import java.io.InputStream
import java.nio.charset.Charset
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 {
private var eFile: EPUBFile? = null
@ -29,6 +36,7 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
eFile = EPUBFile(book)
return eFile!!
}
eFile?.book = book
return eFile!!
}
@ -49,6 +57,11 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
): InputStream? {
return getEFile(book).getImage(href)
}
@Synchronized
fun upBookInfo(book: io.legado.app.data.entities.Book) {
return getEFile(book).upBookInfo()
}
}
private var epubBook: Book? = null
@ -56,28 +69,30 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
init {
try {
val epubReader = EpubReader()
val inputStream = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl)
App.INSTANCE.contentResolver.openInputStream(uri)
appCtx.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
epubBook = epubReader.readEpub(inputStream)
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
App.INSTANCE.externalFilesDir,
"covers",
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
)
}
if (!File(book.coverUrl!!).exists()) {
epubBook!!.coverImage?.inputStream?.use {
val cover = BitmapFactory.decodeStream(it)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
epubBook = readEpub(inputStream)
if (epubBook != null) {
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
appCtx.externalFilesDir,
"covers",
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
)
}
if (!File(book.coverUrl!!).exists()) {
/*部分书籍DRM处理后,封面获取异常,待优化*/
epubBook!!.coverImage?.inputStream?.use {
val cover = BitmapFactory.decodeStream(it)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush()
out.close()
}
}
}
} catch (e: Exception) {
@ -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? {
epubBook?.let { eBook ->
val resource = eBook.resources.getByHref(chapter.url)
val doc = Jsoup.parse(String(resource.data, mCharset))
val elements = doc.body().children()
/*获取当前章节文本*/
var string = getChildChapter(chapter, chapter.url)
val childContends = appDb.epubChapterDao.get(book.bookUrl, chapter.url)
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("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
}
@ -102,11 +191,13 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook ->
val metadata = eBook.metadata
book.name = metadata.firstTitle
private fun upBookInfo() {
if (epubBook == null) {
eFile = null
book.intro = "书籍导入异常"
} else {
val metadata = epubBook!!.metadata
book.name = book.originName
if (metadata.authors.size > 0) {
val author =
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) {
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
if (refs == null || refs.isEmpty()) {
val spineReferences = eBook.spine.spineReferences
@ -130,7 +226,7 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
val doc =
Jsoup.parse(String(resource.data, mCharset))
val elements = doc.getElementsByTag("title")
if (elements.size > 0) {
if (elements != null && elements.size > 0) {
title = elements[0].text()
}
} catch (e: IOException) {
@ -150,10 +246,12 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
i++
}
} else {
parseFirstPage(chapterList, refs)
parseMenu(chapterList, refs, 0)
for (i in chapterList.indices) {
chapterList[i].index = i
}
getChildChapter(chapterList)
}
}
book.latestChapterTitle = chapterList.lastOrNull()?.title
@ -161,19 +259,100 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
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(
chapterList: ArrayList<BookChapter>,
refs: List<TOCReference>?,
level: Int
) {
if (refs == null) return
for (ref in refs) {
refs?.forEach { ref ->
if (ref.resource != null) {
val chapter = BookChapter()
chapter.bookUrl = book.bookUrl
chapter.title = ref.title
chapter.url = ref.completeHref
chapter.startFragmentId = ref.fragmentId
if (durIndex > 0) {
val preIndex = durIndex - 1
chapterList[preIndex].endFragmentId = chapter.startFragmentId
}
chapterList.add(chapter)
durIndex++
}
if (ref.children != null && ref.children.isNotEmpty()) {
parseMenu(chapterList, ref.children, level + 1)

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

@ -1,7 +1,6 @@
package io.legado.app.model.rss
import androidx.annotation.Keep
import io.legado.app.App
import io.legado.app.R
import io.legado.app.data.entities.RssArticle
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.utils.GSON
import io.legado.app.utils.NetworkUtils
import splitties.init.appCtx
import java.util.*
@Keep
@ -27,7 +27,7 @@ object RssParserByRule {
var nextUrl: String? = null
if (body.isNullOrBlank()) {
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")
@ -39,6 +39,7 @@ object RssParserByRule {
val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule(ruleData)
analyzeRule.setContent(body).setBaseUrl(sortUrl)
analyzeRule.setRedirectUrl(sortUrl)
var reverse = false
if (ruleArticles.startsWith("-")) {
reverse = true

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

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

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

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

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

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

@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.view.KeyEvent
import io.legado.app.App
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.AppConfig
import io.legado.app.service.AudioPlayService
import io.legado.app.service.BaseReadAloudService
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.book.read.ReadBookActivity
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.getPrefBoolean
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() {
override fun onReceive(context: Context, intent: Intent) {
if (handleIntent(context, intent) && isOrderedBroadcast) {
abortBroadcast()
}
}
companion object {
fun handleIntent(context: Context, intent: Intent): Boolean {
@ -55,7 +55,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
return true
}
private fun readAloud(context: Context) {
fun readAloud(context: Context, isMediaKey: Boolean = true) {
when {
BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) {
ReadAloud.pause(context)
@ -73,34 +73,23 @@ class MediaButtonReceiver : BroadcastReceiver() {
postEvent(EventBus.MEDIA_BUTTON, true)
ActivityHelp.isExist(AudioPlayActivity::class.java) ->
postEvent(EventBus.MEDIA_BUTTON, true)
else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) {
GlobalScope.launch(Main) {
val lastBook: Book? = withContext(IO) {
App.db.bookDao.lastReadBook
}
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 {
else -> if (AppConfig.mediaButtonOnExit || !isMediaKey) {
appDb.bookDao.lastReadBook?.let {
if (!ActivityHelp.isExist(MainActivity::class.java)) {
Intent(context, MainActivity::class.java).let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
it.putExtra("readAloud", true)
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 io.legado.app.ui.book.search.SearchActivity
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() {
@ -19,21 +20,30 @@ class SharedReceiverActivity : AppCompatActivity() {
}
private fun initIntent() {
if (Intent.ACTION_SEND == intent.action && intent.type == receivingType) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
if (openUrl(it)) {
startActivity<SearchActivity>(Pair("key", it))
when {
Intent.ACTION_SEND == intent.action && intent.type == receivingType -> {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
if (openUrl(it)) {
startActivity<SearchActivity> {
putExtra("key", it)
}
}
}
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Intent.ACTION_PROCESS_TEXT == intent.action
&& intent.type == receivingType
) {
intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let {
if (openUrl(it)) {
startActivity<SearchActivity>(Pair("key", it))
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Intent.ACTION_PROCESS_TEXT == intent.action
&& intent.type == receivingType -> {
intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let {
if (openUrl(it)) {
startActivity<SearchActivity> {
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.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.IntentHelp
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.ui.audio.AudioPlayActivity
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.toast
import splitties.init.appCtx
class AudioPlayService : BaseService(),
@ -133,7 +134,7 @@ class AudioPlayService : BaseService(),
}.onFailure {
it.printStackTrace()
launch {
toast("$url ${it.localizedMessage}")
toastOnUi("$url ${it.localizedMessage}")
stopSelf()
}
}
@ -219,7 +220,7 @@ class AudioPlayService : BaseService(),
if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
launch { toast("error: $what $extra $url") }
launch { toastOnUi("error: $what $extra $url") }
}
return true
}
@ -274,7 +275,7 @@ class AudioPlayService : BaseService(),
.onSuccess { content ->
if (content.isEmpty()) {
withContext(Main) {
toast("未获取到资源链接")
toastOnUi("未获取到资源链接")
}
} else {
contentLoadFinish(chapter, content)
@ -286,7 +287,7 @@ class AudioPlayService : BaseService(),
}
} else {
removeLoading(chapter.index)
toast("book or source is null")
toastOnUi("book or source is null")
}
}
}
@ -321,7 +322,7 @@ class AudioPlayService : BaseService(),
execute {
AudioPlay.book?.let {
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.ACTION_MEDIA_BUTTON,
null,
App.INSTANCE,
appCtx,
MediaButtonReceiver::class.java
),
PendingIntent.FLAG_CANCEL_CURRENT

@ -14,7 +14,6 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
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.utils.getPrefBoolean
import io.legado.app.utils.postEvent
import splitties.init.appCtx
abstract class BaseReadAloudService : BaseService(),
AudioManager.OnAudioFocusChangeListener {
@ -233,7 +233,7 @@ abstract class BaseReadAloudService : BaseService(),
Intent(
Intent.ACTION_MEDIA_BUTTON,
null,
App.INSTANCE,
appCtx,
MediaButtonReceiver::class.java
),
PendingIntent.FLAG_CANCEL_CURRENT

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

@ -2,12 +2,12 @@ package io.legado.app.service
import android.content.Intent
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig
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.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors
import kotlin.math.min
@ -70,7 +70,7 @@ class CheckSourceService : BaseService() {
private fun check(ids: List<String>) {
if (allIds.isNotEmpty()) {
toast("已有书源在校验,等完成后再试")
toastOnUi("已有书源在校验,等完成后再试")
return
}
tasks.clear()
@ -97,7 +97,7 @@ class CheckSourceService : BaseService() {
execute {
if (index < allIds.size) {
val sourceUrl = allIds[index]
App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
check(source)
} ?: onNext(sourceUrl, "")
}
@ -107,22 +107,36 @@ class CheckSourceService : BaseService() {
fun check(source: BookSource) {
execute(context = searchCoroutine) {
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 toc = webBook.getChapterListAwait(this, book)
val content = webBook.getContentAwait(this, book, toc.first())
if (content.isBlank()) {
throw Exception("正文内容为空")
}
}.timeout(60000L)
}.timeout(180000L)
.onError {
source.addGroup("失效")
source.bookSourceComment =
"error:${it.localizedMessage}\n${source.bookSourceComment}"
App.db.bookSourceDao.update(source)
appDb.bookSourceDao.update(source)
}.onSuccess {
source.removeGroup("失效")
App.db.bookSourceDao.update(source)
appDb.bookSourceDao.update(source)
}.onFinally {
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.utils.RealPathUtil
import io.legado.app.utils.msg
import org.jetbrains.anko.downloadManager
import org.jetbrains.anko.toast
import io.legado.app.utils.toastOnUi
import splitties.systemservices.downloadManager
import java.io.File
@ -149,7 +149,7 @@ class DownloadService : BaseService() {
try {
startActivity(intent)
} catch (e: Exception) {
toast(e.msg)
toastOnUi(e.msg)
}
}
}

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

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

Loading…
Cancel
Save