update to latest

pull/1238/head
fisher 4 years ago
commit 6f3e1c9fc8
  1. 99
      .github/workflows/release.yml
  2. 49
      .github/workflows/test.yml
  3. 6
      CHANGELOG.md
  4. 40
      README.md
  5. 34
      app/build.gradle
  6. BIN
      app/cronetlib/cronet_api.jar
  7. BIN
      app/cronetlib/cronet_impl_common_java.jar
  8. BIN
      app/cronetlib/cronet_impl_native_java.jar
  9. BIN
      app/cronetlib/cronet_impl_platform_java.jar
  10. BIN
      app/cronetlib/src/cronet_api-src.jar
  11. BIN
      app/cronetlib/src/cronet_impl_common_java-src.jar
  12. BIN
      app/cronetlib/src/cronet_impl_native_java-src.jar
  13. BIN
      app/cronetlib/src/cronet_impl_platform_java-src.jar
  14. 39
      app/src/main/AndroidManifest.xml
  15. 2
      app/src/main/assets/epub/chapter.html
  16. 129
      app/src/main/assets/updateLog.md
  17. 5
      app/src/main/assets/web/uploadBook/js/common.js
  18. 12
      app/src/main/assets/web/uploadBook/js/html5_fun.js
  19. 5
      app/src/main/java/io/legado/app/App.kt
  20. 28
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  21. 5
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  22. 2
      app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt
  23. 2
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  24. 3
      app/src/main/java/io/legado/app/constant/EventBus.kt
  25. 3
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  26. 10
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  27. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  28. 10
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  29. 2
      app/src/main/java/io/legado/app/data/entities/BookGroup.kt
  30. 11
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  31. 11
      app/src/main/java/io/legado/app/data/entities/rule/LoginRule.kt
  32. 2
      app/src/main/java/io/legado/app/help/AppConfig.kt
  33. 1
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  34. 13
      app/src/main/java/io/legado/app/help/IntentHelp.kt
  35. 6
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  36. 2
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  37. 32
      app/src/main/java/io/legado/app/help/ThemeConfig.kt
  38. 6
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  39. 34
      app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt
  40. 56
      app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt
  41. 357
      app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt
  42. 21
      app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt
  43. 3
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  44. 9
      app/src/main/java/io/legado/app/help/storage/BookWebDav.kt
  45. 3
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  46. 49
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  47. 23
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  48. 7
      app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt
  49. 5
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  50. 5
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  51. 2
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  52. 21
      app/src/main/java/io/legado/app/service/CacheBookService.kt
  53. 11
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  54. 25
      app/src/main/java/io/legado/app/service/DownloadService.kt
  55. 12
      app/src/main/java/io/legado/app/service/WebService.kt
  56. 72
      app/src/main/java/io/legado/app/service/WebTileService.kt
  57. 35
      app/src/main/java/io/legado/app/service/help/AudioPlay.kt
  58. 25
      app/src/main/java/io/legado/app/service/help/CacheBook.kt
  59. 14
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  60. 16
      app/src/main/java/io/legado/app/service/help/Download.kt
  61. 8
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  62. 46
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  63. 23
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt
  64. 80
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt
  65. 67
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt
  66. 46
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  67. 22
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  68. 3
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt
  69. 4
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt
  70. 17
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  71. 4
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  72. 5
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt
  73. 7
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  74. 18
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  75. 43
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  76. 52
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  77. 120
      app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt
  78. 65
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  79. 49
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  80. 23
      app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt
  81. 10
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  82. 14
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  83. 49
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  84. 1
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  85. 31
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  86. 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt
  87. 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  88. 6
      app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt
  89. 45
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  90. 4
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  91. 4
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt
  92. 3
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt
  93. 6
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  94. 4
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  95. 4
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  96. 6
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  97. 2
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  98. 2
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  99. 2
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt
  100. 6
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,99 @@
name: Build and Release
on:
push:
branches:
- master
paths:
- 'CHANGELOG.md'
# schedule:
# - cron: '0 4 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
# 登录蓝奏云后在控制台运行document.cookie
ylogin: ${{ secrets.LANZOU_ID }}
phpdisk_info: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_FOLDER_ID: 'b0f7pt4ja'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
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: Release Apk Sign
run: |
echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks
sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Unify Version Name
run: |
echo "统一版本号"
VERSION=$(date -d "8 hour" -u +3.%y.%m%d%H)
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
sed "/def version/c def version = \"$VERSION\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
- name: Organize the Files
run: |
mkdir -p ${{ github.workspace }}/apk/
cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
- name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado apk
path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou
if: ${{ env.ylogin }}
run: |
path="$GITHUB_WORKSPACE/apk/"
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f7pt4ja"
- name: Release
uses: softprops/action-gh-release@59c3b4891632ff9a897f99a91d7bc557467a3a22
with:
name: legado_app_${{ env.RELEASE_VERSION }}
tag_name: ${{ env.RELEASE_VERSION }}
body_path: ${{ github.workspace }}/CHANGELOG.md
draft: false
prerelease: false
files: ${{ github.workspace }}/apk/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push Assets To "release" Branch
run: |
cd $GITHUB_WORKSPACE/apk || exit 1
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b release
git add *.apk
git commit -m "${{ env.RELEASE_VERSION }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin release
- name: Purge Jsdelivr Cache
run: |
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.repository }}@release/)
if echo $result |grep -q 'success.*true'; then
echo "jsdelivr缓存更新成功"
else
echo $result
fi

@ -1,8 +1,6 @@
name: Android CI name: Test Build
on: on:
release:
types: [published]
push: push:
branches: branches:
- master - master
@ -32,7 +30,6 @@ jobs:
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: | restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}- ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt - name: Clear 18PlusList.txt
run: | run: |
echo "清空18PlusList.txt" echo "清空18PlusList.txt"
@ -45,36 +42,52 @@ jobs:
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Unify Version Name
run: |
echo "统一版本号"
VERSION=$(date -d "8 hour" -u +3.%y.%m%d%H)
VERSIONL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)
echo "VERSION=$VERSIONL" >> $GITHUB_ENV
sed "/def version/c def version = \"$VERSION\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
echo "修改文件名"
mkdir -p ${{ github.workspace }}/apk/
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/`echo "$file"|sed 's/.*\/\(.*\)\(\.apk\)$/\1_原包名\2/'`
done
- name: Apk Live Together - name: Apk Live Together
run: | run: |
echo "设置apk共存" echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i
echo "统一版本号"
sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle - name: Build With Gradle
run: | run: |
echo "开始进行release构建" echo "开始进行release构建"
chmod +x gradlew chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel ./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
echo "修改文件名"
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/`echo "$file"|sed 's/.*\/\(.*\)\(\.apk\)$/\1_共存\2/'`
done
- name: Upload App To Artifact - name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }} if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: legado apk name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou - name: Upload App To Lanzou
if: ${{ env.ylogin }} if: ${{ env.ylogin }}
run: | run: |
path="$GITHUB_WORKSPACE/app/build/outputs/apk/app/release" path="$GITHUB_WORKSPACE/apk/"
files=$(ls $path) python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
for f in $files
do
if [[ $f == *"apk" ]]; then
file=$f
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file"
break
fi
done
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b" echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b"

@ -1,4 +1,4 @@
# 1.0.0 (2020-02-09) **2021/08/09**
1. 修复选择文字不能选择单个文字的bug
2.

