Просмотр исходного кода

Merge branch 'gedoor:master' into master

Xwite 4 лет назад
Родитель
Сommit
5712b755fe
100 измененных файлов с 956 добавлено и 1507 удалено
  1. 6 0
      .github/dependabot.yml
  2. 1 1
      .github/workflows/autoupdatefork.yml
  3. 2 2
      .github/workflows/cronet.yml
  4. 4 4
      .github/workflows/release.yml
  5. 1 1
      .github/workflows/stale.yml
  6. 9 9
      .github/workflows/test.yml
  7. 22 12
      English.md
  8. 0 1
      README.md
  9. 3 5
      app/build.gradle
  10. BIN
      app/cronetlib/cronet_api.jar
  11. BIN
      app/cronetlib/cronet_impl_common_java.jar
  12. BIN
      app/cronetlib/cronet_impl_native_java.jar
  13. BIN
      app/cronetlib/cronet_impl_platform_java.jar
  14. 1 1
      app/src/main/assets/cronet.json
  15. 1 1
      app/src/main/assets/defaultData/directLinkUpload.json
  16. 1 2
      app/src/main/assets/help/ruleHelp.md
  17. 33 0
      app/src/main/assets/updateLog.md
  18. 0 0
      app/src/main/assets/web/bookSource/css/app.1bd41080.css
  19. 0 0
      app/src/main/assets/web/bookSource/css/app.37e72839.css
  20. BIN
      app/src/main/assets/web/bookSource/favicon.ico
  21. BIN
      app/src/main/assets/web/bookSource/fonts/Cascadia-Code-Regular-2.97c09831.ttf
  22. 0 150
      app/src/main/assets/web/bookSource/index.css
  23. 1 439
      app/src/main/assets/web/bookSource/index.html
  24. 0 516
      app/src/main/assets/web/bookSource/index.js
  25. 0 0
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js
  26. 0 0
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map
  27. 0 0
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js
  28. 0 0
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map
  29. 0 0
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js
  30. 0 0
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map
  31. 1 1
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  32. 2 0
      app/src/main/java/io/legado/app/api/controller/BookSourceController.kt
  33. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  34. 12 0
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  35. 5 3
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  36. 2 0
      app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt
  37. 3 1
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  38. 45 0
      app/src/main/java/io/legado/app/help/config/SourceConfig.kt
  39. 14 18
      app/src/main/java/io/legado/app/model/AudioPlay.kt
  40. 3 2
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  41. 5 2
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  42. 13 3
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  43. 1 0
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  44. 1 2
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  45. 6 11
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  46. 2 3
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  47. 1 1
      app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt
  48. 1 1
      app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt
  49. 30 22
      app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt
  50. 1 1
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt
  51. 54 0
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt
  52. 8 0
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt
  53. 30 1
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
  54. 54 0
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt
  55. 8 0
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt
  56. 1 1
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt
  57. 5 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  58. 21 0
      app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt
  59. 1 0
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
  60. 1 1
      app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt
  61. 46 28
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  62. 0 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  63. 85 71
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  64. 2 4
      app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt
  65. 25 0
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  66. 3 0
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  67. 1 1
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  68. 29 10
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  69. 25 5
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
  70. 28 5
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  71. 7 1
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  72. 2 0
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  73. 2 0
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  74. 7 1
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  75. 6 5
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  76. 3 0
      app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt
  77. 9 0
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  78. 4 15
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  79. 2 0
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  80. 2 0
      app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt
  81. 5 1
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  82. 3 1
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  83. 5 1
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  84. 29 2
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  85. 20 0
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt
  86. 7 0
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  87. 15 0
      app/src/main/java/io/legado/app/utils/AnimationExtensions.kt
  88. 0 17
      app/src/main/java/io/legado/app/utils/AnimationUtilsSupport.kt
  89. 3 0
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  90. 7 0
      app/src/main/res/drawable/ic_praise.xml
  91. 55 62
      app/src/main/res/layout/dialog_read_book_style.xml
  92. 5 5
      app/src/main/res/layout/dialog_verification_code_view.xml
  93. 1 1
      app/src/main/res/layout/item_book_source.xml
  94. 44 13
      app/src/main/res/layout/item_change_source.xml
  95. 1 0
      app/src/main/res/layout/item_replace_rule.xml
  96. 1 0
      app/src/main/res/layout/item_rss_source.xml
  97. 1 0
      app/src/main/res/layout/item_rule_sub.xml
  98. 2 2
      app/src/main/res/layout/item_theme_config.xml
  99. 46 35
      app/src/main/res/layout/item_toc_regex.xml
  100. 1 0
      app/src/main/res/layout/item_txt_toc_rule.xml

+ 6 - 0
.github/dependabot.yml

@@ -32,3 +32,9 @@ updates:
     schedule:
       interval: "daily"
     registries: "*"
+    
+  # Maintain dependencies for GitHub Actions
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"

+ 1 - 1
.github/workflows/autoupdatefork.yml

@@ -11,7 +11,7 @@ jobs:
     if: ${{ github.event.repository.owner.id == github.event.sender.id && github.actor != 'gedoor' }}
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         fetch-depth: 0
     - name: Install git

+ 2 - 2
.github/workflows/cronet.yml

@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     if: ${{ github.actor == 'gedoor' }}
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 1
 
@@ -22,7 +22,7 @@ jobs:
           echo "PR_BODY=Changes in the [Git log](https://chromium.googlesource.com/chromium/src/+log/$current_cronet_version..$lastest_cronet_version)" >> $GITHUB_ENV
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@v3
+        uses: peter-evans/create-pull-request@v4
         continue-on-error: true
         with:
           token: ${{ secrets.ACTIONS_TOKEN }}

+ 4 - 4
.github/workflows/release.yml

@@ -42,11 +42,11 @@ jobs:
       VERSION: ${{ needs.prepare.outputs.version }}
       play: ${{ needs.prepare.outputs.play }}
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 1
           
-      - uses: actions/cache@v2
+      - uses: actions/cache@v3
         with:
           path: |
             ~/.gradle/caches
