Merge branch 'gedoor:master' into master

pull/2237/head
Xwite 3 years ago committed by GitHub
commit 5e7b5d1f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .github/dependabot.yml
  2. 2
      .github/workflows/autoupdatefork.yml
  3. 4
      .github/workflows/cronet.yml
  4. 8
      .github/workflows/release.yml
  5. 2
      .github/workflows/stale.yml
  6. 18
      .github/workflows/test.yml
  7. 34
      English.md
  8. 1
      README.md
  9. 8
      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. 2
      app/src/main/assets/cronet.json
  15. 2
      app/src/main/assets/defaultData/directLinkUpload.json
  16. 3
      app/src/main/assets/help/ruleHelp.md
  17. 33
      app/src/main/assets/updateLog.md
  18. 1
      app/src/main/assets/web/bookSource/css/app.1bd41080.css
  19. 1
      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. 150
      app/src/main/assets/web/bookSource/index.css
  23. 440
      app/src/main/assets/web/bookSource/index.html
  24. 516
      app/src/main/assets/web/bookSource/index.js
  25. 2
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js
  26. 1
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map
  27. 2
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js
  28. 1
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map
  29. 14
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js
  30. 1
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map
  31. 2
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  32. 2
      app/src/main/java/io/legado/app/api/controller/BookSourceController.kt
  33. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  34. 12
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  35. 8
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  36. 2
      app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt
  37. 4
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  38. 45
      app/src/main/java/io/legado/app/help/config/SourceConfig.kt
  39. 32
      app/src/main/java/io/legado/app/model/AudioPlay.kt
  40. 5
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  41. 7
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  42. 16
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  43. 1
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  44. 3
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  45. 17
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  46. 5
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  47. 2
      app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt
  48. 2
      app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt
  49. 52
      app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt
  50. 2
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt
  51. 54
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt
  52. 8
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt
  53. 31
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
  54. 54
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt
  55. 8
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt
  56. 2
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt
  57. 7
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  58. 21
      app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt
  59. 1
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
  60. 2
      app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt
  61. 74
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  62. 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  63. 156
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  64. 6
      app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt
  65. 25
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  66. 3
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  67. 2
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  68. 39
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  69. 30
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
  70. 33
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  71. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  72. 2
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  73. 2
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  74. 8
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  75. 11
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  76. 3
      app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt
  77. 9
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  78. 19
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  79. 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  80. 2
      app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt
  81. 6
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  82. 4
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  83. 6
      app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt
  84. 31
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  85. 20
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt
  86. 7
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  87. 15
      app/src/main/java/io/legado/app/utils/AnimationExtensions.kt
  88. 17
      app/src/main/java/io/legado/app/utils/AnimationUtilsSupport.kt
  89. 3
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  90. 7
      app/src/main/res/drawable/ic_praise.xml
  91. 117
      app/src/main/res/layout/dialog_read_book_style.xml
  92. 10
      app/src/main/res/layout/dialog_verification_code_view.xml
  93. 2
      app/src/main/res/layout/item_book_source.xml
  94. 57
      app/src/main/res/layout/item_change_source.xml
  95. 1
      app/src/main/res/layout/item_replace_rule.xml
  96. 1
      app/src/main/res/layout/item_rss_source.xml
  97. 1
      app/src/main/res/layout/item_rule_sub.xml
  98. 4
      app/src/main/res/layout/item_theme_config.xml
  99. 81
      app/src/main/res/layout/item_toc_regex.xml
  100. 1
      app/src/main/res/layout/item_txt_toc_rule.xml
  101. Some files were not shown because too many files have changed in this diff Show More

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

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

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.actor == 'gedoor' }} if: ${{ github.actor == 'gedoor' }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 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 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 - name: Create Pull Request
uses: peter-evans/create-pull-request@v3 uses: peter-evans/create-pull-request@v4
continue-on-error: true continue-on-error: true
with: with:
token: ${{ secrets.ACTIONS_TOKEN }} token: ${{ secrets.ACTIONS_TOKEN }}

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

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

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

@ -1,9 +1,6 @@
# [English](English.md) [中文](README.md) # [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) [![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"> <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"/> <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"> <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> </a>
# Download [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-) # Download [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-)
#### Android #### Android
* [Releases](https://github.com/gedoor/legado/releases/latest) * [Releases](https://github.com/gedoor/legado/releases/latest)
* [Google play - $1.99](https://play.google.com/store/apps/details?id=io.legado.play.release) * [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) * [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) * [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release) * [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
#### IOS #### IOS
* Stopped(No release) - [Github](https://github.com/gedoor/YueDuFlutter) * Stopped(No release) - [Github](https://github.com/gedoor/YueDuFlutter)
<a href="#readme"> <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> </a>
# Community [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-) # Community [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-)
#### Telegram #### 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
[![Discord](https://img.shields.io/discord/560731361414086666?color=%235865f2&label=Discord)](https://discord.gg/VtUfRyzRXn) [![Discord](https://img.shields.io/discord/560731361414086666?color=%235865f2&label=Discord)](https://discord.gg/VtUfRyzRXn)
#### Other #### Other
https://www.yuque.com/legado/wiki/community https://www.yuque.com/legado/wiki/community
<a href="#readme"> <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> </a>
# API [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-) # 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). * 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} * 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: 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 * path type explanation: Book source, subscription source, replacement rules, local txt novel directory rules, online reading engine, theme, reading layout
<a href="#readme"> <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> </a>
# Other [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-) # Other [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-)
##### Disclaimers ##### Disclaimers
https://gedoor.github.io/about.html https://gedoor.github.io/about.html
##### Legado 3.0 ##### Legado 3.0
* [eBook sources rules](https://alanskycn.gitee.io/teachme) * [eBook sources rules](https://alanskycn.gitee.io/teachme)
* [Update Log](/app/src/main/assets/updateLog.md) * [Update Log](/app/src/main/assets/updateLog.md)
* [Help Documentation](/app/src/main/assets/help/appHelp.md) * [Help Documentation](/app/src/main/assets/help/appHelp.md)
* [web](https://github.com/gedoor/legado_web_bookshelf) * [web](https://github.com/gedoor/legado_web_bookshelf)
<a href="#readme"> <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> </a>
# Grateful [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-) # Grateful [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-)
> * org.jsoup:jsoup > * org.jsoup:jsoup
> * cn.wanghaomiao:JsoupXpath > * cn.wanghaomiao:JsoupXpath
> * com.jayway.jsonpath:json-path > * com.jayway.jsonpath:json-path
@ -123,14 +131,16 @@ https://gedoor.github.io/about.html
> * io.noties.markwon:image-glide > * io.noties.markwon:image-glide
> * com.hankcs:hanlp > * com.hankcs:hanlp
> * com.positiondev.epublib:epublib-core > * com.positiondev.epublib:epublib-core
<a href="#readme"> <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> </a>
# Interface [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-) # 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%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"> <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"> <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> </a>

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

@ -164,7 +164,7 @@ dependencies {
implementation('androidx.constraintlayout:constraintlayout:2.1.4') implementation('androidx.constraintlayout:constraintlayout:2.1.4')
implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0') implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')
implementation('androidx.viewpager2:viewpager2:1.0.0') implementation('androidx.viewpager2:viewpager2:1.0.0')
implementation('androidx.webkit:webkit:1.4.0') implementation('androidx.webkit:webkit:1.5.0')
//google //google
implementation('com.google.android.material:material:1.6.1') implementation('com.google.android.material:material:1.6.1')
@ -189,8 +189,6 @@ dependencies {
//media //media
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayer_version") implementation("com.google.android.exoplayer:exoplayer-core:$exoplayer_version")
implementation("com.google.android.exoplayer:extension-okhttp:$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('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('com.jayway.jsonpath:json-path:2.7.0')
implementation('cn.wanghaomiao:JsoupXpath:2.5.1') implementation('cn.wanghaomiao:JsoupXpath:2.5.1')
implementation(project(path: ':epublib')) implementation(project(path: ':epublib'))
@ -260,7 +258,7 @@ dependencies {
//implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0' //implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
//implementation 'com.google.firebase:firebase-perf-ktx:20.0.6' //implementation 'com.google.firebase:firebase-perf-ktx:20.0.6'
// LeakCanary //LeakCanary,
//debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7') //debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7')
//com.github.AmrDeveloper:CodeView代码编辑已集成到应用内 //com.github.AmrDeveloper:CodeView代码编辑已集成到应用内

Binary file not shown.

@ -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,5 +1,5 @@
{ {
"uploadUrl": "http://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", "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天" "summary": "有效期2天"
} }

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

@ -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** **2022/08/02**
* 更新cronet: 104.0.5112.69 * 更新cronet: 104.0.5112.69

@ -0,0 +1 @@
@font-face{font-family:Cascadia;src:url(../fonts/Cascadia-Code-Regular-2.97c09831.ttf)}.editor{display:flex;align-items:stretch;height:100vh}.left{width:calc(50% - 55px);min-width:500px;display:inline-block}.main{width:100%;height:calc(100vh - 64px);overflow-y:auto}textarea{min-height:20px;flex:1;outline:none;border-radius:4px;border:1px solid #ccc;font-size:14px;font-family:Cascadia,Microsoft YaHei UI,Arial,serif;line-height:20px;padding:10px 10px 10px}.fixed[data-v-ab8a8a34]{position:fixed;top:100px;left:50%;transform:translateX(-50%)}.half-circle-spinner [data-v-ab8a8a34],.half-circle-spinner[data-v-ab8a8a34]{box-sizing:border-box}.half-circle-spinner[data-v-ab8a8a34]{width:60px;height:60px;border-radius:100%;position:relative}.half-circle-spinner .circle[data-v-ab8a8a34]{content:"";position:absolute;width:100%;height:100%;border-radius:100%;border:6px solid transparent}.half-circle-spinner .circle.circle-1[data-v-ab8a8a34]{border-top-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite;animation:half-circle-spinner-animation-ab8a8a34 1s infinite}.half-circle-spinner .circle.circle-2[data-v-ab8a8a34]{border-bottom-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate;animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate}@-webkit-keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.warm[data-v-27eae50d]{position:fixed;top:100px;left:50%;transform:translateX(-50%);color:#eee;background-color:#ff1d5e;padding:20px;border-radius:8px;font-size:18px}button[data-v-27eae50d]{float:right;width:70px;height:35px;outline:none;border:unset;border-radius:8px;font-size:14px;transition:background-color .3s ease}.success[data-v-bef92f74],button[data-v-27eae50d]:hover{color:#333;background-color:#7fffd4}.success[data-v-bef92f74]{z-index:3;position:fixed;top:100px;left:50%;transform:translateX(-50%);padding:10px;border-radius:8px}p[data-v-bef92f74]{margin:0}.menu{display:flex;flex-direction:column;justify-content:center}button[data-v-8302f478]{width:100px;height:50px;margin:5px;outline:none;padding:10px;font-size:16px;border-radius:4px;box-sizing:border-box;border:1px solid #333;background-color:transparent;transition:all .3s ease;overflow:hidden}button span[data-v-8302f478]{display:inline-block;transform:translateY(20px);opacity:0;font-size:12px;transition:all .3s ease}button[data-v-8302f478]:hover{color:#fff;background-color:gray}button:hover span[data-v-8302f478]{transform:translateY(0);opacity:1}textarea[data-v-4301c68c]{font-size:14px}input[data-v-f11ea0d0]{outline:none;height:25px;font-size:16px}textarea[data-v-f11ea0d0]{font-size:14px}.search[data-v-260cb454]{border:1px solid #ddd;border-radius:4px}input[data-v-260cb454]{text-align:center;font-size:16px;height:25px;outline:none}.tool[data-v-260cb454]{display:flex;justify-content:space-around;padding:4px 0}.tool button[data-v-260cb454]{border-radius:4px;padding:5px;outline:none;border:none;flex:1;margin:0 2px}.tool button[data-v-260cb454]:hover{background-color:#ddd}input[type=checkbox][data-v-260cb454]{transform:scale(2);margin:0 15px 0}.book_item[data-v-260cb454]{overflow:hidden;display:flex;align-items:center;margin-top:10px;padding:10px;cursor:pointer;background-color:#eee}.book_active[data-v-260cb454]{background-color:#32cd32}.book_index[data-v-260cb454]{width:auto;height:30px;text-align:center;line-height:30px;border-radius:8px;padding:0 10px;background-color:#ffb6c1}.book_info[data-v-260cb454]{display:flex;justify-content:space-between}.book_list[data-v-260cb454]{height:calc(100vh - 125px);overflow-y:auto}a[data-v-c4c82b2c]{display:block;height:25px;transition:margin-left .3s ease}a[data-v-c4c82b2c]:hover{color:red;margin-left:10px}code[data-v-c4c82b2c]{display:inline-block;font-size:16px;font-family:Cascadia Mono,JetBrains Mono,Microsoft YaHei UI,serif;background-color:#f3f4f4;border-radius:4px}.author[data-v-c4c82b2c]{position:fixed;right:20px;bottom:20px}.author a[data-v-c4c82b2c]{padding:5px 10px;border-radius:4px}.author a[data-v-c4c82b2c]:hover{box-shadow:0 0 3px 3px #ccc;background-color:#ddd}.out{min-width:620px}.out,.out .box{flex:1;display:flex;flex-flow:column}a{text-decoration:none;color:#333}body,ul{margin:0;padding:0}ul{width:100%;height:40px;display:flex}ul li{display:block;text-align:center;line-height:40px;width:16%;list-style:none;cursor:pointer}ul li:hover{color:red}.active{color:#fff!important;background-color:hsla(0,0%,50%,.459);border-bottom:unset!important}ul li[data-v-de4729c0]{border-bottom:2px solid #333}a[data-v-de4729c0]{font-size:18px;color:blue}span[data-v-3974490a]{display:flex;justify-content:space-around;width:70px;white-space:nowrap}span i[data-v-3974490a]{font-style:normal}.edit_input[data-v-3974490a]{display:flex;justify-content:space-between;align-items:center;margin:10px 5px}

@ -0,0 +1 @@
@font-face{font-family:Cascadia;src:url(../fonts/Cascadia-Code-Regular-2.97c09831.ttf)}.editor{display:flex;align-items:stretch;height:100vh}.left{width:calc(50% - 55px);min-width:500px;display:inline-block}.main{width:100%;height:calc(100vh - 64px);overflow-y:auto}textarea{min-height:20px;flex:1;outline:none;border-radius:4px;border:1px solid #ccc;font-size:14px;font-family:Cascadia,Microsoft YaHei UI,Arial,serif;line-height:20px;padding:10px 10px 10px}.fixed[data-v-ab8a8a34]{position:fixed;top:100px;left:50%;transform:translateX(-50%)}.half-circle-spinner [data-v-ab8a8a34],.half-circle-spinner[data-v-ab8a8a34]{box-sizing:border-box}.half-circle-spinner[data-v-ab8a8a34]{width:60px;height:60px;border-radius:100%;position:relative}.half-circle-spinner .circle[data-v-ab8a8a34]{content:"";position:absolute;width:100%;height:100%;border-radius:100%;border:6px solid transparent}.half-circle-spinner .circle.circle-1[data-v-ab8a8a34]{border-top-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite;animation:half-circle-spinner-animation-ab8a8a34 1s infinite}.half-circle-spinner .circle.circle-2[data-v-ab8a8a34]{border-bottom-color:#ff1d5e;-webkit-animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate;animation:half-circle-spinner-animation-ab8a8a34 1s infinite alternate}@-webkit-keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes half-circle-spinner-animation-ab8a8a34{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.warm[data-v-27eae50d]{position:fixed;top:100px;left:50%;transform:translateX(-50%);color:#eee;background-color:#ff1d5e;padding:20px;border-radius:8px;font-size:18px}button[data-v-27eae50d]{float:right;width:70px;height:35px;outline:none;border:unset;border-radius:8px;font-size:14px;transition:background-color .3s ease}.success[data-v-bef92f74],button[data-v-27eae50d]:hover{color:#333;background-color:#7fffd4}.success[data-v-bef92f74]{z-index:3;position:fixed;top:100px;left:50%;transform:translateX(-50%);padding:10px;border-radius:8px}p[data-v-bef92f74]{margin:0}.menu{display:flex;flex-direction:column;justify-content:center}button[data-v-7d09e592]{width:100px;height:50px;margin:5px;outline:none;padding:10px;font-size:16px;border-radius:4px;box-sizing:border-box;border:1px solid #333;background-color:transparent;transition:all .3s ease;overflow:hidden}button span[data-v-7d09e592]{display:inline-block;transform:translateY(20px);opacity:0;font-size:12px;transition:all .3s ease}button[data-v-7d09e592]:hover{color:#fff;background-color:gray}button:hover span[data-v-7d09e592]{transform:translateY(0);opacity:1}textarea[data-v-4301c68c]{font-size:14px}input[data-v-f11ea0d0]{outline:none;height:25px;font-size:16px}textarea[data-v-f11ea0d0]{font-size:14px}.search[data-v-260cb454]{border:1px solid #ddd;border-radius:4px}input[data-v-260cb454]{text-align:center;font-size:16px;height:25px;outline:none}.tool[data-v-260cb454]{display:flex;justify-content:space-around;padding:4px 0}.tool button[data-v-260cb454]{border-radius:4px;padding:5px;outline:none;border:none;flex:1;margin:0 2px}.tool button[data-v-260cb454]:hover{background-color:#ddd}input[type=checkbox][data-v-260cb454]{transform:scale(2);margin:0 15px 0}.book_item[data-v-260cb454]{overflow:hidden;display:flex;align-items:center;margin-top:10px;padding:10px;cursor:pointer;background-color:#eee}.book_active[data-v-260cb454]{background-color:#32cd32}.book_index[data-v-260cb454]{width:auto;height:30px;text-align:center;line-height:30px;border-radius:8px;padding:0 10px;background-color:#ffb6c1}.book_info[data-v-260cb454]{display:flex;justify-content:space-between}.book_list[data-v-260cb454]{height:calc(100vh - 125px);overflow-y:auto}a[data-v-c4c82b2c]{display:block;height:25px;transition:margin-left .3s ease}a[data-v-c4c82b2c]:hover{color:red;margin-left:10px}code[data-v-c4c82b2c]{display:inline-block;font-size:16px;font-family:Cascadia Mono,JetBrains Mono,Microsoft YaHei UI,serif;background-color:#f3f4f4;border-radius:4px}.author[data-v-c4c82b2c]{position:fixed;right:20px;bottom:20px}.author a[data-v-c4c82b2c]{padding:5px 10px;border-radius:4px}.author a[data-v-c4c82b2c]:hover{box-shadow:0 0 3px 3px #ccc;background-color:#ddd}.out{min-width:620px}.out,.out .box{flex:1;display:flex;flex-flow:column}a{text-decoration:none;color:#333}body,ul{margin:0;padding:0}ul{width:100%;height:40px;display:flex}ul li{display:block;text-align:center;line-height:40px;width:16%;list-style:none;cursor:pointer}ul li:hover{color:red}.active{color:#fff!important;background-color:hsla(0,0%,50%,.459);border-bottom:unset!important}ul li[data-v-de4729c0]{border-bottom:2px solid #333}a[data-v-de4729c0]{font-size:18px;color:blue}span[data-v-3974490a]{display:flex;justify-content:space-around;width:70px;white-space:nowrap}span i[data-v-3974490a]{font-style:normal}.edit_input[data-v-3974490a]{display:flex;justify-content:space-between;align-items:center;margin:10px 5px}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -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 +1 @@
<!DOCTYPE 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>
<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>

@ -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:
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

@ -44,6 +44,15 @@ interface RssSourceDao {
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun flowEnabled(): Flow<List<RssSource>> 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( @Query(
"""SELECT * FROM rssSources """SELECT * FROM rssSources
where enabled = 1 where enabled = 1
@ -67,6 +76,9 @@ interface RssSourceDao {
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
fun flowGroup(): Flow<List<String>> 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) <> ''") @get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
val allGroup: List<String> val allGroup: List<String>

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

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

@ -310,7 +310,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog) 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? val doublePageHorizontal: String?
get() = appCtx.getPrefString(PreferKey.doublePageHorizontal) get() = appCtx.getPrefString(PreferKey.doublePageHorizontal)

@ -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)
}
}
}
}
}

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

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

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

@ -6,6 +6,7 @@ import androidx.documentfile.provider.DocumentFile
import com.script.SimpleBindings import com.script.SimpleBindings
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BaseSource import io.legado.app.data.entities.BaseSource
import io.legado.app.data.entities.Book 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.help.config.AppConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.* import io.legado.app.utils.*
import org.jsoup.nodes.Entities
import splitties.init.appCtx import splitties.init.appCtx
import java.io.* import java.io.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -77,11 +79,12 @@ object LocalBook {
throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty)) throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
} }
val lh = LinkedHashSet(chapters) val lh = LinkedHashSet(chapters)
lh.forEachIndexed { index, bookChapter -> bookChapter.index = index }
return ArrayList(lh) return ArrayList(lh)
} }
fun getContent(book: Book, chapter: BookChapter): String? { fun getContent(book: Book, chapter: BookChapter): String? {
return try { val content = try {
when { when {
book.isEpub() -> { book.isEpub() -> {
EpubFile.getContent(book, chapter) EpubFile.getContent(book, chapter)
@ -95,8 +98,15 @@ object LocalBook {
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printOnDebug() 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 }
} }
/** /**

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

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

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

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

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

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

@ -2,12 +2,12 @@ package io.legado.app.ui.association
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.databinding.DialogVerificationCodeViewBinding 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.help.glide.OkHttpModelLoader
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.PhotoDialog 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 import io.legado.app.utils.viewbindingdelegate.viewBinding
/** /**
@ -26,12 +28,18 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
* val key = "${sourceOrigin ?: ""}_verificationResult" * val key = "${sourceOrigin ?: ""}_verificationResult"
* CacheManager.get(key) * 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 { arguments = Bundle().apply {
putString("sourceOrigin", sourceOrigin)
putString("imageUrl", imageUrl) putString("imageUrl", imageUrl)
putString("sourceOrigin", sourceOrigin)
putString("sourceName", sourceName)
} }
} }
@ -47,23 +55,23 @@ class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification
initMenu() initMenu()
binding.run { binding.run {
toolBar.setBackgroundColor(primaryColor) toolBar.setBackgroundColor(primaryColor)
val sourceOrigin = arguments?.getString("sourceOrigin") arguments?.let { arguments ->
arguments?.getString("imageUrl")?.let { imageUrl -> toolBar.subtitle = arguments.getString("sourceName")
ImageLoader.load(requireContext(), imageUrl).apply { val sourceOrigin = arguments.getString("sourceOrigin")
sourceOrigin?.let { arguments.getString("imageUrl")?.let { imageUrl ->
apply( ImageLoader.load(requireContext(), imageUrl).apply {
RequestOptions().set( sourceOrigin?.let {
OkHttpModelLoader.sourceOriginOption, apply(
it 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) CacheManager.putMemory(key, it)
dismiss() dismiss()
} }
} }
} }
return false return false
} }

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

@ -5,14 +5,19 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.DiffRecyclerAdapter
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemChangeSourceBinding 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.invisible
import io.legado.app.utils.visible import io.legado.app.utils.visible
import splitties.init.appCtx
import splitties.views.onLongClick 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) { 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 { holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
if (it.bookUrl != callBack.bookUrl) { if (it.bookUrl != callBack.bookUrl) {
@ -120,5 +172,7 @@ class ChangeBookSourceAdapter(
fun editSource(searchBook: SearchBook) fun editSource(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook) fun disableSource(searchBook: SearchBook)
fun deleteSource(searchBook: SearchBook) fun deleteSource(searchBook: SearchBook)
fun setBookScore(searchBook: SearchBook, score: Int)
fun getBookScore(searchBook: SearchBook): Int
} }
} }

@ -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) { private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) {
waitDialog.setText(R.string.load_toc) waitDialog.setText(R.string.load_toc)
waitDialog.show() waitDialog.show()

@ -16,6 +16,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.config.AppConfig 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.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
@ -82,7 +83,23 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
searchCallback = null searchCallback = null
} }
}.map { }.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) }.flowOn(IO)
@Volatile @Volatile
@ -351,6 +368,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
appDb.bookSourceDao.delete(source) appDb.bookSourceDao.delete(source)
appDb.searchBookDao.delete(searchBook) appDb.searchBookDao.delete(searchBook)
SourceConfig.removeSource(source.bookSourceUrl)
} }
} }
searchBooks.remove(searchBook) 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 { interface SourceCallback {
fun searchSuccess(searchBook: SearchBook) fun searchSuccess(searchBook: SearchBook)

@ -5,14 +5,19 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.DiffRecyclerAdapter
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemChangeSourceBinding 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.invisible
import io.legado.app.utils.visible import io.legado.app.utils.visible
import splitties.init.appCtx
import splitties.views.onLongClick 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) { 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 { holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
callBack.openToc(it) callBack.openToc(it)
@ -118,5 +170,7 @@ class ChangeChapterSourceAdapter(
fun editSource(searchBook: SearchBook) fun editSource(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook) fun disableSource(searchBook: SearchBook)
fun deleteSource(searchBook: SearchBook) fun deleteSource(searchBook: SearchBook)
fun setBookScore(searchBook: SearchBook, score: Int)
fun getBookScore(searchBook: SearchBook): Int
} }
} }

@ -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?) { override fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) {
searchBook?.let { searchBook?.let {
binding.loadingToc.show() binding.loadingToc.show()

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

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

@ -14,6 +14,8 @@ import io.legado.app.databinding.ItemArrangeBookBinding
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import java.util.*
import kotlin.collections.HashSet
class BookAdapter(context: Context, val callBack: CallBack) : class BookAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<Book, ItemArrangeBookBinding>(context), RecyclerAdapter<Book, ItemArrangeBookBinding>(context),
@ -121,6 +123,25 @@ class BookAdapter(context: Context, val callBack: CallBack) :
callBack.upSelectCount() 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> { private fun getGroupList(groupId: Long): List<String> {
val groupNames = arrayListOf<String>() val groupNames = arrayListOf<String>()
callBack.groupList.forEach { callBack.groupList.forEach {

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

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

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

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

@ -43,10 +43,18 @@ class ReadMenu @JvmOverloads constructor(
var cnaShowMenu: Boolean = false var cnaShowMenu: Boolean = false
private val callBack: CallBack get() = activity as CallBack private val callBack: CallBack get() = activity as CallBack
private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true) private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true)
private lateinit var menuTopIn: Animation private val menuTopIn: Animation by lazy {
private lateinit var menuTopOut: Animation loadAnimation(context, R.anim.anim_readbook_top_in)
private lateinit var menuBottomIn: Animation }
private lateinit var menuBottomOut: Animation 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 bgColor: Int = context.bottomBackground
private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
private val bottomBackgroundList: ColorStateList = Selector.colorBuild() 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 { init {
initView() initView()
@ -141,19 +201,29 @@ class ReadMenu @JvmOverloads constructor(
activity?.window?.attributes = params activity?.window?.attributes = params
} }
fun runMenuIn() { fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
this.visible() this.visible()
binding.titleBar.visible() binding.titleBar.visible()
binding.bottomMenu.visible() binding.bottomMenu.visible()
binding.titleBar.startAnimation(menuTopIn) if (anim) {
binding.bottomMenu.startAnimation(menuBottomIn) 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 this.onMenuOutEnd = onMenuOutEnd
if (this.isVisible) { if (this.isVisible) {
binding.titleBar.startAnimation(menuTopOut) if (anim) {
binding.bottomMenu.startAnimation(menuBottomOut) 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 { llReadAloud.onLongClick {
runMenuOut { callBack.showReadAloudDialog() } runMenuOut {
callBack.showReadAloudDialog()
}
} }
//界面 //界面
llFont.setOnClickListener { llFont.setOnClickListener {
@ -306,66 +378,8 @@ class ReadMenu @JvmOverloads constructor(
} }
private fun initAnimation() { private fun initAnimation() {
//显示菜单 menuTopIn.setAnimationListener(menuInListener)
menuTopIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in) menuTopOut.setAnimationListener(menuOutListener)
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
})
} }
fun upBookView() { fun upBookView() {

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

@ -11,6 +11,7 @@ import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.prefs.fragment.PreferenceFragment import io.legado.app.lib.prefs.fragment.PreferenceFragment
import io.legado.app.lib.theme.bottomBackground 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.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.provider.ChapterProvider 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.dpToPx
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -70,9 +72,12 @@ class MoreConfigDialog : DialogFragment() {
class ReadPreferenceFragment : PreferenceFragment(), class ReadPreferenceFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_read) addPreferencesFromResource(R.xml.pref_config_read)
upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -137,9 +142,29 @@ class MoreConfigDialog : DialogFragment() {
"clickRegionalConfig" -> { "clickRegionalConfig" -> {
(activity as? ReadBookActivity)?.showClickRegionalConfig() (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) 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)
}
}
} }
} }

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

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

@ -82,13 +82,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
private val initialTextPos = TextPos(0, 0, 0) private val initialTextPos = TextPos(0, 0, 0)
val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop } val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
val pageSlopSquare by lazy { private var pageSlopSquare: Int = slopSquare
if (AppConfig.pageTouchSlop == 0) {
slopSquare
} else {
AppConfig.pageTouchSlop
}
}
private val tlRect = RectF() private val tlRect = RectF()
private val tcRect = RectF() private val tcRect = RectF()
private val trRect = RectF() private val trRect = RectF()
@ -112,6 +106,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
upBg() upBg()
setWillNotDraw(false) setWillNotDraw(false)
upPageAnim() upPageAnim()
upPageSlopSquare()
} }
} }
@ -253,6 +248,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
return true return true
} }
/**
* 更新状态栏
*/
fun upStatusBar() { fun upStatusBar() {
curPage.upStatusBar() curPage.upStatusBar()
prevPage.upStatusBar() prevPage.upStatusBar()
@ -402,6 +400,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
} }
} }
/**
* 点击
*/
private fun click(action: Int) { private fun click(action: Int) {
when (action) { when (action) {
0 -> callBack.showActionMenu() 0 -> callBack.showActionMenu()
@ -441,6 +442,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
} }
} }
/**
* 销毁事件
*/
fun onDestroy() { fun onDestroy() {
pageDelegate?.onDestroy() pageDelegate?.onDestroy()
curPage.cancelSelect() curPage.cancelSelect()
@ -511,6 +515,14 @@ class ReadView(context: Context, attrs: AttributeSet) :
callBack.screenOffTimerStart() 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() { fun aloudStartSelect() {
val selectStartPos = curPage.selectStartPos val selectStartPos = curPage.selectStartPos
if (selectStartPos.relativePagePos > 0) { var pagePos = selectStartPos.relativePagePos
ReadBook.moveToNextPage() 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)
} }
/** /**

@ -2,6 +2,7 @@ package io.legado.app.ui.book.read.page.entities
import kotlin.math.min import kotlin.math.min
@Suppress("unused")
data class TextChapter( data class TextChapter(
val position: Int, val position: Int,
val title: String, val title: String,
@ -61,6 +62,17 @@ data class TextChapter(
return getReadLength(pageIndex + 1) return getReadLength(pageIndex + 1)
} }
/**
* 获取内容
*/
fun getContent(): String {
val stringBuilder = StringBuilder()
pages.forEach {
stringBuilder.append(it.text)
}
return stringBuilder.toString()
}
/** /**
* @return 获取未读文字 * @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() val stringBuilder = StringBuilder()
pages.forEach { if (pages.isNotEmpty()) {
stringBuilder.append(it.text) 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()
} }
/** /**

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

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

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

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

@ -5,6 +5,7 @@ import android.text.TextUtils
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.SourceConfig
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -38,7 +39,12 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
fun del(vararg sources: BookSource) { 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) { fun update(vararg bookSource: BookSource) {

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

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

@ -3,6 +3,7 @@ package io.legado.app.ui.config
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.InputType import android.text.InputType
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -116,6 +117,14 @@ class BackupConfigFragment : PreferenceFragment(),
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT 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.webDavUrl, getPrefString(PreferKey.webDavUrl))
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount)) upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))

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

@ -15,6 +15,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.databinding.ItemFilletTextBinding import io.legado.app.databinding.ItemFilletTextBinding
import io.legado.app.databinding.ItemFindBookBinding import io.legado.app.databinding.ItemFindBookBinding
import io.legado.app.help.config.SourceConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.login.SourceLoginActivity 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) { R.id.menu_del -> Coroutine.async(callBack.scope) {
appDb.bookSourceDao.delete(source) appDb.bookSourceDao.delete(source)
SourceConfig.removeSource(source.bookSourceUrl)
} }
} }
true true

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

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

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

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

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

@ -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.DragSelectTouchHelper
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils
import java.util.*
class RssSourceAdapter(context: Context, val callBack: CallBack) : class RssSourceAdapter(context: Context, val callBack: CallBack) :
@ -151,6 +152,25 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) :
callBack.upCountView() 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) { private fun showMenu(view: View, position: Int) {
val source = getItem(position) ?: return val source = getItem(position) ?: return
val popupMenu = PopupMenu(context, view) val popupMenu = PopupMenu(context, view)

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

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

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

@ -6,6 +6,7 @@ import io.legado.app.constant.AppPattern.dataUriRegex
import android.icu.text.Collator import android.icu.text.Collator
import android.icu.util.ULocale import android.icu.util.ULocale
import android.net.Uri import android.net.Uri
import android.text.Editable
import java.io.File import java.io.File
import java.util.* 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?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri { fun String.parseToUri(): Uri {
return if (isContentScheme()) { return if (isContentScheme()) {
Uri.parse(this) Uri.parse(this)

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save