@ -18,7 +18,12 @@
* [web端](https://github.com/celetor/web-yuedu3) * [web端](https://github.com/celetor/web-yuedu3)
## 下载 ## 下载
<a href="https://play.google.com/store/apps/details?id=io.legado.play.release"><img width="200px" alt="Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or <a href="https://www.coolapk.com/apk/io.legado.app.release"><img width="100px" height="100px" alt="CoolApk" src="https://github.com/Celeter/build/raw/master/.github/scripts/coolapk.png"/></a> or [Releases](https://github.com/gedoor/legado/releases/latest) * [google play](https://play.google.com/store/apps/details?id=io.legado.play.release)
* [酷安](https://www.coolapk.com/apk/io.legado.app.release)
* [release](https://github.com/gedoor/legado/releases/latest)
* [jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/legado@release/)
* [网盘](https://kunfei.lanzoui.com/b0f7pt4ja)
* [测试版](https://kunfei.lanzoui.com/b0f810h4b)
## 阅读API ## 阅读API
* 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。 * 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
@ -27,24 +32,21 @@
* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版 * path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版
## 感谢 ## 感谢
``` > * org.jsoup:jsoup
org.jsoup:jsoup > * cn.wanghaomiao:JsoupXpath
cn.wanghaomiao:JsoupXpath > * com.jayway.jsonpath:json-path
com.jayway.jsonpath:json-path > * com.github.gedoor:rhino-android
com.github.gedoor:rhino-android > * com.squareup.okhttp3:okhttp
com.squareup.okhttp3:okhttp > * com.github.bumptech.glide:glide
com.ljx.rxhttp:rxhttp > * org.nanohttpd:nanohttpd
com.github.bumptech.glide:glide > * org.nanohttpd:nanohttpd-websocket
org.nanohttpd:nanohttpd > * cn.bingoogolapple:bga-qrcode-zxing
org.nanohttpd:nanohttpd-websocket > * com.jaredrummler:colorpicker
cn.bingoogolapple:bga-qrcode-zxing > * org.apache.commons:commons-text
com.jaredrummler:colorpicker > * io.noties.markwon:core
org.apache.commons:commons-text > * io.noties.markwon:image-glide
io.noties.markwon:core > * com.hankcs:hanlp
io.noties.markwon:image-glide > * com.positiondev.epublib:epublib-core
com.hankcs:hanlp
com.positiondev.epublib:epublib-core
```
## 免责声明 ## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/disclaimer.html

@ -90,20 +90,20 @@ android {
applicationId "io.legado.cronet" applicationId "io.legado.cronet"
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"] manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
ndk { ndk {
abiFilters 'arm64-v8a' abiFilters 'arm64-v8a','armeabi-v7a','x86_64','x86'
} }
} }
} }
compileOptions { compileOptions {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8 // Sets Java compatibility to Java 11
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
} }
buildToolsVersion '30.0.3' buildToolsVersion '30.0.3'
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
@ -123,7 +123,6 @@ kapt {
dependencies { dependencies {
coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5') coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5')
implementation(fileTree(dir: 'libs', include: ['*.jar', '*.aar']))
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
androidTestImplementation('androidx.test:runner:1.4.0') androidTestImplementation('androidx.test:runner:1.4.0')
androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0') androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0')
@ -136,10 +135,10 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")
//androidX //androidX
implementation('androidx.appcompat:appcompat:1.3.0') implementation('androidx.appcompat:appcompat:1.3.1')
implementation('androidx.core:core-ktx:1.6.0') implementation('androidx.core:core-ktx:1.6.0')
implementation("androidx.activity:activity-ktx:1.2.3") implementation("androidx.activity:activity-ktx:1.3.0")
implementation("androidx.fragment:fragment-ktx:1.3.5") implementation("androidx.fragment:fragment-ktx:1.3.6")
implementation('androidx.preference:preference-ktx:1.1.1') implementation('androidx.preference:preference-ktx:1.1.1')
implementation('androidx.constraintlayout:constraintlayout:2.0.4') implementation('androidx.constraintlayout:constraintlayout:2.0.4')
implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0') implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')
@ -152,7 +151,7 @@ dependencies {
//media //media
def media2_version = "1.1.3" def media2_version = "1.1.3"
implementation("androidx.media2:media2-session:$media2_version") implementation("androidx.media2:media2-session:$media2_version")
implementation("androidx.media:media:1.3.1") implementation("androidx.media:media:1.4.0")
//implementation "androidx.media2:media2-player:$media2_version" //implementation "androidx.media2:media2-player:$media2_version"
//implementation 'com.google.android.exoplayer:exoplayer:2.13.0' //implementation 'com.google.android.exoplayer:exoplayer:2.13.0'
@ -187,22 +186,19 @@ dependencies {
// //
implementation('com.squareup.okhttp3:okhttp:4.9.1') implementation('com.squareup.okhttp3:okhttp:4.9.1')
def cronet_version = '92.0.4509.1' compileOnly(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
compileOnly("org.microg:cronet-api:$cronet_version") cronetImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
cronetImplementation("org.microg:cronet-native:$cronet_version")
cronetImplementation("org.microg:cronet-api:$cronet_version")
cronetImplementation("org.microg:cronet-common:$cronet_version")
//Glide //Glide
implementation('com.github.bumptech.glide:glide:4.12.0') implementation('com.github.bumptech.glide:glide:4.12.0')
//webServer //webServer
implementation('org.nanohttpd:nanohttpd:2.3.1') def nanoHttpdVersion = "2.3.1"
implementation('org.nanohttpd:nanohttpd-websocket:2.3.1') implementation("org.nanohttpd:nanohttpd:$nanoHttpdVersion")
implementation('org.nanohttpd:nanohttpd-apache-fileupload:2.3.1') implementation("org.nanohttpd:nanohttpd-websocket:$nanoHttpdVersion")
// //
implementation('com.github.jenly1314:zxing-lite:2.1.0') implementation('com.github.jenly1314:zxing-lite:2.1.1')
// //
implementation('com.jaredrummler:colorpicker:1.1.0') implementation('com.jaredrummler:colorpicker:1.1.0')

Binary file not shown.

@ -3,13 +3,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app"> package="io.legado.app">
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@ -22,6 +15,10 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application <application
android:name=".App" android:name=".App"
@ -34,7 +31,9 @@
android:theme="@style/AppTheme.Light" android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!-- 主入口 --> <!-- 主入口 -->
<activity android:name=".ui.welcome.WelcomeActivity"> <activity
android:name=".ui.welcome.WelcomeActivity"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -50,6 +49,7 @@
<activity <activity
android:name=".ui.welcome.Launcher1" android:name=".ui.welcome.Launcher1"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher1"> android:icon="@mipmap/launcher1">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -66,6 +66,7 @@
<activity <activity
android:name=".ui.welcome.Launcher2" android:name=".ui.welcome.Launcher2"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher2"> android:icon="@mipmap/launcher2">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -82,6 +83,7 @@
<activity <activity
android:name=".ui.welcome.Launcher3" android:name=".ui.welcome.Launcher3"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher3"> android:icon="@mipmap/launcher3">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -98,6 +100,7 @@
<activity <activity
android:name=".ui.welcome.Launcher4" android:name=".ui.welcome.Launcher4"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher4"> android:icon="@mipmap/launcher4">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -114,6 +117,7 @@
<activity <activity
android:name=".ui.welcome.Launcher5" android:name=".ui.welcome.Launcher5"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher5"> android:icon="@mipmap/launcher5">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -130,6 +134,7 @@
<activity <activity
android:name=".ui.welcome.Launcher6" android:name=".ui.welcome.Launcher6"
android:enabled="false" android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher6"> android:icon="@mipmap/launcher6">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -151,6 +156,7 @@
<activity <activity
android:name=".ui.book.read.ReadBookActivity" android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" /> <action android:name="com.samsung.android.support.REMOTE_ACTION" />
@ -304,6 +310,7 @@
<!-- 文字处理 --> <!-- 文字处理 -->
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"
android:exported="true"
android:label="@string/receiving_shared_label" android:label="@string/receiving_shared_label"
android:theme="@style/AppTheme.Transparent"> android:theme="@style/AppTheme.Transparent">
<intent-filter> <intent-filter>
@ -325,6 +332,7 @@
<activity <activity
android:name=".ui.association.OnLineImportActivity" android:name=".ui.association.OnLineImportActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/AppTheme.Transparent"> android:theme="@style/AppTheme.Transparent">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -339,6 +347,7 @@
<!-- 打开文件 --> <!-- 打开文件 -->
<activity <activity
android:name=".ui.association.FileAssociationActivity" android:name=".ui.association.FileAssociationActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/AppTheme.Transparent"> android:theme="@style/AppTheme.Transparent">
<!-- VIEW (Open with) action --> <!-- VIEW (Open with) action -->
@ -388,12 +397,24 @@
<service android:name=".service.CheckSourceService" /> <service android:name=".service.CheckSourceService" />
<service android:name=".service.CacheBookService" /> <service android:name=".service.CacheBookService" />
<service android:name=".service.WebService" /> <service android:name=".service.WebService" />
<service
android:name=".service.WebTileService"
android:label="legado Web Service"
android:icon="@drawable/ic_web_service_noti"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name=".service.TTSReadAloudService" /> <service android:name=".service.TTSReadAloudService" />
<service android:name=".service.HttpReadAloudService" /> <service android:name=".service.HttpReadAloudService" />
<service android:name=".service.AudioPlayService" /> <service android:name=".service.AudioPlayService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />
<receiver android:name=".receiver.MediaButtonReceiver"> <receiver
android:name=".receiver.MediaButtonReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>

@ -10,6 +10,8 @@
<!--<div class="logo"> <!--<div class="logo">
<img alt="" class="logo" src="../Images/logo.png"/> <img alt="" class="logo" src="../Images/logo.png"/>
</div>--> </div>-->
<br/>
<br/>
<h2 class="head">{title}</h2> <h2 class="head">{title}</h2>
{content} {content}
</body> </body>

@ -1,21 +1,66 @@
# 更新日志 # 更新日志
* 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。 * 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
## **必读** ## **必读**
【温馨提醒】 *更新前一定要做好备份,以免数据丢失!* 【温馨提醒】 *更新前一定要做好备份,以免数据丢失!*
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 * 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题。先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题, 出现简体变化问题检查一下简繁转换是否关闭
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
* 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法
**2021/08/13**
1. web传书可以使用
2. 修复一些bug
**2021/08/09**
1. 修复选择文字不能选择单个文字的bug
2. 分组可选择封面
**2021/08/08**
1. 背景图片添加模糊设置
2. 书籍信息界面添加置顶操作
3. 自动翻页时屏幕常亮
4. 字典:中文使用百度汉语字典,英文使用海词字典。 by ag2s20150909
5. 导入规则时可以选择添加分组还是替换分组
**2021/08/02**
1. 换源界面功能添加:置顶,置底,删除 by h11128
2. Cronet:优化 by ag2s20150909
3. 优化自动翻页 by jiuZhouWorlds
4. 封面设置移到主题里面,白天和夜间可分别设置
**2021/08/01**
1. 为webService添加快捷操作
2. 规则内替换使用正则错误时自动切换为不使用正则
3. 优化Cronet
4. 阅读界面菜单显示的时候停止按键翻页和自动阅读
5. 切换后台停止自动阅读
**2021/07/29**
1. 修复每次更新都重新导入text规则的bug
2. RSS阅读页添加刷新按钮以应对页面内容过期失效的BUG by JiuZhouWorlds
3. 规则内替换使用正则报错时自动使用非正则替换
**2021/07/27**
**2021/07/26**
1. 修复bug 1. 修复bug
2. web使用api获取封面,不会再出现没有封面的情况 2. web使用api获取封面,不会再出现没有封面的情况
3. 阅读亮度手动调节分别记住白天和夜间模式 3. 阅读亮度手动调节分别记住白天和夜间模式
4. legado://import/auto?src={url}, 自动识别导入类型 4. legado://import/auto?src={url}, 自动识别导入类型
5. 一些优化并更新了一下web首页,感谢沚水 5. 一些优化并更新了一下web首页,感谢沚水, 传书暂时还不好用
**2021/07/22** **2021/07/22**
1. 非关键规则添加try防止报错中断解析 1. 非关键规则添加try防止报错中断解析
2. 添加获取封面的api 2. 添加获取封面的api
3. 获取正文api使用替换规则 3. 获取正文api使用替换规则
@ -23,16 +68,21 @@
5. web书架增加【最近一次更新书籍信息的时间】 5. web书架增加【最近一次更新书籍信息的时间】
6. 采用Flow替换LiveData,优化资源使用 6. 采用Flow替换LiveData,优化资源使用
7. 统一网络一键导入路径legado://import/{path}?src={url} 7. 统一网络一键导入路径legado://import/{path}?src={url}
* path: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig * path: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig
* 添加了txt小说规则,在线朗读引擎,主题,排版 的一键导入支持,老url依然可用 * 添加了txt小说规则,在线朗读引擎,主题,排版 的一键导入支持,老url依然可用
8. 替换规则管理添加置顶所选和置底所选 8. 替换规则管理添加置顶所选和置底所选
**2021/07/16** **2021/07/16**
1. js扩展函数添加删除本地文件方法 1. js扩展函数添加删除本地文件方法
2. js扩展函数对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件,/android/data/{package}/cache/... 2. js扩展函数对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件,/android/data/{package}/cache/...
**2021/07/15** **2021/07/15**
1. 添加js函数来修复开启js沙箱后某些书源失效。by ag2s20150909 1. 添加js函数来修复开启js沙箱后某些书源失效。by ag2s20150909
```kotlin ```kotlin
/** /**
* 获取网络zip文件里面的数据 * 获取网络zip文件里面的数据
@ -41,6 +91,7 @@
* @return zip指定文件的数据 * @return zip指定文件的数据
*/ */
fun getZipStringContent(url: String, path: String): String fun getZipStringContent(url: String, path: String): String
/** /**
* 获取网络zip文件里面的数据 * 获取网络zip文件里面的数据
* @param url zip文件的链接 * @param url zip文件的链接
@ -49,24 +100,32 @@ fun getZipStringContent(url: String, path: String): String
*/ */
fun getZipByteArrayContent(url: String, path: String): ByteArray? fun getZipByteArrayContent(url: String, path: String): ByteArray?
``` ```
* web服务添加一个导航页 * web服务添加一个导航页
**2021/07/11** **2021/07/11**
1. 开启JS沙箱限制 1. 开启JS沙箱限制
* 禁止在js里exec运行命令 * 禁止在js里exec运行命令
* 禁止在js里通过geClass反射 * 禁止在js里通过geClass反射
* 禁止在js里创建File对象 * 禁止在js里创建File对象
* 禁止在js里获取Packages scope * 禁止在js里获取Packages scope
2. 优化并修复bug 2. 优化并修复bug
**2021/07/10** **2021/07/10**
1. 阅读界面长按菜单改回原来样式 1. 阅读界面长按菜单改回原来样式
2. 解决导入书源时重命名分组和保留名称冲突的问题 2. 解决导入书源时重命名分组和保留名称冲突的问题
**2021/07/09** **2021/07/09**
1. 发现url添加json格式, 支持设置标签样式 1. 发现url添加json格式, 支持设置标签样式
* 样式属性可以搜索 [FleboxLayout子元素支持的属性介绍](https://www.jianshu.com/p/3c471953e36d) * 样式属性可以搜索 [FleboxLayout子元素支持的属性介绍](https://www.jianshu.com/p/3c471953e36d)
* 样式属性可省略,有默认值 * 样式属性可省略,有默认值
```json ```json
[ [
{ {
@ -84,31 +143,39 @@ fun getZipByteArrayContent(url: String, path: String): ByteArray?
``` ```
**2021/07/07** **2021/07/07**
1. 默认规则新增类似`jsonPath`的索引写法 by bushixuanqi 1. 默认规则新增类似`jsonPath`的索引写法 by bushixuanqi
* 格式形如 `[index,index, ...]``[!index,index, ...]` 其中`[!`开头表示筛选方式为排除,`index`可以是单个索引,也可以是区间。 * 格式形如 `[index,index, ...]``[!index,index, ...]` 其中`[!`开头表示筛选方式为排除,`index`可以是单个索引,也可以是区间。
* 区间格式为 `start:end``start:end:step`,其中`start`为`0`可省略,`end`为`-1`可省略。 * 区间格式为 `start:end``start:end:step`,其中`start`为`0`可省略,`end`为`-1`可省略。
* 索引、区间两端、区间间隔都支持负数 * 索引、区间两端、区间间隔都支持负数
* 例如 `tag.div[-1, 3:-2:-10, 2]` * 例如 `tag.div[-1, 3:-2:-10, 2]`
* 特殊用法 `tag.div[-1:0]` 可在任意地方让列表反向 * 特殊用法 `tag.div[-1:0]` 可在任意地方让列表反向
2. 允许索引作为@分段后每个部分的首规则,此时相当于前面是`children` 2. 允许索引作为@分段后每个部分的首规则,此时相当于前面是`children`
* `head@.1@text``head@[1]@text``head@children[1]@text` 等价 * `head@.1@text``head@[1]@text``head@children[1]@text` 等价
3. 添加Umd格式支持 by ag2s20150909 3. 添加Umd格式支持 by ag2s20150909
4. 修复web页面按键重复监听的bug 4. 修复web页面按键重复监听的bug
5. 亮度条往中间移了一点,防止误触 5. 亮度条往中间移了一点,防止误触
6. 添加内置字典 6. 添加内置字典
**2021/06/29** **2021/06/29**
* 修复html格式化bug * 修复html格式化bug
* 订阅界面webView支持css prefers-color-scheme: dark 查询,需webView v76或更高版本 * 订阅界面webView支持css prefers-color-scheme: dark 查询,需webView v76或更高版本
* 如webView低于v76可以用js调用activity.isNightTheme()来获取当前是否暗模式 * 如webView低于v76可以用js调用activity.isNightTheme()来获取当前是否暗模式
* 修复一些书籍导出epub失败 by ag2s20150909 * 修复一些书籍导出epub失败 by ag2s20150909
**2021/06/22** **2021/06/22**
* 修复隐藏未读设置不生效的bug * 修复隐藏未读设置不生效的bug
* 修复系统字体大小选择大时导入界面按钮显示不全的bug * 修复系统字体大小选择大时导入界面按钮显示不全的bug
* 修复听书从后台打开时不对的bug * 修复听书从后台打开时不对的bug
**2021/06/20** **2021/06/20**
* viewPager2 改回 viewPager * viewPager2 改回 viewPager
* 添加配置导入文件规则功能 by bushixuanqi * 添加配置导入文件规则功能 by bushixuanqi
* 文件夹分组样式优化(未完成) * 文件夹分组样式优化(未完成)
@ -116,16 +183,19 @@ fun getZipByteArrayContent(url: String, path: String): ByteArray?
* 修复一些bug * 修复一些bug
**2021/06/06** **2021/06/06**
* 添加自定义导出文件名 * 添加自定义导出文件名
* 添加书架文件夹分组样式,未完成 * 添加书架文件夹分组样式,未完成
* viewPager2 3层嵌套有问题,书架换回viewPager * viewPager2 3层嵌套有问题,书架换回viewPager
**2021/05/29** **2021/05/29**
* 谷歌版可使用外部epub模板 * 谷歌版可使用外部epub模板
* Asset文件夹下二级以内目录全文件读取,Asset->文件夹->文件 * Asset文件夹下二级以内目录全文件读取,Asset->文件夹->文件
* epub元数据修改,使修改字体只对正文生效 * epub元数据修改,使修改字体只对正文生效
* 修复epub模板文件的排序问题 * 修复epub模板文件的排序问题
* epub可自定义模板,模板路径为书籍导出目录的Asset文件夹,[模板范例](https://wwa.lanzoux.com/ibjBspkn05i) * epub可自定义模板,模板路径为书籍导出目录的Asset文件夹,[模板范例](https://wwa.lanzoux.com/ibjBspkn05i)
``` ```
Asset中里面必须有Text文件夹,Text文件夹里必须有chapter.html,否则导出正文会为空 Asset中里面必须有Text文件夹,Text文件夹里必须有chapter.html,否则导出正文会为空
chapter.html的关键字有{title}、{content} chapter.html的关键字有{title}、{content}
@ -133,73 +203,84 @@ chapter.html的关键字有{title}、{content}
``` ```
**2021/05/24** **2021/05/24**
* 反转目录后刷新内容 * 反转目录后刷新内容
* 修复上下滑动会导致左右切换问题 * 修复上下滑动会导致左右切换问题
* 精确搜索增加包含关键词的,比如搜索五行 五行天也显示出来, 五天行不显示 * 精确搜索增加包含关键词的,比如搜索五行 五行天也显示出来, 五天行不显示
**2021/05/21** **2021/05/21**
* 添加反转目录功能 * 添加反转目录功能
* 修复分享bug * 修复分享bug
* 详情页添加登录菜单 * 详情页添加登录菜单
* 添加发现界面隐藏配置 * 添加发现界面隐藏配置
**2021/05/16** **2021/05/16**
* 添加总是使用默认封面配置 * 添加总是使用默认封面配置
* 添加一种语言 ptbr translation by mezysinc * 添加一种语言 ptbr translation by mezysinc
* epublib 修bug by ag2s20150909 * epublib 修bug by ag2s20150909
**2021/05/08** **2021/05/08**
* 预下载章节可调整数目 * 预下载章节可调整数目
* 修复低版本Android使用TTS闪退。 by ag2s20150909 * 修复低版本Android使用TTS闪退。 by ag2s20150909
* 修复WebDav报错 * 修复WebDav报错
* 优化翻页动画点击翻页 * 优化翻页动画点击翻页
**2021/05/06** **2021/05/06**
* 修复bug * 修复bug
* url参数添加重置次数,retry * url参数添加重置次数,retry
* 修改默认tts, 手动导入 * 修改默认tts, 手动导入
* 升级android studio * 升级android studio
**2021/04/30** **2021/04/30**
* epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909 * epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909
* 添加高刷设置 * 添加高刷设置
* 其它一些优化 * 其它一些优化
* pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本 * pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本
**2021/04/16** **2021/04/16**
* 去掉google统计,解决华为手机使用崩溃的bug * 去掉google统计,解决华为手机使用崩溃的bug
* 添加规则订阅时判断重复提醒 * 添加规则订阅时判断重复提醒
* 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局 * 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局
**2021/04/08** **2021/04/08**
* 缓存时重新检查并缓存图片 * 缓存时重新检查并缓存图片
* 订阅源调试添加源码查看 * 订阅源调试添加源码查看
* web调试不输出源码 * web调试不输出源码
* 修复bug * 修复bug
* 换源优化 * 换源优化 --- by ag2s20150909
--- by ag2s20150909
* 修复localBook获取书名作者名的逻辑 * 修复localBook获取书名作者名的逻辑
* 修复导出的epub的标题文字过大的bug * 修复导出的epub的标题文字过大的bug
* 优化图片排版 * 优化图片排版
**2021/04/02** **2021/04/02**
* 修复bug * 修复bug
* 书源调试添加源码查看 * 书源调试添加源码查看
* 添加导出epub by ag2s20150909 * 添加导出epub by ag2s20150909
* 换源添加是否校验作者选项 * 换源添加是否校验作者选项
**2021/03/31** **2021/03/31**
* 优化epubLib by ag2s20150909 * 优化epubLib by ag2s20150909
* 升级库,修改弃用方法 * 升级库,修改弃用方法
* tts引擎添加导入导出功能 * tts引擎添加导入导出功能
**2021/03/23** **2021/03/23**
* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 * 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右
* js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?> * js添加一个并发访问的方法 java.ajaxAll(urlList: Array<String>) 返回 Array<StrResponse?>
* 优化目录并发访问 * 优化目录并发访问
* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909 * 添加自定义epublib,支持epub v3解析目录。by ag2s20150909
**2021/03/19** **2021/03/19**
* 修复图片地址参数缺少的bug * 修复图片地址参数缺少的bug
* 修复更改替换规则时多次重新加载正文导致朗读多次停顿的bug * 修复更改替换规则时多次重新加载正文导致朗读多次停顿的bug
* 修复是否使用替换默认值修改后不及时生效的bug * 修复是否使用替换默认值修改后不及时生效的bug
@ -207,11 +288,13 @@ chapter.html的关键字有{title}、{content}
* 百度tsn改为tts * 百度tsn改为tts
**2021/03/15** **2021/03/15**
* 优化图片TEXT样式显示 * 优化图片TEXT样式显示
* 图片url在解析正文时就拼接成绝对url * 图片url在解析正文时就拼接成绝对url
* 修复一些bug * 修复一些bug
**2021/03/08** **2021/03/08**
* 阅读页面停留10分钟之后自动备份进度 * 阅读页面停留10分钟之后自动备份进度
* 添加了针对中文的断行排版处理-by hoodie13, 需要再阅读界面设置里手动开启 * 添加了针对中文的断行排版处理-by hoodie13, 需要再阅读界面设置里手动开启
* 添加朗读快捷方式 * 添加朗读快捷方式
@ -222,6 +305,7 @@ chapter.html的关键字有{title}、{content}
* 图片样式添加TEXT, 阅读界面菜单里可以选择图片样式 * 图片样式添加TEXT, 阅读界面菜单里可以选择图片样式
**2021/02/26** **2021/02/26**
* 添加反转内容功能 * 添加反转内容功能
* 更新章节时若无目录url将自动加载详情页 * 更新章节时若无目录url将自动加载详情页
* 添加变量nextChapterUrl * 添加变量nextChapterUrl
@ -231,76 +315,90 @@ chapter.html的关键字有{title}、{content}
* 不重复解析详情页 * 不重复解析详情页
**2021/02/21** **2021/02/21**
* 下一页规则改为在内容规则之后执行 * 下一页规则改为在内容规则之后执行
* 书籍导出增加编码设置和导出文件夹设置,使用替换设置 * 书籍导出增加编码设置和导出文件夹设置,使用替换设置
* 导入源添加等待框 * 导入源添加等待框
* 修复一些崩溃bug * 修复一些崩溃bug
**2021/02/16** **2021/02/16**
* 修复分享内容不对的bug * 修复分享内容不对的bug
* 优化主题颜色,添加透明度 * 优化主题颜色,添加透明度
* rss分类url支持js * rss分类url支持js
* 打开阅读时同步阅读进度 * 打开阅读时同步阅读进度
**2021/02/09** **2021/02/09**
* 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低 * 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低
* 修复保存书源时不更新书源时间的bug * 修复保存书源时不更新书源时间的bug
* 订阅添加夜间模式,需启用js,还不是很完善 * 订阅添加夜间模式,需启用js,还不是很完善
* 优化源导入界面 * 优化源导入界面
**2021/02/03** **2021/02/03**
* 排版导出文件名修改为配置名称 * 排版导出文件名修改为配置名称
* 取消在线朗读下载文件检测,会导致朗读中断 * 取消在线朗读下载文件检测,会导致朗读中断
* 修复其它一些bug * 修复其它一些bug
**2021/01/30** **2021/01/30**
* 优化阅读记录界面 * 优化阅读记录界面
* 自定义分组可以隐藏,删除按钮移到编辑对话框 * 自定义分组可以隐藏,删除按钮移到编辑对话框
* 修复其它一些bug * 修复其它一些bug
**2021/01/23** **2021/01/23**
* 优化书源校验,从搜索到正文全部校验 * 优化书源校验,从搜索到正文全部校验
* play版可以设置背景图片 * play版可以设置背景图片
* 添加几个js方法,见io.legado.app.help.JsExtensions * 添加几个js方法,见io.legado.app.help.JsExtensions
**2021/01/18** **2021/01/18**
* 增加三星 S Pen 支持 by [dacer](https://github.com/dacer) * 增加三星 S Pen 支持 by [dacer](https://github.com/dacer)
* 订阅添加阅读下载,可以从多个渠道下载 * 订阅添加阅读下载,可以从多个渠道下载
* 修复一些BUG * 修复一些BUG
**2021/01/12** **2021/01/12**
* 修复bug * 修复bug
* 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc) * 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc)
* 换源刷新之前删除原搜索记录 * 换源刷新之前删除原搜索记录
* 优化web调试 * 优化web调试
**2021/01/03** **2021/01/03**
* 导出书单只保留书名与作者,导入时自动查找可用源 * 导出书单只保留书名与作者,导入时自动查找可用源
* 添加预加载设置 * 添加预加载设置
* 选择分组时只搜索分组 * 选择分组时只搜索分组
**2020/12/30** **2020/12/30**
* 解决文件下载异常,在线语音可正常播放 by [Celeter](https://github.com/Celeter) * 解决文件下载异常,在线语音可正常播放 by [Celeter](https://github.com/Celeter)
* 更新默认在线朗读库, 默认id小于0方便下次更新时删除旧数据, 有重复的自己删除 * 更新默认在线朗读库, 默认id小于0方便下次更新时删除旧数据, 有重复的自己删除
* 导入导出书单 * 导入导出书单
* 其它一些优化 * 其它一些优化
**2020/12/27** **2020/12/27**
* 订阅添加搜索和分组 * 订阅添加搜索和分组
* 修复部分手机状态栏bug * 修复部分手机状态栏bug
* 单url订阅支持内容规则和样式 * 单url订阅支持内容规则和样式
**2020/12/19** **2020/12/19**
* 书签转移到文本菜单里,会记录选择的文本和位置 * 书签转移到文本菜单里,会记录选择的文本和位置
* 订阅源添加单url选项,直接打开url * 订阅源添加单url选项,直接打开url
* 订阅源可以put,get数据 * 订阅源可以put,get数据
**2020/12/15** **2020/12/15**
* 修复一些引起崩溃的bug * 修复一些引起崩溃的bug
* 修复搜书和换源可能什么分组都没有的bug * 修复搜书和换源可能什么分组都没有的bug
* 添加同步进度开关,默认开启,在备份与恢复里面 * 添加同步进度开关,默认开启,在备份与恢复里面
**2020/12/13** **2020/12/13**
* 修复bug * 修复bug
* 网络访问框架修改为RxHttp, 有bug及时反馈 * 网络访问框架修改为RxHttp, 有bug及时反馈
* 优化进度同步 * 优化进度同步
@ -308,6 +406,7 @@ chapter.html的关键字有{title}、{content}
* 沉浸模式时阅读界面导航栏透明 * 沉浸模式时阅读界面导航栏透明
**2020/12/09** **2020/12/09**
* 修复bug * 修复bug
* 优化中文排序 * 优化中文排序
* 优化编码识别 * 优化编码识别
@ -315,12 +414,14 @@ chapter.html的关键字有{title}、{content}
* 优化进度同步,进入书籍时同步,每次同步单本书,减少同步文件大小 * 优化进度同步,进入书籍时同步,每次同步单本书,减少同步文件大小
**2020/12/04** **2020/12/04**
* 阅读进度从页数改为字数,排版变化时定位更准确 * 阅读进度从页数改为字数,排版变化时定位更准确
* 修改viewBinding * 修改viewBinding
* 修复中文排序 * 修复中文排序
* 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看 * 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看
**2020/11/18** **2020/11/18**
* 优化导航栏 * 优化导航栏
* js添加java.log(msg: String)用于调试时输出消息 * js添加java.log(msg: String)用于调试时输出消息
* js添加cookie变量,方法见io.legado.app.help.http.api.CookieManager * js添加cookie变量,方法见io.legado.app.help.http.api.CookieManager
@ -328,14 +429,17 @@ chapter.html的关键字有{title}、{content}
* 需要token的网站可以用js来写了,比如阿里tts * 需要token的网站可以用js来写了,比如阿里tts
**2020/11/15** **2020/11/15**
* 正文规则添加字体规则,返回ByteArray * 正文规则添加字体规则,返回ByteArray
* js添加方法: * js添加方法:
``` ```
base64DecodeToByteArray(str: String?): ByteArray? base64DecodeToByteArray(str: String?): ByteArray?
base64DecodeToByteArray(str: String?, flags: Int): ByteArray? base64DecodeToByteArray(str: String?, flags: Int): ByteArray?
``` ```
**2020/11/07** **2020/11/07**
* 详情页菜单添加拷贝URL * 详情页菜单添加拷贝URL
* 解决一些书名太长缓存报错的bug * 解决一些书名太长缓存报错的bug
* 添加备份搜索记录 * 添加备份搜索记录
@ -345,37 +449,47 @@ base64DecodeToByteArray(str: String?, flags: Int): ByteArray?
* web支持图片显示 by [六月](https://github.com/Celeter) * web支持图片显示 by [六月](https://github.com/Celeter)
**2020/10/24** **2020/10/24**
* 修复选择错误的bug * 修复选择错误的bug
* 修复长图最后一张不能滚动的bug * 修复长图最后一张不能滚动的bug
* js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie by [AndyBernie](https://github.com/AndyBernie) * js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie
by [AndyBernie](https://github.com/AndyBernie)
``` ```
java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd
java.getCookie("http://baidu.com", "userid") => 1234 java.getCookie("http://baidu.com", "userid") => 1234
``` ```
* 修复简繁转换没有处理标题 * 修复简繁转换没有处理标题
* 每本书可以单独设置翻页动画,在菜单里 * 每本书可以单独设置翻页动画,在菜单里
* 添加重新分段功能,针对每本书,在菜单里,分段代码来自[tumuyan](https://github.com/tumuyan) * 添加重新分段功能,针对每本书,在菜单里,分段代码来自[tumuyan](https://github.com/tumuyan)
**2020/10/18** **2020/10/18**
* 优化分组管理,默认分组可以重命名了 * 优化分组管理,默认分组可以重命名了
* 修复书架空白的bug,是constraintlayout库新版本的bug * 修复书架空白的bug,是constraintlayout库新版本的bug
* 修复分组和崩溃bug * 修复分组和崩溃bug
**2020/10/11** **2020/10/11**
* 优化书源校验 * 优化书源校验
* 语言切换bug修复 by [h11128](https://github.com/h11128) * 语言切换bug修复 by [h11128](https://github.com/h11128)
**2020/10/07** **2020/10/07**
* 更新时预下载10章 * 更新时预下载10章
* 支持更多分组 * 支持更多分组
* url添加js参数,解析url时执行,可在访问url时处理url,例 * url添加js参数,解析url时执行,可在访问url时处理url,例
``` ```
https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"} https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"}
https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"} https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"}
``` ```
**2020/09/29** **2020/09/29**
* 增加了几个方法用于处理文件 by [Celeter](https://github.com/Celeter) * 增加了几个方法用于处理文件 by [Celeter](https://github.com/Celeter)
``` ```
//文件下载,content为十六进制字符串,url用于生成文件名,返回文件路径 //文件下载,content为十六进制字符串,url用于生成文件名,返回文件路径
downloadFile(content: String, url: String): String downloadFile(content: String, url: String): String
@ -384,5 +498,6 @@ unzipFile(zipPath: String): String
//文件夹内所有文件读取 //文件夹内所有文件读取
getTxtInFolder(unzipPath: String): String getTxtInFolder(unzipPath: String): String
``` ```
* 增加type字段,返回16进制字符串,例:`https://www.baidu.com,{"type":"zip"}` * 增加type字段,返回16进制字符串,例:`https://www.baidu.com,{"type":"zip"}`
* 底部操作栏阴影跟随设置调节 * 底部操作栏阴影跟随设置调节

@ -89,11 +89,6 @@ function checkFile(file) {
return null; return null;
} }
/** /**
* 添加文件时回调的函数 * 添加文件时回调的函数
* @param object file 文件对象 * @param object file 文件对象

@ -123,12 +123,16 @@ try {
//正在上传 //正在上传
isUploading = true; isUploading = true;
//设置上传的数据 //设置上传的数据
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
var data = e.target.result;
var fd = new FormData(); var fd = new FormData();
fd.append("Filename", file.name); fd.append("fileName", file.name);
fd.append("Filedata", file); fd.append("fileData", data);
fd.append("Upload", "Submit Query");
//设置当前的上传对象 //设置当前的上传对象
currUploadfile = file; currUploadfile = file;
if (XHR.readyState > 0) { if (XHR.readyState > 0) {
XHR = new XMLHttpRequest(); XHR = new XMLHttpRequest();
} }
@ -140,7 +144,6 @@ try {
XHR.upload.addEventListener("loadend", loadend, false); XHR.upload.addEventListener("loadend", loadend, false);
XHR.upload.addEventListener("loadstart", loadstart, false); XHR.upload.addEventListener("loadstart", loadstart, false);
XHR.open("POST", config.url); XHR.open("POST", config.url);
XHR.setRequestHeader("Content-Type", "multipart/mixed stream");
XHR.send(fd); XHR.send(fd);
XHR.onreadystatechange = function () { XHR.onreadystatechange = function () {
@ -158,6 +161,7 @@ try {
nextUpload() nextUpload()
} }
}; };
}
} }

@ -14,6 +14,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.LifecycleHelp import io.legado.app.help.LifecycleHelp
import io.legado.app.help.ThemeConfig.applyDayNight import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.cronet.CronetLoader
import io.legado.app.utils.LanguageUtils import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
@ -22,6 +23,10 @@ class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
CrashHandler(this) CrashHandler(this)
if (AppConfig.isCronet) {
//预下载Cronet so
CronetLoader.preDownload()
}
LanguageUtils.setConfiguration(this) LanguageUtils.setConfiguration(this)
createNotificationChannels() createNotificationChannels()
applyDayNight(this) applyDayNight(this)

@ -1,8 +1,7 @@
package io.legado.app.api.controller package io.legado.app.api.controller
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import fi.iki.elonen.NanoFileUpload
import fi.iki.elonen.NanoHTTPD
import io.legado.app.R import io.legado.app.R
import io.legado.app.api.ReturnData import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -19,7 +18,6 @@ import io.legado.app.service.help.ReadBook
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.commons.fileupload.disk.DiskFileItemFactory
import splitties.init.appCtx import splitties.init.appCtx
object BookController { object BookController {
@ -193,31 +191,31 @@ object BookController {
} }
} }
private val bookFileFactory by lazy { fun addLocalBook(parameters: Map<String, List<String>>): ReturnData {
DiskFileItemFactory(0, LocalBook.cacheFolder)
}
fun addLocalBook(session: NanoHTTPD.IHTTPSession, postData: String?): ReturnData {
val returnData = ReturnData() val returnData = ReturnData()
try { try {
NanoFileUpload(bookFileFactory).parseRequest(session).forEach { val fileName = parameters["fileName"]?.firstOrNull()
val path = FileUtils.getPath(LocalBook.cacheFolder, it.name) ?: return returnData.setErrorMsg("fileName 不能为空")
val nameAuthor = LocalBook.analyzeNameAuthor(it.name) val fileData = parameters["fileData"]?.firstOrNull()
?: return returnData.setErrorMsg("fileData 不能为空")
val file = FileUtils.createFileIfNotExist(LocalBook.cacheFolder, fileName)
val fileBytes = Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
file.writeBytes(fileBytes)
val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
val book = Book( val book = Book(
bookUrl = path, bookUrl = file.absolutePath,
name = nameAuthor.first, name = nameAuthor.first,
author = nameAuthor.second, author = nameAuthor.second,
originName = it.name, originName = fileName,
coverUrl = FileUtils.getPath( coverUrl = FileUtils.getPath(
appCtx.externalFiles, appCtx.externalFiles,
"covers", "covers",
"${MD5Utils.md5Encode16(path)}.jpg" "${MD5Utils.md5Encode16(file.absolutePath)}.jpg"
) )
) )
if (book.isEpub()) EpubFile.upBookInfo(book) if (book.isEpub()) EpubFile.upBookInfo(book)
if (book.isUmd()) UmdFile.upBookInfo(book) if (book.isUmd()) UmdFile.upBookInfo(book)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return returnData.setErrorMsg( return returnData.setErrorMsg(

@ -2,6 +2,7 @@ package io.legado.app.base
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.BitmapDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
@ -174,8 +175,8 @@ abstract class BaseActivity<VB : ViewBinding>(
} }
if (imageBg) { if (imageBg) {
try { try {
ThemeConfig.getBgImage(this)?.let { ThemeConfig.getBgImage(this, windowSize)?.let {
window.decorView.background = it window.decorView.background = BitmapDrawable(resources, it)
} }
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
toastOnUi(e.localizedMessage) toastOnUi(e.localizedMessage)

@ -1,5 +1,6 @@
package io.legado.app.base.adapter package io.legado.app.base.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.SparseArray import android.util.SparseArray
import android.view.LayoutInflater import android.view.LayoutInflater
@ -84,6 +85,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@SuppressLint("NotifyDataSetChanged")
@Synchronized @Synchronized
fun setItems(items: List<ITEM>?) { fun setItems(items: List<ITEM>?) {
kotlin.runCatching { kotlin.runCatching {

@ -5,7 +5,7 @@ import java.util.regex.Pattern
@Suppress("RegExpRedundantEscape") @Suppress("RegExpRedundantEscape")
object AppPattern { object AppPattern {
val JS_PATTERN: Pattern = val JS_PATTERN: Pattern =
Pattern.compile("<js>([\\w\\W]+?)</js>|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE) Pattern.compile("<js>([\\w\\W]*?)</js>|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
//匹配格式化后的图片格式 //匹配格式化后的图片格式

@ -3,7 +3,7 @@ package io.legado.app.constant
object EventBus { object EventBus {
const val MEDIA_BUTTON = "mediaButton" const val MEDIA_BUTTON = "mediaButton"
const val RECREATE = "RECREATE" const val RECREATE = "RECREATE"
const val UP_BOOK = "upBookToc" const val UP_BOOKSHELF = "upBookToc"
const val BOOKSHELF_REFRESH = "bookshelfRefresh" const val BOOKSHELF_REFRESH = "bookshelfRefresh"
const val ALOUD_STATE = "aloud_state" const val ALOUD_STATE = "aloud_state"
const val TTS_PROGRESS = "ttsStart" const val TTS_PROGRESS = "ttsStart"
@ -24,4 +24,5 @@ object EventBus {
const val CHECK_SOURCE = "checkSource" const val CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone" const val CHECK_SOURCE_DONE = "checkSourceDone"
const val TIP_COLOR = "tipColor" const val TIP_COLOR = "tipColor"
const val SOURCE_CHANGED = "sourceChanged"
} }

@ -67,6 +67,7 @@ object PreferKey {
const val transparentStatusBar = "transparentStatusBar" const val transparentStatusBar = "transparentStatusBar"
const val immNavigationBar = "immNavigationBar" const val immNavigationBar = "immNavigationBar"
const val defaultCover = "defaultCover" const val defaultCover = "defaultCover"
const val defaultCoverDark = "defaultCoverDark"
const val replaceEnableDefault = "replaceEnableDefault" const val replaceEnableDefault = "replaceEnableDefault"
const val showBrightnessView = "showBrightnessView" const val showBrightnessView = "showBrightnessView"
const val autoClearExpired = "autoClearExpired" const val autoClearExpired = "autoClearExpired"
@ -91,11 +92,13 @@ object PreferKey {
const val cBackground = "colorBackground" const val cBackground = "colorBackground"
const val cBBackground = "colorBottomBackground" const val cBBackground = "colorBottomBackground"
const val bgImage = "backgroundImage" const val bgImage = "backgroundImage"
const val bgImageBlurring = "backgroundImageBlurring"
const val cNPrimary = "colorPrimaryNight" const val cNPrimary = "colorPrimaryNight"
const val cNAccent = "colorAccentNight" const val cNAccent = "colorAccentNight"
const val cNBackground = "colorBackgroundNight" const val cNBackground = "colorBackgroundNight"
const val cNBBackground = "colorBottomBackgroundNight" const val cNBBackground = "colorBottomBackgroundNight"
const val bgImageN = "backgroundImageNight" const val bgImageN = "backgroundImageNight"
const val bgImageNBlurring = "backgroundImageNightBlurring"
} }

@ -24,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class], RuleSub::class],
version = 33, version = 34,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -60,7 +60,7 @@ abstract class AppDatabase : RoomDatabase() {
migration_19_20, migration_20_21, migration_21_22, migration_22_23, migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25, migration_25_26, migration_26_27, migration_23_24, migration_24_25, migration_25_26, migration_26_27,
migration_27_28, migration_28_29, migration_29_30, migration_30_31, migration_27_28, migration_28_29, migration_29_30, migration_30_31,
migration_31_32, migration_32_33 migration_31_32, migration_32_33, migration_33_34
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -331,6 +331,12 @@ abstract class AppDatabase : RoomDatabase() {
) )
} }
} }
private val migration_33_34 = object : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
}
}
} }
} }

@ -62,6 +62,9 @@ interface BookDao {
@get:Query("SELECT COUNT(*) FROM books") @get:Query("SELECT COUNT(*) FROM books")
val allBookCount: Int val allBookCount: Int
@get:Query("select min(`order`) from books")
val minOrder: Int
@get:Query("select max(`order`) from books") @get:Query("select max(`order`) from books")
val maxOrder: Int val maxOrder: Int

@ -1,9 +1,6 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
@Dao @Dao
@ -65,4 +62,9 @@ interface SearchBookDao {
@Query("delete from searchBooks where time < :time") @Query("delete from searchBooks where time < :time")
fun clearExpired(time: Long) fun clearExpired(time: Long)
@Update
fun update(vararg searchBook: SearchBook)
@Delete
fun delete(vararg searchBook: SearchBook)
} }

@ -14,6 +14,7 @@ data class BookGroup(
@PrimaryKey @PrimaryKey
val groupId: Long = 0b1, val groupId: Long = 0b1,
var groupName: String, var groupName: String,
var cover: String? = null,
var order: Int = 0, var order: Int = 0,
var show: Boolean = true var show: Boolean = true
) : Parcelable { ) : Parcelable {
@ -36,6 +37,7 @@ data class BookGroup(
if (other is BookGroup) { if (other is BookGroup) {
return other.groupId == groupId return other.groupId == groupId
&& other.groupName == groupName && other.groupName == groupName
&& other.cover == cover
&& other.order == order && other.order == order
&& other.show == show && other.show == show
} }

@ -46,6 +46,17 @@ data class BookSource(
var ruleContent: ContentRule? = null // 正文页规则 var ruleContent: ContentRule? = null // 正文页规则
) : Parcelable, JsExtensions { ) : Parcelable, JsExtensions {
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
val loginRule by lazy {
if (loginUrl.isJsonObject()) {
return@lazy GSON.fromJsonObject<LoginRule>(loginUrl)
} else {
return@lazy LoginRule(url = loginUrl)
}
}
@delegate:Transient @delegate:Transient
@delegate:Ignore @delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel

@ -0,0 +1,11 @@
package io.legado.app.data.entities.rule
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class LoginRule(
val ui: HashMap<String, String>? = null,
val url: String? = null,
val checkJs: String? = null
) : Parcelable

@ -143,7 +143,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
get() = appCtx.getPrefBoolean(PreferKey.autoRefresh) get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
var threadCount: Int var threadCount: Int
get() = appCtx.getPrefInt(PreferKey.threadCount, 16) get() = appCtx.getPrefInt(PreferKey.threadCount, 8)
set(value) { set(value) {
appCtx.putPrefInt(PreferKey.threadCount, value) appCtx.putPrefInt(PreferKey.threadCount, value)
} }

@ -12,6 +12,7 @@ import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import java.io.File import java.io.File
@Suppress("unused")
object ImageLoader { object ImageLoader {
/** /**

@ -3,7 +3,6 @@ package io.legado.app.help
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import io.legado.app.R import io.legado.app.R
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@ -37,26 +36,22 @@ object IntentHelp {
inline fun <reified T> servicePendingIntent( inline fun <reified T> servicePendingIntent(
context: Context, context: Context,
action: String, action: String,
bundle: Bundle? = null configIntent: Intent.() -> Unit = {}
): PendingIntent? { ): PendingIntent? {
val intent = Intent(context, T::class.java) val intent = Intent(context, T::class.java)
intent.action = action intent.action = action
bundle?.let { configIntent.invoke(intent)
intent.putExtras(bundle)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
inline fun <reified T> activityPendingIntent( inline fun <reified T> activityPendingIntent(
context: Context, context: Context,
action: String, action: String,
bundle: Bundle? = null configIntent: Intent.() -> Unit = {}
): PendingIntent? { ): PendingIntent? {
val intent = Intent(context, T::class.java) val intent = Intent(context, T::class.java)
intent.action = action intent.action = action
bundle?.let { configIntent.invoke(intent)
intent.putExtras(bundle)
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

@ -28,13 +28,13 @@ object LocalConfig {
val ruleHelpVersionIsLast: Boolean val ruleHelpVersionIsLast: Boolean
get() = isLastVersion(1, "ruleHelpVersion") get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean val needUpHttpTTS: Boolean
get() = !isLastVersion(3, "httpTtsVersion") get() = !isLastVersion(3, "httpTtsVersion")
val hasUpTxtTocRule: Boolean val needUpTxtTocRule: Boolean
get() = !isLastVersion(1, "txtTocRuleVersion") get() = !isLastVersion(1, "txtTocRuleVersion")
val hasUpRssSources: Boolean val needUpRssSources: Boolean
get() = !isLastVersion(4, "rssSourceVersion") get() = !isLastVersion(4, "rssSourceVersion")
var versionCode var versionCode

@ -135,7 +135,7 @@ object ReadBookConfig {
//配置写入读取 //配置写入读取
var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46) var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 10)
set(value) { set(value) {
field = value field = value
appCtx.putPrefInt(PreferKey.autoReadSpeed, value) appCtx.putPrefInt(PreferKey.autoReadSpeed, value)

@ -1,9 +1,9 @@
package io.legado.app.help package io.legado.app.help
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.BitmapDrawable import android.util.DisplayMetrics
import android.graphics.drawable.Drawable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import io.legado.app.R import io.legado.app.R
@ -11,6 +11,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
@ -28,6 +29,7 @@ object ThemeConfig {
ReadBookConfig.upBg() ReadBookConfig.upBg()
applyTheme(context) applyTheme(context)
initNightMode() initNightMode()
CoverImageView.upDefaultCover()
postEvent(EventBus.RECREATE, "") postEvent(EventBus.RECREATE, "")
} }
@ -41,14 +43,26 @@ object ThemeConfig {
AppCompatDelegate.setDefaultNightMode(targetMode) AppCompatDelegate.setDefaultNightMode(targetMode)
} }
fun getBgImage(context: Context): Drawable? { fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? {
val bgPath = when (Theme.getTheme()) { val bgCfg = when (Theme.getTheme()) {
Theme.Light -> context.getPrefString(PreferKey.bgImage) Theme.Light -> Pair(
Theme.Dark -> context.getPrefString(PreferKey.bgImageN) context.getPrefString(PreferKey.bgImage),
context.getPrefInt(PreferKey.bgImageBlurring, 0)
)
Theme.Dark -> Pair(
context.getPrefString(PreferKey.bgImageN),
context.getPrefInt(PreferKey.bgImageNBlurring, 0)
)
else -> null else -> null
} } ?: return null
if (bgPath.isNullOrBlank()) return null if (bgCfg.first.isNullOrBlank()) return null
return BitmapDrawable.createFromPath(bgPath) val bgImage = BitmapUtils
.decodeBitmap(bgCfg.first!!, metrics.widthPixels, metrics.heightPixels)
?: return null
if (bgCfg.second == 0) {
return bgImage
}
return BitmapUtils.stackBlur(bgImage, bgCfg.second.toFloat())
} }
fun upConfig() { fun upConfig() {

@ -2,6 +2,7 @@ package io.legado.app.help.http
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.http.cronet.CronetInterceptor import io.legado.app.help.http.cronet.CronetInterceptor
import io.legado.app.help.http.cronet.CronetLoader
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
import okhttp3.Credentials import okhttp3.Credentials
@ -43,8 +44,9 @@ val okHttpClient: OkHttpClient by lazy {
.build() .build()
chain.proceed(request) chain.proceed(request)
}) })
if (AppConfig.isCronet) { if (AppConfig.isCronet && CronetLoader.install()) {
builder.addInterceptor(CronetInterceptor()) //提供CookieJar 用于同步Cookie
builder.addInterceptor(CronetInterceptor(null))
} }

@ -1,6 +1,6 @@
package io.legado.app.help.http.cronet package io.legado.app.help.http.cronet
import io.legado.app.help.http.CookieStore import android.util.Log
import okhttp3.Headers import okhttp3.Headers
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
@ -14,19 +14,28 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
val executor: Executor by lazy { Executors.newSingleThreadExecutor() } val executor: Executor by lazy { Executors.newCachedThreadPool() }
val cronetEngine: ExperimentalCronetEngine by lazy { val cronetEngine: ExperimentalCronetEngine by lazy {
CronetLoader.preDownload()
val builder = ExperimentalCronetEngine.Builder(appCtx) val builder = ExperimentalCronetEngine.Builder(appCtx)
.setStoragePath(appCtx.externalCacheDir?.absolutePath) .setLibraryLoader(CronetLoader)//设置自定义so库加载
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50)) .setStoragePath(appCtx.externalCacheDir?.absolutePath)//设置缓存路径
.enableQuic(true) .enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))//设置缓存模式
.enableQuic(true)//设置支持http/3
.enableHttp2(true) //设置支持http/2
.enablePublicKeyPinningBypassForLocalTrustAnchors(true) .enablePublicKeyPinningBypassForLocalTrustAnchors(true)
.enableHttp2(true) //.enableNetworkQualityEstimator(true)
//Brotli压缩 //Brotli压缩
builder.enableBrotli(true) builder.enableBrotli(true)
return@lazy builder.build() //builder.setExperimentalOptions("{\"quic_version\": \"h3-29\"}")
val engine = builder.build()
Log.d("Cronet", "Cronet Version:" + engine.versionString)
//这会导致Jsoup的网络请求出现问题,暂时不接管系统URL
//URL.setURLStreamHandlerFactory(CronetURLStreamHandlerFactory(engine))
return@lazy engine
} }
@ -35,17 +44,10 @@ fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest {
val url = request.url.toString() val url = request.url.toString()
val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor) val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor)
requestBuilder.setHttpMethod(request.method) requestBuilder.setHttpMethod(request.method)
val cookie = CookieStore.getCookie(url)
if (cookie.length > 1) {
requestBuilder.addHeader("Cookie", cookie)
}
val headers: Headers = request.headers val headers: Headers = request.headers
headers.forEachIndexed { index, _ -> headers.forEachIndexed { index, _ ->
val name = headers.name(index) requestBuilder.addHeader(headers.name(index), headers.value(index))
if (!name.equals("Keep-Alive", true)) {
requestBuilder.addHeader(name, headers.value(index))
}
} }
val requestBody = request.body val requestBody = request.body

@ -1,23 +1,67 @@
package io.legado.app.help.http.cronet package io.legado.app.help.http.cronet
import okhttp3.Call import io.legado.app.help.http.CookieStore
import okhttp3.Interceptor import okhttp3.*
import okhttp3.Request
import okhttp3.Response
import java.io.IOException import java.io.IOException
class CronetInterceptor : Interceptor { class CronetInterceptor(private val cookieJar: CookieJar?) : Interceptor {
@Throws(IOException::class) @Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
return proceedWithCronet(chain.request(), chain.call()) val original: Request = chain.request()
val builder: Request.Builder = original.newBuilder()
//Cronet未初始化
return if (!CronetLoader.install()) {
chain.proceed(original)
} else try {
//移除Keep-Alive,手动设置会导致400 BadRequest
builder.removeHeader("Keep-Alive")
val cookieStr = getCookie(original.url)
//设置Cookie
if (cookieStr.length > 3) {
builder.header("Cookie", cookieStr)
}
val new = builder.build()
val response: Response = proceedWithCronet(new, chain.call())
//从Response 中保存Cookie到CookieJar
cookieJar?.saveFromResponse(new.url, Cookie.parseAll(new.url, response.headers))
response
} catch (e: Exception) {
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理
if (e.message.toString().contains("ERR_CERT_DATE_INVALID", true)) {
chain.proceed(original)
} else {
throw e
}
}
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun proceedWithCronet(request: Request, call: Call): Response { private fun proceedWithCronet(request: Request, call: Call): Response {
val callback = CronetUrlRequestCallback(request, call) val callback = CronetUrlRequestCallback(request, call)
val urlRequest = buildRequest(request, callback) val urlRequest = buildRequest(request, callback)
urlRequest.start() urlRequest.start()
return callback.waitForDone() return callback.waitForDone()
} }
private fun getCookie(url: HttpUrl): String {
val sb = StringBuilder()
//处理从 Cookjar 获取到的Cookies
if (cookieJar != null) {
val cookies = cookieJar.loadForRequest(url)
for (cookie in cookies) {
sb.append(cookie.name).append("=").append(cookie.value).append("; ")
}
}
//处理自定义的Cookie
val cookie = CookieStore.getCookie(url.toString())
if (cookie.length > 3) {
sb.append(cookie)
}
return sb.toString()
}
} }

@ -0,0 +1,357 @@
package io.legado.app.help.http.cronet
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build
import android.text.TextUtils
import android.util.Log
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefString
import org.chromium.net.CronetEngine
import org.chromium.net.impl.ImplVersion
import splitties.init.appCtx
import java.io.*
import java.math.BigInteger
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.*
object CronetLoader : CronetEngine.Builder.LibraryLoader() {
//https://storage.googleapis.com/chromium-cronet/android/92.0.4515.127/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.127.so
//https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@92.0.4515.127/cronet/92.0.4515.127/arm64-v8a/libcronet.92.0.4515.127.so.js
private const val TAG = "CronetLoader"
private val soName = "libcronet." + ImplVersion.getCronetVersion() + ".so"
private val soUrl: String
private val md5Url: String
private val soFile: File
private val downloadFile: File
private var cpuAbi: String? = null
private var md5: String? = appCtx.getPrefString("soMd5")
private val version: String? = appCtx.getPrefString("soVersion", ImplVersion.getCronetVersion())
var download = false
init {
soUrl = ("https://storage.googleapis.com/chromium-cronet/android/"
+ ImplVersion.getCronetVersion() + "/Release/cronet/libs/"
+ getCpuAbi(appCtx) + "/" + soName)
md5Url = ("https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@" +
ImplVersion.getCronetVersion() + "/cronet/" + ImplVersion.getCronetVersion() + "/"
+ getCpuAbi(appCtx) + "/" + soName + ".js")
val dir = appCtx.getDir("lib", Context.MODE_PRIVATE)
soFile = File(dir.toString() + "/" + getCpuAbi(appCtx), soName)
downloadFile = File(appCtx.cacheDir.toString() + "/so_download", soName)
Log.e(TAG, "soName+:$soName")
Log.e(TAG, "destSuccessFile:$soFile")
Log.e(TAG, "tempFile:$downloadFile")
Log.e(TAG, "soUrl:$soUrl")
}
fun install(): Boolean {
return soFile.exists()
}
fun preDownload() {
Coroutine.async {
md5 = getUrlMd5(md5Url)
if (soFile.exists() && md5 == getFileMD5(soFile)) {
Log.e(TAG, "So 库已存在")
} else {
download(soUrl, md5, downloadFile, soFile)
}
Log.e(TAG, soName)
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun loadLibrary(libName: String) {
Log.e(TAG, "libName:$libName")
val start = System.currentTimeMillis()
@Suppress("SameParameterValue")
try {
//非cronet的so调用系统方法加载
if (!libName.contains("cronet")) {
System.loadLibrary(libName)
return
}
//以下逻辑为cronet加载,优先加载本地,否则从远程加载
//首先调用系统行为进行加载
System.loadLibrary(libName)
Log.i(TAG, "load from system")
} catch (e: Throwable) {
//如果找不到,则从远程下载
//删除历史文件
deleteHistoryFile(Objects.requireNonNull(soFile.parentFile), soFile)
md5 = getUrlMd5(md5Url)
Log.i(TAG, "soMD5:$md5")
if (md5 == null || md5!!.length != 32 || soUrl.isEmpty()) {
//如果md5或下载的url为空,则调用系统行为进行加载
System.loadLibrary(libName)
return
}
if (!soFile.exists() || !soFile.isFile) {
soFile.delete()
download(soUrl, md5, downloadFile, soFile)
//如果文件不存在或不是文件,则调用系统行为进行加载
System.loadLibrary(libName)
return
}
if (soFile.exists()) {
//如果文件存在,则校验md5值
val fileMD5 = getFileMD5(soFile)
if (fileMD5 != null && fileMD5.equals(md5, ignoreCase = true)) {
//md5值一样,则加载
System.load(soFile.absolutePath)
Log.e(TAG, "load from:$soFile")
return
}
//md5不一样则删除
soFile.delete()
}
//不存在则下载
download(soUrl, md5, downloadFile, soFile)
//使用系统加载方法
System.loadLibrary(libName)
} finally {
Log.e(TAG, "time:" + (System.currentTimeMillis() - start))
}
}
@SuppressLint("DiscouragedPrivateApi")
private fun getCpuAbi(context: Context): String? {
if (cpuAbi != null) {
return cpuAbi
}
// 5.0以上Application才有primaryCpuAbi字段
try {
val appInfo = context.applicationInfo
val abiField = ApplicationInfo::class.java.getDeclaredField("primaryCpuAbi")
abiField.isAccessible = true
cpuAbi = abiField[appInfo] as String
} catch (e: Exception) {
e.printStackTrace()
}
if (TextUtils.isEmpty(cpuAbi)) {
cpuAbi = Build.SUPPORTED_ABIS[0]
}
return cpuAbi
}
@Suppress("SameParameterValue")
private fun getUrlMd5(url: String): String? {
//这样在下载成功后,遇到无网条件下,只要版本未发生变化也能获取md5
if (md5 != null && md5!!.length == 32 && version == ImplVersion.getCronetVersion()) {
appCtx.putPrefString("soMd5", md5)
appCtx.putPrefString("soVersion", ImplVersion.getCronetVersion())
return md5
}
val inputStream: InputStream
val outputStream: OutputStream
return try {
outputStream = ByteArrayOutputStream()
val connection = URL(url).openConnection() as HttpURLConnection
inputStream = connection.inputStream
val buffer = ByteArray(1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
outputStream.flush()
}
val tmd5 = outputStream.toString()
//成功获取到md5后保存md5和版本
if (tmd5.length == 32) {
appCtx.putPrefString("soMd5", tmd5)
appCtx.putPrefString("soVersion", ImplVersion.getCronetVersion())
}
return tmd5
} catch (e: IOException) {
null
}
}
/**
* 删除历史文件
*/
private fun deleteHistoryFile(dir: File, currentFile: File?) {
val files = dir.listFiles()
@Suppress("SameParameterValue")
if (files != null && files.isNotEmpty()) {
for (f in files) {
if (f.exists() && (currentFile == null || f.absolutePath != currentFile.absolutePath)) {
val delete = f.delete()
Log.e(TAG, "delete file: $f result: $delete")
if (!delete) {
f.deleteOnExit()
}
}
}
}
}
/**
* 下载文件
*/
private fun downloadFileIfNotExist(url: String, destFile: File): Boolean {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val connection = URL(url).openConnection() as HttpURLConnection
inputStream = connection.inputStream
if (destFile.exists()) {
return true
}
destFile.parentFile!!.mkdirs()
destFile.createNewFile()
outputStream = FileOutputStream(destFile)
val buffer = ByteArray(32768)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
outputStream.flush()
}
return true
} catch (e: Throwable) {
e.printStackTrace()
if (destFile.exists() && !destFile.delete()) {
destFile.deleteOnExit()
}
} finally {
if (inputStream != null) {
try {
inputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (outputStream != null) {
try {
outputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return false
}
/**
* 下载并拷贝文件
*/
@Suppress("SameParameterValue")
@Synchronized
private fun download(
url: String,
md5: String?,
downloadTempFile: File,
destSuccessFile: File
) {
if (download) {
return
}
download = true
executor.execute {
val result = downloadFileIfNotExist(url, downloadTempFile)
Log.e(TAG, "download result:$result")
//文件md5再次校验
val fileMD5 = getFileMD5(downloadTempFile)
if (md5 != null && !md5.equals(fileMD5, ignoreCase = true)) {
val delete = downloadTempFile.delete()
if (!delete) {
downloadTempFile.deleteOnExit()
}
download = false
return@execute
}
Log.e(TAG, "download success, copy to $destSuccessFile")
//下载成功拷贝文件
copyFile(downloadTempFile, destSuccessFile)
val parentFile = downloadTempFile.parentFile
@Suppress("SameParameterValue")
deleteHistoryFile(parentFile!!, null)
}
}
/**
* 拷贝文件
*/
private fun copyFile(source: File?, dest: File?): Boolean {
if (source == null || !source.exists() || !source.isFile || dest == null) {
return false
}
if (source.absolutePath == dest.absolutePath) {
return true
}
var fileInputStream: FileInputStream? = null
var os: FileOutputStream? = null
val parent = dest.parentFile
if (parent != null && !parent.exists()) {
val mkdirs = parent.mkdirs()
if (!mkdirs) {
parent.mkdirs()
}
}
try {
fileInputStream = FileInputStream(source)
os = FileOutputStream(dest, false)
val buffer = ByteArray(1024 * 512)
var length: Int
while (fileInputStream.read(buffer).also { length = it } > 0) {
os.write(buffer, 0, length)
}
return true
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
if (os != null) {
try {
os.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return false
}
/**
* 获得文件md5
*/
private fun getFileMD5(file: File): String? {
var fileInputStream: FileInputStream? = null
try {
fileInputStream = FileInputStream(file)
val md5 = MessageDigest.getInstance("MD5")
val buffer = ByteArray(1024)
var numRead: Int
while (fileInputStream.read(buffer).also { numRead = it } > 0) {
md5.update(buffer, 0, numRead)
}
return String.format("%032x", BigInteger(1, md5.digest())).lowercase()
} catch (e: Exception) {
e.printStackTrace()
} catch (e: OutOfMemoryError) {
e.printStackTrace()
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return null
}
}

@ -6,14 +6,13 @@ import io.legado.app.help.http.okHttpClient
import okhttp3.* import okhttp3.*
import okhttp3.EventListener import okhttp3.EventListener
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer
import org.chromium.net.CronetException import org.chromium.net.CronetException
import org.chromium.net.UrlRequest import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo import org.chromium.net.UrlResponseInfo
import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.util.* import java.util.*
class CronetUrlRequestCallback @JvmOverloads internal constructor( class CronetUrlRequestCallback @JvmOverloads internal constructor(
@ -29,8 +28,7 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
private var mResponse: Response private var mResponse: Response
private var mException: IOException? = null private var mException: IOException? = null
private val mResponseCondition = ConditionVariable() private val mResponseCondition = ConditionVariable()
private val mBytesReceived = ByteArrayOutputStream() private val mBuffer = Buffer()
private val mReceiveChannel = Channels.newChannel(mBytesReceived)
@Throws(IOException::class) @Throws(IOException::class)
fun waitForDone(): Response { fun waitForDone(): Response {
@ -64,6 +62,7 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) { override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
this.mResponse = responseFromResponse(this.mResponse, info) this.mResponse = responseFromResponse(this.mResponse, info)
// 用于调试
// val sb: StringBuilder = StringBuilder(info.url).append("\r\n") // val sb: StringBuilder = StringBuilder(info.url).append("\r\n")
// sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:") // sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:")
// .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText) // .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText)
@ -74,6 +73,8 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
// sb.append("[").append(h.key).append("]").append(h.value).append("\r\n"); // sb.append("[").append(h.key).append("]").append(h.value).append("\r\n");
// } // }
// Log.e("Cronet", sb.toString()) // Log.e("Cronet", sb.toString())
//打印协议,用于调试
Log.e("Cronet", info.negotiatedProtocol)
if (eventListener != null) { if (eventListener != null) {
eventListener.responseHeadersEnd(mCall, this.mResponse) eventListener.responseHeadersEnd(mCall, this.mResponse)
eventListener.responseBodyStart(mCall) eventListener.responseBodyStart(mCall)
@ -89,7 +90,8 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
) { ) {
byteBuffer.flip() byteBuffer.flip()
try { try {
mReceiveChannel.write(byteBuffer) //mReceiveChannel.write(byteBuffer)
mBuffer.write(byteBuffer)
} catch (e: IOException) { } catch (e: IOException) {
Log.i(TAG, "IOException during ByteBuffer read. Details: ", e) Log.i(TAG, "IOException during ByteBuffer read. Details: ", e)
throw e throw e
@ -102,8 +104,10 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
eventListener?.responseBodyEnd(mCall, info.receivedByteCount) eventListener?.responseBodyEnd(mCall, info.receivedByteCount)
val contentType: MediaType? = (this.mResponse.header("content-type") val contentType: MediaType? = (this.mResponse.header("content-type")
?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull() ?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull()
// val responseBody: ResponseBody =
// mBytesReceived.toByteArray().toResponseBody(contentType)
val responseBody: ResponseBody = val responseBody: ResponseBody =
mBytesReceived.toByteArray().toResponseBody(contentType) mBuffer.asResponseBody(contentType)
val newRequest = originalRequest.newBuilder().url(info.url).build() val newRequest = originalRequest.newBuilder().url(info.url).build()
this.mResponse = this.mResponse.newBuilder().body(responseBody).request(newRequest).build() this.mResponse = this.mResponse.newBuilder().body(responseBody).request(newRequest).build()
mResponseCondition.open() mResponseCondition.open()
@ -142,11 +146,11 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
companion object { companion object {
private const val TAG = "Callback" private const val TAG = "Callback"
private const val MAX_FOLLOW_COUNT = 20 private const val MAX_FOLLOW_COUNT = 20
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol { private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault()) val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
// Log.e("Cronet", responseInfo.url) // Log.e("Cronet", responseInfo.url)
// Log.e("Cronet", negotiatedProtocol) // Log.e("Cronet", negotiatedProtocol)
return when { return when {
negotiatedProtocol.contains("h3") -> { negotiatedProtocol.contains("h3") -> {
return Protocol.QUIC return Protocol.QUIC
@ -155,6 +159,7 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
Protocol.QUIC Protocol.QUIC
} }
negotiatedProtocol.contains("spdy") -> { negotiatedProtocol.contains("spdy") -> {
@Suppress("DEPRECATION")
Protocol.SPDY_3 Protocol.SPDY_3
} }
negotiatedProtocol.contains("h2") -> { negotiatedProtocol.contains("h2") -> {

@ -3,6 +3,7 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
@ -49,6 +50,8 @@ object Backup {
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async { Coroutine.async {
backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true) backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true)
}.onError {
appCtx.toastOnUi(R.string.autobackup_fail)
} }
} }
} }

@ -102,7 +102,7 @@ object BookWebDav {
suspend fun backUpWebDav(path: String) { suspend fun backUpWebDav(path: String) {
try { try {
if (initWebDav()) { if (initWebDav() && NetworkUtils.isAvailable()) {
val paths = arrayListOf(*Backup.backupFileNames) val paths = arrayListOf(*Backup.backupFileNames)
for (i in 0 until paths.size) { for (i in 0 until paths.size) {
paths[i] = path + File.separator + paths[i] paths[i] = path + File.separator + paths[i]
@ -116,15 +116,13 @@ object BookWebDav {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
appCtx.toastOnUi("WebDav\n${e.localizedMessage}") appCtx.toastOnUi("WebDav\n${e.localizedMessage}")
} }
} }
}
suspend fun exportWebDav(byteArray: ByteArray, fileName: String) { suspend fun exportWebDav(byteArray: ByteArray, fileName: String) {
try { try {
if (initWebDav()) { if (initWebDav() && NetworkUtils.isAvailable()) {
// 默认导出到legado文件夹下exports目录 // 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/" val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话 // 在legado文件夹创建exports目录,如果不存在的话
@ -141,6 +139,7 @@ object BookWebDav {
} }
fun uploadBookProgress(book: Book) { fun uploadBookProgress(book: Book) {
if (!NetworkUtils.isAvailable()) return
Coroutine.async { Coroutine.async {
val bookProgress = BookProgress( val bookProgress = BookProgress(
name = book.name, name = book.name,
@ -159,7 +158,7 @@ object BookWebDav {
} }
suspend fun getBookProgress(book: Book): BookProgress? { suspend fun getBookProgress(book: Book): BookProgress? {
if (initWebDav()) { if (initWebDav() && NetworkUtils.isAvailable()) {
val url = getProgressUrl(book) val url = getProgressUrl(book)
WebDav(url).download()?.let { byteArray -> WebDav(url).download()?.let { byteArray ->
val json = String(byteArray) val json = String(byteArray)

@ -58,7 +58,8 @@ object Restore {
//默认忽略keys //默认忽略keys
private val ignorePrefKeys = arrayOf( private val ignorePrefKeys = arrayOf(
PreferKey.defaultCover PreferKey.defaultCover,
PreferKey.defaultCoverDark
) )
private val readPrefKeys = arrayOf( private val readPrefKeys = arrayOf(
PreferKey.readStyleSelect, PreferKey.readStyleSelect,

@ -7,7 +7,6 @@ import org.jsoup.select.Collector
import org.jsoup.select.Elements import org.jsoup.select.Elements
import org.jsoup.select.Evaluator import org.jsoup.select.Evaluator
import org.seimicrawler.xpath.JXNode import org.seimicrawler.xpath.JXNode
import java.util.*
/** /**
* Created by GKF on 2018/1/25. * Created by GKF on 2018/1/25.
@ -16,10 +15,6 @@ import java.util.*
@Keep @Keep
class AnalyzeByJSoup(doc: Any) { class AnalyzeByJSoup(doc: Any) {
companion object { companion object {
/**
* "class", "id", "tag", "text", "children"
*/
val validKeys = arrayOf("class", "id", "tag", "text", "children")
fun parse(doc: Any): Element { fun parse(doc: Any): Element {
return when (doc) { return when (doc) {
@ -75,7 +70,7 @@ class AnalyzeByJSoup(doc: Any) {
val results = ArrayList<List<String>>() val results = ArrayList<List<String>>()
for (ruleStrX in ruleStrS) { for (ruleStrX in ruleStrS) {
val temp: List<String>? = val temp: ArrayList<String>? =
if (sourceRule.isCss) { if (sourceRule.isCss) {
val lastIndex = ruleStrX.lastIndexOf('@') val lastIndex = ruleStrX.lastIndexOf('@')
getResultLast( getResultLast(
@ -87,11 +82,8 @@ class AnalyzeByJSoup(doc: Any) {
} }
if (!temp.isNullOrEmpty()) { if (!temp.isNullOrEmpty()) {
results.add(temp)
results.add(temp) //!temp.isNullOrEmpty()时,results.isNotEmpty()为true
if (ruleAnalyzes.elementsType == "||") break if (ruleAnalyzes.elementsType == "||") break
} }
} }
if (results.size > 0) { if (results.size > 0) {
@ -185,7 +177,7 @@ class AnalyzeByJSoup(doc: Any) {
/** /**
* 获取内容列表 * 获取内容列表
*/ */
private fun getResultList(ruleStr: String): List<String>? { private fun getResultList(ruleStr: String): ArrayList<String>? {
if (ruleStr.isEmpty()) return null if (ruleStr.isEmpty()) return null
@ -214,33 +206,43 @@ class AnalyzeByJSoup(doc: Any) {
/** /**
* 根据最后一个规则获取内容 * 根据最后一个规则获取内容
*/ */
private fun getResultLast(elements: Elements, lastRule: String): List<String> { private fun getResultLast(elements: Elements, lastRule: String): ArrayList<String> {
val textS = ArrayList<String>() val textS = ArrayList<String>()
try { try {
when (lastRule) { when (lastRule) {
"text" -> for (element in elements) { "text" -> for (element in elements) {
textS.add(element.text()) val text = element.text()
if (text.isNotEmpty()) {
textS.add(text)
}
} }
"textNodes" -> for (element in elements) { "textNodes" -> for (element in elements) {
val tn = arrayListOf<String>() val tn = arrayListOf<String>()
val contentEs = element.textNodes() val contentEs = element.textNodes()
for (item in contentEs) { for (item in contentEs) {
val temp = item.text().trim { it <= ' ' } val text = item.text().trim { it <= ' ' }
if (temp.isNotEmpty()) { if (text.isNotEmpty()) {
tn.add(temp) tn.add(text)
} }
} }
if (tn.isNotEmpty()) {
textS.add(tn.joinToString("\n")) textS.add(tn.joinToString("\n"))
} }
}
"ownText" -> for (element in elements) { "ownText" -> for (element in elements) {
textS.add(element.ownText()) val text = element.ownText()
if (text.isNotEmpty()) {
textS.add(text)
}
} }
"html" -> { "html" -> {
elements.select("script").remove() elements.select("script").remove()
elements.select("style").remove() elements.select("style").remove()
val html = elements.outerHtml() val html = elements.outerHtml()
if (html.isNotEmpty()) {
textS.add(html) textS.add(html)
} }
}
"all" -> textS.add(elements.outerHtml()) "all" -> textS.add(elements.outerHtml())
else -> for (element in elements) { else -> for (element in elements) {
@ -260,22 +262,16 @@ class AnalyzeByJSoup(doc: Any) {
/** /**
* 1.支持阅读原有写法':'分隔索引!.表示筛选方式索引可为负数 * 1.支持阅读原有写法':'分隔索引!.表示筛选方式索引可为负数
*
* 例如 tag.div.-1:10:2 tag.div!0:3 * 例如 tag.div.-1:10:2 tag.div!0:3
* *
* 2. 支持与jsonPath类似的[]索引写法 * 2. 支持与jsonPath类似的[]索引写法
*
* 格式形如 [it,it] [!it,it] 其中[!开头表示筛选方式为排除it为单个索引或区间 * 格式形如 [it,it] [!it,it] 其中[!开头表示筛选方式为排除it为单个索引或区间
*
* 区间格式为 start:end start:end:step其中start为0可省略end为-1可省略 * 区间格式为 start:end start:end:step其中start为0可省略end为-1可省略
*
* 索引区间两端及间隔都支持负数 * 索引区间两端及间隔都支持负数
*
* 例如 tag.div[-1, 3:-2:-10, 2] * 例如 tag.div[-1, 3:-2:-10, 2]
*
* 特殊用法 tag.div[-1:0] 可在任意地方让列表反向 * 特殊用法 tag.div[-1:0] 可在任意地方让列表反向
*
* */ * */
@Suppress("UNCHECKED_CAST")
data class ElementsSingle( data class ElementsSingle(
var split: Char = '.', var split: Char = '.',
var beforeRule: String = "", var beforeRule: String = "",
@ -322,7 +318,6 @@ class AnalyzeByJSoup(doc: Any) {
} else for (ix in lastIndexs downTo 0) { //indexs不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 } else for (ix in lastIndexs downTo 0) { //indexs不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序
if (indexs[ix] is Triple<*, *, *>) { //区间 if (indexs[ix] is Triple<*, *, *>) { //区间
val (startx, endx, stepx) = indexs[ix] as Triple<Int?, Int?, Int> //还原储存时的类型 val (startx, endx, stepx) = indexs[ix] as Triple<Int?, Int?, Int> //还原储存时的类型
val start = if (startx == null) 0 //左端省略表示0 val start = if (startx == null) 0 //左端省略表示0
@ -401,7 +396,7 @@ class AnalyzeByJSoup(doc: Any) {
var rl = rus[len] var rl = rus[len]
if (rl == ' ') continue //跳过空格 if (rl == ' ') continue //跳过空格
if (rl in '0'..'9') l += rl //将数值累接入临时字串中,遇到分界符才取出 if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出
else if (rl == '-') curMinus = true else if (rl == '-') curMinus = true
else { else {
@ -461,7 +456,7 @@ class AnalyzeByJSoup(doc: Any) {
val rl = rus[len] val rl = rus[len]
if (rl == ' ') continue //跳过空格 if (rl == ' ') continue //跳过空格
if (rl in '0'..'9') l += rl //将数值累接入临时字串中,遇到分界符才取出 if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出
else if (rl == '-') curMinus = true else if (rl == '-') curMinus = true
else { else {

@ -23,7 +23,7 @@ import kotlin.collections.HashMap
* 解析规则获取结果 * 解析规则获取结果
*/ */
@Keep @Keep
@Suppress("unused", "RegExpRedundantEscape") @Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate")
class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions { class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
var book = if (ruleData is BaseBook) ruleData else null var book = if (ruleData is BaseBook) ruleData else null
@ -31,8 +31,11 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
var chapter: BookChapter? = null var chapter: BookChapter? = null
var nextChapterUrl: String? = null var nextChapterUrl: String? = null
var content: Any? = null var content: Any? = null
private set
var baseUrl: String? = null var baseUrl: String? = null
private set
var redirectUrl: URL? = null var redirectUrl: URL? = null
private set
private var isJSON: Boolean = false private var isJSON: Boolean = false
private var isRegex: Boolean = false private var isRegex: Boolean = false
@ -337,9 +340,10 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
* 正则替换 * 正则替换
*/ */
private fun replaceRegex(result: String, rule: SourceRule): String { private fun replaceRegex(result: String, rule: SourceRule): String {
if (rule.replaceRegex.isEmpty()) return result
var vResult = result var vResult = result
if (rule.replaceRegex.isNotEmpty()) {
vResult = if (rule.replaceFirst) { vResult = if (rule.replaceFirst) {
kotlin.runCatching {
val pattern = Pattern.compile(rule.replaceRegex) val pattern = Pattern.compile(rule.replaceRegex)
val matcher = pattern.matcher(vResult) val matcher = pattern.matcher(vResult)
if (matcher.find()) { if (matcher.find()) {
@ -347,8 +351,14 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
} else { } else {
"" ""
} }
}.getOrElse {
vResult.replaceFirst(rule.replaceRegex, rule.replacement)
}
} else { } else {
kotlin.runCatching {
vResult.replace(rule.replaceRegex.toRegex(), rule.replacement) vResult.replace(rule.replaceRegex.toRegex(), rule.replacement)
}.getOrElse {
vResult.replace(rule.replaceRegex, rule.replacement)
} }
} }
return vResult return vResult
@ -396,7 +406,6 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
/** /**
* 规则类 * 规则类
*/ */
inner class SourceRule internal constructor( inner class SourceRule internal constructor(
ruleStr: String, ruleStr: String,
internal var mode: Mode = Mode.Default internal var mode: Mode = Mode.Default
@ -427,10 +436,6 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
mode = Mode.XPath mode = Mode.XPath
ruleStr.substring(7) ruleStr.substring(7)
} }
ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头
mode = Mode.XPath
ruleStr
}
ruleStr.startsWith("@Json:", true) -> { ruleStr.startsWith("@Json:", true) -> {
mode = Mode.Json mode = Mode.Json
ruleStr.substring(6) ruleStr.substring(6)
@ -439,6 +444,10 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
mode = Mode.Json mode = Mode.Json
ruleStr ruleStr
} }
ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头
mode = Mode.XPath
ruleStr
}
else -> ruleStr else -> ruleStr
} }
//分离put //分离put

@ -7,7 +7,6 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import splitties.init.appCtx import splitties.init.appCtx
@ -27,7 +26,7 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C
private var searchIndex = -1 private var searchIndex = -1
private fun initSearchPool() { private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
} }
fun search(searchId: Long, key: String) { fun search(searchId: Long, key: String) {
@ -74,12 +73,12 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C
searchPage, searchPage,
context = searchPool!! context = searchPool!!
).timeout(30000L) ).timeout(30000L)
.onSuccess(IO) { .onSuccess(searchPool) {
if (searchId == mSearchId) { if (searchId == mSearchId) {
callBack.onSearchSuccess(it) callBack.onSearchSuccess(it)
} }
} }
.onFinally { .onFinally(searchPool) {
synchronized(this) { synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) { if (searchIndex < bookSourceList.lastIndex) {
search(searchId) search(searchId)

@ -22,7 +22,6 @@ import io.legado.app.utils.postEvent
* Created by GKF on 2018/1/6. * Created by GKF on 2018/1/6.
* 监听耳机键 * 监听耳机键
*/ */
class MediaButtonReceiver : BroadcastReceiver() { class MediaButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@ -36,8 +35,8 @@ class MediaButtonReceiver : BroadcastReceiver() {
fun handleIntent(context: Context, intent: Intent): Boolean { fun handleIntent(context: Context, intent: Intent): Boolean {
val intentAction = intent.action val intentAction = intent.action
if (Intent.ACTION_MEDIA_BUTTON == intentAction) { if (Intent.ACTION_MEDIA_BUTTON == intentAction) {
val keyEvent = val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false ?: return false
val keycode: Int = keyEvent.keyCode val keycode: Int = keyEvent.keyCode
val action: Int = keyEvent.action val action: Int = keyEvent.action
if (action == KeyEvent.ACTION_DOWN) { if (action == KeyEvent.ACTION_DOWN) {

@ -161,6 +161,7 @@ class AudioPlayService : BaseService(),
} }
private fun resume() { private fun resume() {
try {
pause = false pause = false
if (!mediaPlayer.isPlaying) { if (!mediaPlayer.isPlaying) {
mediaPlayer.start() mediaPlayer.start()
@ -172,6 +173,10 @@ class AudioPlayService : BaseService(),
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification() upNotification()
} catch (e: Exception) {
e.printStackTrace()
stopSelf()
}
} }
private fun adjustProgress(position: Int) { private fun adjustProgress(position: Int) {

@ -1,5 +1,6 @@
package io.legado.app.service package io.legado.app.service
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -225,6 +226,7 @@ abstract class BaseReadAloudService : BaseService(),
/** /**
* 初始化MediaSession, 注册多媒体按钮 * 初始化MediaSession, 注册多媒体按钮
*/ */
@SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() { private fun initMediaSession() {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {

@ -27,11 +27,12 @@ import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min
class CacheBookService : BaseService() { class CacheBookService : BaseService() {
private val threadCount = AppConfig.threadCount private val threadCount = AppConfig.threadCount
private var searchPool = private var cachePool =
Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(min(threadCount, 8)).asCoroutineDispatcher()
private var tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }
@ -82,7 +83,7 @@ class CacheBookService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
tasks.clear() tasks.clear()
searchPool.close() cachePool.close()
handler.removeCallbacks(runnable) handler.removeCallbacks(runnable)
downloadMap.clear() downloadMap.clear()
finalMap.clear() finalMap.clear()
@ -133,7 +134,7 @@ class CacheBookService : BaseService() {
return return
} }
downloadCount[bookUrl] = DownloadCount() downloadCount[bookUrl] = DownloadCount()
execute { execute(context = cachePool) {
appDb.bookChapterDao.getChapterList(bookUrl, start, end).let { appDb.bookChapterDao.getChapterList(bookUrl, start, end).let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val chapters = CopyOnWriteArraySet<BookChapter>() val chapters = CopyOnWriteArraySet<BookChapter>()
@ -158,7 +159,7 @@ class CacheBookService : BaseService() {
private fun download() { private fun download() {
downloadingCount += 1 downloadingCount += 1
val task = Coroutine.async(this, context = searchPool) { val task = Coroutine.async(this, context = cachePool) {
if (!isActive) return@async if (!isActive) return@async
val bookChapter: BookChapter? = synchronized(this@CacheBookService) { val bookChapter: BookChapter? = synchronized(this@CacheBookService) {
downloadMap.forEach { downloadMap.forEach {
@ -185,16 +186,16 @@ class CacheBookService : BaseService() {
return@async return@async
} }
if (!BookHelp.hasImageContent(book, bookChapter)) { if (!BookHelp.hasImageContent(book, bookChapter)) {
webBook.getContent(this, book, bookChapter, context = searchPool) webBook.getContent(this, book, bookChapter, context = cachePool)
.timeout(60000L) .timeout(60000L)
.onError { .onError(cachePool) {
synchronized(this) { synchronized(this) {
downloadingList.remove(bookChapter.url) downloadingList.remove(bookChapter.url)
} }
notificationContent = "getContentError${it.localizedMessage}" notificationContent = "getContentError${it.localizedMessage}"
upNotification() upNotification()
} }
.onSuccess { .onSuccess(cachePool) {
synchronized(this@CacheBookService) { synchronized(this@CacheBookService) {
downloadCount[book.bookUrl]?.increaseSuccess() downloadCount[book.bookUrl]?.increaseSuccess()
downloadCount[book.bookUrl]?.increaseFinished() downloadCount[book.bookUrl]?.increaseFinished()
@ -217,7 +218,7 @@ class CacheBookService : BaseService() {
downloadCount.remove(book.bookUrl) downloadCount.remove(book.bookUrl)
} }
} }
}.onFinally { }.onFinally(cachePool) {
postDownloading(true) postDownloading(true)
} }
} else { } else {
@ -227,7 +228,7 @@ class CacheBookService : BaseService() {
postDownloading(true) postDownloading(true)
} }
} }
}.onError { }.onError(cachePool) {
notificationContent = "ERROR:${it.localizedMessage}" notificationContent = "ERROR:${it.localizedMessage}"
CacheBook.addLog(notificationContent) CacheBook.addLog(notificationContent)
upNotification() upNotification()

@ -17,14 +17,13 @@ import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private var threadCount = AppConfig.threadCount private var threadCount = AppConfig.threadCount
private var searchCoroutine = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() private var searchCoroutine = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
private var tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>() private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>() private val checkedIds = ArrayList<String>()
@ -95,7 +94,7 @@ class CheckSourceService : BaseService() {
synchronized(this) { synchronized(this) {
processIndex++ processIndex++
} }
execute { execute(context = searchCoroutine) {
if (index < allIds.size) { if (index < allIds.size) {
val sourceUrl = allIds[index] val sourceUrl = allIds[index]
appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source -> appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
@ -130,14 +129,14 @@ class CheckSourceService : BaseService() {
throw Exception("正文内容为空") throw Exception("正文内容为空")
} }
}.timeout(180000L) }.timeout(180000L)
.onError(IO) { .onError(searchCoroutine) {
source.addGroup("失效") source.addGroup("失效")
source.bookSourceComment = """ source.bookSourceComment = """
"error:${it.localizedMessage} "error:${it.localizedMessage}
${source.bookSourceComment}" ${source.bookSourceComment}"
""".trimIndent() """.trimIndent()
appDb.bookSourceDao.update(source) appDb.bookSourceDao.update(source)
}.onSuccess(IO) { }.onSuccess(searchCoroutine) {
source.removeGroup("失效") source.removeGroup("失效")
source.bookSourceComment = source.bookSourceComment source.bookSourceComment = source.bookSourceComment
?.split("\n") ?.split("\n")
@ -145,7 +144,7 @@ class CheckSourceService : BaseService() {
it.startsWith("error:") it.startsWith("error:")
}?.joinToString("\n") }?.joinToString("\n")
appDb.bookSourceDao.update(source) appDb.bookSourceDao.update(source)
}.onFinally { }.onFinally(searchCoroutine) {
onNext(source.bookSourceUrl, source.bookSourceName) onNext(source.bookSourceUrl, source.bookSourceName)
} }
} }

@ -11,7 +11,6 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
@ -152,27 +151,21 @@ class DownloadService : BaseService() {
.setOngoing(true) .setOngoing(true)
.setContentTitle(getString(R.string.action_download)) .setContentTitle(getString(R.string.action_download))
notificationBuilder.setContentIntent( notificationBuilder.setContentIntent(
IntentHelp.servicePendingIntent<DownloadService>( IntentHelp.servicePendingIntent<DownloadService>(this, IntentAction.play) {
this, putExtra("downloadId", downloadId)
IntentAction.play, }
bundleOf("downloadId" to downloadId)
)
) )
notificationBuilder.addAction( notificationBuilder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.cancel), getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>( IntentHelp.servicePendingIntent<DownloadService>(this, IntentAction.stop) {
this, putExtra("downloadId", downloadId)
IntentAction.stop, }
bundleOf("downloadId" to downloadId)
)
) )
notificationBuilder.setDeleteIntent( notificationBuilder.setDeleteIntent(
IntentHelp.servicePendingIntent<DownloadService>( IntentHelp.servicePendingIntent<DownloadService>(this, IntentAction.stop) {
this, putExtra("downloadId", downloadId)
IntentAction.stop, }
bundleOf("downloadId" to downloadId)
)
) )
notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
notificationBuilder.setContentText(content) notificationBuilder.setContentText(content)

@ -10,6 +10,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.web.HttpServer import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer import io.legado.app.web.WebSocketServer
@ -26,11 +27,7 @@ class WebService : BaseService() {
} }
fun stop(context: Context) { fun stop(context: Context) {
if (isRun) { context.stopService<WebService>()
val intent = Intent(context, WebService::class.java)
intent.action = IntentAction.stop
context.startService(intent)
}
} }
} }
@ -44,6 +41,7 @@ class WebService : BaseService() {
isRun = true isRun = true
notificationContent = getString(R.string.service_starting) notificationContent = getString(R.string.service_starting)
upNotification() upNotification()
WebTileService.setState(this, true)
} }
override fun onDestroy() { override fun onDestroy() {
@ -56,6 +54,7 @@ class WebService : BaseService() {
webSocketServer?.stop() webSocketServer?.stop()
} }
postEvent(EventBus.WEB_SERVICE, "") postEvent(EventBus.WEB_SERVICE, "")
WebTileService.setState(this, false)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -113,6 +112,9 @@ class WebService : BaseService() {
.setOngoing(true) .setOngoing(true)
.setContentTitle(getString(R.string.web_service)) .setContentTitle(getString(R.string.web_service))
.setContentText(notificationContent) .setContentText(notificationContent)
.setContentIntent(
IntentHelp.activityPendingIntent<MainActivity>(this, "webService")
)
builder.addAction( builder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.cancel), getString(R.string.cancel),

@ -0,0 +1,72 @@
package io.legado.app.service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import io.legado.app.constant.IntentAction
import io.legado.app.utils.startService
/**
* web服务快捷开关
*/
@RequiresApi(Build.VERSION_CODES.N)
class WebTileService : TileService() {
companion object {
fun setState(context: Context, active: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.startService<WebTileService> {
action = if (active) {
IntentAction.start
} else {
IntentAction.stop
}
}
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
when (intent?.action) {
IntentAction.start -> {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
}
IntentAction.stop -> {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return super.onStartCommand(intent, flags, startId)
}
override fun onStartListening() {
super.onStartListening()
if (WebService.isRun) {
qsTile.state = Tile.STATE_ACTIVE
qsTile.updateTile()
} else {
qsTile.state = Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
override fun onClick() {
super.onClick()
if (WebService.isRun) {
WebService.stop(this)
} else {
WebService.start(this)
}
}
}

@ -13,6 +13,7 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.AudioPlayService import io.legado.app.service.AudioPlayService
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.startService
object AudioPlay { object AudioPlay {
var titleData = MutableLiveData<String>() var titleData = MutableLiveData<String>()
@ -52,43 +53,43 @@ object AudioPlay {
fun pause(context: Context) { fun pause(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) context.startService<AudioPlayService> {
intent.action = IntentAction.pause action = IntentAction.pause
context.startService(intent) }
} }
} }
fun resume(context: Context) { fun resume(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) context.startService<AudioPlayService> {
intent.action = IntentAction.resume action = IntentAction.resume
context.startService(intent) }
} }
} }
fun stop(context: Context) { fun stop(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) context.startService<AudioPlayService> {
intent.action = IntentAction.stop action = IntentAction.stop
context.startService(intent) }
} }
} }
fun adjustSpeed(context: Context, adjust: Float) { fun adjustSpeed(context: Context, adjust: Float) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) context.startService<AudioPlayService> {
intent.action = IntentAction.adjustSpeed action = IntentAction.adjustSpeed
intent.putExtra("adjust", adjust) putExtra("adjust", adjust)
context.startService(intent) }
} }
} }
fun adjustProgress(context: Context, position: Int) { fun adjustProgress(context: Context, position: Int) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) context.startService<AudioPlayService> {
intent.action = IntentAction.adjustProgress action = IntentAction.adjustProgress
intent.putExtra("position", position) putExtra("position", position)
context.startService(intent) }
} }
} }

@ -1,7 +1,6 @@
package io.legado.app.service.help package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -9,6 +8,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService import io.legado.app.service.CacheBookService
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.startService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import splitties.init.appCtx import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -29,27 +29,24 @@ object CacheBook {
} }
fun start(context: Context, bookUrl: String, start: Int, end: Int) { fun start(context: Context, bookUrl: String, start: Int, end: Int) {
Intent(context, CacheBookService::class.java).let { context.startService<CacheBookService> {
it.action = IntentAction.start action = IntentAction.start
it.putExtra("bookUrl", bookUrl) putExtra("bookUrl", bookUrl)
it.putExtra("start", start) putExtra("start", start)
it.putExtra("end", end) putExtra("end", end)
context.startService(it)
} }
} }
fun remove(context: Context, bookUrl: String) { fun remove(context: Context, bookUrl: String) {
Intent(context, CacheBookService::class.java).let { context.startService<CacheBookService> {
it.action = IntentAction.remove action = IntentAction.remove
it.putExtra("bookUrl", bookUrl) putExtra("bookUrl", bookUrl)
context.startService(it)
} }
} }
fun stop(context: Context) { fun stop(context: Context) {
Intent(context, CacheBookService::class.java).let { context.startService<CacheBookService> {
it.action = IntentAction.stop action = IntentAction.stop
context.startService(it)
} }
} }

@ -1,11 +1,11 @@
package io.legado.app.service.help package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.service.CheckSourceService import io.legado.app.service.CheckSourceService
import io.legado.app.utils.startService
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
object CheckSource { object CheckSource {
@ -20,17 +20,15 @@ object CheckSource {
sources.map { sources.map {
selectedIds.add(it.bookSourceUrl) selectedIds.add(it.bookSourceUrl)
} }
Intent(context, CheckSourceService::class.java).let { context.startService<CheckSourceService> {
it.action = IntentAction.start action = IntentAction.start
it.putExtra("selectIds", selectedIds) putExtra("selectIds", selectedIds)
context.startService(it)
} }
} }
fun stop(context: Context) { fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let { context.startService<CheckSourceService> {
it.action = IntentAction.stop action = IntentAction.stop
context.startService(it)
} }
} }
} }

@ -1,25 +1,23 @@
package io.legado.app.service.help package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.service.DownloadService import io.legado.app.service.DownloadService
import io.legado.app.utils.startService
object Download { object Download {
fun start(context: Context, downloadId: Long, fileName: String) { fun start(context: Context, downloadId: Long, fileName: String) {
Intent(context, DownloadService::class.java).let { context.startService<DownloadService> {
it.action = IntentAction.start action = IntentAction.start
it.putExtra("downloadId", downloadId) putExtra("downloadId", downloadId)
it.putExtra("fileName", fileName) putExtra("fileName", fileName)
context.startService(it)
} }
} }
fun stop(context: Context) { fun stop(context: Context) {
Intent(context, DownloadService::class.java).let { context.startService<DownloadService> {
it.action = IntentAction.stop action = IntentAction.stop
context.startService(it)
} }
} }

@ -416,21 +416,21 @@ object ReadBook {
.getContent(book, chapter.title, content) .getContent(book, chapter.title, content)
val textChapter = ChapterProvider val textChapter = ChapterProvider
.getTextChapter(book, chapter, contents, chapterSize) .getTextChapter(book, chapter, contents, chapterSize)
when (val offset = chapter.index - durChapterIndex) {
val offset = chapter.index - durChapterIndex
if (upContent) callBack?.upContent(offset, resetPageOffset)
when (offset) {
0 -> { 0 -> {
curTextChapter = textChapter curTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset)
callBack?.upView() callBack?.upView()
curPageChanged() curPageChanged()
callBack?.contentLoadFinish() callBack?.contentLoadFinish()
} }
-1 -> { -1 -> {
prevTextChapter = textChapter prevTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> {
nextTextChapter = textChapter nextTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
} }
} }

@ -20,7 +20,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -83,6 +83,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
} }
} }
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_book_source) binding.toolBar.setTitle(R.string.import_book_source)
@ -106,7 +107,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -143,16 +144,16 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
} }
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} }
@ -168,13 +169,23 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()
appDb.bookSourceDao.allGroup.forEach { group -> appDb.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
} }
textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList()) editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp editView.dropDownHeight = 180.dp
} }
@ -182,21 +193,22 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
alertBinding.root alertBinding.root
} }
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = group viewModel.groupName = alertBinding.editView.text?.toString()
item.title = getString(R.string.diy_edit_source_group_title, group) if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
} }
} }
noButton() noButton()
}.show() }.show()
} }
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
inner class SourcesAdapter(context: Context) : inner class SourcesAdapter(context: Context) :
RecyclerAdapter<BookSource, ItemSourceImportBinding>(context) { RecyclerAdapter<BookSource, ItemSourceImportBinding>(context) {

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -18,8 +19,10 @@ import io.legado.app.help.storage.Restore
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.splitNotBlank
class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -28,7 +31,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
val checkSources = arrayListOf<BookSource?>() val checkSources = arrayListOf<BookSource?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -37,7 +41,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -49,6 +54,7 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSelect(finally: () -> Unit) { fun importSelect(finally: () -> Unit) {
execute { execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName val keepName = AppConfig.importKeepName
val selectSource = arrayListOf<BookSource>() val selectSource = arrayListOf<BookSource>()
selectStatus.forEachIndexed { index, b -> selectStatus.forEachIndexed { index, b ->
@ -61,8 +67,17 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
source.customOrder = it.customOrder source.customOrder = it.customOrder
} }
} }
if (groupName != null) { if (!group.isNullOrEmpty()) {
source.bookSourceGroup = groupName if (isAddGroup) {
val groups = linkedSetOf<String>()
source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
source.bookSourceGroup = groups.joinToString(",")
} else {
source.bookSourceGroup = group
}
} }
selectSource.add(source) selectSource.add(source)
} }

@ -1,11 +1,14 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -13,15 +16,23 @@ import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.WaitDialog import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.dp
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible import io.legado.app.utils.visible
class ImportReplaceRuleDialog : BaseDialogFragment() { class ImportReplaceRuleDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
companion object { companion object {
fun start( fun start(
@ -65,10 +76,12 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
return inflater.inflate(R.layout.dialog_recycler_view, container) return inflater.inflate(R.layout.dialog_recycler_view, container)
} }
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_replace_rule) binding.toolBar.setTitle(R.string.import_replace_rule)
binding.rotateLoading.show() binding.rotateLoading.show()
initMenu()
adapter = SourcesAdapter(requireContext()) adapter = SourcesAdapter(requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
@ -87,7 +100,7 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -123,17 +136,65 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
viewModel.import(source) viewModel.import(source)
} }
private fun initMenu() {
binding.toolBar.setOnMenuItemClickListener(this)
binding.toolBar.inflateMenu(R.menu.import_replace)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return true
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) {
val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>()
appDb.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp
}
customView {
alertBinding.root
}
okButton {
viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = alertBinding.editView.text?.toString()
if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
}
}
noButton()
}.show()
}
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allRules.size viewModel.allRules.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allRules.size viewModel.allRules.size
) )
} }
@ -155,6 +216,15 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
binding.run { binding.run {
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
cbSourceName.text = item.name cbSourceName.text = item.name
val localRule = viewModel.checkRules[holder.layoutPosition]
tvSourceState.text = when {
localRule == null -> "新规则"
item.pattern != localRule.pattern
|| item.replacement != localRule.replacement
|| item.isRegex != localRule.isRegex
|| item.scope != localRule.scope -> "更新"
else -> "已存在"
}
} }
} }

@ -3,6 +3,7 @@ package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -11,8 +12,11 @@ import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text import io.legado.app.help.http.text
import io.legado.app.help.storage.OldReplace import io.legado.app.help.storage.OldReplace
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.splitNotBlank
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -20,7 +24,8 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
val checkRules = arrayListOf<ReplaceRule?>() val checkRules = arrayListOf<ReplaceRule?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -29,7 +34,8 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -39,6 +45,42 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
return count return count
} }
fun importSelect(finally: () -> Unit) {
execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName
val selectRules = arrayListOf<ReplaceRule>()
selectStatus.forEachIndexed { index, b ->
if (b) {
val rule = allRules[index]
if (keepName) {
checkRules[index]?.let {
rule.name = it.name
rule.group = it.group
rule.order = it.order
}
}
if (!group.isNullOrEmpty()) {
if (isAddGroup) {
val groups = linkedSetOf<String>()
rule.group?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
rule.group = groups.joinToString(",")
} else {
rule.group = group
}
}
selectRules.add(rule)
}
}
appDb.replaceRuleDao.insert(*selectRules.toTypedArray())
}.onFinally {
finally.invoke()
}
}
fun import(text: String) { fun import(text: String) {
execute { execute {
if (text.isAbsUrl()) { if (text.isAbsUrl()) {
@ -59,27 +101,12 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
} }
} }
fun importSelect(finally: () -> Unit) {
execute {
val keepName = AppConfig.importKeepName
val selectRules = arrayListOf<ReplaceRule>()
selectStatus.forEachIndexed { index, b ->
if (b) {
val rule = allRules[index]
selectRules.add(rule)
}
}
appDb.replaceRuleDao.insert(*selectRules.toTypedArray())
}.onFinally {
finally.invoke()
}
}
private fun comparisonSource() { private fun comparisonSource() {
execute { execute {
allRules.forEach { allRules.forEach {
checkRules.add(null) val rule = appDb.replaceRuleDao.findById(it.id)
selectStatus.add(false) checkRules.add(rule)
selectStatus.add(rule == null)
} }
}.onSuccess { }.onSuccess {
successLiveData.postValue(allRules.size) successLiveData.postValue(allRules.size)

@ -20,7 +20,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -80,6 +80,7 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
return inflater.inflate(R.layout.dialog_recycler_view, container) return inflater.inflate(R.layout.dialog_recycler_view, container)
} }
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_rss_source) binding.toolBar.setTitle(R.string.import_rss_source)
@ -103,7 +104,7 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -140,16 +141,16 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
} }
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} }
@ -165,13 +166,23 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()
appDb.rssSourceDao.allGroup.forEach { group -> appDb.rssSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
} }
textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList()) editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp editView.dropDownHeight = 180.dp
} }
@ -179,21 +190,22 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
alertBinding.root alertBinding.root
} }
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = group viewModel.groupName = alertBinding.editView.text?.toString()
item.title = getString(R.string.diy_edit_source_group_title, group) if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
} }
} }
noButton() noButton()
}.show() }.show()
} }
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
inner class SourcesAdapter(context: Context) : inner class SourcesAdapter(context: Context) :
RecyclerAdapter<RssSource, ItemSourceImportBinding>(context) { RecyclerAdapter<RssSource, ItemSourceImportBinding>(context) {

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -16,6 +17,7 @@ import io.legado.app.help.storage.Restore
import io.legado.app.utils.* import io.legado.app.utils.*
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -24,7 +26,8 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
val checkSources = arrayListOf<RssSource?>() val checkSources = arrayListOf<RssSource?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -33,7 +36,8 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -45,6 +49,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSelect(finally: () -> Unit) { fun importSelect(finally: () -> Unit) {
execute { execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName val keepName = AppConfig.importKeepName
val selectSource = arrayListOf<RssSource>() val selectSource = arrayListOf<RssSource>()
selectStatus.forEachIndexed { index, b -> selectStatus.forEachIndexed { index, b ->
@ -57,8 +62,17 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
source.customOrder = it.customOrder source.customOrder = it.customOrder
} }
} }
if (groupName != null) { if (!group.isNullOrEmpty()) {
source.sourceGroup = groupName if (isAddGroup) {
val groups = linkedSetOf<String>()
source.sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
source.sourceGroup = groups.joinToString(",")
} else {
source.sourceGroup = group
}
} }
selectSource.add(source) selectSource.add(source)
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.book.arrange package io.legado.app.ui.book.arrange
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -88,6 +89,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
} }
} }
@SuppressLint("NotifyDataSetChanged")
fun selectAll(selectAll: Boolean) { fun selectAll(selectAll: Boolean) {
if (selectAll) { if (selectAll) {
getItems().forEach { getItems().forEach {
@ -100,6 +102,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
callBack.upSelectCount() callBack.upSelectCount()
} }
@SuppressLint("NotifyDataSetChanged")
fun revertSelection() { fun revertSelection() {
getItems().forEach { getItems().forEach {
if (selectedBooks.contains(it)) { if (selectedBooks.contains(it)) {

@ -4,12 +4,14 @@ import android.app.Application
import android.content.Intent import android.content.Intent
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -98,6 +100,8 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
} else { } else {
loadChapterList(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) } loadChapterList(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, book1.bookUrl)
} }
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.book.cache package io.legado.app.ui.book.cache
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -44,6 +45,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override val viewModel by viewModels<CacheViewModel>() override val viewModel by viewModels<CacheViewModel>()
private val exportBookPathKey = "exportBookPath" private val exportBookPathKey = "exportBookPath"
private val exportTypes = arrayListOf("txt", "epub")
lateinit var adapter: CacheAdapter lateinit var adapter: CacheAdapter
private var booksFlowJob: Job? = null private var booksFlowJob: Job? = null
private var menu: Menu? = null private var menu: Menu? = null
@ -92,6 +94,10 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace
menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName
menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav
menu.findItem(R.id.menu_export_type)?.title =
"${getString(R.string.export_type)}(${getTypeName()})"
menu.findItem(R.id.menu_export_charset)?.title =
"${getString(R.string.export_charset)}(${AppConfig.exportCharset})"
return super.onMenuOpened(featureId, menu) return super.onMenuOpened(featureId, menu)
} }
@ -175,6 +181,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
} }
@SuppressLint("NotifyDataSetChanged")
private fun initGroupData() { private fun initGroupData() {
launch { launch {
appDb.bookGroupDao.flowAll().collect { appDb.bookGroupDao.flowAll().collect {
@ -300,6 +307,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
private fun alertExportFileName() { private fun alertExportFileName() {
alert(R.string.export_file_name) { alert(R.string.export_file_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "file name js"
editView.setText(AppConfig.bookExportFileName) editView.setText(AppConfig.bookExportFileName)
} }
customView { alertBinding.root } customView { alertBinding.root }
@ -310,8 +318,14 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}.show() }.show()
} }
private fun getTypeName(): String {
return exportTypes.getOrElse(AppConfig.exportType) {
exportTypes[0]
}
}
private fun showExportTypeConfig() { private fun showExportTypeConfig() {
selector(R.string.export_type, arrayListOf("txt", "epub")) { _, i -> selector(R.string.export_type, exportTypes) { _, i ->
AppConfig.exportType = i AppConfig.exportType = i
} }
} }
@ -319,6 +333,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
private fun showCharsetConfig() { private fun showCharsetConfig() {
alert(R.string.set_charset) { alert(R.string.set_charset) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "charset name"
editView.setFilterValues(charsets) editView.setFilterValues(charsets)
editView.setText(AppConfig.exportCharset) editView.setText(AppConfig.exportCharset)
} }

@ -33,7 +33,7 @@ import javax.script.SimpleBindings
class CacheViewModel(application: Application) : BaseViewModel(application) { class CacheViewModel(application: Application) : BaseViewModel(application) {
fun getExportFileName(book: Book): String { private fun getExportFileName(book: Book): String {
val jsStr = AppConfig.bookExportFileName val jsStr = AppConfig.bookExportFileName
if (jsStr.isNullOrBlank()) { if (jsStr.isNullOrBlank()) {
return "${book.name} 作者:${book.getRealAuthor()}" return "${book.name} 作者:${book.getRealAuthor()}"
@ -56,6 +56,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
}.onError { }.onError {
finally(it.localizedMessage ?: "ERROR") finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess { }.onSuccess {
finally(context.getString(R.string.success)) finally(context.getString(R.string.success))
} }
@ -188,6 +189,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
}.onError { }.onError {
finally(it.localizedMessage ?: "ERROR") finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess { }.onSuccess {
finally(context.getString(R.string.success)) finally(context.getString(R.string.success))
} }

@ -14,9 +14,8 @@ import io.legado.app.base.BaseDialogFragment
import io.legado.app.databinding.DialogChangeCoverBinding import io.legado.app.databinding.DialogChangeCoverBinding
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getSize
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize
class ChangeCoverDialog : BaseDialogFragment(), class ChangeCoverDialog : BaseDialogFragment(),
@ -45,7 +44,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
} }

@ -14,7 +14,6 @@ import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
@ -50,7 +49,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
private fun initSearchPool() { private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
searchIndex = -1 searchIndex = -1
} }
@ -105,7 +104,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
val task = WebBook(source) val task = WebBook(source)
.searchBook(viewModelScope, name, context = searchPool!!) .searchBook(viewModelScope, name, context = searchPool!!)
.timeout(60000L) .timeout(60000L)
.onSuccess(Dispatchers.IO) { .onSuccess(searchPool) {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val searchBook = it[0] val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author if (searchBook.name == name && searchBook.author == author
@ -119,7 +118,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
} }
} }
.onFinally { .onFinally(searchPool) {
searchNext() searchNext()
} }
tasks.add(task) tasks.add(task)

@ -62,6 +62,11 @@ class ChangeSourceAdapter(
when (it) { when (it) {
"name" -> tvOrigin.text = item.originName "name" -> tvOrigin.text = item.originName
"latest" -> tvLast.text = item.getDisplayLastChapterTitle() "latest" -> tvLast.text = item.getDisplayLastChapterTitle()
"upCurSource" -> if (callBack.bookUrl == item.bookUrl) {
ivChecked.visible()
} else {
ivChecked.invisible()
}
} }
} }
} }
@ -88,6 +93,16 @@ class ChangeSourceAdapter(
R.id.menu_disable_book_source -> { R.id.menu_disable_book_source -> {
callBack.disableSource(searchBook) callBack.disableSource(searchBook)
} }
R.id.menu_top_source -> {
callBack.topSource(searchBook)
}
R.id.menu_bottom_source -> {
callBack.bottomSource(searchBook)
}
R.id.menu_delete_book_source -> {
callBack.deleteSource(searchBook)
updateItems(0, itemCount, listOf<Int>())
}
} }
true true
} }
@ -98,5 +113,8 @@ class ChangeSourceAdapter(
val bookUrl: String? val bookUrl: String?
fun changeTo(searchBook: SearchBook) fun changeTo(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook) fun disableSource(searchBook: SearchBook)
fun topSource(searchBook: SearchBook)
fun bottomSource(searchBook: SearchBook)
fun deleteSource(searchBook: SearchBook)
} }
} }

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -53,7 +55,7 @@ class ChangeSourceDialog : BaseDialogFragment(),
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
} }
@ -198,9 +200,7 @@ class ChangeSourceDialog : BaseDialogFragment(),
} }
override fun changeTo(searchBook: SearchBook) { override fun changeTo(searchBook: SearchBook) {
val book = searchBook.toBook() changeSource(searchBook)
book.upInfoFromOld(callBack?.oldBook)
callBack?.changeTo(book)
dismissAllowingStateLoss() dismissAllowingStateLoss()
} }
@ -211,6 +211,31 @@ class ChangeSourceDialog : BaseDialogFragment(),
viewModel.disableSource(searchBook) viewModel.disableSource(searchBook)
} }
override fun topSource(searchBook: SearchBook) {
viewModel.topSource(searchBook)
}
override fun bottomSource(searchBook: SearchBook) {
viewModel.bottomSource(searchBook)
}
override fun deleteSource(searchBook: SearchBook) {
viewModel.del(searchBook)
if (bookUrl == searchBook.bookUrl) {
viewModel.firstSourceOrNull(searchBook)?.let {
changeSource(it)
}
}
}
private fun changeSource(searchBook: SearchBook) {
val book = searchBook.toBook()
book.upInfoFromOld(callBack?.oldBook)
callBack?.changeTo(book)
searchBook.time = System.currentTimeMillis()
viewModel.updateSource(searchBook)
}
/** /**
* 更新分组菜单 * 更新分组菜单
*/ */
@ -236,6 +261,16 @@ class ChangeSourceDialog : BaseDialogFragment(),
} }
} }
override fun observeLiveBus() {
observeEvent<String>(EventBus.SOURCE_CHANGED) {
adapter.notifyItemRangeChanged(
0,
adapter.itemCount,
bundleOf(Pair("upCurSource", bookUrl))
)
}
}
interface CallBack { interface CallBack {
val oldBook: Book? val oldBook: Book?
fun changeTo(book: Book) fun changeTo(book: Book)

@ -24,6 +24,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import splitties.init.appCtx import splitties.init.appCtx
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.min
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount private val threadCount = AppConfig.threadCount
@ -56,7 +57,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
private fun initSearchPool() { private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
searchIndex = -1 searchIndex = -1
} }
@ -132,7 +133,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
val task = webBook val task = webBook
.searchBook(viewModelScope, name, context = searchPool!!) .searchBook(viewModelScope, name, context = searchPool!!)
.timeout(60000L) .timeout(60000L)
.onSuccess(IO) { .onSuccess(searchPool) {
it.forEach { searchBook -> it.forEach { searchBook ->
if (searchBook.name == name) { if (searchBook.name == name) {
if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author)) if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author))
@ -151,7 +152,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
} }
.onFinally { .onFinally(searchPool) {
synchronized(this) { synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) { if (searchIndex < bookSourceList.lastIndex) {
search() search()
@ -241,4 +242,49 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
fun topSource(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
val minOrder = appDb.bookSourceDao.minOrder - 1
source.customOrder = minOrder
searchBook.originOrder = source.customOrder
appDb.bookSourceDao.update(source)
updateSource(searchBook)
}
upAdapter()
}
}
fun bottomSource(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
val maxOrder = appDb.bookSourceDao.maxOrder + 1
source.customOrder = maxOrder
searchBook.originOrder = source.customOrder
appDb.bookSourceDao.update(source)
updateSource(searchBook)
}
upAdapter()
}
}
fun updateSource(searchBook: SearchBook) {
appDb.searchBookDao.update(searchBook)
}
fun del(searchBook: SearchBook) {
execute {
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
appDb.bookSourceDao.delete(source)
appDb.searchBookDao.delete(searchBook)
}
}
searchBooks.remove(searchBook)
upAdapter()
}
fun firstSourceOrNull(searchBook: SearchBook): SearchBook? {
return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl }
}
} }

@ -0,0 +1,120 @@
package io.legado.app.ui.book.group
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogBookGroupEditBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import splitties.views.onClick
class GroupEditDialog : BaseDialogFragment() {
companion object {
fun start(fragmentManager: FragmentManager, bookGroup: BookGroup? = null) {
GroupEditDialog().apply {
arguments = Bundle().apply {
putParcelable("group", bookGroup)
}
}.show(fragmentManager, "bookGroupEdit")
}
}
private val binding by viewBinding(DialogBookGroupEditBinding::bind)
private val viewModel by viewModels<GroupViewModel>()
private var bookGroup: BookGroup? = null
val selectImage = registerForActivityResult(SelectImageContract()) {
it?.second?.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", name)
file.writeBytes(bytes)
binding.ivCover.load(file.absolutePath)
}
}
override fun onStart() {
super.onStart()
val dm = requireActivity().windowSize
dialog?.window?.setLayout(
(dm.widthPixels * 0.9).toInt(),
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_book_group_edit, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
bookGroup = arguments?.getParcelable("group")
bookGroup?.let {
binding.tieGroupName.setText(it.groupName)
binding.ivCover.load(it.cover)
} ?: let {
binding.toolBar.title = getString(R.string.add_group)
binding.btnDelete.gone()
binding.ivCover.load()
}
binding.run {
ivCover.onClick {
selectImage.launch(null)
}
btnCancel.onClick {
dismiss()
}
btnOk.onClick {
val groupName = tieGroupName.text?.toString()
if (groupName.isNullOrEmpty()) {
toastOnUi("分组名称不能为空")
} else {
bookGroup?.let {
it.groupName = groupName
it.cover = binding.ivCover.path
viewModel.upGroup(it) {
dismiss()
}
} ?: let {
viewModel.addGroup(groupName, binding.ivCover.path) {
dismiss()
}
}
}
}
btnDelete.onClick {
deleteGroup {
bookGroup?.let {
viewModel.delGroup(it) {
dismiss()
}
}
}
}
}
}
private fun deleteGroup(ok: () -> Unit) {
alert(R.string.delete, R.string.sure_del) {
okButton {
ok.invoke()
}
noButton()
}.show()
}
}

@ -1,6 +1,5 @@
package io.legado.app.ui.book.group package io.legado.app.ui.book.group
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -18,20 +17,17 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemBookGroupManageBinding import io.legado.app.databinding.ItemBookGroupManageBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getSize
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible import io.legado.app.utils.visible
import io.legado.app.utils.windowSize
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,7 +39,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
} }
@ -94,60 +90,11 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
} }
return true return true
} }
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
noButton()
}.show().requestInputMethod()
}
@SuppressLint("InflateParams")
private fun editGroup(bookGroup: BookGroup) {
alert(title = getString(R.string.group_edit)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
editView.setText(bookGroup.groupName)
}
if (bookGroup.groupId >= 0) {
neutralButton(R.string.delete) {
deleteGroup(bookGroup)
}
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
viewModel.upGroup(bookGroup.copy(groupName = it))
}
}
noButton()
}.show().requestInputMethod()
}
private fun deleteGroup(bookGroup: BookGroup) {
alert(R.string.delete, R.string.sure_del) {
okButton {
viewModel.delGroup(bookGroup)
}
noButton()
}.show()
}
private inner class GroupAdapter(context: Context) : private inner class GroupAdapter(context: Context) :
RecyclerAdapter<BookGroup, ItemBookGroupManageBinding>(context), RecyclerAdapter<BookGroup, ItemBookGroupManageBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
@ -173,7 +120,11 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun registerListener(holder: ItemViewHolder, binding: ItemBookGroupManageBinding) { override fun registerListener(holder: ItemViewHolder, binding: ItemBookGroupManageBinding) {
binding.run { binding.run {
tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } tvEdit.setOnClickListener {
getItem(holder.layoutPosition)?.let { bookGroup ->
GroupEditDialog.start(childFragmentManager, bookGroup)
}
}
swShow.setOnCheckedChangeListener { buttonView, isChecked -> swShow.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) { if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {

@ -1,6 +1,5 @@
package io.legado.app.ui.book.group package io.legado.app.ui.book.group
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -20,18 +19,15 @@ import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogBookGroupPickerBinding import io.legado.app.databinding.DialogBookGroupPickerBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.ItemGroupSelectBinding import io.legado.app.databinding.ItemGroupSelectBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getSize
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -61,7 +57,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
} }
@ -116,46 +112,11 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
} }
return true return true
} }
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
noButton()
}.show().requestInputMethod()
}
@SuppressLint("InflateParams")
private fun editGroup(bookGroup: BookGroup) {
alert(title = getString(R.string.group_edit)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
editView.setText(bookGroup.groupName)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
viewModel.upGroup(bookGroup.copy(groupName = it))
}
}
noButton()
}.show().requestInputMethod()
}
private inner class GroupAdapter(context: Context) : private inner class GroupAdapter(context: Context) :
RecyclerAdapter<BookGroup, ItemGroupSelectBinding>(context), RecyclerAdapter<BookGroup, ItemGroupSelectBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
@ -192,7 +153,9 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
} }
} }
} }
tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } tvEdit.setOnClickListener {
GroupEditDialog.start(childFragmentManager, getItem(holder.layoutPosition))
}
} }
} }

@ -7,7 +7,15 @@ import io.legado.app.data.entities.BookGroup
class GroupViewModel(application: Application) : BaseViewModel(application) { class GroupViewModel(application: Application) : BaseViewModel(application) {
fun addGroup(groupName: String) { fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
execute {
appDb.bookGroupDao.update(*bookGroup)
}.onFinally {
finally?.invoke()
}
}
fun addGroup(groupName: String, cover: String?, finally: () -> Unit) {
execute { execute {
var id = 1L var id = 1L
val idsSum = appDb.bookGroupDao.idsSum val idsSum = appDb.bookGroupDao.idsSum
@ -17,19 +25,16 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
val bookGroup = BookGroup( val bookGroup = BookGroup(
groupId = id, groupId = id,
groupName = groupName, groupName = groupName,
cover = cover,
order = appDb.bookGroupDao.maxOrder.plus(1) order = appDb.bookGroupDao.maxOrder.plus(1)
) )
appDb.bookGroupDao.insert(bookGroup) appDb.bookGroupDao.insert(bookGroup)
}.onFinally {
finally()
} }
} }
fun upGroup(vararg bookGroup: BookGroup) { fun delGroup(vararg bookGroup: BookGroup, finally: () -> Unit) {
execute {
appDb.bookGroupDao.update(*bookGroup)
}
}
fun delGroup(vararg bookGroup: BookGroup) {
execute { execute {
appDb.bookGroupDao.delete(*bookGroup) appDb.bookGroupDao.delete(*bookGroup)
bookGroup.forEach { group -> bookGroup.forEach { group ->
@ -39,6 +44,8 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
} }
appDb.bookDao.update(*books.toTypedArray()) appDb.bookDao.update(*books.toTypedArray())
} }
}.onFinally {
finally()
} }
} }

@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.RequestOptions.bitmapTransform
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.appDb import io.legado.app.data.appDb
@ -36,6 +37,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -151,6 +153,14 @@ class BookInfoActivity :
viewModel.loadBookInfo(it, false) viewModel.loadBookInfo(it, false)
} }
} }
R.id.menu_login -> viewModel.bookSource?.let {
startActivity<SourceLoginActivity> {
putExtra("sourceUrl", it.bookSourceUrl)
putExtra("loginUrl", it.loginUrl)
putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME])
}
}
R.id.menu_top -> viewModel.topBook()
R.id.menu_copy_book_url -> viewModel.bookData.value?.bookUrl?.let { R.id.menu_copy_book_url -> viewModel.bookData.value?.bookUrl?.let {
sendToClip(it) sendToClip(it)
} ?: toastOnUi(R.string.no_book) } ?: toastOnUi(R.string.no_book)

@ -5,6 +5,7 @@ import android.content.Intent
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -13,6 +14,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -167,6 +169,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
upChangeDurChapterIndex(newBook, oldTocSize, it) upChangeDurChapterIndex(newBook, oldTocSize, it)
} }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
} }
} }
@ -192,6 +196,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun topBook() {
execute {
bookData.value?.let { book ->
val minOrder = appDb.bookDao.minOrder
book.order = minOrder - 1
appDb.bookDao.update(book)
}
}
}
fun saveBook(success: (() -> Unit)? = null) { fun saveBook(success: (() -> Unit)? = null) {
execute { execute {
bookData.value?.let { book -> bookData.value?.let { book ->

@ -5,27 +5,24 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.databinding.ActivityBookInfoEditBinding import io.legado.app.databinding.ActivityBookInfoEditBinding
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.utils.* import io.legado.app.utils.FileUtils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.externalFiles
import io.legado.app.utils.read
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import java.io.File
class BookInfoEditActivity : class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(), VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val selectCoverResult = private val selectCover = registerForActivityResult(SelectImageContract()) {
registerForActivityResult(ActivityResultContracts.GetContent()) { it?.second?.let { uri ->
it?.let { uri ->
coverChangeTo(uri) coverChangeTo(uri)
} }
} }
@ -62,7 +59,7 @@ class BookInfoEditActivity :
} }
} }
tvSelectCover.setOnClickListener { tvSelectCover.setOnClickListener {
selectCoverResult.launch("image/*") selectCover.launch(null)
} }
tvRefreshCover.setOnClickListener { tvRefreshCover.setOnClickListener {
viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString()
@ -105,37 +102,11 @@ class BookInfoEditActivity :
} }
private fun coverChangeTo(uri: Uri) { private fun coverChangeTo(uri: Uri) {
if (uri.isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(this, uri)
doc?.name?.let {
var file = this.externalFiles var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", it) file = FileUtils.createFileIfNotExist(file, "covers", name)
kotlin.runCatching { file.writeBytes(bytes)
DocumentUtils.readBytes(this, doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
coverChangeTo(file.absolutePath) coverChangeTo(file.absolutePath)
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(this, uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name)
file.writeBytes(imgFile.readBytes())
coverChangeTo(file.absolutePath)
}
}
}
.request()
} }
} }

@ -280,6 +280,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
private fun alertImportFileName() { private fun alertImportFileName() {
alert(R.string.import_file_name) { alert(R.string.import_file_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "js"
editView.setText(AppConfig.bookImportFileName) editView.setText(AppConfig.bookImportFileName)
} }
customView { alertBinding.root } customView { alertBinding.root }

@ -125,6 +125,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
private var loadStates: Boolean = false private var loadStates: Boolean = false
override val pageFactory: TextPageFactory get() = binding.readView.pageFactory override val pageFactory: TextPageFactory get() = binding.readView.pageFactory
override val headerHeight: Int get() = binding.readView.curPage.headerHeight override val headerHeight: Int get() = binding.readView.curPage.headerHeight
private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -140,6 +141,10 @@ class ReadBookActivity : ReadBookBaseActivity(),
upMenu() upMenu()
upView() upView()
} }
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
viewModel.initData(intent) viewModel.initData(intent)
} }
@ -163,6 +168,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
autoPageStop()
mHandler.removeCallbacks(backupRunnable) mHandler.removeCallbacks(backupRunnable)
ReadBook.saveRead() ReadBook.saveRead()
timeBatteryReceiver?.let { timeBatteryReceiver?.let {
@ -326,6 +332,9 @@ class ReadBookActivity : ReadBookBaseActivity(),
* 按键事件 * 按键事件
*/ */
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (menuLayoutIsVisible) {
return super.onKeyDown(keyCode, event)
}
when { when {
isPrevKey(keyCode) -> { isPrevKey(keyCode) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
@ -589,7 +598,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
resetPageOffset: Boolean, resetPageOffset: Boolean,
success: (() -> Unit)? success: (() -> Unit)?
) { ) {
runOnUI { launch {
autoPageProgress = 0 autoPageProgress = 0
binding.readView.upContent(relativePosition, resetPageOffset) binding.readView.upContent(relativePosition, resetPageOffset)
binding.readMenu.setSeekPage(ReadBook.durPageIndex()) binding.readMenu.setSeekPage(ReadBook.durPageIndex())
@ -602,13 +611,13 @@ class ReadBookActivity : ReadBookBaseActivity(),
* 更新视图 * 更新视图
*/ */
override fun upView() { override fun upView() {
runOnUI { launch {
binding.readMenu.upBookView() binding.readMenu.upBookView()
} }
} }
override fun upPageAnim() { override fun upPageAnim() {
runOnUI { launch {
binding.readView.upPageAnim() binding.readView.upPageAnim()
} }
} }
@ -617,7 +626,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
* 页面改变 * 页面改变
*/ */
override fun pageChanged() { override fun pageChanged() {
runOnUI { launch {
autoPageProgress = 0 autoPageProgress = 0
binding.readMenu.setSeekPage(ReadBook.durPageIndex()) binding.readMenu.setSeekPage(ReadBook.durPageIndex())
mHandler.postDelayed(backupRunnable, 600000) mHandler.postDelayed(backupRunnable, 600000)
@ -677,6 +686,8 @@ class ReadBookActivity : ReadBookBaseActivity(),
binding.readView.upContent(1) binding.readView.upContent(1)
autoPagePlus() autoPagePlus()
binding.readMenu.setAutoPage(true) binding.readMenu.setAutoPage(true)
screenTimeOut = -1L
screenOffTimerStart()
} }
} }
@ -685,16 +696,20 @@ class ReadBookActivity : ReadBookBaseActivity(),
mHandler.removeCallbacks(autoPageRunnable) mHandler.removeCallbacks(autoPageRunnable)
binding.readView.upContent() binding.readView.upContent()
binding.readMenu.setAutoPage(false) binding.readMenu.setAutoPage(false)
upScreenTimeOut()
} }
private fun autoPagePlus() { private fun autoPagePlus() {
var delayMillis = ReadBookConfig.autoReadSpeed * 1000L / binding.readView.height var delayMillis = ReadBookConfig.autoReadSpeed * 1000L / binding.readView.height
var scrollOffset = 1 var scrollOffset = 1
if (delayMillis < 20) { if (delayMillis < 20) {
scrollOffset = 20 / delayMillis.toInt() var delayInt = delayMillis.toInt()
if (delayInt == 0) delayInt = 1
scrollOffset = 20 / delayInt
delayMillis = 20 delayMillis = 20
} }
mHandler.removeCallbacks(autoPageRunnable) mHandler.removeCallbacks(autoPageRunnable)
if (!menuLayoutIsVisible) {
if (binding.readView.isScroll) { if (binding.readView.isScroll) {
binding.readView.curPage.scroll(-scrollOffset) binding.readView.curPage.scroll(-scrollOffset)
} else { } else {
@ -708,6 +723,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
binding.readView.invalidate() binding.readView.invalidate()
} }
} }
}
mHandler.postDelayed(autoPageRunnable, delayMillis) mHandler.postDelayed(autoPageRunnable, delayMillis)
} }
@ -957,9 +973,8 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
private fun upScreenTimeOut() { private fun upScreenTimeOut() {
getPrefString(PreferKey.keepLight)?.let { val keepLightPrefer = getPrefString(PreferKey.keepLight)?.toInt() ?: 0
screenTimeOut = it.toLong() * 1000 screenTimeOut = keepLightPrefer * 1000L
}
screenOffTimerStart() screenOffTimerStart()
} }

@ -202,6 +202,7 @@ abstract class ReadBookBaseActivity :
fun showCharsetConfig() { fun showCharsetConfig() {
alert(R.string.set_charset) { alert(R.string.set_charset) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "charset"
editView.setFilterValues(charsets) editView.setFilterValues(charsets)
editView.setText(ReadBook.book?.charset) editView.setText(ReadBook.book?.charset)
} }

@ -5,6 +5,7 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -20,6 +21,7 @@ import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -207,6 +209,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
upChangeDurChapterIndex(newBook, oldTocSize, it) upChangeDurChapterIndex(newBook, oldTocSize, it)
} }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
} }
} }