@@ -81,14 +81,14 @@ jobs:
           cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
           
       - name: Upload App To Artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: legado_${{ env.product }}
           path: ${{ github.workspace }}/apk/*.apk
 
       - name: Release
         if: ${{ env.product == 'app' }}
-        uses: softprops/action-gh-release@59c3b4891632ff9a897f99a91d7bc557467a3a22
+        uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
         with:
           name: legado_app_${{ env.VERSION }}
           tag_name: ${{ env.VERSION }}

+ 1 - 1
.github/workflows/stale.yml

@@ -18,7 +18,7 @@ jobs:
       issues: write
 
     steps:
-    - uses: actions/stale@v3
+    - uses: actions/stale@v5
       with:
         repo-token: ${{ secrets.GITHUB_TOKEN }}
         stale-issue-message: '由于长期没有状态更新,该问题将于5天后自动关闭。如有需要可重新打开。'

+ 9 - 9
.github/workflows/test.yml

@@ -47,10 +47,10 @@ jobs:
       VERSION: ${{ needs.prepare.outputs.version }}
       VERSIONL: ${{ needs.prepare.outputs.versionL }}
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 0
-      - uses: actions/cache@v2
+      - uses: actions/cache@v3
         with:
           path: |
             ~/.gradle/caches
@@ -90,7 +90,7 @@ jobs:
             mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk
           done
       - name: Upload App To Artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: legado.${{ env.product }}.${{ env.type }}
           path: ${{ github.workspace }}/apk/*.apk
@@ -106,8 +106,8 @@ jobs:
       # 蓝奏云里的文件夹ID(阅读3测试版:2670621)
       LANZOU_FOLDER_ID: '2670621'
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/download-artifact@v2
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
         with:
           path: apk/
       - working-directory: apk/
@@ -124,8 +124,8 @@ jobs:
     runs-on: ubuntu-latest
     if: ${{ github.event_name != 'pull_request' && github.actor == 'gedoor' }}
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/download-artifact@v2
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
         with:
           path: apk/
       - working-directory: apk/
@@ -150,8 +150,8 @@ jobs:
       CHANNEL_ID: ${{ secrets.CHANNEL_ID }}
       BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/download-artifact@v2
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
         with:
           path: apk/
       - working-directory: apk/

+ 22 - 12
English.md

@@ -1,9 +1,6 @@
 # [English](English.md) [中文](README.md)
 
 [![icon_android](https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/icon_android.png)](https://play.google.com/store/apps/details?id=io.legado.play.release)
-<a href="https://data.newrank.cn/m/s.html?s=NykyOzI9MS5LNQ%3D%3D" target="_blank">
-    <img src="https://img.shields.io/badge/-微信关注“开源阅读”公众号-orange.svg" alt="#" align="right">
-</a>
 
 <div align="center">
 <img width="125" height="125" src="https://github.com/gedoor/legado/raw/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" alt="legado"/>  
@@ -49,65 +46,76 @@ You can customize the book source, set your own rules, and capture web page data
 
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # Download [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-)
+
 #### Android
+
 * [Releases](https://github.com/gedoor/legado/releases/latest)
 * [Google play - $1.99](https://play.google.com/store/apps/details?id=io.legado.play.release)
 * [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
-* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
 * [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
 * [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
 
 
 #### IOS
+
 * Stopped(No release) - [Github](https://github.com/gedoor/YueDuFlutter)
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # Community [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-)
 
 #### Telegram
-[![Telegram-group](https://img.shields.io/badge/Telegram-%E7%BE%A4%E7%BB%84-blue)](https://t.me/yueduguanfang) [![Telegram-channel](https://img.shields.io/badge/Telegram-%E9%A2%91%E9%81%93-blue)](https://t.me/legado_channels)
+
+[![Telegram-group](https://img.shields.io/badge/Telegram-group-blue)](https://t.me/yueduguanfang) [![Telegram-channel](https://img.shields.io/badge/Telegram-channel-blue)](https://t.me/legado_channels)
 
 #### Discord
+
 [![Discord](https://img.shields.io/discord/560731361414086666?color=%235865f2&label=Discord)](https://discord.gg/VtUfRyzRXn)
 
 #### Other
+
 https://www.yuque.com/legado/wiki/community
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # API [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-)
+
 * Legado 3.0 The API is provided in 2 ways: `Web way` and `Content Provider way`. You can call it yourself as needed in [here](api.md). 
 * One-click import by url recall reading, url format: legado://import/{path}?src={url}
 * Path Type: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig
 * path type explanation: Book source, subscription source, replacement rules, local txt novel directory rules, online reading engine, theme, reading layout
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # Other [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-)
+
 ##### Disclaimers
+
 https://gedoor.github.io/about.html
+
 ##### Legado 3.0
+
 * [eBook sources rules](https://alanskycn.gitee.io/teachme)
 * [Update Log](/app/src/main/assets/updateLog.md)
 * [Help Documentation](/app/src/main/assets/help/appHelp.md)
 * [web](https://github.com/gedoor/legado_web_bookshelf)
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # Grateful [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-)
+
 > * org.jsoup:jsoup
 > * cn.wanghaomiao:JsoupXpath
 > * com.jayway.jsonpath:json-path
@@ -123,14 +131,16 @@ https://gedoor.github.io/about.html
 > * io.noties.markwon:image-glide
 > * com.hankcs:hanlp
 > * com.positiondev.epublib:epublib-core
+
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>
 
 # Interface [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-)
+
 <img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg" width="270">
 <img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B4.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg" width="270"><img src="https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg" width="270">
 
 <a href="#readme">
-    <img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
+    <img src="https://img.shields.io/badge/-Top-orange.svg" alt="#" align="right">
 </a>

+ 0 - 1
README.md

@@ -46,7 +46,6 @@ Legado is a free and open source novel reader for Android.
 * [Releases](https://github.com/gedoor/legado/releases/latest)
 * [Google play - $1.99](https://play.google.com/store/apps/details?id=io.legado.play.release)
 * [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
-* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
 * [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
 * [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
 

+ 3 - 5
app/build.gradle

@@ -164,7 +164,7 @@ dependencies {
     implementation('androidx.constraintlayout:constraintlayout:2.1.4')
     implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')
     implementation('androidx.viewpager2:viewpager2:1.0.0')
-    implementation('androidx.webkit:webkit:1.4.0')
+    implementation('androidx.webkit:webkit:1.5.0')
 
     //google
     implementation('com.google.android.material:material:1.6.1')
@@ -189,8 +189,6 @@ dependencies {
 
     //media
     implementation("androidx.media:media:1.6.0")
-
-
     implementation("com.google.android.exoplayer:exoplayer-core:$exoplayer_version")
     implementation("com.google.android.exoplayer:extension-okhttp:$exoplayer_version")
 
@@ -209,7 +207,7 @@ dependencies {
     implementation('io.github.jeremyliao:live-event-bus-x:1.8.0')
 
     //规则相关
-    implementation('org.jsoup:jsoup:1.15.2')
+    implementation('org.jsoup:jsoup:1.15.3')
     implementation('com.jayway.jsonpath:json-path:2.7.0')
     implementation('cn.wanghaomiao:JsoupXpath:2.5.1')
     implementation(project(path: ':epublib'))
@@ -260,7 +258,7 @@ dependencies {
     //implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
     //implementation 'com.google.firebase:firebase-perf-ktx:20.0.6'
 
-    // LeakCanary
+    //LeakCanary, 内存泄露检测
     //debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7')
 
     //com.github.AmrDeveloper:CodeView代码编辑已集成到应用内

BIN
app/cronetlib/cronet_api.jar


BIN
app/cronetlib/cronet_impl_common_java.jar


BIN
app/cronetlib/cronet_impl_native_java.jar


BIN
app/cronetlib/cronet_impl_platform_java.jar


+ 1 - 1
app/src/main/assets/cronet.json

@@ -1 +1 @@
-{"armeabi-v7a":"a36bb21eec579e2c4bed0009eb0f1587","x86":"afce9bdd790c215374bb906919179ca5","arm64-v8a":"3ea9fa21d9b18860cfa12957c39dded1","x86_64":"717c1ebe0d1d3562f5c91c2b2606d2c2","version":"104.0.5112.69"}
+{"arm64-v8a":"11ded68691faa98fb1ab552210c8d4a8","armeabi-v7a":"4a986b523de502fc85ba6ea51f6f7184","x86_64":"80a642112fe079f3d689a6420a27e926","x86":"e72bf103350222d212bcb0b641733d43","version":"105.0.5195.77"}

+ 1 - 1
app/src/main/assets/defaultData/directLinkUpload.json

@@ -1,5 +1,5 @@
 {
   "uploadUrl": "http://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}",
-  "downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'http://shuyuan.mgz6.cc/shuyuan/' + result",
+  "downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result",
   "summary": "有效期2天"
 }

+ 1 - 2
app/src/main/assets/help/ruleHelp.md

@@ -152,5 +152,4 @@ let options = {
 ```
 
 * 购买操作
-> 返回购买链接,可直接填写链接或者JavaScript  
-> 可用变量 book chapter java(不支持AnalyzeRule函数) source
+> 可直接填写链接或者JavaScript,如果执行结果是字符串链接将会自动打开浏览器

+ 33 - 0
app/src/main/assets/updateLog.md

@@ -11,6 +11,39 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
+**2022/09/03**
+
+* 更新cronet: 105.0.5195.77
+* 更新web端书源编辑 by jgckM
+
+**2022/08/31**
+
+* 更新cronet: 105.0.5195.68
+* 修复一些bug
+* 给书源点赞 by SJJ-dot
+* 墨水屏模式下,书籍信息页不显示模糊书籍封面作为背景
+
+**2022/08/23**
+
+* 验证码输入框显示来自哪个书源
+* 修复一键导入丢失url参数的问题
+
+**2022/08/19**
+
+* 更新cronet: 104.0.5112.97
+* 现在选择文本朗读会从选择处一直往下朗读,朗读更方便
+* 阅读界面的TXT目录正则 弹框中也支持正则标题示例
+* 一些优化 by 821938089
+* 优化epub封面加载,修复图片读取,修复目录索引问题
+* 本地内容获取时尝试HTML实体解码,优化报错提示和添加日志
+* 优化书源校验,注释Error行
+* 订阅源、书架管理添加选中所选区间
+* 优化订阅源分组显示已启用分组,订阅源管理增加已启用、已禁用、需要登录、未分组分组
+* 订阅源长按菜单添加禁用源、分组管理排序
+* 添加返回时提示放入书架设置
+* 优化备份设置子文件夹对话框内容显示和校验
+* 修复搜索手动停止滑到底又重新搜索
+
 **2022/08/02**
 
 * 更新cronet: 104.0.5112.69

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/css/app.1bd41080.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/css/app.37e72839.css


BIN
app/src/main/assets/web/bookSource/favicon.ico


BIN
app/src/main/assets/web/bookSource/fonts/Cascadia-Code-Regular-2.97c09831.ttf


+ 0 - 150
app/src/main/assets/web/bookSource/index.css

@@ -1,150 +0,0 @@
-body {
-  margin: 0;
-}
-.editor {
-  display: flex;
-  align-items: stretch;
-}
-.setbox,
-.menu,
-.outbox {
-  flex: 1;
-  display: flex;
-  flex-flow: column;
-  max-height: 100vh;
-  overflow-y: auto;
-}
-.menu {
-  justify-content: center;
-  max-width: 90px;
-  margin: 0 5px;
-}
-.menu .button {
-  width: 90px;
-  height: 30px;
-  min-height: 30px;
-  margin: 5px 0px;
-  cursor: pointer;
-}
-@keyframes stroker {
-  0% {
-    stroke-dashoffset: 0;
-  }
-  100% {
-    stroke-dashoffset: -240;
-  }
-}
-.button rect {
-  width: 100%;
-  height: 100%;
-  fill: transparent;
-  stroke: #666;
-  stroke-width: 2px;
-}
-.button rect.busy {
-  stroke: #fd1850;
-  stroke-dasharray: 30 90;
-  animation: stroker 1s linear infinite;
-}
-.button text {
-  text-anchor: middle;
-  dominant-baseline: middle;
-}
-.setbox {
-  min-width: 40em;
-}
-.rules {
-  overflow: auto;
-}
-.tabbox {
-  flex: 1;
-  display: flex;
-  flex-flow: column;
-}
-.rules > * {
-  display: flex;
-  margin: 2px 0;
-}
-.rules textarea {
-  flex: 1;
-  margin-left: 5px;
-}
-.rules > *,
-.rules > * > div,
-.rules textarea {
-  min-height: 1em;
-}
-textarea {
-  word-break: break-all;
-}
-.tabtitle {
-  display: flex;
-  z-index: 1;
-  justify-content: flex-end;
-}
-.tabtitle > div {
-  cursor: pointer;
-  padding: 1px 10px 0 10px;
-  border-bottom: 3px solid transparent;
-  font-weight: bold;
-}
-.tabtitle > .this {
-  color: #4f9da6;
-  border-bottom-color: #4ebbe4;
-}
-.tabbody {
-  flex: 1;
-  display: flex;
-  margin-top: -1px;
-  border: 1px solid #a9a9a9;
-  height: 0;
-}
-.tabbody > * {
-  flex: 1;
-  flex-flow: column;
-  display: none;
-}
-.tabbody > .this {
-  display: flex;
-}
-.tabbody > * > .titlebar {
-  display: flex;
-}
-.tabbody > * > .titlebar > * {
-  flex: 1;
-  margin: 1px 1px 1px 1px;
-}
-.tabbody > * > .context {
-  flex: 1;
-  flex-flow: column;
-  border: 0;
-  padding: 5px;
-  overflow-y: auto;
-}
-.tabbody > * > .inputbox {
-  border: 0;
-  border-bottom: #a9a9a9 solid 1px;
-  height: 15px;
-  text-align: center;
-}
-.link > * {
-  display: flex;
-  margin: 5px;
-  border-bottom: 1px solid;
-  text-decoration: none;
-}
-#RuleList > label > * {
-  background: #eee;
-  padding-left: 3px;
-  margin: 2px 0;
-  cursor: pointer;
-}
-#RuleList input[type="radio"] {
-  display: none;
-}
-#RuleList input[type="radio"]:checked + * {
-  background: #15cda8;
-}
-.isError {
-  color: #ff0000;
-}

+ 1 - 439
app/src/main/assets/web/bookSource/index.html

@@ -1,439 +1 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-    <meta charset="UTF-8">
-    <title>阅读3.0源编辑器_V4.0</title>
-    <link rel="icon" href="../favicon.ico">
-    <link rel="stylesheet" type="text/css" href="index.css"/>
-</head>
-
-<body>
-<div class="editor">
-    <div class="setbox">
-        <div>
-            <a href="../index.html">←主页</a>
-            <b>书源</b>
-        </div>
-        <div class="rules">
-            <div><b>基本</b></div>
-            <div>
-                <div>源域名 :</div>
-                <textarea rows="1" id="bookSourceUrl" class="base" title="bookSourceUrl"
-                          placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
-            </div>
-            <div>
-                <div>源类型 :</div>
-                <textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
-                          placeholder="&lt;必填&gt;0:文本 1:音频 2:图片 3:文件(只提供下载的网站)"></textarea>
-            </div>
-            <div>
-                <div>源名称 :</div>
-                <textarea rows="1" id="bookSourceName" class="base" title="bookSourceName"
-                          placeholder="&lt;必填&gt;会显示在源列表"></textarea>
-            </div>
-            <div>
-                <div>源分组 :</div>
-                <textarea rows="1" id="bookSourceGroup" class="base" title="bookSourceGroup"
-                          placeholder="&lt;选填&gt;描述源的特征信息"></textarea>
-            </div>
-            <div>
-                <div>源注释 :</div>
-                <textarea rows="1" id="bookSourceComment" class="base" title="bookSourceComment"
-                          placeholder="&lt;选填&gt;描述源作者和状态"></textarea>
-            </div>
-            <div>
-                <div>登录地址:</div>
-                <textarea rows="1" id="loginUrl" class="base" title="loginUrl"
-                          placeholder="&lt;选填&gt;填写网站登录网址,仅在需要登录的源有用"></textarea>
-            </div>
-            <div>
-                <div>登录界面:</div>
-                <textarea rows="3" id="loginUi" class="base" title="loginUi"
-                          placeholder="&lt;选填&gt;自定义登录界面"></textarea>
-            </div>
-            <div>
-                <div>登录检测:</div>
-                <textarea rows="3" id="loginCheckJs" class="base" title="loginCheckJs"
-                          placeholder="&lt;选填&gt;登录检测js"></textarea>
-            </div>
-            <div>
-                <div>并发率 :</div>
-                <textarea rows="1" id="concurrentRate" class="base" title="concurrentRate"
-                          placeholder="&lt;选填&gt;并发率"></textarea>
-            </div>
-            <div>
-                <div>请求头 :</div>
-                <textarea rows="3" id="header" class="base" title="header"
-                          placeholder="&lt;选填&gt;客户端标识"></textarea>
-            </div>
-            <div>
-                <div>链接验证:</div>
-                <textarea rows="1" id="bookUrlPattern" class="base" title="bookUrlPattern"
-                          placeholder="&lt;选填&gt;当详情页URL与源URL的域名不一致时有效,用于添加网址"></textarea>
-            </div>
-            <p></p>
-            <div><b>搜索</b></div>
-            <div>
-                <div>搜索地址:</div>
-                <textarea rows="1" id="searchUrl" class="base" title="searchUrl"
-                          placeholder="[域名可省略]/search.php@kw={{key}}"></textarea>
-            </div>
-            <div>
-                <div>校验文字:</div>
-                <textarea rows="1" id="ruleSearch_checkKeyWord" class="ruleSearch"
-                          title="checkKeyWord"
-                          placeholder="校验关键字"></textarea>
-            </div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="1" id="ruleSearch_bookList" class="ruleSearch" title="bookList"
-                          placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleSearch_name" class="ruleSearch" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleSearch_author" class="ruleSearch" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleSearch_kind" class="ruleSearch" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleSearch_wordCount" class="ruleSearch" title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleSearch_lastChapter" class="ruleSearch"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleSearch_intro" class="ruleSearch" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleSearch_coverUrl" class="ruleSearch" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>详情地址:</div>
-                <textarea rows="1" id="ruleSearch_bookUrl" class="ruleSearch" title="bookUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
-            </div>
-            <p></p>
-            <div><b>发现</b></div>
-            <div>
-                <div>发现地址:</div>
-                <textarea rows="6" id="exploreUrl" class="base" title="exploreUrl"
-                          placeholder="内容能显示在发现菜单&#10;每行一条发现分类(网址域名可省略),例:&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
-            </div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="1" id="ruleExplore_bookList" class="ruleExplore" title="bookList"
-                          placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleExplore_name" class="ruleExplore" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleExplore_author" class="ruleExplore" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleExplore_kind" class="ruleExplore" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleExplore_wordCount" class="ruleExplore" title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleExplore_lastChapter" class="ruleExplore"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleExplore_intro" class="ruleExplore" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleExplore_coverUrl" class="ruleExplore" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>详情地址:</div>
-                <textarea rows="1" id="ruleExplore_bookUrl" class="ruleExplore" title="bookUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
-            </div>
-            <p></p>
-            <div><b>详情</b></div>
-            <div>
-                <div>预处理 :</div>
-                <textarea rows="3" id="ruleBookInfo_init" class="ruleBookInfo" title="init"
-                          placeholder="用于加速详情信息检索,只支持AllInOne规则"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleBookInfo_name" class="ruleBookInfo" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleBookInfo_author" class="ruleBookInfo" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleBookInfo_kind" class="ruleBookInfo" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleBookInfo_wordCount" class="ruleBookInfo"
-                          title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleBookInfo_lastChapter" class="ruleBookInfo"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleBookInfo_intro" class="ruleBookInfo" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleBookInfo_coverUrl" class="ruleBookInfo" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>目录地址:</div>
-                <textarea rows="1" id="ruleBookInfo_tocUrl" class="ruleBookInfo" title="tocUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)"></textarea>
-            </div>
-            <p></p>
-            <div><b>目录</b></div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="3" id="ruleToc_chapterList" class="ruleToc" title="chapterList"
-                          placeholder="选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>章节名称:</div>
-                <textarea rows="1" id="ruleToc_chapterName" class="ruleToc" title="chapterName"
-                          placeholder="选择章节名称 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>章节地址:</div>
-                <textarea rows="1" id="ruleToc_chapterUrl" class="ruleToc" title="chapterUrl"
-                          placeholder="选择章节链接 (规则结果为String类型的Url)"></textarea>
-            </div>
-            <div>
-                <div>卷名标识:</div>
-                <textarea rows="1" id="ruleToc_isVolume" class="ruleToc" title="isVolume"
-                          placeholder="章节名称是否是卷名 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>收费标识:</div>
-                <textarea rows="1" id="ruleToc_isVip" class="ruleToc" title="isVip"
-                          placeholder="章节是否为VIP章节 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>购买标识:</div>
-                <textarea rows="1" id="ruleToc_isPay" class="ruleToc" title="isPay"
-                          placeholder="章节是否为已购买 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>章节信息:</div>
-                <textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"
-                          placeholder="选择章节信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>翻页规则:</div>
-                <textarea rows="1" id="ruleToc_nextTocUrl" class="ruleToc" title="nextTocUrl"
-                          placeholder="选择目录下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea>
-            </div>
-            <p></p>
-            <div><b>正文</b></div>
-            <div>
-                <div>脚本注入:</div>
-                <textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
-                          placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
-            </div>
-            <div>
-                <div>正文规则:</div>
-                <textarea rows="1" id="ruleContent_content" class="ruleContent" title="content"
-                          placeholder="选择正文内容 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>翻页规则:</div>
-                <textarea rows="1" id="ruleContent_nextContentUrl" class="ruleContent"
-                          title="nextContentUrl"
-                          placeholder="选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"></textarea>
-            </div>
-            <div>
-                <div>资源正则:</div>
-                <textarea rows="1" id="ruleContent_sourceRegex" class="ruleContent"
-                          title="sourceRegex"
-                          placeholder="匹配资源的url特征,用于嗅探"></textarea>
-            </div>
-            <div>
-                <div>替换规则:</div>
-                <textarea rows="1" id="ruleContent_replaceRegex" class="ruleContent"
-                          title="replaceRegex"
-                          placeholder="多页内容合并后替换,用于正文净化"></textarea>
-            </div>
-            <div>
-                <div>图片样式:</div>
-                <textarea rows="1" id="ruleContent_imageStyle" class="ruleContent"
-                          title="imageStyle"
-                          placeholder="FULL:铺满 不填:默认样式"></textarea>
-            </div>
-            <div>
-                <div>购买操作:</div>
-                <textarea rows="1" id="ruleContent_payAction" class="ruleContent"
-                          title="payAction"
-                          placeholder="购买章节 返回链接或js"></textarea>
-            </div>
-            <p></p>
-            <div><b>其它规则</b></div>
-            <div>
-                <div>启用搜索:</div>
-                <textarea rows="1" id="enabled" class="base" title="enabled"
-                          placeholder="启用: true  关闭: false (可选,默认true)"></textarea>
-            </div>
-            <div>
-                <div>启用发现:</div>
-                <textarea rows="1" id="enabledExplore" class="base" title="enabledExplore"
-                          placeholder="启用: true  关闭: false (可选,默认true)"></textarea>
-            </div>
-            <div>
-                <div>搜索权重:</div>
-                <textarea rows="1" id="weight" class="base" title="weight"
-                          placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea>
-            </div>
-            <div>
-                <div>排序编号:</div>
-                <textarea rows="1" id="customOrder" class="base" title="customOrder"
-                          placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
-            </div>
-            <div style="display:none;">
-                <div>更新时间:</div>
-                <textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime"
-                          placeholder="毫秒级时间戳 (自动生成) | 请勿手动填写"></textarea>
-            </div>
-        </div>
-    </div>
-    <div class="menu">
-        <svg class="button">
-            <text x="50%" y="55%">⇈推送源</text>
-            <rect id="push"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⇊拉取源</text>
-            <rect id="pull"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⋘编辑源</text>
-            <rect id="editor"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⋙生成源</text>
-            <rect id="conver"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">✗清空表单</text>
-            <rect id="initial"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">↶撤销操作</text>
-            <rect id="undo"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">↷重做操作</text>
-            <rect id="redo"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⇏调试源</text>
-            <rect id="debug"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">✓保存源</text>
-            <rect id="accept"></rect>
-        </svg>
-    </div>
-    <div class="outbox">
-        <div class="tabbox">
-            <div class="tabtitle">
-                <div name="编辑源" class="tab1 this">编辑源</div>
-                <div name="调试源" class="tab2">调试源</div>
-                <div name="源列表" class="tab3">源列表</div>
-                <div name="帮助信息" class="tab4">帮助信息</div>
-            </div>
-            <div class="tabbody">
-                <div class="tab1 this">
-                        <textarea class="context" id="RuleJsonString"
-                                  placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
-                </div>
-                <div class="tab2">
-                    <input type="text" class="inputbox" id="DebugKey" placeholder="输入搜索关键字,默认搜「我的」">
-                    <textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
-                </div>
-                <div class="tab3">
-                    <input type="text" class="inputbox" id="Filter"
-                           placeholder="输入筛选关键词(源名称、源URL或源分组)后按回车筛选源">
-                    <div class="titlebar">
-                        <button id="Import">导入源文件</button>
-                        <button id="Export">导出源文件</button>
-                        <button id="Delete">删除选中源</button>
-                        <button id="ClrAll">清空列表</button>
-                    </div>
-                    <div class="context" id="RuleList"></div>
-                </div>
-                <div class="tab4">
-                    <div class="context link">
-                        <a target="_blank" href="https://alanskycn.gitee.io/teachme">源制作教程</a>
-                        <a target="_blank"
-                           href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a>
-                        <a target="_blank"
-                           href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
-                        <a target="_blank" href="https://www.w3cschool.cn/regex_rmjc">正则表达式教程</a>
-                        <a target="_blank" href="https://regexr.com">正则表达式在线验证工具</a>
-                        <div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义
-                            <br>(?s) 前缀表示跨行解析
-                            <br>(?m) 前缀表示逐行匹配
-                            <br>(?i) 前缀表示忽略大小写
-                        </div>
-                        <a target="_blank" href="https://www.beta.browxy.com">代码在线运行工具</a>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-<script type="text/javascript" src="index.js"></script>
-</body>
-
-</html>
+<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="./favicon.ico"><title>legado_web_editor</title><script defer="defer" src="./js/chunk-vendors.86212f98.js"></script><script defer="defer" src="./js/app.836bd8bb.js"></script><link href="./css/app.37e72839.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but legado_web_editor doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

+ 0 - 516
app/src/main/assets/web/bookSource/index.js

@@ -1,516 +0,0 @@
-// 简化js原生选择器
-function $(selector) { return document.querySelector(selector); }
-function $$(selector) { return document.querySelectorAll(selector); }
-// 读写Hash值(val未赋值时为读取)
-function hashParam(key, val) {
-	let hashstr = decodeURIComponent(window.location.hash);
-	let regKey = new RegExp(`${key}=([^&]*)`);
-	let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;
-	if (val == undefined) return getVal;
-	if (hashstr == '' || hashstr == '#') {
-		window.location.hash = `#${key}=${val}`;
-	}
-	else {
-		if (getVal) window.location.hash = hashstr.replace(getVal, val);
-		else {
-			window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;
-		}
-	}
-}
-// 创建源规则容器对象
-function Container() {
-	let ruleJson = {};
-	let searchJson = {};
-	let exploreJson = {};
-	let bookInfoJson = {};
-	let tocJson = {};
-	let contentJson = {};
-
-	// 基本以及其他
-	$$('.rules .base').forEach(item => ruleJson[item.title] = '');
-	ruleJson.lastUpdateTime = 0;
-	ruleJson.customOrder = 0;
-	ruleJson.weight = 0;
-	ruleJson.enabled = true;
-	ruleJson.enabledExplore = true;
-
-	// 搜索规则
-	$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
-	ruleJson.ruleSearch = searchJson;
-
-	// 发现规则
-	$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
-	ruleJson.ruleExplore = exploreJson;
-
-	// 详情页规则
-	$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
-	ruleJson.ruleBookInfo = bookInfoJson;
-
-	// 目录规则
-	$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
-	ruleJson.ruleToc = tocJson;
-
-	// 正文规则
-	$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
-	ruleJson.ruleContent = contentJson;
-
-	return ruleJson;
-}
-// 选项卡Tab切换事件处理
-function showTab(tabName) {
-	$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
-	$$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });
-	$(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';
-	$(`.tabtitle>*[name=${tabName}]`).className += ' this';
-	hashParam('tab', tabName);
-}
-// 源列表列表标签构造函数
-function newRule(rule) {
-	return `<label for="${rule.bookSourceUrl}"><input type="radio" name="rule" id="${rule.bookSourceUrl}"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;
-}
-// 缓存规则列表
-var RuleSources = [];
-if (localStorage.getItem('BookSources')) {
-	RuleSources = JSON.parse(localStorage.getItem('BookSources'));
-	RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
-}
-// 页面加载完成事件
-window.onload = () => {
-	$$('.tabtitle>*').forEach(item => {
-		item.addEventListener('click', () => {
-			showTab(item.innerHTML);
-		});
-	});
-	if (hashParam('tab')) showTab(hashParam('tab'));
-}
-// 获取数据
-function HttpGet(url) {
-	return fetch(hashParam('domain') ? hashParam('domain') + url : url)
-		.then(res => res.json()).catch(err => console.error('Error:', err));
-}
-// 提交数据
-function HttpPost(url, data) {
-	return fetch(hashParam('domain') ? hashParam('domain') + url : url, {
-		body: JSON.stringify(data),
-		method: 'POST',
-		mode: "cors",
-		headers: new Headers({
-			'Content-Type': 'application/json;charset=utf-8'
-		})
-	}).then(res => res.json()).catch(err => console.error('Error:', err));
-}
-// 将源表单转化为源对象
-function rule2json() {
-	let RuleJSON = Container();
-	// 转换base
-	Object.keys(RuleJSON).forEach(key => {
-		if (!key.startsWith("rule")) {
-			RuleJSON[key] = $('#' + key).value;
-		}
-	});
-
-	// 转换搜索规则
-	let searchJson = {};
-	Object.keys(RuleJSON.ruleSearch).forEach(key => {
-		if ($('#' + 'ruleSearch_' + key).value)
-			searchJson[key] = $('#' + 'ruleSearch_' + key).value;
-	});
-	RuleJSON.ruleSearch = searchJson;
-
-	// 转换发现规则
-	let exploreJson = {};
-	Object.keys(RuleJSON.ruleExplore).forEach(key => {
-		if ($('#' + 'ruleExplore_' + key).value)
-			exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
-	});
-	RuleJSON.ruleExplore = exploreJson;
-
-	// 转换详情页规则
-	let bookInfoJson = {};
-	Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
-		if ($('#' + 'ruleBookInfo_' + key).value)
-			bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
-	});
-	RuleJSON.ruleBookInfo = bookInfoJson;
-
-	// 转换目录规则
-	let tocJson = {};
-	Object.keys(RuleJSON.ruleToc).forEach(key => {
-		if ($('#' + 'ruleToc_' + key).value)
-			tocJson[key] = $('#' + 'ruleToc_' + key).value;
-	});
-	RuleJSON.ruleToc = tocJson;
-
-	// 转换正文规则
-	let contentJson = {};
-	Object.keys(RuleJSON.ruleContent).forEach(key => {
-		if ($('#' + 'ruleContent_' + key).value)
-			contentJson[key] = $('#' + 'ruleContent_' + key).value;
-	});
-	RuleJSON.ruleContent = contentJson;
-
-	RuleJSON.lastUpdateTime = new Date().getTime();
-	RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
-	RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
-	RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.bookSourceType);
-	RuleJSON.enabled = RuleJSON.enabled == '' || String(RuleJSON.enabled).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
-	RuleJSON.enabledExplore = RuleJSON.enabledExplore == '' || String(RuleJSON.enabledExplore).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
-	return RuleJSON;
-}
-// 将源对象填充到源表单
-function json2rule(RuleEditor) {
-	let RuleJSON = Container();
-	// 转换base
-	Object.keys(RuleJSON).forEach(key => {
-		if (!key.startsWith("rule")) {
-			let val = RuleEditor[key];
-			if (typeof val == "number") {
-				$("#" + key).value = val ? String(val) : '0';
-			}
-			else if (typeof val == "boolean") {
-				$("#" + key).value = val ? String(val) : 'false';
-			}
-			else {
-				$("#" + key).value = val ? String(val) : '';
-			}
-		}
-	});
-
-	// 转换搜索规则
-	if (RuleEditor.ruleSearch) {
-		let searchJson = RuleEditor.ruleSearch;
-		Object.keys(RuleJSON.ruleSearch).forEach(key => {
-			$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
-		});
-	}
-
-	// 转换发现规则
-	if (RuleEditor.ruleExplore) {
-		let exploreJson = RuleEditor.ruleExplore;
-		Object.keys(RuleJSON.ruleExplore).forEach(key => {
-			$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
-		});
-	}
-
-	// 转换详情页规则
-	if (RuleEditor.ruleBookInfo) {
-		let bookInfoJson = RuleEditor.ruleBookInfo;
-        Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
-			$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
-		});
-	}
-
-	// 转换目录规则
-	if (RuleEditor.ruleToc) {
-		let tocJson = RuleEditor.ruleToc;
-		Object.keys(RuleJSON.ruleToc).forEach(key => {
-			$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
-		});
-	}
-
-	// 转换正文规则
-	if (RuleEditor.ruleContent) {
-		let contentJson = RuleEditor.ruleContent;
-        Object.keys(RuleJSON.ruleContent).forEach(key => {
-			$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
-		});
-	}
-}
-// 记录操作过程
-var course = { "old": [], "now": {}, "new": [] };
-if (localStorage.getItem('bookSourceCourse')) {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	json2rule(course.now);
-}
-else {
-	course.now = rule2json();
-	window.localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-}
-function todo() {
-	course.old.push(Object.assign({}, course.now));
-	course.now = rule2json();
-	course.new = [];
-	if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小
-	localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-}
-function undo() {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	if (course.old.length > 0) {
-		course.new.push(course.now);
-		course.now = course.old.pop();
-		localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-		json2rule(course.now);
-	}
-}
-function redo() {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	if (course.new.length > 0) {
-		course.old.push(course.now);
-		course.now = course.new.pop();
-		localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-		json2rule(course.now);
-	}
-}
-function setRule(editRule) {
-	let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);
-	if ($(`input[id="${editRule.bookSourceUrl}"]`)) {
-		Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });
-		$(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;
-	} else {
-		RuleSources.push(editRule);
-		$('#RuleList').innerHTML += newRule(editRule);
-	}
-}
-$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });
-$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });
-// 处理按钮点击事件
-$('.menu').addEventListener('click', e => {
-	let thisNode = e.target;
-	thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :
-		thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;
-	if (!thisNode) return;
-	if (thisNode.getAttribute('class') == 'busy') return;
-	thisNode.setAttribute('class', 'busy');
-	switch (thisNode.id) {
-		case 'push':
-			$$('#RuleList>label>div').forEach(item => { item.className = ''; });
-			(async () => {
-				await HttpPost(`/saveBookSources`, RuleSources).then(json => {
-					if (json.isSuccess) {
-						let okData = json.data;
-						if (Array.isArray(okData)) {
-							let failMsg = ``;
-							if (RuleSources.length > okData.length) {
-								RuleSources.forEach(item => {
-									if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }
-									else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }
-								});
-								failMsg = '\n推送失败的源将用红色字体标注!';
-							}
-							alert(`批量推送源到「阅读3.0APP」\n共计: ${RuleSources.length} 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}`);
-						}
-						else {
-							alert(`批量推送源到「阅读3.0APP」成功!\n共计: ${RuleSources.length} 条`);
-						}
-					}
-					else {
-						alert(`批量推送源失败!\nErrorMsg: ${json.errorMsg}`);
-					}
-				}).catch(err => { alert(`批量推送源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		case 'pull':
-			showTab('源列表');
-			(async () => {
-				await HttpGet(`/getBookSources`).then(json => {
-					if (json.isSuccess) {
-						$('#RuleList').innerHTML = ''
-						localStorage.setItem('BookSources', JSON.stringify(RuleSources = json.data));
-						RuleSources.forEach(item => {
-							$('#RuleList').innerHTML += newRule(item);
-						});
-						alert(`成功拉取 ${RuleSources.length} 条源`);
-					}
-					else {
-						alert(`批量拉取源失败!\nErrorMsg: ${json.errorMsg}`);
-					}
-				}).catch(err => { alert(`批量拉取源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		case 'editor':
-			if ($('#RuleJsonString').value == '') break;
-			try {
-				json2rule(JSON.parse($('#RuleJsonString').value));
-				todo();
-			} catch (error) {
-				console.log(error);
-				alert(error);
-			}
-			break;
-		case 'conver':
-			showTab('编辑源');
-			$('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);
-			break;
-		case 'initial':
-			$$('.rules textarea').forEach(item => { item.value = '' });
-			todo();
-			break;
-		case 'undo':
-			undo()
-			break;
-		case 'redo':
-			redo()
-			break;
-		case 'debug':
-			showTab('调试源');
-			let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1));
-			let DebugInfos = $('#DebugConsole');
-			function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
-			let saveRule = [rule2json()];
-			HttpPost(`/saveBookSources`, saveRule).then(sResult => {
-				if (sResult.isSuccess) {
-					let sKey = DebugKey.value ? DebugKey.value : '我的';
-					$('#DebugConsole').value = `源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
-					let ws = new WebSocket(`${wsOrigin}/bookSourceDebug`);
-					ws.onopen = () => {
-						ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
-					};
-					ws.onmessage = (msg) => {
-					    console.log('[调试]', msg);
-						DebugPrint(msg.data);
-					};
-					ws.onerror = (err) => {
-						throw `${err.data}`;
-					}
-					ws.onclose = () => {
-						thisNode.setAttribute('class', '');
-						DebugPrint(`\n调试服务已关闭!`);
-					}
-				} else throw `${sResult.errorMsg}`;
-			}).catch(err => {
-				DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
-				thisNode.setAttribute('class', '');
-			});
-			return;
-		case 'accept':
-			(async () => {
-				let saveRule = [rule2json()];
-				await HttpPost(`/saveBookSource`, saveRule[0]).then(json => {
-					alert(json.isSuccess ? `源《${saveRule[0].bookSourceName}》已成功保存到「阅读3.0APP」` : `源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
-					setRule(saveRule[0]);
-				}).catch(err => { alert(`保存源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		default:
-	}
-	setTimeout(() => { thisNode.setAttribute('class', ''); }, 500);
-});
-$('#DebugKey').addEventListener('keydown', e => {
-	if (e.keyCode == 13) {
-		let clickEvent = document.createEvent('MouseEvents');
-		clickEvent.initEvent("click", true, false);
-		$('#debug').dispatchEvent(clickEvent);
-	}
-});
-$('#Filter').addEventListener('keydown', e => {
-	if (e.keyCode == 13) {
-		let cashList = [];
-		$('#RuleList').innerHTML = "";
-		let sKey = Filter.value ? Filter.value : '';
-		if (sKey == '') {
-			cashList = RuleSources;
-		} else {
-			let patt = new RegExp(sKey);
-			RuleSources.forEach(source => {
-				if (patt.test(source.bookSourceUrl) || patt.test(source.bookSourceName) || patt.test(source.bookSourceGroup)) {
-					cashList.push(source);
-				}
-			})
-		}
-		cashList.forEach(source => {
-			$('#RuleList').innerHTML += newRule(source);
-		})
-	}
-});
-
-// 列表规则更改事件
-$('#RuleList').addEventListener('click', e => {
-	let editRule = null;
-	if (e.target && e.target.getAttribute('name') == 'rule') {
-		editRule = rule2json();
-		json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));
-	} else return;
-	if (editRule.bookSourceUrl == '') return;
-	if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
-	setRule(editRule);
-	localStorage.setItem('BookSources', JSON.stringify(RuleSources));
-});
-// 处理列表按钮事件
-$('.tab3>.titlebar').addEventListener('click', e => {
-	let thisNode = e.target;
-	if (thisNode.nodeName != 'BUTTON') return;
-	switch (thisNode.id) {
-		case 'Import':
-			let fileImport = document.createElement('input');
-			fileImport.type = 'file';
-			fileImport.accept = '.json';
-			fileImport.addEventListener('change', () => {
-				let file = fileImport.files[0];
-				let reader = new FileReader();
-				reader.onloadend = function (evt) {
-					if (evt.target.readyState == FileReader.DONE) {
-						let fileText = evt.target.result;
-						try {
-							let fileJson = JSON.parse(fileText);
-							let newSources = [];
-							newSources.push(...fileJson);
-							if (window.confirm(`如何处理导入的源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
-								localStorage.setItem('BookSources', JSON.stringify(RuleSources = newSources));
-								$('#RuleList').innerHTML = ''
-								RuleSources.forEach(item => {
-									$('#RuleList').innerHTML += newRule(item);
-								});
-							}
-							else {
-								newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
-								RuleSources.push(...newSources);
-								localStorage.setItem('BookSources', JSON.stringify(RuleSources));
-								newSources.forEach(item => {
-									$('#RuleList').innerHTML += newRule(item);
-								});
-							}
-							alert(`成功导入 ${newSources.length} 条源`);
-						}
-						catch (err) {
-							alert(`导入源文件失败!\n${err}`);
-						}
-					}
-				};
-				reader.readAsText(file);
-			}, false);
-			fileImport.click();
-			break;
-		case 'Export':
-			let fileExport = document.createElement('a');
-			fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;
-			let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" });
-			fileExport.href = window.URL.createObjectURL(myBlob);
-			fileExport.click();
-			break;
-		case 'Delete':
-			let selectRule = $('#RuleList input:checked');
-			if (!selectRule) {
-				alert(`没有源被选中!`);
-				return;
-			}
-			if (confirm(`确定要删除选定源吗?\n(同时删除APP内源)`)) {
-				let selectRuleUrl = selectRule.id;
-				let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的源
-				let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl));  // 提取待留下的源
-				HttpPost(`/deleteBookSources`, deleteSources).then(json => {
-					if (json.isSuccess) {
-						let selectNode = document.getElementById(selectRuleUrl).parentNode;
-						selectNode.parentNode.removeChild(selectNode);
-						localStorage.setItem('BookSources', JSON.stringify(RuleSources = laveSources));
-						if ($('#bookSourceUrl').value == selectRuleUrl) {
-							$$('.rules textarea').forEach(item => { item.value = '' });
-							todo();
-						}
-						console.log(deleteSources);
-						console.log(`以上源已删除!`)
-					}
-				}).catch(err => { alert(`删除源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-			}
-			break;
-		case 'ClrAll':
-			if (confirm(`确定要清空当前源列表吗?\n(不会删除APP内源)`)) {
-				localStorage.setItem('BookSources', JSON.stringify(RuleSources = []));
-				$('#RuleList').innerHTML = ''
-			}
-			break;
-		default:
-	}
-});

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.2c3e42a8.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.836bd8bb.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map


+ 1 - 1
app/src/main/java/io/legado/app/api/controller/BookController.kt

@@ -180,7 +180,7 @@ object BookController {
             ?: return returnData.setErrorMsg("未找到书源")
         try {
             content = runBlocking {
-                WebBook.getContentAwait(this, bookSource, book, chapter).let {
+                WebBook.getContentAwait(bookSource, book, chapter).let {
                     val contentProcessor = ContentProcessor.get(book.name, book.origin)
                     contentProcessor.getContent(book, chapter, it, includeTitle = false)
                         .joinToString("\n")

+ 2 - 0
app/src/main/java/io/legado/app/api/controller/BookSourceController.kt

@@ -5,6 +5,7 @@ import android.text.TextUtils
 import io.legado.app.api.ReturnData
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
+import io.legado.app.help.config.SourceConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonArray
 
@@ -70,6 +71,7 @@ object BookSourceController {
             GSON.fromJsonArray<BookSource>(postData).getOrThrow()?.let {
                 it.forEach { source ->
                     appDb.bookSourceDao.delete(source)
+                    SourceConfig.removeSource(source.bookSourceUrl)
                 }
             }
         }.onFailure {

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -109,6 +109,7 @@ object PreferKey {
     const val welcomeShowIcon = "welcomeShowIcon"
     const val welcomeShowIconDark = "welcomeShowIconDark"
     const val pageTouchSlop = "pageTouchSlop"
+    const val showAddToShelfAlert = "showAddToShelfAlert"
 
     const val cPrimary = "colorPrimary"
     const val cAccent = "colorAccent"

+ 12 - 0
app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt

@@ -44,6 +44,15 @@ interface RssSourceDao {
     @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
     fun flowEnabled(): Flow<List<RssSource>>
 
+    @Query("SELECT * FROM rssSources where enabled = 0 order by customOrder")
+    fun flowDisabled(): Flow<List<RssSource>>
+
+    @Query("select * from rssSources where loginUrl is not null and loginUrl != ''")
+    fun flowLogin(): Flow<List<RssSource>>
+
+    @Query("select * from rssSources where sourceGroup is null or sourceGroup = '' or sourceGroup like '%未分组%'")
+    fun flowNoGroup(): Flow<List<RssSource>>
+
     @Query(
         """SELECT * FROM rssSources 
         where enabled = 1 
@@ -67,6 +76,9 @@ interface RssSourceDao {
     @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
     fun flowGroup(): Flow<List<String>>
 
+    @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> '' and enabled = 1")
+    fun flowGroupEnabled(): Flow<List<String>>
+
     @get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
     val allGroup: List<String>
 

+ 5 - 3
app/src/main/java/io/legado/app/help/SourceAnalyzer.kt

@@ -165,6 +165,7 @@ object SourceAnalyzer {
                 }
                 source.loginCheckJs = sourceAny.loginCheckJs
                 source.bookSourceComment = sourceAny.bookSourceComment
+                source.variableComment = sourceAny.variableComment
                 source.lastUpdateTime = sourceAny.lastUpdateTime
                 source.respondTime = sourceAny.respondTime
                 source.weight = sourceAny.weight
@@ -220,13 +221,14 @@ object SourceAnalyzer {
         var customOrder: Int = 0,                       // 手动排序编号
         var enabled: Boolean = true,                    // 是否启用
         var enabledExplore: Boolean = true,             // 启用发现
-        var enabledCookieJar: Boolean = false,             // 启用CookieJar
+        var enabledCookieJar: Boolean = false,          // 启用CookieJar
         var concurrentRate: String? = null,             // 并发率
         var header: String? = null,                     // 请求头
         var loginUrl: Any? = null,                      // 登录规则
         var loginUi: Any? = null,                       // 登录UI
-        var loginCheckJs: String? = null,               //登录检测js
-        var bookSourceComment: String? = "",            //书源注释
+        var loginCheckJs: String? = null,               // 登录检测js
+        var bookSourceComment: String? = "",            // 书源注释
+        var variableComment: String? = null,            // 变量说明
         var lastUpdateTime: Long = 0,                   // 最后更新时间,用于排序
         var respondTime: Long = 180000L,                // 响应时间,用于排序
         var weight: Int = 0,                            // 智能排序的权重

+ 2 - 0
app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt

@@ -27,6 +27,7 @@ object SourceVerificationHelp {
                 appCtx.startActivity<VerificationCodeActivity> {
                     putExtra("imageUrl", url)
                     putExtra("sourceOrigin", source.getKey())
+                    putExtra("sourceName", source.getTag())
                 }
             } else {
                 startBrowser(source, url, title, true)
@@ -58,6 +59,7 @@ object SourceVerificationHelp {
             putExtra("title", title)
             putExtra("url", url)
             putExtra("sourceOrigin", source.getKey())
+            putExtra("sourceName", source.getTag())
             putExtra("sourceVerificationEnable", saveResult)
             IntentData.put(url, source.getHeaderMap(true))
         }

+ 3 - 1
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -310,7 +310,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
 
     val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog)
 
-    val loadCoverOnlyWifi = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)
+    val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)
+
+    val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true)
 
     val doublePageHorizontal: String?
         get() = appCtx.getPrefString(PreferKey.doublePageHorizontal)

+ 45 - 0
app/src/main/java/io/legado/app/help/config/SourceConfig.kt

@@ -0,0 +1,45 @@
+package io.legado.app.help.config
+
+import android.content.Context.MODE_PRIVATE
+import androidx.core.content.edit
+import splitties.init.appCtx
+
+object SourceConfig {
+    private val sp = appCtx.getSharedPreferences("SourceConfig", MODE_PRIVATE)
+    fun setBookScore(origin: String, name: String, author: String, score: Int) {
+        sp.edit {
+            val preScore = getBookScore(origin, name, author)
+            var newScore = score
+            if (preScore != 0) {
+                newScore = score - preScore
+            }
+
+            putInt(origin, getSourceScore(origin) + newScore)
+
+            putInt("${origin}_${name}_${author}", score)
+        }
+    }
+
+    fun getBookScore(origin: String, name: String, author: String): Int {
+        return sp.getInt("${origin}_${name}_${author}", 0)
+    }
+
+    fun getSourceScore(origin: String): Int {
+        return sp.getInt(origin, 0)
+    }
+
+
+    fun removeSource(origin: String) {
+        sp.all.keys.filter {
+            it.startsWith(origin)
+        }.let {
+            sp.edit {
+                it.forEach {
+                    remove(it)
+                }
+            }
+        }
+    }
+
+
+}

+ 14 - 18
app/src/main/java/io/legado/app/model/AudioPlay.kt

@@ -105,7 +105,7 @@ object AudioPlay {
                 book.durChapterIndex = index
                 book.durChapterPos = 0
                 durChapter = null
-                saveRead(book)
+                saveRead()
                 play(context)
             }
         }
@@ -120,7 +120,7 @@ object AudioPlay {
                 book.durChapterIndex = book.durChapterIndex - 1
                 book.durChapterPos = 0
                 durChapter = null
-                saveRead(book)
+                saveRead()
                 play(context)
             }
         }
@@ -134,17 +134,11 @@ object AudioPlay {
             book.durChapterIndex = book.durChapterIndex + 1
             book.durChapterPos = 0
             durChapter = null
-            saveRead(book)
+            saveRead()
             play(context)
         }
     }
 
-    fun addTimer() {
-        val intent = Intent(appCtx, AudioPlayService::class.java)
-        intent.action = IntentAction.addTimer
-        appCtx.startService(intent)
-    }
-
     fun setTimer(minute: Int) {
         val intent = Intent(appCtx, AudioPlayService::class.java)
         intent.action = IntentAction.setTimer
@@ -152,16 +146,18 @@ object AudioPlay {
         appCtx.startService(intent)
     }
 
-    fun saveRead(book: Book) {
-        book.lastCheckCount = 0
-        book.durChapterTime = System.currentTimeMillis()
-        Coroutine.async {
-            appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
-                book.durChapterTitle = it.getDisplayTitle(
-                    ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
-                )
+    fun saveRead() {
+        book?.let { book ->
+            book.lastCheckCount = 0
+            book.durChapterTime = System.currentTimeMillis()
+            Coroutine.async {
+                appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
+                    book.durChapterTitle = it.getDisplayTitle(
+                        ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
+                    )
+                }
+                book.save()
             }
-            book.save()
         }
     }
 

+ 3 - 2
app/src/main/java/io/legado/app/model/ReadAloud.kt

@@ -39,13 +39,14 @@ object ReadAloud {
     fun play(
         context: Context,
         play: Boolean = true,
-        pageIndex: Int = ReadBook.durPageIndex
+        pageIndex: Int = ReadBook.durPageIndex,
+        startPos: Int = 0
     ) {
         val intent = Intent(context, aloudClass)
         intent.action = IntentAction.play
         intent.putExtra("play", play)
         intent.putExtra("pageIndex", pageIndex)
-
+        intent.putExtra("startPos", startPos)
         context.startService(intent)
     }
 

+ 5 - 2
app/src/main/java/io/legado/app/model/localBook/EpubFile.kt

@@ -19,6 +19,7 @@ import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
 import java.io.InputStream
+import java.net.URLDecoder
 import java.nio.charset.Charset
 
 class EpubFile(var book: Book) {
@@ -116,7 +117,9 @@ class EpubFile(var book: Book) {
          * <image width="1038" height="670" xlink:href="..."/>
          * ...titlepage.xhtml
          */
