Merge branch 'gedoor:master' into master

pull/2237/head
Xwite 2 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:
interval: "daily"
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' }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install git

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

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

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

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

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

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

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

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\"}",
"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天"
}

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

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

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

@ -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("未找到书源")
try {
content = runBlocking {
WebBook.getContentAwait(this, bookSource, book, chapter).let {
WebBook.getContentAwait(bookSource, book, chapter).let {
val contentProcessor = ContentProcessor.get(book.name, book.origin)
contentProcessor.getContent(book, chapter, it, includeTitle = false)
.joinToString("\n")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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.util.ULocale
import android.net.Uri
import android.text.Editable
import java.io.File
import java.util.*
@ -13,6 +14,8 @@ fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri {
return if (isContentScheme()) {
Uri.parse(this)

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save