update to latest

pull/1238/head
fisher 3 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. 151
      app/src/main/assets/updateLog.md
  17. 5
      app/src/main/assets/web/uploadBook/js/common.js
  18. 72
      app/src/main/assets/web/uploadBook/js/html5_fun.js
  19. 5
      app/src/main/java/io/legado/app/App.kt
  20. 48
      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. 30
      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. 11
      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. 53
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  47. 27
      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. 25
      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. 66
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  63. 45
      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. 87
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt
  66. 66
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  67. 44
      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. 57
      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. 51
      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. 53
      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. 6
      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:
release:
types: [published]
push:
branches:
- master
@ -32,7 +30,6 @@ jobs:
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt
run: |
echo "清空18PlusList.txt"
@ -45,36 +42,52 @@ jobs:
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)
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
run: |
echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -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
run: |
echo "开始进行release构建"
chmod +x gradlew
./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
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk
path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou
if: ${{ env.ylogin }}
run: |
path="$GITHUB_WORKSPACE/app/build/outputs/apk/app/release"
files=$(ls $path)
for f in $files
do
if [[ $f == *"apk" ]]; then
file=$f
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file"
break
fi
done
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID"
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/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)
## 下载
<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
* 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
@ -27,24 +32,21 @@
* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版
## 感谢
```
org.jsoup:jsoup
cn.wanghaomiao:JsoupXpath
com.jayway.jsonpath:json-path
com.github.gedoor:rhino-android
com.squareup.okhttp3:okhttp
com.ljx.rxhttp:rxhttp
com.github.bumptech.glide:glide
org.nanohttpd:nanohttpd
org.nanohttpd:nanohttpd-websocket
cn.bingoogolapple:bga-qrcode-zxing
com.jaredrummler:colorpicker
org.apache.commons:commons-text
io.noties.markwon:core
io.noties.markwon:image-glide
com.hankcs:hanlp
com.positiondev.epublib:epublib-core
```
> * org.jsoup:jsoup
> * cn.wanghaomiao:JsoupXpath
> * com.jayway.jsonpath:json-path
> * com.github.gedoor:rhino-android
> * com.squareup.okhttp3:okhttp
> * com.github.bumptech.glide:glide
> * org.nanohttpd:nanohttpd
> * org.nanohttpd:nanohttpd-websocket
> * cn.bingoogolapple:bga-qrcode-zxing
> * com.jaredrummler:colorpicker
> * org.apache.commons:commons-text
> * io.noties.markwon:core
> * io.noties.markwon:image-glide
> * com.hankcs:hanlp
> * com.positiondev.epublib:epublib-core
## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html

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

Binary file not shown.

@ -3,13 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
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.ACCESS_NETWORK_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.DOWNLOAD_WITHOUT_NOTIFICATION" />
<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
android:name=".App"
@ -34,7 +31,9 @@
android:theme="@style/AppTheme.Light"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
<!-- 主入口 -->
<activity android:name=".ui.welcome.WelcomeActivity">
<activity
android:name=".ui.welcome.WelcomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -50,6 +49,7 @@
<activity
android:name=".ui.welcome.Launcher1"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher1">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -66,6 +66,7 @@
<activity
android:name=".ui.welcome.Launcher2"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher2">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -82,6 +83,7 @@
<activity
android:name=".ui.welcome.Launcher3"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher3">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -98,6 +100,7 @@
<activity
android:name=".ui.welcome.Launcher4"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher4">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -114,6 +117,7 @@
<activity
android:name=".ui.welcome.Launcher5"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher5">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -130,6 +134,7 @@
<activity
android:name=".ui.welcome.Launcher6"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/launcher6">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -151,6 +156,7 @@
<activity
android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
@ -304,6 +310,7 @@
<!-- 文字处理 -->
<activity
android:name=".receiver.SharedReceiverActivity"
android:exported="true"
android:label="@string/receiving_shared_label"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
@ -325,6 +332,7 @@
<activity
android:name=".ui.association.OnLineImportActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/AppTheme.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -339,6 +347,7 @@
<!-- 打开文件 -->
<activity
android:name=".ui.association.FileAssociationActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Transparent">
<!-- VIEW (Open with) action -->
@ -388,12 +397,24 @@
<service android:name=".service.CheckSourceService" />
<service android:name=".service.CacheBookService" />
<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.HttpReadAloudService" />
<service android:name=".service.AudioPlayService" />
<service android:name=".service.DownloadService" />
<receiver android:name=".receiver.MediaButtonReceiver">
<receiver
android:name=".receiver.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>

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

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

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