-        if (chapter.url.contains("titlepage.xhtml")) {
+        if (chapter.url.contains("titlepage.xhtml") ||
+            chapter.url.contains("cover.xhtml")
+        ) {
             return "<img src=\"cover.jpeg\" />"
         }
         /*获取当前章节文本*/
@@ -189,7 +192,7 @@ class EpubFile(var book: Book) {
 
     private fun getImage(href: String): InputStream? {
         if (href == "cover.jpeg") return epubBook?.coverImage?.inputStream
-        val abHref = href.replace("../", "")
+        val abHref = URLDecoder.decode(href.replace("../", ""), "UTF-8")
         return epubBook?.resources?.getByHref(abHref)?.inputStream
     }
 

+ 13 - 3
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -6,6 +6,7 @@ import androidx.documentfile.provider.DocumentFile
 import com.script.SimpleBindings
 import io.legado.app.R
 import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppLog
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.Book
@@ -16,6 +17,7 @@ import io.legado.app.help.BookHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.utils.*
+import org.jsoup.nodes.Entities
 import splitties.init.appCtx
 import java.io.*
 import java.util.regex.Pattern
@@ -77,11 +79,12 @@ object LocalBook {
             throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
         }
         val lh = LinkedHashSet(chapters)
+        lh.forEachIndexed { index, bookChapter -> bookChapter.index = index }
         return ArrayList(lh)
     }
 
     fun getContent(book: Book, chapter: BookChapter): String? {
-        return try {
+        val content = try {
             when {
                 book.isEpub() -> {
                     EpubFile.getContent(book, chapter)
@@ -95,8 +98,15 @@ object LocalBook {
             }
         } catch (e: Exception) {
             e.printOnDebug()
-            e.localizedMessage
-        }
+            AppLog.put("获取本地书籍内容失败\n${e.localizedMessage}", e)
+            "获取本地书籍内容失败\n${e.localizedMessage}"
+        }?.replace("&lt;img", "&lt; img", true)
+        content ?: return null
+        return kotlin.runCatching {
+            Entities.unescape(content)
+        }.onFailure {
+            AppLog.put("HTML实体解码失败\n${it.localizedMessage}", it)
+        }.getOrElse { content }
     }
 
     /**

+ 1 - 0
app/src/main/java/io/legado/app/model/webBook/BookContent.kt

@@ -111,6 +111,7 @@ object BookContent {
             }
         }
         var contentStr = content.toString()
+        //全文替换
         val replaceRegex = contentRule.replaceRegex
         if (!replaceRegex.isNullOrEmpty()) {
             contentStr = analyzeRule.getString(replaceRegex, contentStr)

+ 1 - 2
app/src/main/java/io/legado/app/model/webBook/WebBook.kt

@@ -238,12 +238,11 @@ object WebBook {
         context: CoroutineContext = Dispatchers.IO
     ): Coroutine<String> {
         return Coroutine.async(scope, context) {
-            getContentAwait(scope, bookSource, book, bookChapter, nextChapterUrl, needSave)
+            getContentAwait(bookSource, book, bookChapter, nextChapterUrl, needSave)
         }
     }
 
     suspend fun getContentAwait(
-        scope: CoroutineScope,
         bookSource: BookSource,
         book: Book,
         bookChapter: BookChapter,

+ 6 - 11
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -118,24 +118,19 @@ abstract class BaseReadAloudService : BaseService(),
     }
 
     private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
+        this.pageIndex = pageIndex
         textChapter = ReadBook.curTextChapter
         textChapter?.let { textChapter ->
             nowSpeak = 0
             readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
             contentList.clear()
-            if (getPrefBoolean(PreferKey.readAloudByPage)) {
-                for (index in pageIndex..textChapter.lastIndex) {
-                    textChapter.getPage(index)?.text?.split("\n")?.let {
-                        contentList.addAll(it)
+            val readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
+            textChapter.getNeedReadAloud(pageIndex, readAloudByPage, startPos)
+                .split("\n").forEach { text ->
+                    if (text.isNotEmpty()) {
+                        contentList.add(text)
                     }
                 }
-            } else {
-                textChapter.getUnRead(pageIndex).split("\n").forEach {
-                    if (it.isNotEmpty()) {
-                        contentList.add(it)
-                    }
-                }
-            }
             if (play) play()
         }
     }

+ 2 - 3
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -136,7 +136,7 @@ class CheckSourceService : BaseService() {
             source.bookSourceComment = source.bookSourceComment
                 ?.split("\n\n")
                 ?.filterNot {
-                    it.startsWith("Error: ")
+                    it.startsWith("// Error: ")
                 }?.joinToString("\n")
             //校验搜索书籍
             if (CheckSource.checkSearch) {
@@ -186,7 +186,7 @@ class CheckSourceService : BaseService() {
                     !is NoStackTraceException -> source.addGroup("网站失效")
                 }
                 source.bookSourceComment =
-                    "Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank())
+                    "// Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank())
                         "" else "\n\n${source.bookSourceComment}"
                 Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
             }.onSuccess(searchCoroutine) {
@@ -219,7 +219,6 @@ class CheckSourceService : BaseService() {
                     //校验正文
                     if (CheckSource.checkContent) {
                         WebBook.getContentAwait(
-                            this,
                             bookSource = source,
                             book = mBook,
                             bookChapter = toc.first(),

+ 1 - 1
app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt

@@ -1,7 +1,7 @@
 package io.legado.app.ui.about
 
+import java.text.SimpleDateFormat
 import android.content.Context
-import android.icu.text.SimpleDateFormat
 import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem

+ 1 - 1
app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt

@@ -46,7 +46,7 @@ class OnLineImportActivity :
             finallyDialog(getString(R.string.error), it)
         }
         intent.data?.let {
-            val url = it.getQueryParameter("src")
+            val url = it.query?.substringAfter("src=")
             if (url.isNullOrBlank()) {
                 finish()
                 return

+ 30 - 22
app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt

@@ -2,12 +2,12 @@ package io.legado.app.ui.association
 
 import android.annotation.SuppressLint
 import android.os.Bundle
+import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
-import android.view.MenuItem
 import androidx.appcompat.widget.Toolbar
-import com.bumptech.glide.request.RequestOptions
 import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.request.RequestOptions
 import io.legado.app.R
 import io.legado.app.base.BaseDialogFragment
 import io.legado.app.databinding.DialogVerificationCodeViewBinding
@@ -17,7 +17,9 @@ import io.legado.app.help.glide.ImageLoader
 import io.legado.app.help.glide.OkHttpModelLoader
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.dialog.PhotoDialog
-import io.legado.app.utils.*
+import io.legado.app.utils.applyTint
+import io.legado.app.utils.setLayout
+import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 
 /**
@@ -26,12 +28,18 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
  * val key = "${sourceOrigin ?: ""}_verificationResult"
  * CacheManager.get(key)
  */
-class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view), Toolbar.OnMenuItemClickListener {
+class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view),
+    Toolbar.OnMenuItemClickListener {
 
-    constructor(imageUrl: String, sourceOrigin: String? = null) : this() {
+    constructor(
+        imageUrl: String,
+        sourceOrigin: String? = null,
+        sourceName: String? = null
+    ) : this() {
         arguments = Bundle().apply {
-            putString("sourceOrigin", sourceOrigin)
             putString("imageUrl", imageUrl)
+            putString("sourceOrigin", sourceOrigin)
+            putString("sourceName", sourceName)
         }
     }
 
@@ -47,23 +55,23 @@ class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification
         initMenu()
         binding.run {
             toolBar.setBackgroundColor(primaryColor)
-            val sourceOrigin = arguments?.getString("sourceOrigin")
-            arguments?.getString("imageUrl")?.let { imageUrl ->
-                ImageLoader.load(requireContext(), imageUrl).apply {
-                    sourceOrigin?.let {
-                        apply(
-                            RequestOptions().set(
-                                OkHttpModelLoader.sourceOriginOption,
-                                it
+            arguments?.let { arguments ->
+                toolBar.subtitle = arguments.getString("sourceName")
+                val sourceOrigin = arguments.getString("sourceOrigin")
+                arguments.getString("imageUrl")?.let { imageUrl ->
+                    ImageLoader.load(requireContext(), imageUrl).apply {
+                        sourceOrigin?.let {
+                            apply(
+                                RequestOptions().set(OkHttpModelLoader.sourceOriginOption, it)
                             )
-                        )
+                        }
+                    }.error(R.drawable.image_loading_error)
+                        .diskCacheStrategy(DiskCacheStrategy.NONE)
+                        .skipMemoryCache(true)
+                        .into(verificationCodeImageView)
+                    verificationCodeImageView.setOnClickListener {
+                        showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
                     }
-                }.error(R.drawable.image_loading_error)
-                    .diskCacheStrategy(DiskCacheStrategy.NONE)
-                    .skipMemoryCache(true)
-                    .into(verificationCodeImageView)
-                verificationCodeImageView.setOnClickListener {
-                    showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
                 }
             }
         }
@@ -86,7 +94,7 @@ class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification
                     CacheManager.putMemory(key, it)
                     dismiss()
                 }
-           }
+            }
         }
         return false
     }

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt

@@ -37,9 +37,9 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
                             loadChapterList(book)
                         }
                     }
-                    saveRead(book)
                 }
             }