@ -73,7 +73,7 @@ class AutoReadDialog : BaseDialogFragment() {
} }
private fun initData() { private fun initData() {
val speed = if (ReadBookConfig.autoReadSpeed < 10) 10 else ReadBookConfig.autoReadSpeed val speed = if (ReadBookConfig.autoReadSpeed < 2) 2 else ReadBookConfig.autoReadSpeed
binding.tvReadSpeed.text = String.format("%ds", speed) binding.tvReadSpeed.text = String.format("%ds", speed)
binding.seekAutoRead.progress = speed binding.seekAutoRead.progress = speed
} }
@ -81,13 +81,13 @@ class AutoReadDialog : BaseDialogFragment() {
private fun initOnChange() { private fun initOnChange() {
binding.seekAutoRead.setOnSeekBarChangeListener(object : SeekBarChangeListener { binding.seekAutoRead.setOnSeekBarChangeListener(object : SeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val speed = if (progress < 10) 10 else progress val speed = if (progress < 2) 2 else progress
binding.tvReadSpeed.text = String.format("%ds", speed) binding.tvReadSpeed.text = String.format("%ds", speed)
} }
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
ReadBookConfig.autoReadSpeed = ReadBookConfig.autoReadSpeed =
if (binding.seekAutoRead.progress < 10) 10 else binding.seekAutoRead.progress if (binding.seekAutoRead.progress < 2) 2 else binding.seekAutoRead.progress
upTtsSpeechRate() upTtsSpeechRate()
} }
}) })