@ -123,42 +123,46 @@ try {
//正在上传
isUploading = true;
//设置上传的数据
var fd = new FormData();
fd.append("Filename", file.name);
fd.append("Filedata", file);
fd.append("Upload", "Submit Query");
//设置当前的上传对象
currUploadfile = file;
if (XHR.readyState > 0) {
XHR = new XMLHttpRequest();
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
var data = e.target.result;
var fd = new FormData();
fd.append("fileName", file.name);
fd.append("fileData", data);
//设置当前的上传对象
currUploadfile = file;
if (XHR.readyState > 0) {
XHR = new XMLHttpRequest();
}
XHR.upload.addEventListener("progress", progress, false);
XHR.upload.addEventListener("load", requestLoad, false);
XHR.upload.addEventListener("error", error, false);
XHR.upload.addEventListener("abort", abort, false);
XHR.upload.addEventListener("loadend", loadend, false);
XHR.upload.addEventListener("loadstart", loadstart, false);
XHR.open("POST", config.url);
XHR.send(fd);
XHR.onreadystatechange = function () {
//只要上传完成不管成功失败
if (XHR.readyState == 4) {
console.log("onreadystatechange ", XHR.status, +new Date());
if (XHR.status == 200) {
uploadSuccess(currUploadfile, {}, XHR.status)
} else {
uploadError()
}
//进行下一个上传
nextUpload()
}
};
}
XHR.upload.addEventListener("progress", progress, false);
XHR.upload.addEventListener("load", requestLoad, false);
XHR.upload.addEventListener("error", error, false);
XHR.upload.addEventListener("abort", abort, false);
XHR.upload.addEventListener("loadend", loadend, false);
XHR.upload.addEventListener("loadstart", loadstart, false);
XHR.open("POST", config.url);
XHR.setRequestHeader("Content-Type", "multipart/mixed stream");
XHR.send(fd);
XHR.onreadystatechange = function () {
//只要上传完成不管成功失败
if (XHR.readyState == 4) {
console.log("onreadystatechange ", XHR.status, +new Date());
if (XHR.status == 200) {
uploadSuccess(currUploadfile, {}, XHR.status)
} else {
uploadError()
}
//进行下一个上传
nextUpload()
}
};
}
//请求完成,无论失败或成功

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

@ -1,8 +1,7 @@
package io.legado.app.api.controller
import android.util.Base64
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.api.ReturnData
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.utils.*
import kotlinx.coroutines.runBlocking
import org.apache.commons.fileupload.disk.DiskFileItemFactory
import splitties.init.appCtx
object BookController {
@ -193,31 +191,31 @@ object BookController {
}
}
private val bookFileFactory by lazy {
DiskFileItemFactory(0, LocalBook.cacheFolder)
}
fun addLocalBook(session: NanoHTTPD.IHTTPSession, postData: String?): ReturnData {
fun addLocalBook(parameters: Map<String, List<String>>): ReturnData {
val returnData = ReturnData()
try {
NanoFileUpload(bookFileFactory).parseRequest(session).forEach {
val path = FileUtils.getPath(LocalBook.cacheFolder, it.name)
val nameAuthor = LocalBook.analyzeNameAuthor(it.name)
val book = Book(
bookUrl = path,
name = nameAuthor.first,
author = nameAuthor.second,
originName = it.name,
coverUrl = FileUtils.getPath(
appCtx.externalFiles,
"covers",
"${MD5Utils.md5Encode16(path)}.jpg"
)
val fileName = parameters["fileName"]?.firstOrNull()
?: return returnData.setErrorMsg("fileName 不能为空")
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(
bookUrl = file.absolutePath,
name = nameAuthor.first,
author = nameAuthor.second,
originName = fileName,
coverUrl = FileUtils.getPath(
appCtx.externalFiles,
"covers",
"${MD5Utils.md5Encode16(file.absolutePath)}.jpg"
)
if (book.isEpub()) EpubFile.upBookInfo(book)
if (book.isUmd()) UmdFile.upBookInfo(book)
appDb.bookDao.insert(book)
}
)
if (book.isEpub()) EpubFile.upBookInfo(book)
if (book.isUmd()) UmdFile.upBookInfo(book)
appDb.bookDao.insert(book)
} catch (e: Exception) {
e.printStackTrace()
return returnData.setErrorMsg(

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

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

@ -5,7 +5,7 @@ import java.util.regex.Pattern
@Suppress("RegExpRedundantEscape")
object AppPattern {
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]*?)\\}\\}")
//匹配格式化后的图片格式

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

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

@ -24,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class],
version = 33,
version = 34,
exportSchema = true
)
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_23_24, migration_24_25, migration_25_26, migration_26_27,
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()
.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")
val allBookCount: Int
@get:Query("select min(`order`) from books")
val minOrder: Int
@get:Query("select max(`order`) from books")
val maxOrder: Int

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

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