+            saveRead()
         }
     }
 

+ 54 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt

@@ -5,14 +5,19 @@ import android.os.Bundle
 import android.view.View
 import android.view.ViewGroup
 import androidx.appcompat.widget.PopupMenu
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.isVisible
 import androidx.recyclerview.widget.DiffUtil
 import io.legado.app.R
 import io.legado.app.base.adapter.DiffRecyclerAdapter
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.databinding.ItemChangeSourceBinding
+import io.legado.app.utils.getCompatColor
+import io.legado.app.utils.gone
 import io.legado.app.utils.invisible
 import io.legado.app.utils.visible
+import splitties.init.appCtx
 import splitties.views.onLongClick
 
 
@@ -68,10 +73,57 @@ class ChangeBookSourceAdapter(
                     }
                 }
             }
+            val score = callBack.getBookScore(item)
+            if (score > 0) {
+                binding.ivBad.gone()
+                binding.ivGood.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+            } else if (score < 0) {
+                binding.ivGood.gone()
+                binding.ivBad.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
+            } else {
+                binding.ivGood.visible()
+                binding.ivBad.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+            }
         }
     }
 
     override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
+        binding.ivGood.setOnClickListener {
+            if (binding.ivBad.isVisible) {
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
+                binding.ivBad.gone()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 1)
+                }
+            } else {
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                binding.ivBad.visible()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 0)
+                }
+            }
+        }
+        binding.ivBad.setOnClickListener {
+            if (binding.ivGood.isVisible) {
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
+                binding.ivGood.gone()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, -1)
+                }
+            } else {
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+                binding.ivGood.visible()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 0)
+                }
+            }
+        }
         holder.itemView.setOnClickListener {
             getItem(holder.layoutPosition)?.let {
                 if (it.bookUrl != callBack.bookUrl) {
@@ -120,5 +172,7 @@ class ChangeBookSourceAdapter(
         fun editSource(searchBook: SearchBook)
         fun disableSource(searchBook: SearchBook)
         fun deleteSource(searchBook: SearchBook)
+        fun setBookScore(searchBook: SearchBook, score: Int)
+        fun getBookScore(searchBook: SearchBook): Int
     }
 }

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -294,6 +294,14 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
         }
     }
 