@ -6,7 +6,6 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
@ -21,8 +20,6 @@ import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.getSecondaryTextColor import io.legado.app.lib.theme.getSecondaryTextColor
@ -46,14 +43,15 @@ class BgTextConfigDialog : BaseDialogFragment() {
private var primaryTextColor = 0 private var primaryTextColor = 0
private var secondaryTextColor = 0 private var secondaryTextColor = 0
private val importFormNet = "网络导入" private val importFormNet = "网络导入"
private val selectBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { private val selectBgImage = registerForActivityResult(SelectImageContract()) {
setBgFromUri(it) it?.second?.let { uri ->
setBgFromUri(uri)
}
} }
private val selectExportDir = registerForActivityResult(FilePicker()) { private val selectExportDir = registerForActivityResult(FilePicker()) {
it?.let { it ?: return@registerForActivityResult
exportConfig(it) exportConfig(it)
} }
}
private val selectImportDoc = registerForActivityResult(FilePicker()) { private val selectImportDoc = registerForActivityResult(FilePicker()) {
it ?: return@registerForActivityResult it ?: return@registerForActivityResult
if (it.toString() == importFormNet) { if (it.toString() == importFormNet) {
@ -122,7 +120,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
ivBg.setImageResource(R.drawable.ic_image) ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor) ivBg.setColorFilter(primaryTextColor)
root.setOnClickListener { root.setOnClickListener {
selectBgImage.launch("image/*") selectBgImage.launch(null)
} }
} }
} }
@ -142,6 +140,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
binding.ivEdit.setOnClickListener { binding.ivEdit.setOnClickListener {
alert(R.string.style_name) { alert(R.string.style_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "name"
editView.setText(ReadBookConfig.durConfig.name) editView.setText(ReadBookConfig.durConfig.name)
} }
customView { alertBinding.root } customView { alertBinding.root }
@ -344,35 +343,13 @@ class BgTextConfigDialog : BaseDialogFragment() {
} }
private fun setBgFromUri(uri: Uri) { private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(requireContext(), uri) var file = requireContext().externalFiles
doc?.name?.let { file = FileUtils.createFileIfNotExist(file, "bg", name)
val file = file.writeBytes(bytes)
FileUtils.createFileIfNotExist(requireContext().externalFiles, "bg", it)
kotlin.runCatching {
DocumentUtils.readBytes(requireContext(), doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
ReadBookConfig.durConfig.setCurBg(2, file.absolutePath) ReadBookConfig.durConfig.setCurBg(2, file.absolutePath)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
ReadBookConfig.durConfig.setCurBg(2, path)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)
}
}
.request()
} }
} }
} }