@ -46,6 +46,17 @@ data class BookSource(
var ruleContent: ContentRule? = null // 正文页规则
) : 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:Ignore
@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)
var threadCount: Int
get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
get() = appCtx.getPrefInt(PreferKey.threadCount, 8)
set(value) {
appCtx.putPrefInt(PreferKey.threadCount, value)
}

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

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

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

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

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

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

@ -1,6 +1,6 @@
package io.legado.app.help.http.cronet
import io.legado.app.help.http.CookieStore
import android.util.Log
import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.Request
@ -14,19 +14,28 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors
val executor: Executor by lazy { Executors.newSingleThreadExecutor() }
val executor: Executor by lazy { Executors.newCachedThreadPool() }
val cronetEngine: ExperimentalCronetEngine by lazy {
CronetLoader.preDownload()
val builder = ExperimentalCronetEngine.Builder(appCtx)
.setStoragePath(appCtx.externalCacheDir?.absolutePath)
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))
.enableQuic(true)
.setLibraryLoader(CronetLoader)//设置自定义so库加载
.setStoragePath(appCtx.externalCacheDir?.absolutePath)//设置缓存路径
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))//设置缓存模式
.enableQuic(true)//设置支持http/3
.enableHttp2(true) //设置支持http/2
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
.enableHttp2(true)
//.enableNetworkQualityEstimator(true)
//Brotli压缩
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 requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor)
requestBuilder.setHttpMethod(request.method)
val cookie = CookieStore.getCookie(url)
if (cookie.length > 1) {
requestBuilder.addHeader("Cookie", cookie)
}
val headers: Headers = request.headers
headers.forEachIndexed { index, _ ->
val name = headers.name(index)
if (!name.equals("Keep-Alive", true)) {
requestBuilder.addHeader(name, headers.value(index))
}
requestBuilder.addHeader(headers.name(index), headers.value(index))
}
val requestBody = request.body

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