+    override fun setBookScore(searchBook: SearchBook, score: Int) {
+        viewModel.setBookScore(searchBook,score)
+    }
+
+    override fun getBookScore(searchBook: SearchBook): Int {
+        return viewModel.getBookScore(searchBook)
+    }
+
     private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) {
         waitDialog.setText(R.string.load_toc)
         waitDialog.show()

+ 30 - 1
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt

@@ -16,6 +16,7 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.config.AppConfig
+import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.webBook.WebBook
@@ -82,7 +83,23 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
             searchCallback = null
         }
     }.map {
-        searchBooks.sortedBy { it.originOrder }
+        searchBooks.sortedWith { o1, o2 ->
+            val o1bs = SourceConfig.getBookScore(o1.origin, o1.name, o1.author)
+            val o2bs = SourceConfig.getBookScore(o2.origin, o2.name, o2.author)
+            when {
+                o1bs - o2bs > 0 -> -1
+                o1bs - o2bs < 0 -> 1
+                else -> {
+                    val o1ss = SourceConfig.getSourceScore(o1.origin)
+                    val o2ss = SourceConfig.getSourceScore(o2.origin)
+                    when {
+                        o1ss - o2ss > 0 -> -1
+                        o1ss - o2ss < 0 -> 1
+                        else -> o1.originOrder - o2.originOrder
+                    }
+                }
+            }
+        }
     }.flowOn(IO)
 
     @Volatile
@@ -351,6 +368,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
             appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
                 appDb.bookSourceDao.delete(source)
                 appDb.searchBookDao.delete(searchBook)
+                SourceConfig.removeSource(source.bookSourceUrl)
             }
         }
         searchBooks.remove(searchBook)
@@ -379,6 +397,17 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
         }
     }
 
+    fun setBookScore(searchBook: SearchBook, score: Int) {
+        execute {
+            SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
+            searchCallback?.upAdapter()
+        }
+    }
+
+    fun getBookScore(searchBook: SearchBook): Int {
+        return SourceConfig.getBookScore(searchBook.origin, searchBook.name, searchBook.author)
+    }
+
     interface SourceCallback {
 
         fun searchSuccess(searchBook: SearchBook)

+ 54 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt

@@ -5,14 +5,19 @@ import android.os.Bundle
 import android.view.View
 import android.view.ViewGroup
 import androidx.appcompat.widget.PopupMenu
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.isVisible
 import androidx.recyclerview.widget.DiffUtil
 import io.legado.app.R
 import io.legado.app.base.adapter.DiffRecyclerAdapter
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.databinding.ItemChangeSourceBinding
+import io.legado.app.utils.getCompatColor
+import io.legado.app.utils.gone
 import io.legado.app.utils.invisible
 import io.legado.app.utils.visible
+import splitties.init.appCtx
 import splitties.views.onLongClick
 
 
@@ -68,10 +73,57 @@ class ChangeChapterSourceAdapter(
                     }
                 }
             }
+            val score = callBack.getBookScore(item)
+            if (score > 0) {
+                binding.ivBad.gone()
+                binding.ivGood.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+            } else if (score < 0) {
+                binding.ivGood.gone()
+                binding.ivBad.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
+            } else {
+                binding.ivGood.visible()
+                binding.ivBad.visible()
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+            }
         }
     }
 
     override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
+        binding.ivGood.setOnClickListener {
+            if (binding.ivBad.isVisible) {
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
+                binding.ivBad.gone()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 1)
+                }
+            } else {
+                DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
+                binding.ivBad.visible()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 0)
+                }
+            }
+        }
+        binding.ivBad.setOnClickListener {
+            if (binding.ivGood.isVisible) {
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
+                binding.ivGood.gone()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, -1)
+                }
+            } else {
+                DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
+                binding.ivGood.visible()
+                getItem(holder.layoutPosition)?.let {
+                    callBack.setBookScore(it, 0)
+                }
+            }
+        }
         holder.itemView.setOnClickListener {
             getItem(holder.layoutPosition)?.let {
                 callBack.openToc(it)
@@ -118,5 +170,7 @@ class ChangeChapterSourceAdapter(
         fun editSource(searchBook: SearchBook)
         fun disableSource(searchBook: SearchBook)
         fun deleteSource(searchBook: SearchBook)
+        fun setBookScore(searchBook: SearchBook, score: Int)
+        fun getBookScore(searchBook: SearchBook): Int
     }
 }

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -314,6 +314,14 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
         }
     }
 