@ -11,9 +11,9 @@ import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.databinding.DialogReadPaddingBinding import io.legado.app.databinding.DialogReadPaddingBinding
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.getSize
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize
class PaddingConfigDialog : BaseDialogFragment() { class PaddingConfigDialog : BaseDialogFragment() {
@ -21,7 +21,7 @@ class PaddingConfigDialog : BaseDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.let { dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
val attr = it.attributes val attr = it.attributes

@ -19,8 +19,8 @@ import io.legado.app.lib.theme.backgroundColor
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.utils.getPrefLong import io.legado.app.utils.getPrefLong
import io.legado.app.utils.getSize
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.windowSize
import splitties.init.appCtx import splitties.init.appCtx
class ReadAloudConfigDialog : DialogFragment() { class ReadAloudConfigDialog : DialogFragment() {
@ -28,7 +28,7 @@ class ReadAloudConfigDialog : DialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent) it.setBackgroundDrawableResource(R.color.transparent)
it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)

@ -54,7 +54,7 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
} }
@ -134,6 +134,7 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
alert(R.string.import_on_line) { alert(R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "url"
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)

@ -45,7 +45,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = requireActivity().getSize() val dm = requireActivity().windowSize
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt()) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt())
} }
@ -135,8 +135,8 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
cacheUrls.add(0, defaultUrl) cacheUrls.add(0, defaultUrl)
} }
requireContext().alert(titleResource = R.string.import_on_line) { requireContext().alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater) val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
alertBinding.apply { textInputLayout.hint = "url"
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)