@ -3,6 +3,7 @@ package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.help.DefaultData
@ -49,6 +50,8 @@ object Backup {
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async {
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) {
try {
if (initWebDav()) {
if (initWebDav() && NetworkUtils.isAvailable()) {
val paths = arrayListOf(*Backup.backupFileNames)
for (i in 0 until paths.size) {
paths[i] = path + File.separator + paths[i]
@ -116,15 +116,13 @@ object BookWebDav {
}
}
} 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) {
try {
if (initWebDav()) {
if (initWebDav() && NetworkUtils.isAvailable()) {
// 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话
@ -141,6 +139,7 @@ object BookWebDav {
}
fun uploadBookProgress(book: Book) {
if (!NetworkUtils.isAvailable()) return
Coroutine.async {
val bookProgress = BookProgress(
name = book.name,
@ -159,7 +158,7 @@ object BookWebDav {
}
suspend fun getBookProgress(book: Book): BookProgress? {
if (initWebDav()) {
if (initWebDav() && NetworkUtils.isAvailable()) {
val url = getProgressUrl(book)
WebDav(url).download()?.let { byteArray ->
val json = String(byteArray)

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

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

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

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

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

@ -161,17 +161,22 @@ class AudioPlayService : BaseService(),
}
private fun resume() {
pause = false
if (!mediaPlayer.isPlaying) {
mediaPlayer.start()
mediaPlayer.seekTo(position)
try {
pause = false
if (!mediaPlayer.isPlaying) {
mediaPlayer.start()
mediaPlayer.seekTo(position)
}
handler.removeCallbacks(mpRunnable)
handler.postDelayed(mpRunnable, 1000)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification()
} catch (e: Exception) {
e.printStackTrace()
stopSelf()
}
handler.removeCallbacks(mpRunnable)
handler.postDelayed(mpRunnable, 1000)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification()
}
private fun adjustProgress(position: Int) {

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

@ -27,11 +27,12 @@ import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
import kotlin.math.min
class CacheBookService : BaseService() {
private val threadCount = AppConfig.threadCount
private var searchPool =
Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var cachePool =
Executors.newFixedThreadPool(min(threadCount, 8)).asCoroutineDispatcher()
private var tasks = CompositeCoroutine()
private val handler = Handler(Looper.getMainLooper())
private var runnable: Runnable = Runnable { upDownload() }
@ -82,7 +83,7 @@ class CacheBookService : BaseService() {
override fun onDestroy() {
tasks.clear()
searchPool.close()
cachePool.close()
handler.removeCallbacks(runnable)
downloadMap.clear()
finalMap.clear()
@ -133,7 +134,7 @@ class CacheBookService : BaseService() {
return
}
downloadCount[bookUrl] = DownloadCount()
execute {
execute(context = cachePool) {
appDb.bookChapterDao.getChapterList(bookUrl, start, end).let {
if (it.isNotEmpty()) {
val chapters = CopyOnWriteArraySet<BookChapter>()
@ -158,7 +159,7 @@ class CacheBookService : BaseService() {
private fun download() {
downloadingCount += 1
val task = Coroutine.async(this, context = searchPool) {
val task = Coroutine.async(this, context = cachePool) {
if (!isActive) return@async
val bookChapter: BookChapter? = synchronized(this@CacheBookService) {
downloadMap.forEach {
@ -185,16 +186,16 @@ class CacheBookService : BaseService() {
return@async
}
if (!BookHelp.hasImageContent(book, bookChapter)) {
webBook.getContent(this, book, bookChapter, context = searchPool)
webBook.getContent(this, book, bookChapter, context = cachePool)
.timeout(60000L)
.onError {
.onError(cachePool) {
synchronized(this) {
downloadingList.remove(bookChapter.url)
}
notificationContent = "getContentError${it.localizedMessage}"
upNotification()
}
.onSuccess {
.onSuccess(cachePool) {
synchronized(this@CacheBookService) {
downloadCount[book.bookUrl]?.increaseSuccess()
downloadCount[book.bookUrl]?.increaseFinished()
@ -217,7 +218,7 @@ class CacheBookService : BaseService() {
downloadCount.remove(book.bookUrl)
}
}
}.onFinally {
}.onFinally(cachePool) {
postDownloading(true)
}
} else {
@ -227,7 +228,7 @@ class CacheBookService : BaseService() {
postDownloading(true)
}
}
}.onError {
}.onError(cachePool) {
notificationContent = "ERROR:${it.localizedMessage}"
CacheBook.addLog(notificationContent)
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.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import kotlin.math.min
class CheckSourceService : BaseService() {
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 val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>()
@ -95,7 +94,7 @@ class CheckSourceService : BaseService() {
synchronized(this) {
processIndex++
}
execute {
execute(context = searchCoroutine) {
if (index < allIds.size) {
val sourceUrl = allIds[index]
appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
@ -130,14 +129,14 @@ class CheckSourceService : BaseService() {
throw Exception("正文内容为空")
}
}.timeout(180000L)
.onError(IO) {
.onError(searchCoroutine) {
source.addGroup("失效")
source.bookSourceComment = """
"error:${it.localizedMessage}
${source.bookSourceComment}"
""".trimIndent()
appDb.bookSourceDao.update(source)
}.onSuccess(IO) {
}.onSuccess(searchCoroutine) {
source.removeGroup("失效")
source.bookSourceComment = source.bookSourceComment
?.split("\n")
@ -145,7 +144,7 @@ class CheckSourceService : BaseService() {
it.startsWith("error:")
}?.joinToString("\n")
appDb.bookSourceDao.update(source)
}.onFinally {
}.onFinally(searchCoroutine) {
onNext(source.bookSourceUrl, source.bookSourceName)
}
}

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

@ -10,6 +10,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.constant.PreferKey
import io.legado.app.help.IntentHelp
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.*
import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer
@ -26,11 +27,7 @@ class WebService : BaseService() {
}
fun stop(context: Context) {
if (isRun) {
val intent = Intent(context, WebService::class.java)
intent.action = IntentAction.stop
context.startService(intent)
}
context.stopService<WebService>()
}
}
@ -44,6 +41,7 @@ class WebService : BaseService() {
isRun = true
notificationContent = getString(R.string.service_starting)
upNotification()
WebTileService.setState(this, true)
}
override fun onDestroy() {
@ -56,6 +54,7 @@ class WebService : BaseService() {
webSocketServer?.stop()
}
postEvent(EventBus.WEB_SERVICE, "")
WebTileService.setState(this, false)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -113,6 +112,9 @@ class WebService : BaseService() {
.setOngoing(true)
.setContentTitle(getString(R.string.web_service))
.setContentText(notificationContent)
.setContentIntent(
IntentHelp.activityPendingIntent<MainActivity>(this, "webService")
)
builder.addAction(
R.drawable.ic_stop_black_24dp,
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.service.AudioPlayService
import io.legado.app.utils.postEvent
import io.legado.app.utils.startService
object AudioPlay {
var titleData = MutableLiveData<String>()
@ -52,43 +53,43 @@ object AudioPlay {
fun pause(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.pause
context.startService(intent)
context.startService<AudioPlayService> {
action = IntentAction.pause
}
}
}
fun resume(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.resume
context.startService(intent)
context.startService<AudioPlayService> {
action = IntentAction.resume
}
}
}
fun stop(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.stop
context.startService(intent)
context.startService<AudioPlayService> {
action = IntentAction.stop
}
}
}
fun adjustSpeed(context: Context, adjust: Float) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.adjustSpeed
intent.putExtra("adjust", adjust)
context.startService(intent)
context.startService<AudioPlayService> {
action = IntentAction.adjustSpeed
putExtra("adjust", adjust)
}
}
}
fun adjustProgress(context: Context, position: Int) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.adjustProgress
intent.putExtra("position", position)
context.startService(intent)
context.startService<AudioPlayService> {
action = IntentAction.adjustProgress
putExtra("position", position)
}
}
}

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

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

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

@ -416,21 +416,21 @@ object ReadBook {
.getContent(book, chapter.title, content)
val textChapter = ChapterProvider
.getTextChapter(book, chapter, contents, chapterSize)
val offset = chapter.index - durChapterIndex
if (upContent) callBack?.upContent(offset, resetPageOffset)
when (offset) {
when (val offset = chapter.index - durChapterIndex) {
0 -> {
curTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset)
callBack?.upView()
curPageChanged()
callBack?.contentLoadFinish()
}
-1 -> {
prevTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
1 -> {
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.data.appDb
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.ItemSourceImportBinding
import io.legado.app.help.AppConfig
@ -83,6 +83,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_book_source)
@ -106,7 +107,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
}
binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll()
val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll
@ -143,16 +144,16 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
}
private fun upSelectText() {
if (viewModel.isSelectAll()) {
if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString(
R.string.select_cancel_count,
viewModel.selectCount(),
viewModel.selectCount,
viewModel.allSources.size
)
} else {
binding.tvFooterLeft.text = getString(
R.string.select_all_count,
viewModel.selectCount(),
viewModel.selectCount,
viewModel.allSources.size
)
}
@ -168,28 +169,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
@SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_new_group -> {
alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>()
appDb.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp
}
customView {
alertBinding.root
}
okButton {
alertBinding.editView.text?.toString()?.let { group ->
viewModel.groupName = group
item.title = getString(R.string.diy_edit_source_group_title, group)
}
}
noButton()
}.show()
}
R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
@ -198,6 +178,38 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
return false
}
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()
}
inner class SourcesAdapter(context: Context) :
RecyclerAdapter<BookSource, ItemSourceImportBinding>(context) {

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
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.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.splitNotBlank
class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null
val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>()
@ -28,27 +31,30 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
val checkSources = arrayListOf<BookSource?>()
val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean {
selectStatus.forEach {
if (!it) {
return false
val isSelectAll: Boolean
get() {
selectStatus.forEach {
if (!it) {
return false
}
}
return true
}
return true
}
fun selectCount(): Int {
var count = 0
selectStatus.forEach {
if (it) {
count++
val selectCount: Int
get() {
var count = 0
selectStatus.forEach {
if (it) {
count++
}
}
return count
}
return count
}
fun importSelect(finally: () -> Unit) {
execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName
val selectSource = arrayListOf<BookSource>()
selectStatus.forEachIndexed { index, b ->
@ -61,8 +67,17 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
source.customOrder = it.customOrder
}
}
if (groupName != null) {
source.bookSourceGroup = groupName
if (!group.isNullOrEmpty()) {
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)
}

@ -1,11 +1,14 @@
package io.legado.app.ui.association
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
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.adapter.ItemViewHolder
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.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
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.visible
class ImportReplaceRuleDialog : BaseDialogFragment() {
class ImportReplaceRuleDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
companion object {
fun start(
@ -65,10 +76,12 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
return inflater.inflate(R.layout.dialog_recycler_view, container)
}
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_replace_rule)
binding.rotateLoading.show()
initMenu()
adapter = SourcesAdapter(requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
@ -87,7 +100,7 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
}
binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll()
val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll
@ -123,17 +136,65 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
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() {
if (viewModel.isSelectAll()) {
if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString(
R.string.select_cancel_count,
viewModel.selectCount(),
viewModel.selectCount,
viewModel.allRules.size
)
} else {
binding.tvFooterLeft.text = getString(
R.string.select_all_count,
viewModel.selectCount(),
viewModel.selectCount,
viewModel.allRules.size
)
}
@ -155,6 +216,15 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
binding.run {
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
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 androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule
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.storage.OldReplace
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.splitNotBlank
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null
val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>()
@ -20,23 +24,61 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
val checkRules = arrayListOf<ReplaceRule?>()
val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean {
selectStatus.forEach {
if (!it) {
return false
val isSelectAll: Boolean
get() {
selectStatus.forEach {
if (!it) {
return false
}
}
return true
}
return true
}
fun selectCount(): Int {
var count = 0
selectStatus.forEach {
if (it) {
count++
val selectCount: Int
get() {
var count = 0
selectStatus.forEach {
if (it) {
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()
}
return count
}
fun import(text: String) {
@ -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() {
execute {
allRules.forEach {
checkRules.add(null)
selectStatus.add(false)
val rule = appDb.replaceRuleDao.findById(it.id)
checkRules.add(rule)
selectStatus.add(rule == null)
}
}.onSuccess {
successLiveData.postValue(allRules.size)

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

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

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

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

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

@ -33,7 +33,7 @@ import javax.script.SimpleBindings
class CacheViewModel(application: Application) : BaseViewModel(application) {
fun getExportFileName(book: Book): String {
private fun getExportFileName(book: Book): String {
val jsStr = AppConfig.bookExportFileName
if (jsStr.isNullOrBlank()) {
return "${book.name} 作者:${book.getRealAuthor()}"
@ -56,6 +56,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
}
}.onError {
finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess {
finally(context.getString(R.string.success))
}
@ -188,6 +189,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
}
}.onError {
finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess {
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.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.getSize
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize
class ChangeCoverDialog : BaseDialogFragment(),
@ -45,7 +44,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
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.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.CopyOnWriteArraySet
@ -50,7 +49,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
searchIndex = -1
}
@ -105,7 +104,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
val task = WebBook(source)
.searchBook(viewModelScope, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(Dispatchers.IO) {
.onSuccess(searchPool) {
if (it.isNotEmpty()) {
val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author
@ -119,7 +118,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
}
.onFinally {
.onFinally(searchPool) {
searchNext()
}
tasks.add(task)

@ -62,6 +62,11 @@ class ChangeSourceAdapter(
when (it) {
"name" -> tvOrigin.text = item.originName
"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 -> {
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
}
@ -98,5 +113,8 @@ class ChangeSourceAdapter(
val bookUrl: String?
fun changeTo(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 androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@ -53,7 +55,7 @@ class ChangeSourceDialog : BaseDialogFragment(),
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
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) {
val book = searchBook.toBook()
book.upInfoFromOld(callBack?.oldBook)
callBack?.changeTo(book)
changeSource(searchBook)
dismissAllowingStateLoss()
}
@ -211,6 +211,31 @@ class ChangeSourceDialog : BaseDialogFragment(),
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 {
val oldBook: Book?
fun changeTo(book: Book)

@ -24,6 +24,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import splitties.init.appCtx
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
import kotlin.math.min
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private val threadCount = AppConfig.threadCount
@ -56,7 +57,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher()
searchIndex = -1
}
@ -132,7 +133,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
val task = webBook
.searchBook(viewModelScope, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(IO) {
.onSuccess(searchPool) {
it.forEach { searchBook ->
if (searchBook.name == name) {
if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author))
@ -151,7 +152,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
}
.onFinally {
.onFinally(searchPool) {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
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
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
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.data.appDb
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogRecyclerViewBinding
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.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider
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.visible
import io.legado.app.utils.windowSize
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -43,7 +39,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
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 {
when (item?.itemId) {
R.id.menu_add -> addGroup()
R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
}
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) :
RecyclerAdapter<BookGroup, ItemBookGroupManageBinding>(context),
ItemTouchCallback.Callback {
@ -173,7 +120,11 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun registerListener(holder: ItemViewHolder, binding: ItemBookGroupManageBinding) {
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 ->
if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let {

@ -1,6 +1,5 @@
package io.legado.app.ui.book.group
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
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.entities.BookGroup
import io.legado.app.databinding.DialogBookGroupPickerBinding
import io.legado.app.databinding.DialogEditTextBinding
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.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider
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.windowSize
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -61,7 +57,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
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 {
when (item?.itemId) {
R.id.menu_add -> addGroup()
R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
}
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) :
RecyclerAdapter<BookGroup, ItemGroupSelectBinding>(context),
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) {
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 {
var id = 1L
val idsSum = appDb.bookGroupDao.idsSum
@ -17,19 +25,16 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
val bookGroup = BookGroup(
groupId = id,
groupName = groupName,
cover = cover,
order = appDb.bookGroupDao.maxOrder.plus(1)
)
appDb.bookGroupDao.insert(bookGroup)
}.onFinally {
finally()
}
}
fun upGroup(vararg bookGroup: BookGroup) {
execute {
appDb.bookGroupDao.update(*bookGroup)
}
}
fun delGroup(vararg bookGroup: BookGroup) {
fun delGroup(vararg bookGroup: BookGroup, finally: () -> Unit) {
execute {
appDb.bookGroupDao.delete(*bookGroup)
bookGroup.forEach { group ->
@ -39,6 +44,8 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
}
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 io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType
import io.legado.app.constant.Theme
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.source.edit.BookSourceEditActivity
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.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -151,6 +153,14 @@ class BookInfoActivity :
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 {
sendToClip(it)
} ?: toastOnUi(R.string.no_book)

@ -5,6 +5,7 @@ import android.content.Intent
import androidx.lifecycle.MutableLiveData
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@ -13,6 +14,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
@ -167,6 +169,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
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) {
execute {
bookData.value?.let { book ->

@ -5,30 +5,27 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
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.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 java.io.File
class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack {
private val selectCoverResult =
registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
coverChangeTo(uri)
}
private val selectCover = registerForActivityResult(SelectImageContract()) {
it?.second?.let { uri ->
coverChangeTo(uri)
}
}
override val binding by viewBinding(ActivityBookInfoEditBinding::inflate)
override val viewModel by viewModels<BookInfoEditViewModel>()
@ -62,7 +59,7 @@ class BookInfoEditActivity :
}
}
tvSelectCover.setOnClickListener {
selectCoverResult.launch("image/*")
selectCover.launch(null)
}
tvRefreshCover.setOnClickListener {
viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString()
@ -105,37 +102,11 @@ class BookInfoEditActivity :
}
private fun coverChangeTo(uri: Uri) {
if (uri.isContentScheme()) {
val doc = DocumentFile.fromSingleUri(this, uri)
doc?.name?.let {
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", it)
kotlin.runCatching {
DocumentUtils.readBytes(this, doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
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()
uri.read(this) { name, bytes ->
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", name)
file.writeBytes(bytes)
coverChangeTo(file.absolutePath)
}
}

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

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

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

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

@ -73,7 +73,7 @@ class AutoReadDialog : BaseDialogFragment() {
}
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.seekAutoRead.progress = speed
}
@ -81,13 +81,13 @@ class AutoReadDialog : BaseDialogFragment() {
private fun initOnChange() {
binding.seekAutoRead.setOnSeekBarChangeListener(object : SeekBarChangeListener {
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)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
ReadBookConfig.autoReadSpeed =
if (binding.seekAutoRead.progress < 10) 10 else binding.seekAutoRead.progress
if (binding.seekAutoRead.progress < 2) 2 else binding.seekAutoRead.progress
upTtsSpeechRate()
}
})

@ -6,7 +6,6 @@ import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog
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.lib.dialogs.alert
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.getPrimaryTextColor
import io.legado.app.lib.theme.getSecondaryTextColor
@ -46,13 +43,14 @@ class BgTextConfigDialog : BaseDialogFragment() {
private var primaryTextColor = 0
private var secondaryTextColor = 0
private val importFormNet = "网络导入"
private val selectBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
setBgFromUri(it)
private val selectBgImage = registerForActivityResult(SelectImageContract()) {
it?.second?.let { uri ->
setBgFromUri(uri)
}
}
private val selectExportDir = registerForActivityResult(FilePicker()) {
it?.let {
exportConfig(it)
}
it ?: return@registerForActivityResult
exportConfig(it)
}
private val selectImportDoc = registerForActivityResult(FilePicker()) {
it ?: return@registerForActivityResult
@ -122,7 +120,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor)
root.setOnClickListener {
selectBgImage.launch("image/*")
selectBgImage.launch(null)
}
}
}
@ -142,6 +140,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
binding.ivEdit.setOnClickListener {
alert(R.string.style_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "name"
editView.setText(ReadBookConfig.durConfig.name)
}
customView { alertBinding.root }
@ -344,35 +343,13 @@ class BgTextConfigDialog : BaseDialogFragment() {
}
private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.name?.let {
val file =
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.upBg()
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()
uri.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "bg", name)
file.writeBytes(bytes)
ReadBookConfig.durConfig.setCurBg(2, file.absolutePath)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)
}
}
}

@ -11,9 +11,9 @@ import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.databinding.DialogReadPaddingBinding
import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.getSize
import io.legado.app.utils.postEvent
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize
class PaddingConfigDialog : BaseDialogFragment() {
@ -21,7 +21,7 @@ class PaddingConfigDialog : BaseDialogFragment() {
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
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.help.ReadAloud
import io.legado.app.utils.getPrefLong
import io.legado.app.utils.getSize
import io.legado.app.utils.postEvent
import io.legado.app.utils.windowSize
import splitties.init.appCtx
class ReadAloudConfigDialog : DialogFragment() {
@ -28,7 +28,7 @@ class ReadAloudConfigDialog : DialogFragment() {
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)

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

@ -45,7 +45,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onStart() {
super.onStart()
val dm = requireActivity().getSize()
val dm = requireActivity().windowSize
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)
}
requireContext().alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
alertBinding.apply {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "url"
editView.setFilterValues(cacheUrls)
editView.delCallBack = {
cacheUrls.remove(it)

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

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

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

@ -343,8 +343,10 @@ object ChapterProvider {
}
x = x1
}
val words1 = words.copyOfRange(bodyIndent.length, words.size)
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList)
if (words.size > bodyIndent.length) {
val words1 = words.copyOfRange(bodyIndent.length, words.size)
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList)
}
}
/**

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

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

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

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

Loading…
Cancel
Save