+    override fun setBookScore(searchBook: SearchBook, score: Int) {
+        viewModel.setBookScore(searchBook, score)
+    }
+
+    override fun getBookScore(searchBook: SearchBook): Int {
+        return viewModel.getBookScore(searchBook)
+    }
+
     override fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) {
         searchBook?.let {
             binding.loadingToc.show()

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt

@@ -35,7 +35,7 @@ class ChangeChapterSourceViewModel(application: Application) :
         execute {
             val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
                 ?: throw NoStackTraceException("书源不存在")
-            WebBook.getContentAwait(this, bookSource, book, chapter, nextChapterUrl, false)
+            WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false)
         }.onSuccess {
             success.invoke(it)
         }.onError {

+ 5 - 2
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -21,6 +21,7 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityBookInfoBinding
 import io.legado.app.databinding.DialogEditTextBinding
+import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.lib.theme.bottomBackground
@@ -247,8 +248,10 @@ class BookInfoActivity :
 
     private fun showCover(book: Book) {
         binding.ivCover.load(book.getDisplayCover(), book.name, book.author, false, book.origin)
-        BookCover.loadBlur(this, book.getDisplayCover())
-            .into(binding.bgBook)
+        if(!AppConfig.isEInkMode) {
+            BookCover.loadBlur(this, book.getDisplayCover())
+                .into(binding.bgBook)
+        }
     }
 
     private fun upLoading(isLoading: Boolean, chapterList: List<BookChapter>? = null) {

+ 21 - 0
app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt

@@ -14,6 +14,8 @@ import io.legado.app.databinding.ItemArrangeBookBinding
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
+import java.util.*
+import kotlin.collections.HashSet
 
 class BookAdapter(context: Context, val callBack: CallBack) :
     RecyclerAdapter<Book, ItemArrangeBookBinding>(context),
@@ -121,6 +123,25 @@ class BookAdapter(context: Context, val callBack: CallBack) :
         callBack.upSelectCount()
     }
 
+    fun checkSelectedInterval() {
+        val selectedPosition = linkedSetOf<Int>()
+        getItems().forEachIndexed { index, it ->
+            if (selectedBooks.contains(it)) {
+                selectedPosition.add(index)
+            }
+        }
+        val minPosition = Collections.min(selectedPosition)
+        val maxPosition = Collections.max(selectedPosition)
+        val itemCount = maxPosition - minPosition + 1
+        for (i in minPosition..maxPosition) {
+            getItem(i)?.let {
+                selectedBooks.add(it)
+            }
+        }
+        notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
+        callBack.upSelectCount()
+    }
+
     private fun getGroupList(groupId: Long): List<String> {
         val groupNames = arrayListOf<String>()
         callBack.groupList.forEach {

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt

@@ -192,6 +192,7 @@ class BookshelfManageActivity :
                 viewModel.upCanUpdate(adapter.selection, false)
             R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0)
             R.id.menu_change_source -> showDialogFragment<SourcePickerDialog>()
+            R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
         }
         return false
     }

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt

@@ -126,7 +126,7 @@ class ContentEditDialog : BaseDialogFragment(R.layout.dialog_content_edit) {
                     content = null
                     BookHelp.delContent(book, chapter)
                     if (!book.isLocalBook()) ReadBook.bookSource?.let { bookSource ->
-                        WebBook.getContentAwait(this, bookSource, book, chapter)
+                        WebBook.getContentAwait(bookSource, book, chapter)
                     }
                 }
                 return@execute content ?: let {

+ 46 - 28
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -25,6 +25,7 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.BookHelp
 import io.legado.app.help.IntentData
+import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadTipConfig
 import io.legado.app.help.coroutine.Coroutine
@@ -35,6 +36,7 @@ import io.legado.app.lib.dialogs.selector
 import io.legado.app.lib.theme.accentColor
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
+import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.receiver.TimeBatteryReceiver
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.about.AppLogDialog
@@ -182,6 +184,7 @@ class ReadBookActivity : BaseReadBookActivity(),
     override fun onWindowFocusChanged(hasFocus: Boolean) {
         super.onWindowFocusChanged(hasFocus)
         upSystemUiVisibility()
+        binding.readMenu.upBrightnessState()
     }
 
     override fun onConfigurationChanged(newConfig: Configuration) {
@@ -960,31 +963,41 @@ class ReadBookActivity : BaseReadBookActivity(),
     }
 
     override fun payAction() {
-        Coroutine.async(this) {
-            val book = ReadBook.book ?: throw NoStackTraceException("no book")
+        ReadBook.book?.let { book ->
+            if (book.isLocalBook()) return
             val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
-                ?: throw NoStackTraceException("no chapter")
-            val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
-            val payAction = source.getContentRule().payAction
-            if (payAction.isNullOrEmpty()) {
-                throw NoStackTraceException("no pay action")
-            }
-            JsUtils.evalJs(payAction) {
-                it["java"] = source
-                it["source"] = source
-                it["book"] = book
-                it["chapter"] = chapter
-            }
-        }.onSuccess {
-            if (it.isNotBlank()) {
-                startActivity<WebViewActivity> {
-                    putExtra("title", getString(R.string.chapter_pay))
-                    putExtra("url", it)
-                    IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
+            if (chapter == null) {
+                toastOnUi("no chapter")
+                return
+            }
+            alert(R.string.chapter_pay) {
+                setMessage(chapter.title)
+                yesButton {
+                    Coroutine.async {
+                        val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
+                        val payAction = source.getContentRule().payAction
+                        if (payAction.isNullOrEmpty()) {
+                            throw NoStackTraceException("no pay action")
+                        }
+                        val analyzeRule = AnalyzeRule(book, source)
+                        analyzeRule.setBaseUrl(chapter.url)
+                        analyzeRule.chapter = chapter
+                        analyzeRule.evalJS(payAction).toString()
+                    }.onSuccess {
+                        if (it.isNotBlank()) {
+                            startActivity<WebViewActivity> {
+                                putExtra("title", R.string.chapter_pay)
+                                putExtra("url", it)
+                                IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
+                            }
+                        }
+                    }.onError {
+                        AppLog.putDebug(it.localizedMessage)
+                        toastOnUi(it.localizedMessage)
+                    }
                 }
+                noButton()
             }
-        }.onError {
-            toastOnUi(it.localizedMessage)
         }
     }
 
@@ -1147,13 +1160,17 @@ class ReadBookActivity : BaseReadBookActivity(),
     override fun finish() {
         ReadBook.book?.let {
             if (!ReadBook.inBookshelf) {
-                alert(title = getString(R.string.add_to_shelf)) {
-                    setMessage(getString(R.string.check_add_bookshelf, it.name))
-                    okButton {
-                        ReadBook.inBookshelf = true
-                        setResult(Activity.RESULT_OK)
+                if (!AppConfig.showAddToShelfAlert) {
+                    viewModel.removeFromBookshelf { super.finish() }
+                } else {
+                    alert(title = getString(R.string.add_to_shelf)) {
+                        setMessage(getString(R.string.check_add_bookshelf, it.name))
+                        okButton {
+                            ReadBook.inBookshelf = true
+                            setResult(Activity.RESULT_OK)
+                        }
+                        noButton { viewModel.removeFromBookshelf { super.finish() } }
                     }
-                    noButton { viewModel.removeFromBookshelf { super.finish() } }
                 }
             } else {
                 super.finish()
@@ -1190,6 +1207,7 @@ class ReadBookActivity : BaseReadBookActivity(),
         }
         observeEvent<Boolean>(EventBus.UP_CONFIG) {
             upSystemUiVisibility()
+            readView.upPageSlopSquare()
             readView.upBg()
             readView.upStyle()
             readView.upBgAlpha()

+ 0 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -216,7 +216,6 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
                 toc.first()
             }
             WebBook.getContentAwait(
-                this,
                 bookSource = source,
                 book = book,
                 bookChapter = toc[book.durChapterIndex],

+ 85 - 71
app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt

@@ -43,10 +43,18 @@ class ReadMenu @JvmOverloads constructor(
     var cnaShowMenu: Boolean = false
     private val callBack: CallBack get() = activity as CallBack
     private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true)
-    private lateinit var menuTopIn: Animation
-    private lateinit var menuTopOut: Animation
-    private lateinit var menuBottomIn: Animation
-    private lateinit var menuBottomOut: Animation
+    private val menuTopIn: Animation by lazy {
+        loadAnimation(context, R.anim.anim_readbook_top_in)
+    }
+    private val menuTopOut: Animation by lazy {
+        loadAnimation(context, R.anim.anim_readbook_top_out)
+    }
+    private val menuBottomIn: Animation by lazy {
+        loadAnimation(context, R.anim.anim_readbook_bottom_in)
+    }
+    private val menuBottomOut: Animation by lazy {
+        loadAnimation(context, R.anim.anim_readbook_bottom_out)
+    }
     private val bgColor: Int = context.bottomBackground
     private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
     private val bottomBackgroundList: ColorStateList = Selector.colorBuild()
@@ -71,6 +79,58 @@ class ReadMenu @JvmOverloads constructor(
             }
         }
     }
+    private val menuInListener = object : Animation.AnimationListener {
+        override fun onAnimationStart(animation: Animation) {
+            binding.tvSourceAction.isGone = ReadBook.isLocalBook
+            binding.tvLogin.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
+            binding.tvPay.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
+                    || ReadBook.curTextChapter?.isVip != true
+                    || ReadBook.curTextChapter?.isPay == true
+            callBack.upSystemUiVisibility()
+            binding.llBrightness.visible(showBrightnessView)
+        }
+
+        @SuppressLint("RtlHardcoded")
+        override fun onAnimationEnd(animation: Animation) {
+            val navigationBarHeight =
+                if (ReadBookConfig.hideNavigationBar) {
+                    activity?.navigationBarHeight ?: 0
+                } else {
+                    0
+                }
+            binding.run {
+                vwMenuBg.setOnClickListener { runMenuOut() }
+                root.padding = 0
+                when (activity?.navigationBarGravity) {
+                    Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
+                    Gravity.LEFT -> root.leftPadding = navigationBarHeight
+                    Gravity.RIGHT -> root.rightPadding = navigationBarHeight
+                }
+            }
+            callBack.upSystemUiVisibility()
+            if (!LocalConfig.readMenuHelpVersionIsLast) {
+                callBack.showReadMenuHelp()
+            }
+        }
+
+        override fun onAnimationRepeat(animation: Animation) = Unit
+    }
+    private val menuOutListener = object : Animation.AnimationListener {
+        override fun onAnimationStart(animation: Animation) {
+            binding.vwMenuBg.setOnClickListener(null)
+        }
+
+        override fun onAnimationEnd(animation: Animation) {
+            this@ReadMenu.invisible()
+            binding.titleBar.invisible()
+            binding.bottomMenu.invisible()
+            cnaShowMenu = false
+            onMenuOutEnd?.invoke()
+            callBack.upSystemUiVisibility()
+        }
+
+        override fun onAnimationRepeat(animation: Animation) = Unit
+    }
 
     init {
         initView()
@@ -141,19 +201,29 @@ class ReadMenu @JvmOverloads constructor(
         activity?.window?.attributes = params
     }
 
-    fun runMenuIn() {
+    fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
         this.visible()
         binding.titleBar.visible()
         binding.bottomMenu.visible()
-        binding.titleBar.startAnimation(menuTopIn)
-        binding.bottomMenu.startAnimation(menuBottomIn)
+        if (anim) {
+            binding.titleBar.startAnimation(menuTopIn)
+            binding.bottomMenu.startAnimation(menuBottomIn)
+        } else {
+            menuInListener.onAnimationStart(menuBottomIn)
+            menuInListener.onAnimationEnd(menuBottomIn)
+        }
     }
 
-    fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) {
+    fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
         this.onMenuOutEnd = onMenuOutEnd
         if (this.isVisible) {
-            binding.titleBar.startAnimation(menuTopOut)
-            binding.bottomMenu.startAnimation(menuBottomOut)
+            if (anim) {
+                binding.titleBar.startAnimation(menuTopOut)
+                binding.bottomMenu.startAnimation(menuBottomOut)
+            } else {
+                menuOutListener.onAnimationStart(menuBottomOut)
+                menuOutListener.onAnimationEnd(menuBottomOut)
+            }
         }
     }
 
@@ -288,7 +358,9 @@ class ReadMenu @JvmOverloads constructor(
             }
         }
         llReadAloud.onLongClick {
-            runMenuOut { callBack.showReadAloudDialog() }
+            runMenuOut {
+                callBack.showReadAloudDialog()
+            }
         }
         //界面
         llFont.setOnClickListener {
@@ -306,66 +378,8 @@ class ReadMenu @JvmOverloads constructor(
     }
 
     private fun initAnimation() {
-        //显示菜单
-        menuTopIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in)
-        menuBottomIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
-        menuTopIn.setAnimationListener(object : Animation.AnimationListener {
-            override fun onAnimationStart(animation: Animation) {
-                binding.tvSourceAction.isGone = ReadBook.isLocalBook
-                binding.tvLogin.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
-                binding.tvPay.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
-                        || ReadBook.curTextChapter?.isVip != true
-                        || ReadBook.curTextChapter?.isPay == true
-                callBack.upSystemUiVisibility()
-                binding.llBrightness.visible(showBrightnessView)
-            }
-
-            @SuppressLint("RtlHardcoded")
-            override fun onAnimationEnd(animation: Animation) {
-                val navigationBarHeight =
-                    if (ReadBookConfig.hideNavigationBar) {
-                        activity?.navigationBarHeight ?: 0
-                    } else {
-                        0
-                    }
-                binding.run {
-                    vwMenuBg.setOnClickListener { runMenuOut() }
-                    root.padding = 0
-                    when (activity?.navigationBarGravity) {
-                        Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
-                        Gravity.LEFT -> root.leftPadding = navigationBarHeight
-                        Gravity.RIGHT -> root.rightPadding = navigationBarHeight
-                    }
-                }
-                callBack.upSystemUiVisibility()
-                if (!LocalConfig.readMenuHelpVersionIsLast) {
-                    callBack.showReadMenuHelp()
-                }
-            }
-
-            override fun onAnimationRepeat(animation: Animation) = Unit
-        })
-
-        //隐藏菜单
-        menuTopOut = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_out)
-        menuBottomOut =
-            AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
-        menuTopOut.setAnimationListener(object : Animation.AnimationListener {
-            override fun onAnimationStart(animation: Animation) {
-                binding.vwMenuBg.setOnClickListener(null)
-            }
-
-            override fun onAnimationEnd(animation: Animation) {
-                this@ReadMenu.invisible()
-                binding.titleBar.invisible()
-                binding.bottomMenu.invisible()
-                cnaShowMenu = false
-                onMenuOutEnd?.invoke()
-                callBack.upSystemUiVisibility()
-            }
-
-            override fun onAnimationRepeat(animation: Animation) = Unit
-        })
+        menuTopIn.setAnimationListener(menuInListener)
+        menuTopOut.setAnimationListener(menuOutListener)
     }
 
     fun upBookView() {

+ 2 - 4
app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt

@@ -33,10 +33,8 @@ class SearchMenu @JvmOverloads constructor(
     private val callBack: CallBack get() = activity as CallBack
     private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true)
 
-    private val menuBottomIn: Animation =
-        AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
-    private val menuBottomOut: Animation =
-        AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
+    private val menuBottomIn: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_in)
+    private val menuBottomOut: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_out)
     private val bgColor: Int = context.bottomBackground
     private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
     private val bottomBackgroundList: ColorStateList =

+ 25 - 0
app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt

@@ -11,6 +11,7 @@ import androidx.preference.Preference
 import io.legado.app.R
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
+import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.lib.prefs.fragment.PreferenceFragment
 import io.legado.app.lib.theme.bottomBackground
@@ -18,6 +19,7 @@ import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.ui.widget.number.NumberPickerDialog
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.postEvent
@@ -70,9 +72,12 @@ class MoreConfigDialog : DialogFragment() {
     class ReadPreferenceFragment : PreferenceFragment(),
         SharedPreferences.OnSharedPreferenceChangeListener {
 
+        private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
+
         @SuppressLint("RestrictedApi")
         override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
             addPreferencesFromResource(R.xml.pref_config_read)
+            upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
         }
 
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -137,9 +142,29 @@ class MoreConfigDialog : DialogFragment() {
                 "clickRegionalConfig" -> {
                     (activity as? ReadBookActivity)?.showClickRegionalConfig()
                 }
+                PreferKey.pageTouchSlop -> {
+                    NumberPickerDialog(requireContext())
+                        .setTitle(getString(R.string.page_touch_slop_dialog_title))
+                        .setMaxValue(9999)
+                        .setMinValue(0)
+                        .setValue(AppConfig.pageTouchSlop)
+                        .show {
+                            AppConfig.pageTouchSlop = it
+                            postEvent(EventBus.UP_CONFIG, false)
+                        }
+                }
             }
             return super.onPreferenceTreeClick(preference)
         }
 
+        @Suppress("SameParameterValue")
+        private fun upPreferenceSummary(preferenceKey: String, value: String?) {
+            val preference = findPreference<Preference>(preferenceKey) ?: return
+            when (preferenceKey) {
+                PreferKey.pageTouchSlop -> preference.summary =
+                    getString(R.string.page_touch_slop_summary, value)
+            }
+        }
+
     }
 }

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt

@@ -169,12 +169,14 @@ class TocRegexDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
             alertBinding.apply {
                 tvRuleName.setText(tocRule.name)
                 tvRuleRegex.setText(tocRule.rule)
+                tvRuleExample.setText(tocRule.example)
             }
             customView { alertBinding.root }
             okButton {
                 alertBinding.apply {
                     tocRule.name = tvRuleName.text.toString()
                     tocRule.rule = tvRuleRegex.text.toString()
+                    tocRule.example = tvRuleExample.text.toString()
                     viewModel.saveRule(tocRule)
                 }
             }
@@ -200,6 +202,7 @@ class TocRegexDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
                 if (payloads.isEmpty()) {
                     root.setBackgroundColor(context.backgroundColor)
                     rbRegexName.text = item.name
+                    titleExample.text = item.example
                     rbRegexName.isChecked = item.name == selectedName
                     swtEnabled.isChecked = item.enable
                 } else {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -446,7 +446,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                 return book.createBookMark().apply {
                     chapterIndex = page.chapterIndex
                     chapterPos = chapter.getReadLength(page.index) +
-                            page.getSelectStartLength(selectStart.lineIndex, selectStart.charIndex)
+                            page.getPosByLineColumn(selectStart.lineIndex, selectStart.charIndex)
                     chapterName = chapter.title
                     bookText = getSelectedText()
                 }

+ 29 - 10
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -82,13 +82,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
     private val initialTextPos = TextPos(0, 0, 0)
 
     val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
-    val pageSlopSquare by lazy {
-        if (AppConfig.pageTouchSlop == 0) {
-            slopSquare
-        } else {
-            AppConfig.pageTouchSlop
-        }
-    }
+    private var pageSlopSquare: Int = slopSquare
     private val tlRect = RectF()
     private val tcRect = RectF()
     private val trRect = RectF()
@@ -112,6 +106,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
             upBg()
             setWillNotDraw(false)
             upPageAnim()
+            upPageSlopSquare()
         }
     }
 
@@ -253,6 +248,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
         return true
     }
 
+    /**
+     * 更新状态栏
+     */
     fun upStatusBar() {
         curPage.upStatusBar()
         prevPage.upStatusBar()
@@ -402,6 +400,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
         }
     }
 
+    /**
+     * 点击
+     */
     private fun click(action: Int) {
         when (action) {
             0 -> callBack.showActionMenu()
@@ -441,6 +442,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
         }
     }
 
+    /**
+     * 销毁事件
+     */
     fun onDestroy() {
         pageDelegate?.onDestroy()
         curPage.cancelSelect()
@@ -511,6 +515,14 @@ class ReadView(context: Context, attrs: AttributeSet) :
         callBack.screenOffTimerStart()
     }
 
+    /**
+     * 更新滑动距离
+     */
+    fun upPageSlopSquare() {
+        val pageTouchSlop = AppConfig.pageTouchSlop
+        this.pageSlopSquare = if (pageTouchSlop == 0) slopSquare else pageTouchSlop
+    }
+
     /**
      * 更新样式
      */