@ -250,7 +250,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[2] != charIndex selectStart[2] != charIndex
) { ) {
if (selectToInt(relativePos, lineIndex, charIndex) if (selectToInt(relativePos, lineIndex, charIndex)
< selectToInt(selectEnd) <= selectToInt(selectEnd)
) { ) {
selectStart[0] = relativePos selectStart[0] = relativePos
selectStart[1] = lineIndex selectStart[1] = lineIndex
@ -276,7 +276,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|| selectEnd[2] != charIndex || selectEnd[2] != charIndex
) { ) {
if (selectToInt(relativePos, lineIndex, charIndex) if (selectToInt(relativePos, lineIndex, charIndex)
> selectToInt(selectStart) >= selectToInt(selectStart)
) { ) {
selectEnd[0] = relativePos selectEnd[0] = relativePos
selectEnd[1] = lineIndex selectEnd[1] = lineIndex

@ -85,7 +85,9 @@ class PageView(context: Context) : FrameLayout(context) {
vwTopDivider.visible(it.showHeaderLine) vwTopDivider.visible(it.showHeaderLine)
vwBottomDivider.visible(it.showFooterLine) vwBottomDivider.visible(it.showFooterLine)
pageNvBar.layoutParams = pageNvBar.layoutParams.apply { pageNvBar.layoutParams = pageNvBar.layoutParams.apply {
height = if (it.hideNavigationBar) 0 else App.navigationBarHeight height =
if (it.hideNavigationBar) 0
else App.navigationBarHeight
} }
} }
contentTextView.upVisibleRect() contentTextView.upVisibleRect()

@ -43,9 +43,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
upContent() upContent()
} }
var isScroll = false var isScroll = false
var prevPage: PageView = PageView(context) val prevPage by lazy { PageView(context) }
var curPage: PageView = PageView(context) val curPage by lazy { PageView(context) }
var nextPage: PageView = PageView(context) val nextPage by lazy { PageView(context) }
val defaultAnimationSpeed = 300 val defaultAnimationSpeed = 300
private var pressDown = false private var pressDown = false
private var isMove = false private var isMove = false

@ -343,9 +343,11 @@ object ChapterProvider {
} }
x = x1 x = x1
} }
if (words.size > bodyIndent.length) {
val words1 = words.copyOfRange(bodyIndent.length, words.size) val words1 = words.copyOfRange(bodyIndent.length, words.size)
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList) addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList)
} }
}
/** /**
* 无缩进,两端对齐 * 无缩进,两端对齐

@ -444,7 +444,7 @@ class BookSourceEditActivity :
val rect = Rect() val rect = Rect()
// 获取当前页面窗口的显示范围 // 获取当前页面窗口的显示范围
window.decorView.getWindowVisibleDisplayFrame(rect) window.decorView.getWindowVisibleDisplayFrame(rect)
val screenHeight = this@BookSourceEditActivity.getSize().heightPixels val screenHeight = this@BookSourceEditActivity.windowSize.heightPixels
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val preShowing = mIsSoftKeyBoardShowing val preShowing = mIsSoftKeyBoardShowing
if (abs(keyboardHeight) > screenHeight / 5) { if (abs(keyboardHeight) > screenHeight / 5) {

@ -1,5 +1,6 @@
package io.legado.app.ui.book.source.edit package io.legado.app.ui.book.source.edit
import android.annotation.SuppressLint
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
@ -12,6 +13,7 @@ import io.legado.app.databinding.ItemSourceEditBinding
class BookSourceEditAdapter : RecyclerView.Adapter<BookSourceEditAdapter.MyViewHolder>() { class BookSourceEditAdapter : RecyclerView.Adapter<BookSourceEditAdapter.MyViewHolder>() {
var editEntities: ArrayList<EditEntity> = ArrayList() var editEntities: ArrayList<EditEntity> = ArrayList()
@SuppressLint("NotifyDataSetChanged")
set(value) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()

@ -327,6 +327,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun checkSource() { private fun checkSource() {
alert(titleResource = R.string.search_book_key) { alert(titleResource = R.string.search_book_key) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "search word"
editView.setText(CheckSource.keyword) editView.setText(CheckSource.keyword)
} }
customView { alertBinding.root } customView { alertBinding.root }
@ -346,7 +347,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun selectionAddToGroups() { private fun selectionAddToGroups() {
alert(titleResource = R.string.add_group) { alert(titleResource = R.string.add_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name) textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList()) editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp editView.dropDownHeight = 180.dp
} }
@ -366,7 +367,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun selectionRemoveFromGroups() { private fun selectionRemoveFromGroups() {
alert(titleResource = R.string.remove_group) { alert(titleResource = R.string.remove_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name) textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList()) editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp editView.dropDownHeight = 180.dp
} }
@ -400,6 +401,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_on_line) { alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "url"
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)

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

Loading…
Cancel
Save