@@ -563,10 +575,17 @@ class ReadView(context: Context, attrs: AttributeSet) :
      */
     fun aloudStartSelect() {
         val selectStartPos = curPage.selectStartPos
-        if (selectStartPos.relativePagePos > 0) {
-            ReadBook.moveToNextPage()
+        var pagePos = selectStartPos.relativePagePos
+        val line = selectStartPos.lineIndex
+        val column = selectStartPos.charIndex
+        while (pagePos > 0) {
+            if (!ReadBook.moveToNextPage()) {
+                ReadBook.moveToNextChapter(false)
+            }
+            pagePos--
         }
-        //TODO 未完成
+        val startPos = curPage.textPage.getPosByLineColumn(line, column)
+        ReadAloud.play(context, startPos = startPos)
     }
 
     /**

+ 25 - 5
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.book.read.page.entities
 
 import kotlin.math.min
 
+@Suppress("unused")
 data class TextChapter(
     val position: Int,
     val title: String,
@@ -61,6 +62,17 @@ data class TextChapter(
         return getReadLength(pageIndex + 1)
     }
 
+    /**
+     * 获取内容
+     */
+    fun getContent(): String {
+        val stringBuilder = StringBuilder()
+        pages.forEach {
+            stringBuilder.append(it.text)
+        }
+        return stringBuilder.toString()
+    }
+
     /**
      * @return 获取未读文字
      */
@@ -75,14 +87,22 @@ data class TextChapter(
     }
 
     /**
-     * 获取内容
+     * @return 需要朗读的文本列表
+     * @param pageIndex 起始页
+     * @param pageSplit 是否分页
+     * @param startPos 从当前页什么地方开始朗读
      */
-    fun getContent(): String {
+    fun getNeedReadAloud(pageIndex: Int, pageSplit: Boolean, startPos: Int): String {
         val stringBuilder = StringBuilder()
-        pages.forEach {
-            stringBuilder.append(it.text)
+        if (pages.isNotEmpty()) {
+            for (index in pageIndex..pages.lastIndex) {
+                stringBuilder.append(pages[index].text)
+                if (pageSplit && !stringBuilder.endsWith("\n")) {
+                    stringBuilder.append("\n")
+                }
+            }
         }
-        return stringBuilder.toString()
+        return stringBuilder.substring(startPos).toString()
     }
 
     /**

+ 28 - 5
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -34,6 +34,9 @@ data class TextPage(
         }
     }
 
+    /**
+     * 底部对齐更新行位置
+     */
     fun upLinesPosition() {
         if (!ReadBookConfig.textBottomJustify) return
         if (textLines.size <= 1) return
@@ -77,6 +80,9 @@ data class TextPage(
         }
     }
 
+    /**
+     * 计算文字位置
+     */
     @Suppress("DEPRECATION")
     fun format(): TextPage {
         if (textLines.isEmpty()) isMsgPage = true
@@ -105,9 +111,7 @@ data class TextPage(
                     val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
                     val x1 = x + cw
                     textLine.textChars.add(
-                        TextChar(
-                            char, start = x, end = x1
-                        )
+                        TextChar(char, start = x, end = x1)
                     )
                     x = x1
                 }
@@ -118,6 +122,9 @@ data class TextPage(
         return this
     }
 
+    /**
+     * 移除朗读标志
+     */
     fun removePageAloudSpan(): TextPage {
         textLines.forEach { textLine ->
             textLine.isReadAloud = false
@@ -125,6 +132,10 @@ data class TextPage(
         return this
     }
 
+    /**
+     * 更新朗读标志
+     * @param aloudSpanStart 朗读文字开始位置
+     */
     fun upPageAloudSpan(aloudSpanStart: Int) {
         removePageAloudSpan()
         var lineStart = 0
@@ -151,6 +162,9 @@ data class TextPage(
         }
     }
 
+    /**
+     * 阅读进度
+     */
     val readProgress: String
         get() {
             val df = DecimalFormat("0.0%")
@@ -167,15 +181,24 @@ data class TextPage(
             return percent
         }
 
-    fun getSelectStartLength(lineIndex: Int, charIndex: Int): Int {
+    /**
+     * 根据行和列返回字符在本页的位置
+     * @param lineIndex 字符在第几行
+     * @param columnIndex 字符在第几列
+     * @return 字符在本页位置
+     */
+    fun getPosByLineColumn(lineIndex: Int, columnIndex: Int): Int {
         var length = 0
         val maxIndex = min(lineIndex, lineSize)
         for (index in 0 until maxIndex) {
             length += textLines[index].charSize
         }
-        return length + charIndex
+        return length + columnIndex
     }
 
+    /**
+     * @return 页面所在章节
+     */
     fun getTextChapter(): TextChapter? {
         ReadBook.curTextChapter?.let {
             if (it.position == chapterIndex) {

+ 7 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt

@@ -66,6 +66,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
     private var menu: Menu? = null
     private var precisionSearchMenuItem: MenuItem? = null
     private var groups = linkedSetOf<String>()
+    private var isManualStopSearch = false
     private val searchFinishCallback: (isEmpty: Boolean) -> Unit = searchFinish@{ isEmpty ->
         val searchGroup = AppConfig.searchGroup
         if (!isEmpty || searchGroup.isEmpty()) return@searchFinish
@@ -155,6 +156,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
             override fun onQueryTextSubmit(query: String?): Boolean {
                 searchView.clearFocus()
                 query?.let {
+                    isManualStopSearch = false
                     viewModel.saveSearchKey(query)
                     viewModel.searchKey = ""
                     viewModel.search(it)
@@ -221,6 +223,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
                 .setPressedColor(ColorUtils.darkenColor(accentColor))
                 .create()
         binding.fbStop.setOnClickListener {
+            isManualStopSearch = true
             viewModel.stop()
             binding.refreshProgressBar.isAutoLoading = false
         }
@@ -266,6 +269,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
      * 滚动到底部事件
      */
     private fun scrollToBottom() {
+        if (isManualStopSearch) {
+            return
+        }
         if (viewModel.isSearchLiveData.value == false
             && viewModel.searchKey.isNotEmpty()
             && loadMoreView.hasMore
@@ -335,7 +341,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
         historyFlowJob?.cancel()
         historyFlowJob = launch {
             when {
-                key.isNullOrBlank() -> appDb.searchKeywordDao.flowByUsage()
+                key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime()
                 else -> appDb.searchKeywordDao.flowSearch(key)
             }.conflate().collect {
                 historyKeyAdapter.setItems(it)

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt

@@ -7,6 +7,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.RuleComplete
+import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.http.CookieStore
 import io.legado.app.help.http.newCallStrResponse
 import io.legado.app.help.http.okHttpClient
@@ -40,6 +41,7 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
             oldSourceUrl?.let {
                 if (oldSourceUrl != source.bookSourceUrl) {
                     appDb.bookSourceDao.delete(it)
+                    SourceConfig.removeSource(it)
                 }
             }
             oldSourceUrl = source.bookSourceUrl

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt

@@ -207,10 +207,12 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
             source.enabledExplore -> {
                 iv.setColorFilter(Color.GREEN)
                 iv.visible()
+                iv.contentDescription = context.getString(R.string.tag_explore_enabled)
             }
             else -> {
                 iv.setColorFilter(Color.RED)
                 iv.visible()
+                iv.contentDescription = context.getString(R.string.tag_explore_disabled)
             }
         }
     }

+ 7 - 1
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt

@@ -5,6 +5,7 @@ import android.text.TextUtils
 import io.legado.app.base.BaseViewModel
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
+import io.legado.app.help.config.SourceConfig
 import io.legado.app.utils.*
 import java.io.File
 import java.io.FileOutputStream
@@ -38,7 +39,12 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
     }
 
     fun del(vararg sources: BookSource) {
-        execute { appDb.bookSourceDao.delete(*sources) }
+        execute {
+            appDb.bookSourceDao.delete(*sources)
+            sources.forEach {
+                SourceConfig.removeSource(it.bookSourceUrl)
+            }
+        }
     }
 
     fun update(vararg bookSource: BookSource) {

+ 6 - 5
app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt

@@ -22,10 +22,7 @@ import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.recycler.VerticalDivider
-import io.legado.app.utils.applyTint
-import io.legado.app.utils.requestInputMethod
-import io.legado.app.utils.setLayout
-import io.legado.app.utils.splitNotBlank
+import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import kotlinx.coroutines.launch
 
@@ -62,7 +59,11 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
                 it.map { group ->
                     groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
                 }
-                adapter.setItems(groups.toList())
+                adapter.setItems(
+                    groups.sortedWith { o1, o2 ->
+                        o1.cnCompare(o2)
+                    }
+                )
             }
         }
     }

+ 3 - 0
app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt

@@ -45,6 +45,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         binding.titleBar.title = intent.getStringExtra("title") ?: getString(R.string.loading)
+        binding.titleBar.subtitle = intent.getStringExtra("sourceName")
         viewModel.initData(intent) {
             val url = viewModel.baseUrl
             val headerMap = viewModel.headerMap
@@ -124,12 +125,14 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
     private fun upWebViewTheme() {
         if (AppConfig.isNightTheme) {
             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
+                @Suppress("DEPRECATION")
                 WebSettingsCompat.setForceDarkStrategy(
                     binding.webView.settings,
                     WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
                 )
             }
             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+                @Suppress("DEPRECATION")
                 WebSettingsCompat.setForceDark(
                     binding.webView.settings,
                     WebSettingsCompat.FORCE_DARK_ON

+ 9 - 0
app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.config
 import android.content.SharedPreferences
 import android.net.Uri
 import android.os.Bundle
+import android.text.Editable
 import android.text.InputType
 import android.view.Menu
 import android.view.MenuInflater
@@ -116,6 +117,14 @@ class BackupConfigFragment : PreferenceFragment(),
                     InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
             }
         }
+        findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
+            it.setOnBindEditTextListener { editText ->
+                editText.text = AppConfig.webDavDir?.toEditable()
+            }
+            it.setOnPreferenceChangeListener { _, newValue ->
+                (newValue as String).isNotBlank()
+            }
+        }
         upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
         upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
         upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))

+ 4 - 15
app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt

@@ -6,7 +6,6 @@ import android.content.SharedPreferences
 import android.content.pm.PackageManager
 import android.os.Bundle
 import android.view.View
-import android.view.ViewConfiguration
 import androidx.core.view.postDelayed
 import androidx.fragment.app.activityViewModels
 import androidx.preference.ListPreference
@@ -28,7 +27,9 @@ import io.legado.app.ui.widget.number.NumberPickerDialog
 import io.legado.app.utils.*
 import splitties.init.appCtx
 
-
+/**
+ * 其它设置
+ */
 class OtherConfigFragment : PreferenceFragment(),
     SharedPreferences.OnSharedPreferenceChangeListener {
 
@@ -43,7 +44,6 @@ class OtherConfigFragment : PreferenceFragment(),
             AppConfig.defaultBookTreeUri = treeUri.toString()
         }
     }
-    private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
@@ -60,7 +60,6 @@ class OtherConfigFragment : PreferenceFragment(),
         }
         upPreferenceSummary(PreferKey.checkSource, CheckSource.summary)
         upPreferenceSummary(PreferKey.bitmapCacheSize, AppConfig.bitmapCacheSize.toString())
-        upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -120,16 +119,7 @@ class OtherConfigFragment : PreferenceFragment(),
                         ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
                     }
             }
-            PreferKey.pageTouchSlop -> {
-                NumberPickerDialog(requireContext())
-                    .setTitle(getString(R.string.page_touch_slop_dialog_title))
-                    .setMaxValue(9999)
-                    .setMinValue(0)
-                    .setValue(AppConfig.pageTouchSlop)
-                    .show {
-                        AppConfig.pageTouchSlop = it
-                    }
-            }
+
         }
         return super.onPreferenceTreeClick(preference)
     }
@@ -181,7 +171,6 @@ class OtherConfigFragment : PreferenceFragment(),
             PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value)
             PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value)
             PreferKey.bitmapCacheSize -> preference.summary = getString(R.string.bitmap_cache_size_summary, value)
-            PreferKey.pageTouchSlop -> preference.summary = getString(R.string.page_touch_slop_summary, value)
             else -> if (preference is ListPreference) {
                 val index = preference.findIndexOfValue(value)
                 // Set the summary to reflect the new value.

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt

@@ -15,6 +15,7 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.databinding.ItemFilletTextBinding
 import io.legado.app.databinding.ItemFindBookBinding
+import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.lib.theme.accentColor
 import io.legado.app.ui.login.SourceLoginActivity
@@ -174,6 +175,7 @@ class ExploreAdapter(context: Context, val callBack: CallBack) :
                 }
                 R.id.menu_del -> Coroutine.async(callBack.scope) {
                     appDb.bookSourceDao.delete(source)
+                    SourceConfig.removeSource(source.bookSourceUrl)
                 }
             }
             true

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt

@@ -63,6 +63,7 @@ class RssAdapter(context: Context, val callBack: CallBack) :
                 R.id.menu_top -> callBack.toTop(rssSource)
                 R.id.menu_edit -> callBack.edit(rssSource)
                 R.id.menu_del -> callBack.del(rssSource)
+                R.id.menu_disable -> callBack.disable(rssSource)
             }
             true
         }
@@ -74,5 +75,6 @@ class RssAdapter(context: Context, val callBack: CallBack) :
         fun toTop(rssSource: RssSource)
         fun edit(rssSource: RssSource)
         fun del(rssSource: RssSource)
+        fun disable(rssSource: RssSource)
     }
 }

+ 5 - 1
app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt

@@ -123,7 +123,7 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
     private fun initGroupData() {
         groupsFlowJob?.cancel()
         groupsFlowJob = launch {
-            appDb.rssSourceDao.flowGroup().conflate().collect {
+            appDb.rssSourceDao.flowGroupEnabled().conflate().collect {
                 groups.clear()
                 it.map { group ->
                     groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
@@ -181,4 +181,8 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
     override fun del(rssSource: RssSource) {
         viewModel.del(rssSource)
     }
+
+    override fun disable(rssSource: RssSource) {
+        viewModel.disable(rssSource)
+    }
 }

+ 3 - 1
app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt

@@ -141,7 +141,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
                             SelectItem(getString(R.string.action_save), "save"),
                             SelectItem(getString(R.string.select_folder), "selectFolder")
                         )
-                    ) { _, charSequence, i ->
+                    ) { _, charSequence, _ ->
                         when (charSequence.value) {
                             "save" -> saveImage(webPic)
                             "selectFolder" -> selectSaveFolder(null)
@@ -219,12 +219,14 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
     private fun upWebViewTheme() {
         if (AppConfig.isNightTheme) {
             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
+                @Suppress("DEPRECATION")
                 WebSettingsCompat.setForceDarkStrategy(
                     binding.webView.settings,
                     WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
                 )
             }
             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+                @Suppress("DEPRECATION")
                 WebSettingsCompat.setForceDark(
                     binding.webView.settings,
                     WebSettingsCompat.FORCE_DARK_ON

+ 5 - 1
app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt

@@ -65,7 +65,11 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
                 it.map { group ->
                     groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
                 }
-                adapter.setItems(groups.toList())
+                adapter.setItems(
+                    groups.sortedWith { o1, o2 ->
+                        o1.cnCompare(o2)
+                    }
+                )
             }
         }
     }

+ 29 - 2
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt

@@ -50,6 +50,9 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
     override val viewModel by viewModels<RssSourceViewModel>()
     private val importRecordKey = "rssSourceRecordKey"
     private val adapter by lazy { RssSourceAdapter(this, this) }
+    private val searchView: SearchView by lazy {
+        binding.titleBar.findViewById(R.id.search_view)
+    }
     private var sourceFlowJob: Job? = null
     private var groups = hashSetOf<String>()
     private var groupMenu: SubMenu? = null
@@ -120,10 +123,21 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
             R.id.menu_import_qr -> qrCodeResult.launch()
             R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
             R.id.menu_import_default -> viewModel.importDefault()
+            R.id.menu_enabled_group -> {
+                searchView.setQuery(getString(R.string.enabled), true)
+            }
+            R.id.menu_disabled_group -> {
+                searchView.setQuery(getString(R.string.disabled), true)
+            }
+            R.id.menu_group_login -> {
+                searchView.setQuery(getString(R.string.need_login), true)
+            }
+            R.id.menu_group_null -> {
+                searchView.setQuery(getString(R.string.no_group), true)
+            }
             R.id.menu_help -> showHelp()
             else -> if (item.groupId == R.id.source_group) {
-                binding.titleBar.findViewById<SearchView>(R.id.search_view)
-                    .setQuery("group:${item.title}", true)
+                searchView.setQuery("group:${item.title}", true)
             }
         }
         return super.onCompatOptionsItemSelected(item)
@@ -144,6 +158,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
             R.id.menu_share_source -> viewModel.saveToFile(adapter.selection) {
                 share(it)
             }
+            R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
         }
         return true
     }
@@ -240,6 +255,18 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
                 searchKey.isNullOrBlank() -> {
                     appDb.rssSourceDao.flowAll()
                 }
+                searchKey == getString(R.string.enabled) -> {
+                    appDb.rssSourceDao.flowEnabled()
+                }
+                searchKey == getString(R.string.disabled) -> {
+                    appDb.rssSourceDao.flowDisabled()
+                }
+                searchKey == getString(R.string.need_login) -> {
+                    appDb.rssSourceDao.flowLogin()
+                }
+                searchKey == getString(R.string.no_group) -> {
+                    appDb.rssSourceDao.flowNoGroup()
+                }
                 searchKey.startsWith("group:") -> {
                     val key = searchKey.substringAfter("group:")
                     appDb.rssSourceDao.flowGroupSearch(key)

+ 20 - 0
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt

@@ -17,6 +17,7 @@ import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
 import io.legado.app.utils.ColorUtils
+import java.util.*
 
 
 class RssSourceAdapter(context: Context, val callBack: CallBack) :
@@ -151,6 +152,25 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) :
         callBack.upCountView()
     }
 
+    fun checkSelectedInterval() {
+        val selectedPosition = linkedSetOf<Int>()
+        getItems().forEachIndexed { index, it ->
+            if (selected.contains(it)) {
+                selectedPosition.add(index)
+            }
+        }
+        val minPosition = Collections.min(selectedPosition)
+        val maxPosition = Collections.max(selectedPosition)
+        val itemCount = maxPosition - minPosition + 1
+        for (i in minPosition..maxPosition) {
+            getItem(i)?.let {
+                selected.add(it)
+            }
+        }
+        notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
+        callBack.upCountView()
+    }
+
     private fun showMenu(view: View, position: Int) {
         val source = getItem(position) ?: return
         val popupMenu = PopupMenu(context, view)

+ 7 - 0
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt

@@ -133,4 +133,11 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
         }
     }
 
+    fun disable(rssSource: RssSource) {
+        execute {
+            rssSource.enabled = false
+            appDb.rssSourceDao.update(rssSource)
+        }
+    }
+
 }

+ 15 - 0
app/src/main/java/io/legado/app/utils/AnimationExtensions.kt

@@ -0,0 +1,15 @@
+package io.legado.app.utils
+
+import android.content.Context
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import androidx.annotation.AnimRes
+import io.legado.app.help.config.AppConfig
+
+fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
+    val animation = AnimationUtils.loadAnimation(context, id)
+    if (AppConfig.isEInkMode) {
+        animation.duration = 0
+    }
+    return animation
+}

+ 0 - 17
app/src/main/java/io/legado/app/utils/AnimationUtilsSupport.kt

@@ -1,17 +0,0 @@
-package io.legado.app.utils
-
-import android.content.Context
-import android.view.animation.Animation
-import android.view.animation.AnimationUtils
-import androidx.annotation.AnimRes
-import io.legado.app.help.config.AppConfig
-
-object AnimationUtilsSupport {
-    fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
-        val animation = AnimationUtils.loadAnimation(context, id)
-        if (AppConfig.isEInkMode) {
-            animation.duration = 0
-        }
-        return animation
-    }
-}

+ 3 - 0
app/src/main/java/io/legado/app/utils/StringExtensions.kt

@@ -6,6 +6,7 @@ import io.legado.app.constant.AppPattern.dataUriRegex
 import android.icu.text.Collator
 import android.icu.util.ULocale
 import android.net.Uri
+import android.text.Editable
 import java.io.File
 import java.util.*
 
@@ -13,6 +14,8 @@ fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
 
 fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
 
+fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
+
 fun String.parseToUri(): Uri {
     return if (isContentScheme()) {
         Uri.parse(this)

+ 7 - 0
app/src/main/res/drawable/ic_praise.xml

@@ -0,0 +1,7 @@
+<vector android:height="24dp" android:viewportHeight="1024"
+    android:viewportWidth="1024" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M939.4,423.4c-23,-37.3 -62.9,-60.8 -107,-63.2 -2.8,-0.4 -5.5,-0.6 -8.3,-0.6l-152.2,-0.1c6.6,-29 10,-58.7 10,-88.7 0,-28.2 -3.2,-57.1 -9.6,-86 -0.4,-2 -1.1,-4 -1.9,-5.9 -15.8,-57.3 -67.4,-96.8 -127.3,-96.8 -72.8,0 -132.1,59.3 -132.1,132.1 0,3.3 0.1,6.7 0.4,10 -2.2,57.2 -32.1,109.9 -80.3,141.4 -14.4,9.4 -18.5,28.8 -9.1,43.2 9.4,14.4 28.8,18.5 43.2,9.1 65.8,-42.9 106.4,-115.3 108.7,-193.7 0,-1.3 -0,-2.7 -0.1,-4 -0.2,-2 -0.3,-4 -0.3,-6 0,-38.4 31.2,-69.6 69.6,-69.6 32.6,0 60.5,22.2 67.8,54 0.4,1.5 0.8,3 1.4,4.5 4.7,22.8 7.1,45.6 7.1,67.7 0,37.5 -6.2,74.4 -18.5,109.6 -3.3,9.5 -1.8,20.1 4,28.3 5.9,8.2 15.3,13.1 25.4,13.2l193.2,0.1c1.4,0.3 2.9,0.4 4.3,0.5 24.1,1 45.9,13.6 58.6,34.1 7.8,12.3 11.4,26.8 10.4,41.9 -0.1,0.9 -0.1,1.9 -0.1,2.9 0,0.9 0,1.7 0.1,2.5 0,0.3 -0,0.7 -0.1,1 -0.1,0.4 -0.2,0.8 -0.3,1.2l-75,330.4c-0.9,1.3 -1.7,2.6 -2.4,4 -5.9,11.8 -14.7,21.5 -25.3,28.2 -10.7,6.8 -23.1,10.6 -35.8,11.1 -0.9,-0.1 -513.8,-0.2 -513.8,-0.2 -0.5,-0 -0.9,-0 -1.4,-0 0,0 -111,0.2 -112.7,0.5 -1.9,0 -3.4,-1.5 -3.4,-3.4l0.3,-416.3c0,-1.9 1.5,-3.4 3,-3.5l1.2,0.1c1.2,0.1 2.7,0.3 3.5,0.2l83.8,-0.2 0,339.4c0,17.3 14,31.2 31.2,31.2s31.2,-14 31.2,-31.2L281,435.4c0,-1.8 0,-8.6 0,-10.4 0,-17.3 -14,-30.9 -31.2,-30.9 -1.5,0 -117.5,0.3 -119.4,0.3 -36.3,0 -65.9,29.6 -65.9,65.9l-0.3,416.3c0,36.3 29.6,65.9 65.9,65.9 2.5,0 111.4,-0.5 111.4,-0.5 0.5,0 0.9,0 1.4,0 0,0 511.5,0.3 512.5,0.3 25.5,0 50.3,-7.2 71.6,-20.7 19.6,-12.3 35.8,-29.9 46.8,-51 3.7,-5.6 6.4,-11.9 8.3,-18.6 0.1,-0.4 0.2,-0.8 0.3,-1.2l74.9,-330.3c1.6,-6.2 2.3,-12.6 2,-19C960.9,473.4 953.9,446.3 939.4,423.4z"/>
+    <path android:fillColor="#FF000000" android:pathData="M450,518.7c-17.3,0 -31.2,14 -31.2,31.2l0,30.5c0,17.3 14,31.2 31.2,31.2 17.3,0 31.2,-14 31.2,-31.2l0,-30.5C481.3,532.6 467.3,518.7 450,518.7z"/>
+    <path android:fillColor="#FF000000" android:pathData="M693.8,518.7c-17.3,0 -31.2,14 -31.2,31.2l0,30.5c0,17.3 14,31.2 31.2,31.2 17.3,0 31.2,-14 31.2,-31.2l0,-30.5C725,532.6 711.1,518.7 693.8,518.7z"/>
+    <path android:fillColor="#FF000000" android:pathData="M648.9,660.1c-14.3,-9.4 -33.6,-5.4 -43.2,8.8 -0.1,0.2 -13.6,19.8 -34.2,19.8 -20,0 -32.4,-18.1 -33.3,-19.4 -9.2,-14.4 -28.3,-18.8 -42.8,-9.8 -14.7,9.1 -19.2,28.4 -10.1,43 11.2,18 42,48.6 86.2,48.6 44,0 75.1,-30.3 86.6,-48.2C667.4,688.5 663.2,669.5 648.9,660.1z"/>
+</vector>

+ 55 - 62
app/src/main/res/layout/dialog_read_book_style.xml

@@ -3,32 +3,28 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tool="http://schemas.android.com/tools"
     android:id="@+id/root_view"
-    android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@color/background"
-    android:padding="10dp">
+    android:orientation="vertical">
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:gravity="center_vertical">
-
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="0.5" />
+        android:paddingHorizontal="16dp">
 
         <io.legado.app.ui.book.read.config.TextFontWeightConverter
             android:id="@+id/text_font_weight_converter"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
@@ -42,12 +38,12 @@
             android:id="@+id/tv_text_font"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
             android:text="@string/text_font"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
@@ -61,12 +57,12 @@
             android:id="@+id/tv_text_indent"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
             android:text="@string/text_indent"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
@@ -80,11 +76,11 @@
             android:id="@+id/chinese_converter"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
@@ -98,12 +94,12 @@
             android:id="@+id/tv_padding"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
             android:text="@string/padding"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
@@ -117,30 +113,24 @@
             android:id="@+id/tv_tip"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:gravity="center"
             android:paddingLeft="6dp"
-            android:paddingRight="6dp"
             android:paddingTop="4dp"
+            android:paddingRight="6dp"
             android:paddingBottom="4dp"
             android:text="@string/information"
-            android:gravity="center"
             android:textSize="14sp"
             app:isBottomBackground="true"
             app:radius="3dp" />
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="0.5" />
-
     </LinearLayout>
 
     <io.legado.app.ui.widget.DetailSeekBar
         android:id="@+id/dsb_text_size"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginTop="8dp"
         app:isBottomBackground="true"
         app:max="45"
         app:title="@string/text_size" />
@@ -149,8 +139,7 @@
         android:id="@+id/dsb_text_letter_spacing"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
+        android:layout_marginHorizontal="16dp"
         app:isBottomBackground="true"
         app:max="100"
         app:title="@string/text_letter_spacing" />
@@ -159,8 +148,7 @@
         android:id="@+id/dsb_line_size"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
+        android:layout_marginHorizontal="16dp"
         app:isBottomBackground="true"
         app:max="20"
         app:title="@string/line_size" />
@@ -169,8 +157,7 @@
         android:id="@+id/dsb_paragraph_spacing"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
+        android:layout_marginHorizontal="16dp"
         app:isBottomBackground="true"
         app:max="20"
         app:title="@string/paragraph_size" />
@@ -179,36 +166,37 @@
         android:id="@+id/vw_bg_fg"
         android:layout_width="match_parent"
         android:layout_height="0.8dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginVertical="8dp"
         android:background="@color/btn_bg_press" />
 
     <TextView
         android:id="@+id/tv_page_anim"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingTop="3dp"
-        android:paddingBottom="3dp"
-        android:paddingStart="6dp"
-        android:paddingEnd="6dp"
-        android:text="@string/page_anim" />
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginBottom="4dp"
+        android:alpha="0.75"
+        android:text="@string/page_anim"
+        android:textSize="12sp" />
 
     <RadioGroup
         android:id="@+id/rg_page_anim"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
-        android:orientation="horizontal"
-        android:gravity="center">
+        android:layout_marginHorizontal="11dp"
+        android:gravity="center"
+        android:orientation="horizontal">
 
         <io.legado.app.lib.theme.view.ThemeRadioNoButton
             android:id="@+id/rb_anim0"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_margin="4dp"
             android:layout_weight="1"
-            android:layout_margin="5dp"
-            android:padding="5dp"
             android:button="@null"
             android:gravity="center"
+            android:padding="5dp"
             android:singleLine="true"
             android:text="@string/page_anim_cover"
             app:isBottomBackground="true" />
@@ -217,11 +205,11 @@
             android:id="@+id/rb_anim1"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_margin="4dp"
             android:layout_weight="1"
-            android:layout_margin="5dp"
-            android:padding="5dp"
             android:button="@null"
             android:gravity="center"
+            android:padding="5dp"
             android:singleLine="true"
             android:text="@string/page_anim_slide"
             app:isBottomBackground="true" />
@@ -230,11 +218,11 @@
             android:id="@+id/rb_simulation_anim"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_margin="4dp"
             android:layout_weight="1"
-            android:layout_margin="5dp"
-            android:padding="5dp"
             android:button="@null"
             android:gravity="center"
+            android:padding="5dp"
             android:singleLine="true"
             android:text="@string/page_anim_simulation"
             app:isBottomBackground="true" />
@@ -243,11 +231,11 @@
             android:id="@+id/rb_scroll_anim"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_margin="4dp"
             android:layout_weight="1"
-            android:layout_margin="5dp"
-            android:padding="5dp"
             android:button="@null"
             android:gravity="center"
+            android:padding="5dp"
             android:singleLine="true"
             android:text="@string/page_anim_scroll"
             app:isBottomBackground="true" />
@@ -256,11 +244,11 @@
             android:id="@+id/rb_no_anim"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_margin="4dp"
             android:layout_weight="1"
-            android:layout_margin="5dp"
-            android:padding="5dp"
             android:button="@null"
             android:gravity="center"
+            android:padding="5dp"
             android:singleLine="true"
             android:text="@string/page_anim_none"
             app:isBottomBackground="true" />
@@ -271,24 +259,25 @@
         android:id="@+id/vw_bg_fg1"
         android:layout_width="match_parent"
         android:layout_height="0.8dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginVertical="8dp"
         android:background="@color/btn_bg_press" />
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        android:gravity="center_vertical">
+        android:gravity="center_vertical"
+        android:orientation="horizontal">
 
         <TextView
             android:id="@+id/tv_bg_ts"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
             android:layout_weight="1"
-            android:paddingStart="6dp"
-            android:paddingEnd="6dp"
-            android:paddingTop="5dp"
-            android:paddingBottom="5dp"
-            android:text="@string/text_bg_style" />
+            android:alpha="0.75"
+            android:text="@string/text_bg_style"
+            android:textSize="12sp" />
 
         <TextView
             android:id="@+id/tv_share_layout"
@@ -300,8 +289,8 @@
             android:id="@+id/cb_share_layout"
             android:layout_width="20dp"
             android:layout_height="20dp"
-            android:layout_marginRight="6dp"
-            android:layout_marginLeft="6dp" />
+            android:layout_marginStart="6dp"
+            android:layout_marginEnd="16dp" />
 
     </LinearLayout>
 
@@ -309,7 +298,11 @@
         android:id="@+id/rv_style"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="16dp"
+        android:clipToPadding="false"
         android:orientation="horizontal"
+        android:paddingHorizontal="12dp"
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         tool:listitem="@layout/item_read_style" />
 

+ 5 - 5
app/src/main/res/layout/dialog_verification_code_view.xml

@@ -10,26 +10,26 @@
         android:id="@+id/tool_bar"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:background="@color/background_menu"
         android:elevation="5dp"
         android:theme="?attr/actionBarStyle"
-        android:background="@color/background_menu"
-        app:title="@string/input_verification_code"
         app:popupTheme="@style/AppTheme.PopupOverlay"
+        app:title="@string/input_verification_code"
         app:titleTextAppearance="@style/ToolbarTitle" />
 
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/verification_code_image_view"
         android:layout_width="match_parent"
         android:layout_height="100dp"
-        android:scaleType="centerCrop"
         android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:scaleType="centerCrop"
         tools:ignore="UnusedAttribute" />
 
     <io.legado.app.ui.widget.text.TextInputLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:padding="3dp"
-        android:layout_marginTop="8dp">
+        android:layout_marginTop="8dp"
+        android:padding="3dp">
 
         <io.legado.app.lib.theme.view.ThemeEditText
             android:id="@+id/verification_code"

+ 1 - 1
app/src/main/res/layout/item_book_source.xml

@@ -54,6 +54,7 @@
         android:padding="6dp"
         android:src="@drawable/ic_more_vert"
         android:tint="@color/primaryText"
+        android:contentDescription="@string/more_menu"
         app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
@@ -63,7 +64,6 @@
         android:id="@+id/iv_explore"
         android:layout_width="8dp"
         android:layout_height="8dp"
-        android:contentDescription="@string/more_menu"
         android:scaleType="centerCrop"
         android:src="@color/md_green_600"
         app:layout_constraintRight_toRightOf="@id/iv_menu_more"

+ 44 - 13
app/src/main/res/layout/item_change_source.xml

@@ -1,56 +1,87 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:background="?android:attr/selectableItemBackground"
-    android:paddingLeft="10dp"
-    android:paddingTop="10dp"
-    android:paddingBottom="10dp"
     tools:ignore="RtlHardcoded,RtlSymmetry">
 
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_good"
+        android:layout_width="30dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="5dp"
+        android:padding="5dp"
+        android:src="@drawable/ic_praise"
+        app:layout_constraintBottom_toTopOf="@+id/iv_bad"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:tint="@color/md_red_A200" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_bad"
+        android:layout_width="30dp"
+        android:layout_height="0dp"
+        android:layout_marginBottom="5dp"
+        android:padding="5dp"
+        android:rotationX="180"
+        android:src="@drawable/ic_praise"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/iv_good"
+        app:tint="@color/md_blue_A200" />
+
     <TextView
         android:id="@+id/tv_origin"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginLeft="30dp"
+        android:layout_marginTop="10dp"
         android:singleLine="true"
         android:textColor="@color/primaryText"
-        app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintRight_toLeftOf="@+id/tv_author" />
+        app:layout_constraintRight_toLeftOf="@+id/tv_author"
+        app:layout_constraintTop_toTopOf="parent" />
 
     <TextView
         android:id="@+id/tv_author"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:maxWidth="160dp"
         android:singleLine="true"
         android:textColor="@color/secondaryText"
-        android:maxWidth="160dp"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintRight_toLeftOf="@+id/iv_checked" />
+        app:layout_constraintRight_toLeftOf="@+id/iv_checked"
+        app:layout_constraintTop_toTopOf="parent" />
 
     <TextView
         android:id="@+id/tv_last"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginLeft="30dp"
+        android:layout_marginBottom="10dp"
         android:singleLine="true"
         android:textColor="@color/secondaryText"
-        app:layout_constraintTop_toBottomOf="@+id/tv_origin"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintRight_toLeftOf="@+id/iv_checked" />
+        app:layout_constraintRight_toLeftOf="@+id/iv_checked"
+        app:layout_constraintTop_toBottomOf="@+id/tv_origin" />
 
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/iv_checked"
         android:layout_width="40dp"
         android:layout_height="40dp"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
         android:padding="8dp"
         android:src="@drawable/ic_check"
         android:visibility="invisible"
-        app:tint="@color/primaryText"
+        app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent" />
+        app:tint="@color/primaryText"
+        tools:visibility="visible" />
+
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 0
app/src/main/res/layout/item_replace_rule.xml

@@ -42,5 +42,6 @@
         android:background="?attr/selectableItemBackgroundBorderless"
         android:src="@drawable/ic_more_vert"
         android:tint="@color/primaryText"
+        android:contentDescription="@string/more_menu"
         tools:ignore="RtlHardcoded" />
 </LinearLayout>

+ 1 - 0
app/src/main/res/layout/item_rss_source.xml

@@ -42,6 +42,7 @@
         android:layout_width="36dp"
         android:layout_height="36dp"
         android:background="?attr/selectableItemBackgroundBorderless"
+        android:contentDescription="@string/more_menu"
         android:padding="6dp"
         android:src="@drawable/ic_more_vert"
         android:tint="@color/primaryText"

+ 1 - 0
app/src/main/res/layout/item_rule_sub.xml

@@ -65,6 +65,7 @@
         android:padding="6dp"
         android:src="@drawable/ic_more_vert"
         android:tint="@color/primaryText"
+        android:contentDescription="@string/more_menu"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"

+ 2 - 2
app/src/main/res/layout/item_theme_config.xml

@@ -34,8 +34,8 @@
         android:layout_width="36dp"
         android:layout_height="36dp"
         android:background="?attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/more_menu"
-        android:tooltipText="@string/more_menu"
+        android:contentDescription="@string/delete"
+        android:tooltipText="@string/delete"
         android:padding="6dp"
         android:src="@drawable/ic_clear_all"
         android:tint="@color/primaryText"

+ 46 - 35
app/src/main/res/layout/item_toc_regex.xml

@@ -3,45 +3,56 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="8dp"
+    android:padding="16dp"
     android:gravity="center_vertical"
-    android:orientation="horizontal">
+    android:orientation="vertical">
 
-    <io.legado.app.lib.theme.view.ThemeRadioButton
-        android:id="@+id/rb_regex_name"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:maxLines="1" />
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical">
+        <io.legado.app.lib.theme.view.ThemeRadioButton
+            android:id="@+id/rb_regex_name"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:maxLines="1" />
+
+        <io.legado.app.lib.theme.view.ThemeSwitch
+            android:id="@+id/swt_enabled"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
 
-    <io.legado.app.lib.theme.view.ThemeSwitch
-        android:id="@+id/swt_enabled"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content" />
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_edit"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_gravity="center"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/edit"
+            android:padding="6dp"
+            android:tooltipText="@string/edit"
+            android:src="@drawable/ic_edit"
+            android:tint="@color/primaryText"
+            tools:ignore="UnusedAttribute" />
 
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_edit"
-        android:layout_width="36dp"
-        android:layout_height="36dp"
-        android:layout_gravity="center"
-        android:background="?android:attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/edit"
-        android:padding="6dp"
-        android:tooltipText="@string/edit"
-        android:src="@drawable/ic_edit"
-        android:tint="@color/primaryText"
-        tools:ignore="UnusedAttribute" />
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_delete"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:background="?attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/delete"
+            android:tooltipText="@string/delete"
+            android:padding="6dp"
+            android:src="@drawable/ic_clear_all"
+            android:tint="@color/primaryText"
+            tools:ignore="UnusedAttribute" />
+    </LinearLayout>
 
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_delete"
-        android:layout_width="36dp"
-        android:layout_height="36dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/more_menu"
-        android:tooltipText="@string/more_menu"
-        android:padding="6dp"
-        android:src="@drawable/ic_clear_all"
-        android:tint="@color/primaryText"
-        tools:ignore="UnusedAttribute" />
+    <TextView
+        android:id="@+id/title_example"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="12sp" />
 
 </LinearLayout>

+ 1 - 0
app/src/main/res/layout/item_txt_toc_rule.xml

@@ -50,6 +50,7 @@
             android:padding="6dp"
             android:src="@drawable/ic_more_vert"
             android:tint="@color/primaryText"
+            android:contentDescription="@string/more_menu"
             tools:ignore="RtlHardcoded" />
 
     </LinearLayout>

Некоторые файлы не были показаны из-за большого количества измененных файлов