Merge pull request #1 from gedoor/master

更新新代码
pull/885/head
guibiaoguo 5 years ago committed by GitHub
commit a45517349a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      .github/workflows/legado.yml
  2. 17
      README.md
  3. 3
      app/build.gradle
  4. 2
      app/src/main/AndroidManifest.xml
  5. 148
      app/src/main/assets/defaultData/httpTTS.json
  6. 175
      app/src/main/assets/help/regexHelp.md
  7. 19
      app/src/main/assets/updateLog.md
  8. 1
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  9. 11
      app/src/main/java/io/legado/app/api/ReturnData.kt
  10. 4
      app/src/main/java/io/legado/app/api/controller/BookshelfController.kt
  11. 9
      app/src/main/java/io/legado/app/api/controller/SourceController.kt
  12. 5
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  13. 26
      app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt
  14. 122
      app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt
  15. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  16. 8
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  17. 2
      app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt
  18. 3
      app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
  19. 3
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  20. 93
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  21. 3
      app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt
  22. 2
      app/src/main/java/io/legado/app/help/AppConfig.kt
  23. 7
      app/src/main/java/io/legado/app/help/DefaultData.kt
  24. 4
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  25. 8
      app/src/main/java/io/legado/app/help/IntentHelp.kt
  26. 26
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  27. 6
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  28. 6
      app/src/main/java/io/legado/app/help/ThemeConfig.kt
  29. 2
      app/src/main/java/io/legado/app/help/http/parser/TextParser.kt
  30. 50
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  31. 10
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  32. 40
      app/src/main/java/io/legado/app/model/Debug.kt
  33. 22
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  34. 1
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  35. 65
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  36. 4
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  37. 13
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  38. 39
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  39. 38
      app/src/main/java/io/legado/app/model/webBook/PreciseSearch.kt
  40. 2
      app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt
  41. 155
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  42. 10
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  43. 8
      app/src/main/java/io/legado/app/service/CacheBookService.kt
  44. 4
      app/src/main/java/io/legado/app/service/help/CacheBook.kt
  45. 2
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  46. 22
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  47. 4
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  48. 10
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  49. 4
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  50. 2
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  51. 2
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  52. 6
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  53. 2
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  54. 4
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  55. 16
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  56. 15
      app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt
  57. 30
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  58. 14
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  59. 2
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt
  60. 4
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt
  61. 4
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  62. 7
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  63. 7
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  64. 11
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt
  65. 10
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  66. 46
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  67. 24
      app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt
  68. 20
      app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt
  69. 4
      app/src/main/java/io/legado/app/ui/filepicker/entity/JavaBean.kt
  70. 20
      app/src/main/java/io/legado/app/ui/filepicker/utils/ConvertUtils.kt
  71. 16
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  72. 71
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  73. 67
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  74. 21
      app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt
  75. 42
      app/src/main/java/io/legado/app/ui/replace/DiffCallBack.kt
  76. 42
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  77. 38
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt
  78. 29
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  79. 32
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt
  80. 4
      app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt
  81. 8
      app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt
  82. 7
      app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt
  83. 9
      app/src/main/java/io/legado/app/utils/FragmentExtensions.kt
  84. 25
      app/src/main/java/io/legado/app/utils/StringUtils.kt
  85. 6
      app/src/main/java/io/legado/app/utils/SystemUtils.kt
  86. 19
      app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt
  87. 18
      app/src/main/java/io/legado/app/utils/UIUtils.kt
  88. 8
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  89. 40
      app/src/main/java/io/legado/app/utils/ZipUtils.kt
  90. 2
      app/src/main/java/io/legado/app/web/HttpServer.kt
  91. 7
      app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt
  92. 1
      app/src/main/java/io/legado/app/web/WebSocketServer.kt
  93. 3
      app/src/main/res/layout/activity_book_source.xml
  94. 3
      app/src/main/res/layout/activity_rss_source.xml
  95. 46
      app/src/main/res/layout/dialog_bookmark.xml
  96. 13
      app/src/main/res/layout/dialog_recycler_view.xml
  97. 5
      app/src/main/res/layout/item_http_tts.xml
  98. 5
      app/src/main/res/menu/book_read.xml
  99. 19
      app/src/main/res/menu/main_bookshelf.xml
  100. 2
      app/src/main/res/mipmap-anydpi-v26/launcher3.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -28,18 +28,23 @@ jobs:
# 蓝奏云里的文件夹名(需手动在蓝奏云创建)
LANZOU_FOLDER: '阅读3测试版'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'false'
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
- name: Set Up JDK
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: clear 18PlusList.txt
java-version: 14
- name: Clear 18PlusList.txt
run: |
echo "清空18PlusList.txt"
echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt
- name: release apk sign
- name: App Minify
if: ${{ github.event_name == 'release'}}
run: |
sed '/minifyEnabled/i\ shrinkResources true' $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/minifyEnabled false/minifyEnabled true/' $GITHUB_WORKSPACE/app/build.gradle -i
- name: Release Apk Sign
run: |
echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks
@ -47,12 +52,12 @@ jobs:
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: apk live together
- name: Apk Live Together
run: |
echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i
- name: build with gradle
- name: Build With Gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
@ -63,12 +68,12 @@ jobs:
with:
name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk
- name: Set up Python 3.8
- name: Set Up Python 3
if: ${{ env.LANZOU_PSD }}
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Set up lanzou-api
python-version: 3.x
- name: Dependence Install
if: ${{ env.LANZOU_PSD }}
run: |
pip install requests
@ -79,12 +84,12 @@ jobs:
run: |
import requests,os
res = requests.get('https://raw.githubusercontent.com/Celeter/build/master/.github/scripts/API_lanzou.py')
with open(os.environ["FILEPATH"], 'wb') as f:
with open(os.environ["FILE_PATH"], 'wb') as f:
f.write(res.content)
env:
FILEPATH: ${{ github.workspace }}/.github/lzy.py
FILE_PATH: ${{ github.workspace }}/.github/lzy.py
- name: Upload App To Lanzou
if: ${{ env.LANZOU_PSD }}
run: python $GITHUB_WORKSPACE/.github/lzy.py
env:
UPLOAD_FOLDER: ${{ github.workspace }}/app/build/outputs/apk/app/release/
UPLOAD_FOLDER: ${{ github.workspace }}/app/build/outputs/apk/app/release/

@ -1,6 +1,15 @@
# legado
<div>
<img width="192" height="192" align="left" src="https://github.com/gedoor/legado/raw/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" alt="legado"/>
<h1>Legado</h1>
<br>
<p>阅读3.0, 阅读是一款可以自定义来源阅读网络内容的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。</p>
</div>
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Build Action](https://github.com/gedoor/legado/workflows/Android%20CI/badge.svg)](https://github.com/gedoor/legado/actions)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
[![Build Action](https://github.com/gedoor/legado/workflows/Android%20CI/badge.svg)](https://github.com/gedoor/legado/actions)
[![Downloads](https://img.shields.io/github/downloads/gedoor/legado/total.svg)](https://github.com/gedoor/legado/releases/latest)
[![GitHub issues](https://img.shields.io/github/issues/gedoor/legado)](https://github.com/gedoor/legado/issues)
[![GitHub contributors](https://img.shields.io/github/contributors/gedoor/legado)](https://github.com/gedoor/legado/graphs/contributors)
## 阅读3.0
* [书源规则](https://alanskycn.gitee.io/teachme/)
@ -8,9 +17,9 @@
* [帮助文档](/app/src/main/assets/help/appHelp.md)
## 下载
<a href="https://play.google.com/store/apps/details?id=io.legado.play.release"><img width="200px" alt="Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or <a href="https://www.coolapk.com/apk/io.legado.app.release"><img width="100px" height="100px" alt="CoolApk" src="https://static.coolapk.com/static/web/v8/images/header-logo.png"/></a> or [Releases](https://github.com/gedoor/legado/releases/latest)
<a href="https://play.google.com/store/apps/details?id=io.legado.play.release"><img width="200px" alt="Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or <a href="https://www.coolapk.com/apk/io.legado.app.release"><img width="100px" height="100px" alt="CoolApk" src="https://github.com/Celeter/build/raw/master/.github/scripts/coolapk.png"/></a> or [Releases](https://github.com/gedoor/legado/releases/latest)
### 阅读API
## 阅读API
阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
## 感谢

@ -97,6 +97,7 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
buildToolsVersion '30.0.3'
}
resourcePlaceholders {
@ -195,6 +196,8 @@ dependencies {
//MarkDown
implementation 'io.noties.markwon:core:4.6.0'
implementation 'io.noties.markwon:image-glide:4.6.0'
implementation 'io.noties.markwon:ext-tables:4.6.0'
implementation 'io.noties.markwon:html:4.6.0'
//
implementation 'com.hankcs:hanlp:portable-1.7.8'

@ -200,7 +200,7 @@
<!-- 搜索界面 -->
<activity
android:name=".ui.book.search.SearchActivity"
android:launchMode="singleTask" />
android:launchMode="standard" />
<!-- 关于界面 -->
<activity
android:name=".ui.about.AboutActivity"

@ -1,62 +1,142 @@
[
{
"id": 1598233029304,
"name": "度小美",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -1,
"name": "度丫丫",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029305,
"name": "度小宇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -2,
"name": "度博文①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029306,
"name": "度逍遥",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -3,
"name": "度博文②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029307,
"name": "度丫丫",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -4,
"name": "度博文③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029308,
"id": -5,
"name": "度小娇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029309,
"name": "度米朵",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -6,
"name": "度小宇",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029310,
"name": "度博文",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -7,
"name": "度小童",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029311,
"name": "度小",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -8,
"name": "度小",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029312,
"id": -9,
"name": "度小萌",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -10,
"name": "度小鹿①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -11,
"name": "度小鹿②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -12,
"name": "度小鹿③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -13,
"name": "度米朵①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -14,
"name": "度米朵②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -15,
"name": "度逍遥-基础",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029313,
"name": "百度骚男",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -16,
"name": "度逍遥-精品①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029314,
"name": "百度评书",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"id": -17,
"name": "度逍遥-精品②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029315,
"id": -18,
"name": "情感女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -19,
"name": "情感男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -20,
"name": "标准女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -21,
"name": "标准男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -22,
"name": "治愈女声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5120&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -23,
"name": "治愈男声",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -24,
"name": "甜美女声①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -25,
"name": "甜美女声②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -26,
"name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -27,
"name": "百度评书①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -28,
"name": "百度评书②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
}
]
]

@ -30,18 +30,10 @@
## 1. 基本匹配
正则表达式只是我们用于在文本中检索字母和数字的模式。例如正则表达式 `cat`,表示: 字母 `c` 后面跟着一个字母 `a`,再后面跟着一个字母 `t`
<pre>
"cat" => The <a href="#learn-regex"><strong>cat</strong></a> sat on the mat
</pre>
正则表达式只是我们用于在文本中检索字母和数字的模式。例如正则表达式 `cat`,表示: 字母 `c` 后面跟着一个字母 `a`,再后面跟着一个字母 `t`<pre>"cat" => The <a href="#learn-regex"><strong>cat</strong></a> sat on the mat</pre>
正则表达式 `123` 会匹配字符串 "123"。通过将正则表达式中的每个字符逐个与要匹配的字符串中的每个字符进行比较,来完成正则匹配。
正则表达式通常区分大小写,因此正则表达式 `Cat` 与字符串 "cat" 不匹配。
<pre>
"Cat" => The cat sat on the <a href="#learn-regex"><strong>Cat</strong></a>
</pre>
正则表达式通常区分大小写,因此正则表达式 `Cat` 与字符串 "cat" 不匹配。<pre>"Cat" => The cat sat on the <a href="#learn-regex"><strong>Cat</strong></a></pre>
## 2. 元字符
@ -66,35 +58,19 @@
## 2.1 英文句号
英文句号 `.` 是元字符的最简单的例子。元字符 `.` 可以匹配任意单个字符。它不会匹配换行符和新行的字符。例如正则表达式 `.ar`,表示: 任意字符后面跟着一个字母 `a`
再后面跟着一个字母 `r`
<pre>
".ar" => The <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
再后面跟着一个字母 `r`<pre>".ar" => The <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.</pre>
## 2.2 字符集
字符集也称为字符类。方括号被用于指定字符集。使用字符集内的连字符来指定字符范围。方括号内的字符范围的顺序并不重要。
例如正则表达式 `[Tt]he`,表示: 大写 `T` 或小写 `t` ,后跟字母 `h`,再后跟字母 `e`
<pre>
"[Tt]he" => <a href="#learn-regex"><strong>The</strong></a> car parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
例如正则表达式 `[Tt]he`,表示: 大写 `T` 或小写 `t` ,后跟字母 `h`,再后跟字母 `e`<pre>"[Tt]he" => <a href="#learn-regex"><strong>The</strong></a> car parked in <a href="#learn-regex"><strong>the</strong></a> garage.</pre>
然而,字符集中的英文句号表示它字面的含义。正则表达式 `ar[.]`,表示小写字母 `a`,后面跟着一个字母 `r`,再后面跟着一个英文句号 `.` 字符。
<pre>
"ar[.]" => A garage is a good place to park a c<a href="#learn-regex"><strong>ar.</strong></a>
</pre>
然而,字符集中的英文句号表示它字面的含义。正则表达式 `ar[.]`,表示小写字母 `a`,后面跟着一个字母 `r`,再后面跟着一个英文句号 `.` 字符。<pre>"ar[.]" => A garage is a good place to park a c<a href="#learn-regex"><strong>ar.</strong></a></pre>
### 2.2.1 否定字符集
一般来说插入字符 `^` 表示一个字符串的开始,但是当它在方括号内出现时,它会取消字符集。例如正则表达式 `[^c]ar`,表示: 除了字母 `c` 以外的任意字符,后面跟着字符 `a`
再后面跟着一个字母 `r`
<pre>
"[^c]ar" => The car <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
再后面跟着一个字母 `r`<pre>"[^c]ar" => The car <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.</pre>
## 2.3 重复
@ -103,85 +79,42 @@
### 2.3.1 星号
该符号 `*` 表示匹配上一个匹配规则的零次或多次。正则表达式 `a*` 表示小写字母 `a` 可以重复零次或者多次。但是它如果出现在字符集或者字符类之后,它表示整个字符集的重复。
例如正则表达式 `[a-z]*`,表示: 一行中可以包含任意数量的小写字母。
<pre>
"[a-z]*" => T<a href="#learn-regex"><strong>he</strong></a> <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>parked</strong></a> <a href="#learn-regex"><strong>in</strong></a> <a href="#learn-regex"><strong>the</strong></a> <a href="#learn-regex"><strong>garage</strong></a> #21.
</pre>
例如正则表达式 `[a-z]*`,表示: 一行中可以包含任意数量的小写字母。<pre>"[a-z]*" => T<a href="#learn-regex"><strong>he</strong></a> <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>parked</strong></a> <a href="#learn-regex"><strong>in</strong></a> <a href="#learn-regex"><strong>the</strong></a> <a href="#learn-regex"><strong>garage</strong></a> #21.</pre>
`*` 符号可以与元符号 `.` 用在一起,用来匹配任意字符串 `.*`。该 `*` 符号可以与空格符 `\s` 一起使用,用来匹配一串空格字符。
例如正则表达式 `\s*cat\s*`,表示: 零个或多个空格,后面跟小写字母 `c`,再后面跟小写字母 `a`,再再后面跟小写字母 `t`,后面再跟零个或多个空格。
<pre>
"\s*cat\s*" => The fat<a href="#learn-regex"><strong> cat </strong></a>sat on the <a href="#learn-regex"><strong>cat</strong></a>.
</pre>
例如正则表达式 `\s*cat\s*`,表示: 零个或多个空格,后面跟小写字母 `c`,再后面跟小写字母 `a`,再再后面跟小写字母 `t`,后面再跟零个或多个空格。<pre>"\s*cat\s*" => The fat<a href="#learn-regex"><strong> cat </strong></a>sat on the <a href="#learn-regex"><strong>cat</strong></a>.</pre>
### 2.3.2 加号
该符号 `+` 匹配上一个字符的一次或多次。例如正则表达式 `c.+t`,表示: 一个小写字母 `c`,后跟任意数量的字符,后跟小写字母 `t`
<pre>
"c.+t" => The fat <a href="#learn-regex"><strong>cat sat on the mat</strong></a>.
</pre>
该符号 `+` 匹配上一个字符的一次或多次。例如正则表达式 `c.+t`,表示: 一个小写字母 `c`,后跟任意数量的字符,后跟小写字母 `t`<pre>"c.+t" => The fat <a href="#learn-regex"><strong>cat sat on the mat</strong></a>.</pre>
### 2.3.3 问号
在正则表达式中,元字符 `?` 用来表示前一个字符是可选的。该符号匹配前一个字符的零次或一次。
例如正则表达式 `[T]?he`,表示: 可选的大写字母 `T`,后面跟小写字母 `h`,后跟小写字母 `e`
<pre>
"[T]he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.
</pre>
<pre>
"[T]?he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in t<a href="#learn-regex"><strong>he</strong></a> garage.
</pre>
例如正则表达式 `[T]?he`,表示: 可选的大写字母 `T`,后面跟小写字母 `h`,后跟小写字母 `e`<pre>"[T]he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.</pre><pre>"[T]?he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in t<a href="#learn-regex"><strong>he</strong></a> garage.</pre>
## 2.4 花括号
在正则表达式中花括号(也被称为量词 ?)用于指定字符或一组字符可以重复的次数。例如正则表达式 `[0-9]{2,3}`,表示: 匹配至少2位数字但不超过3位(0到9范围内的字符)。
<pre>
"[0-9]{2,3}" => The number was 9.<a href="#learn-regex"><strong>999</strong></a>7 but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
我们可以省略第二个数字。例如正则表达式 `[0-9]{2,}`,表示: 匹配2个或更多个数字。如果我们也删除逗号,则正则表达式 `[0-9]{2}`,表示: 匹配正好为2位数的数字。
在正则表达式中花括号(也被称为量词 ?)用于指定字符或一组字符可以重复的次数。例如正则表达式 `[0-9]{2,3}`,表示: 匹配至少2位数字但不超过3位(0到9范围内的字符)。<pre>"[0-9]{2,3}" => The number was 9.<a href="#learn-regex"><strong>999</strong></a>7 but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.</pre>
<pre>
"[0-9]{2,}" => The number was 9.<a href="#learn-regex"><strong>9997</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
<pre>
"[0-9]{2}" => The number was 9.<a href="#learn-regex"><strong>99</strong></a><a href="#learn-regex"><strong>97</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
我们可以省略第二个数字。例如正则表达式 `[0-9]{2,}`,表示: 匹配2个或更多个数字。如果我们也删除逗号,则正则表达式 `[0-9]{2}`,表示: 匹配正好为2位数的数字。<pre>"[0-9]{2,}" => The number was 9.<a href="#learn-regex"><strong>9997</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.</pre><pre>"[0-9]{2}" => The number was 9.<a href="#learn-regex"><strong>99</strong></a><a href="#learn-regex"><strong>97</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.</pre>
## 2.5 字符组
字符组是一组写在圆括号内的子模式 `(...)`。正如我们在正则表达式中讨论的那样,如果我们把一个量词放在一个字符之后,它会重复前一个字符。
但是,如果我们把量词放在一个字符组之后,它会重复整个字符组。
例如正则表达式 `(ab)*` 表示匹配零个或多个的字符串 "ab"。我们还可以在字符组中使用元字符 `|`。例如正则表达式 `(c|g|p)ar`,表示: 小写字母 `c`、`g` 或 `p` 后面跟字母 `a`,后跟字母 `r`
<pre>
"(c|g|p)ar" => The <a href="#learn-regex"><strong>car</strong></a> is <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
例如正则表达式 `(ab)*` 表示匹配零个或多个的字符串 "ab"。我们还可以在字符组中使用元字符 `|`。例如正则表达式 `(c|g|p)ar`,表示: 小写字母 `c`、`g` 或 `p` 后面跟字母 `a`,后跟字母 `r`<pre>"(c|g|p)ar" => The <a href="#learn-regex"><strong>car</strong></a> is <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.</pre>
## 2.6 分支结构
在正则表达式中垂直条 `|` 用来定义分支结构,分支结构就像多个表达式之间的条件。现在你可能认为这个字符集和分支机构的工作方式一样。
但是字符集和分支结构巨大的区别是字符集只在字符级别上有作用,然而分支结构在表达式级别上依然可以使用。
例如正则表达式 `(T|t)he|car`,表示: 大写字母 `T` 或小写字母 `t`,后面跟小写字母 `h`,后跟小写字母 `e` 或小写字母 `c`,后跟小写字母 `a`,后跟小写字母 `r`
<pre>
"(T|t)he|car" => <a href="#learn-regex"><strong>The</strong></a> <a href="#learn-regex"><strong>car</strong></a> is parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
例如正则表达式 `(T|t)he|car`,表示: 大写字母 `T` 或小写字母 `t`,后面跟小写字母 `h`,后跟小写字母 `e` 或小写字母 `c`,后跟小写字母 `a`,后跟小写字母 `r`<pre>"(T|t)he|car" => <a href="#learn-regex"><strong>The</strong></a> <a href="#learn-regex"><strong>car</strong></a> is parked in <a href="#learn-regex"><strong>the</strong></a> garage.</pre>
## 2.7 转义特殊字符
正则表达式中使用反斜杠 `\` 来转义下一个字符。这将允许你使用保留字符来作为匹配字符 `{ } [ ] / \ + * . $ ^ | ?`。在特殊字符前面加 `\`,就可以使用它来做匹配字符。
例如正则表达式 `.` 是用来匹配除了换行符以外的任意字符。现在要在输入字符串中匹配 `.` 字符,正则表达式 `(f|c|m)at\.?`,表示: 小写字母 `f`、`c` 或者 `m` 后跟小写字母 `a`,后跟小写字母 `t`,后跟可选的 `.` 字符。
<pre>
"(f|c|m)at\.?" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> sat on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
例如正则表达式 `.` 是用来匹配除了换行符以外的任意字符。现在要在输入字符串中匹配 `.` 字符,正则表达式 `(f|c|m)at\.?`,表示: 小写字母 `f`、`c` 或者 `m` 后跟小写字母 `a`,后跟小写字母 `t`,后跟可选的 `.` 字符。<pre>"(f|c|m)at\.?" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> sat on the <a href="#learn-regex"><strong>mat.</strong></a></pre>
## 2.8 定位符
@ -192,27 +125,11 @@
插入符号 `^` 符号用于检查匹配字符是否是输入字符串的第一个字符。如果我们使用正则表达式 `^a` (如果a是起始符号)匹配字符串 `abc`,它会匹配到 `a`
但是如果我们使用正则表达式 `^b`,它是匹配不到任何东西的,因为在字符串 `abc` 中 "b" 不是起始字符。
让我们来看看另一个正则表达式 `^(T|t)he`,这表示: 大写字母 `T` 或小写字母 `t` 是输入字符串的起始符号,后面跟着小写字母 `h`,后跟小写字母 `e`
<pre>
"(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
<pre>
"^(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.
</pre>
让我们来看看另一个正则表达式 `^(T|t)he`,这表示: 大写字母 `T` 或小写字母 `t` 是输入字符串的起始符号,后面跟着小写字母 `h`,后跟小写字母 `e`<pre>"(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in <a href="#learn-regex"><strong>the</strong></a> garage.</pre><pre>"^(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.</pre>
### 2.8.2 美元符号
美元 `$` 符号用于检查匹配字符是否是输入字符串的最后一个字符。例如正则表达式 `(at\.)$`,表示: 小写字母 `a`,后跟小写字母 `t`,后跟一个 `.` 字符,且这个匹配器必须是字符串的结尾。
<pre>
"(at\.)" => The fat c<a href="#learn-regex"><strong>at.</strong></a> s<a href="#learn-regex"><strong>at.</strong></a> on the m<a href="#learn-regex"><strong>at.</strong></a>
</pre>
<pre>
"(at\.)$" => The fat cat sat on the m<a href="#learn-regex"><strong>at.</strong></a>
</pre>
美元 `$` 符号用于检查匹配字符是否是输入字符串的最后一个字符。例如正则表达式 `(at\.)$`,表示: 小写字母 `a`,后跟小写字母 `t`,后跟一个 `.` 字符,且这个匹配器必须是字符串的结尾。<pre>"(at\.)" => The fat c<a href="#learn-regex"><strong>at.</strong></a> s<a href="#learn-regex"><strong>at.</strong></a> on the m<a href="#learn-regex"><strong>at.</strong></a></pre><pre>"(at\.)$" => The fat cat sat on the m<a href="#learn-regex"><strong>at.</strong></a></pre>
## 3. 简写字符集
@ -246,37 +163,21 @@
正向先行断言认为第一部分的表达式必须是先行断言表达式。返回的匹配结果仅包含与第一部分表达式匹配的文本。
要在一个括号内定义一个正向先行断言,在括号中问号和等号是这样使用的 `(?=...)`。先行断言表达式写在括号中的等号后面。
例如正则表达式 `(T|t)he(?=\sfat)`,表示: 匹配大写字母 `T` 或小写字母 `t`,后面跟字母 `h`,后跟字母 `e`
在括号中,我们定义了正向先行断言,它会引导正则表达式引擎匹配 `The``the` 后面跟着 `fat`
<pre>
"(T|t)he(?=\sfat)" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.
</pre>
在括号中,我们定义了正向先行断言,它会引导正则表达式引擎匹配 `The``the` 后面跟着 `fat`<pre>"(T|t)he(?=\sfat)" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.</pre>
### 4.2 负向先行断言
当我们需要从输入字符串中获取不匹配表达式的内容时,使用负向先行断言。负向先行断言的定义跟我们定义的正向先行断言一样,
唯一的区别是不是等号 `=`,我们使用否定符号 `!`,例如 `(?!...)`
我们来看看下面的正则表达式 `(T|t)he(?!\sfat)`,表示: 从输入字符串中获取全部 `The` 或者 `the` 且不匹配 `fat` 前面加上一个空格字符。
<pre>
"(T|t)he(?!\sfat)" => The fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.
</pre>
我们来看看下面的正则表达式 `(T|t)he(?!\sfat)`,表示: 从输入字符串中获取全部 `The` 或者 `the` 且不匹配 `fat` 前面加上一个空格字符。<pre>"(T|t)he(?!\sfat)" => The fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.</pre>
### 4.3 正向后行断言
正向后行断言是用于获取在特定模式之前的所有匹配内容。正向后行断言表示为 `(?<=...)`。例如正则表达式 `(?<=(T|t)he\s)(fat|mat)`,表示: 从输入字符串中获取在单词 `The``the` 之后的所有 `fat``mat` 单词。
<pre>
"(?<=(T|t)he\s)(fat|mat)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the <a href="#learn-regex"><strong>mat</strong></a>.
</pre>
正向后行断言是用于获取在特定模式之前的所有匹配内容。正向后行断言表示为 `(?<=...)`。例如正则表达式 `(?<=(T|t)he\s)(fat|mat)`,表示: 从输入字符串中获取在单词 `The``the` 之后的所有 `fat``mat` 单词。<pre>"(?<=(T|t)he\s)(fat|mat)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the <a href="#learn-regex"><strong>mat</strong></a>.</pre>
### 4.4 负向后行断言
负向后行断言是用于获取不在特定模式之前的所有匹配的内容。负向后行断言表示为 `(?<!...)`。例如正则表达式 `(?<!(T|t)he\s)(cat)`,表示: 在输入字符中获取所有不在 `The``the` 之后的所有单词 `cat`
<pre>
"(?&lt;!(T|t)he\s)(cat)" => The cat sat on <a href="#learn-regex"><strong>cat</strong></a>.
</pre>
负向后行断言是用于获取不在特定模式之前的所有匹配的内容。负向后行断言表示为 `(?<!...)`。例如正则表达式 `(?<!(T|t)he\s)(cat)`,表示: 在输入字符中获取所有不在 `The``the` 之后的所有单词 `cat`<pre>"(?&lt;!(T|t)he\s)(cat)" => The cat sat on <a href="#learn-regex"><strong>cat</strong></a>.</pre>
## 5. 标记
@ -291,46 +192,22 @@
### 5.1 不区分大小写
`i` 修饰符用于执行不区分大小写匹配。例如正则表达式 `/The/gi`,表示: 大写字母 `T`,后跟小写字母 `h`,后跟字母 `e`
但是在正则匹配结束时 `i` 标记会告诉正则表达式引擎忽略这种情况。正如你所看到的,我们还使用了 `g` 标记,因为我们要在整个输入字符串中搜索匹配。
<pre>
"The" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.
</pre>
<pre>
"/The/gi" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.
</pre>
但是在正则匹配结束时 `i` 标记会告诉正则表达式引擎忽略这种情况。正如你所看到的,我们还使用了 `g` 标记,因为我们要在整个输入字符串中搜索匹配。<pre>"The" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.</pre><pre>"/The/gi" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.</pre>
### 5.2 全局搜索
`g` 修饰符用于执行全局匹配 (会查找所有匹配,不会在查找到第一个匹配时就停止)。
例如正则表达式 `/.(at)/g`,表示: 除换行符之外的任意字符,后跟小写字母 `a`,后跟小写字母 `t`
因为我们在正则表达式的末尾使用了 `g` 标记,它会从整个输入字符串中找到每个匹配项。
<pre>
".(at)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the mat.
</pre>
<pre>
"/.(at)/g" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> <a href="#learn-regex"><strong>sat</strong></a> on the <a href="#learn-regex"><strong>mat</strong></a>.
</pre>
因为我们在正则表达式的末尾使用了 `g` 标记,它会从整个输入字符串中找到每个匹配项。<pre>".(at)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the mat.</pre><pre>"/.(at)/g" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> <a href="#learn-regex"><strong>sat</strong></a> on the <a href="#learn-regex"><strong>mat</strong></a>.</pre>
### 5.3 多行匹配
`m` 修饰符被用来执行多行的匹配。正如我们前面讨论过的 `(^, $)`,使用定位符来检查匹配字符是输入字符串开始或者结束。但是我们希望每一行都使用定位符,所以我们就使用 `m` 修饰符。
例如正则表达式 `/at(.)?$/gm`,表示: 小写字母 `a`,后跟小写字母 `t`,匹配除了换行符以外任意字符零次或一次。而且因为 `m` 标记,现在正则表达式引擎匹配字符串中每一行的末尾。
<pre>
"/.at(.)?$/" => The fat
例如正则表达式 `/at(.)?$/gm`,表示: 小写字母 `a`,后跟小写字母 `t`,匹配除了换行符以外任意字符零次或一次。而且因为 `m` 标记,现在正则表达式引擎匹配字符串中每一行的末尾。<pre>"/.at(.)?$/" => The fat
cat sat
on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
<pre>
"/.at(.)?$/gm" => The <a href="#learn-regex"><strong>fat</strong></a>
on the <a href="#learn-regex"><strong>mat.</strong></a></pre><pre>"/.at(.)?$/gm" => The <a href="#learn-regex"><strong>fat</strong></a>
cat <a href="#learn-regex"><strong>sat</strong></a>
on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
on the <a href="#learn-regex"><strong>mat.</strong></a></pre>
## 常用正则表达式

@ -3,6 +3,25 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2021/01/04**
* 修复bug
**2021/01/03**
* 导出书单只保留书名与作者,导入时自动查找可用源
* 添加预加载设置
* 选择分组时只搜索分组
**2020/12/30**
* 解决文件下载异常,在线语音可正常播放 by [Celeter](https://github.com/Celeter)
* 更新默认在线朗读库, 默认id小于0方便下次更新时删除旧数据, 有重复的自己删除
* 导入导出书单
* 其它一些优化
**2020/12/27**
* 订阅添加搜索和分组
* 修复部分手机状态栏bug
* 单url订阅支持内容规则和样式
**2020/12/19**
* 书签转移到文本菜单里,会记录选择的文本和位置
* 订阅源添加单url选项,直接打开url

@ -12,7 +12,6 @@ import android.net.Uri
import com.google.gson.Gson
import io.legado.app.api.controller.BookshelfController
import io.legado.app.api.controller.SourceController
import io.legado.app.web.utils.ReturnData
import java.util.*
/**

@ -1,4 +1,4 @@
package io.legado.app.web.utils
package io.legado.app.api
class ReturnData {
@ -6,18 +6,13 @@ class ReturnData {
var isSuccess: Boolean = false
private set
var errorMsg: String? = null
var errorMsg: String = "未知错误,请联系开发者!"
private set
var data: Any? = null
private set
init {
this.isSuccess = false
this.errorMsg = "未知错误,请联系开发者!"
}
fun setErrorMsg(errorMsg: String?): ReturnData {
fun setErrorMsg(errorMsg: String): ReturnData {
this.isSuccess = false
this.errorMsg = errorMsg
return this

@ -1,6 +1,7 @@
package io.legado.app.api.controller
import io.legado.app.App
import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
@ -10,7 +11,6 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.cnCompare
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefInt
import io.legado.app.web.utils.ReturnData
import kotlinx.coroutines.runBlocking
object BookshelfController {
@ -66,7 +66,7 @@ object BookshelfController {
} else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { source ->
runBlocking {
WebBook(source).getContentSuspend(book, chapter)
WebBook(source).getContentAwait(this, book, chapter)
}.let {
saveBookReadIndex(book, index)
returnData.setData(it)

@ -3,11 +3,12 @@ package io.legado.app.api.controller
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.api.ReturnData
import io.legado.app.data.entities.BookSource
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject
import io.legado.app.web.utils.ReturnData
import io.legado.app.utils.msg
object SourceController {
@ -22,7 +23,7 @@ object SourceController {
fun saveSource(postData: String?): ReturnData {
val returnData = ReturnData()
try {
kotlin.runCatching {
val bookSource = GSON.fromJsonObject<BookSource>(postData)
if (bookSource != null) {
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
@ -34,8 +35,8 @@ object SourceController {
} else {
returnData.setErrorMsg("转换书源失败")
}
} catch (e: Exception) {
returnData.setErrorMsg(e.localizedMessage)
}.onFailure {
returnData.setErrorMsg(it.msg)
}
return returnData
}

@ -32,13 +32,10 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun show(manager: FragmentManager, tag: String?) {
try {
kotlin.runCatching {
//在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit()
super.show(manager, tag)
} catch (e: Exception) {
//同一实例使用不同的tag会异常,这里捕获一下
e.printStackTrace()
}
}

@ -26,8 +26,6 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
}
}
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
@ -47,39 +45,46 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
recyclerView.adapter = this
}
@Synchronized
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
kotlin.runCatching {
asyncListDiffer.submitList(items)
}
}
@Synchronized
fun setItem(position: Int, item: ITEM) {
synchronized(lock) {
kotlin.runCatching {
val list = ArrayList(asyncListDiffer.currentList)
list[position] = item
asyncListDiffer.submitList(list)
}
}
fun updateItem(item: ITEM) =
synchronized(lock) {
@Synchronized
fun updateItem(item: ITEM) {
kotlin.runCatching {
val index = asyncListDiffer.currentList.indexOf(item)
if (index >= 0) {
asyncListDiffer.currentList[index] = item
notifyItemChanged(index)
}
}
}
fun updateItem(position: Int, payload: Any) =
synchronized(lock) {
@Synchronized
fun updateItem(position: Int, payload: Any) {
kotlin.runCatching {
val size = itemCount
if (position in 0 until size) {
notifyItemChanged(position, payload)
}
}
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) =
synchronized(lock) {
@Synchronized
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
kotlin.runCatching {
val size = itemCount
if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged(
@ -89,6 +94,7 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
)
}
}
}
fun isEmpty() = asyncListDiffer.currentList.isEmpty()

@ -26,8 +26,6 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
private val items: MutableList<ITEM> = mutableListOf()
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
@ -45,42 +43,49 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
recyclerView.adapter = this
}
@Synchronized
fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
synchronized(lock) {
kotlin.runCatching {
val index = headerItems.size()
headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header)
notifyItemInserted(index)
}
}
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) =
synchronized(lock) {
@Synchronized
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching {
val index = getActualItemCount() + footerItems.size()
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
notifyItemInserted(index)
}
}
fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) =
synchronized(lock) {
@Synchronized
fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching {
val index = headerItems.indexOfValue(header)
if (index >= 0) {
headerItems.remove(index)
notifyItemRemoved(index)
}
}
}
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) =
synchronized(lock) {
@Synchronized
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching {
val index = footerItems.indexOfValue(footer)
if (index >= 0) {
footerItems.remove(index)
notifyItemRemoved(getActualItemCount() + index - 2)
}
}
}
@Synchronized
fun setItems(items: List<ITEM>?) {
synchronized(lock) {
kotlin.runCatching {
if (this.items.isNotEmpty()) {
this.items.clear()
}
@ -92,8 +97,46 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
fun setItems(items: List<ITEM>?, diffResult: DiffUtil.DiffResult) {
synchronized(lock) {
@Synchronized
fun setItems(items: List<ITEM>?, itemCallback: DiffUtil.ItemCallback<ITEM>) {
kotlin.runCatching {
val callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return itemCount
}
override fun getNewListSize(): Int {
return (items?.size ?: 0) + getHeaderCount() + getFooterCount()
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = getItem(oldItemPosition - getHeaderCount())
?: return true
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return true
return itemCallback.areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem = getItem(oldItemPosition - getHeaderCount())
?: return true
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return true
return itemCallback.areContentsTheSame(oldItem, newItem)
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = getItem(oldItemPosition - getHeaderCount())
?: return null
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return null
return itemCallback.getChangePayload(oldItem, newItem)
}
}
val diffResult = DiffUtil.calculateDiff(callback)
if (this.items.isNotEmpty()) {
this.items.clear()
}
@ -105,8 +148,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun setItem(position: Int, item: ITEM) {
synchronized(lock) {
kotlin.runCatching {
val oldSize = getActualItemCount()
if (position in 0 until oldSize) {
this.items[position] = item
@ -116,8 +160,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun addItem(item: ITEM) {
synchronized(lock) {
kotlin.runCatching {
val oldSize = getActualItemCount()
if (this.items.add(item)) {
notifyItemInserted(oldSize + getHeaderCount())
@ -126,8 +171,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) {
kotlin.runCatching {
if (this.items.addAll(position, newItems)) {
notifyItemRangeInserted(position + getHeaderCount(), newItems.size)
}
@ -135,8 +181,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun addItems(newItems: List<ITEM>) {
synchronized(lock) {
kotlin.runCatching {
val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) {
if (oldSize == 0 && getHeaderCount() == 0) {
@ -149,8 +196,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun removeItem(position: Int) {
synchronized(lock) {
kotlin.runCatching {
if (this.items.removeAt(position) != null) {
notifyItemRemoved(position + getHeaderCount())
}
@ -158,8 +206,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun removeItem(item: ITEM) {
synchronized(lock) {
kotlin.runCatching {
if (this.items.remove(item)) {
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
}
@ -167,8 +216,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun removeItems(items: List<ITEM>) {
synchronized(lock) {
kotlin.runCatching {
if (this.items.removeAll(items)) {
notifyDataSetChanged()
}
@ -176,8 +226,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
@Synchronized
fun swapItem(oldPosition: Int, newPosition: Int) {
synchronized(lock) {
kotlin.runCatching {
val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) {
val srcPosition = oldPosition + getHeaderCount()
@ -189,8 +240,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
}
fun updateItem(item: ITEM) =
synchronized(lock) {
@Synchronized
fun updateItem(item: ITEM) {
kotlin.runCatching {
val index = this.items.indexOf(item)
if (index >= 0) {
this.items[index] = item
@ -198,17 +250,21 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
}
onCurrentListChanged()
}
}
fun updateItem(position: Int, payload: Any) =
synchronized(lock) {
@Synchronized
fun updateItem(position: Int, payload: Any) {
kotlin.runCatching {
val size = getActualItemCount()
if (position in 0 until size) {
notifyItemChanged(position + getHeaderCount(), payload)
}
}
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) =
synchronized(lock) {
@Synchronized
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
kotlin.runCatching {
val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged(
@ -218,11 +274,15 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
)
}
}
}
fun clearItems() = synchronized(lock) {
this.items.clear()
notifyDataSetChanged()
onCurrentListChanged()
@Synchronized
fun clearItems() {
kotlin.runCatching {
this.items.clear()
notifyDataSetChanged()
onCurrentListChanged()
}
}
fun isEmpty() = items.isEmpty()

@ -64,6 +64,7 @@ object PreferKey {
const val importKeepName = "importKeepName"
const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress"
const val preDownload = "preDownload"
const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent"

@ -14,6 +14,9 @@ interface BookSourceDao {
@Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey or bookSourceComment like :searchKey order by customOrder asc")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
fun liveDataGroupSearch(searchKey: String = ""): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
fun liveDataEnabled(): LiveData<List<BookSource>>
@ -26,6 +29,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
fun liveExplore(key: String): LiveData<List<BookSource>>
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key) order by customOrder asc")
fun liveGroupExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>>
@ -33,7 +39,7 @@ interface BookSourceDao {
fun liveGroupEnabled(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''")
fun liveGroupExplore(): LiveData<List<String>>
fun liveExploreGroup(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean>

@ -28,4 +28,6 @@ interface HttpTTSDao {
@Update
fun update(vararg httpTTS: HttpTTS)
@Query("delete from httpTTS where id < 0")
fun deleteDefault()
}

@ -14,6 +14,9 @@ interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
fun liveDataSearch(key: String): LiveData<List<ReplaceRule>>
@Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC")
fun liveDataGroupSearch(key: String): LiveData<List<ReplaceRule>>
@get:Query("SELECT MIN(sortOrder) FROM replace_rules")
val minOrder: Int

@ -25,6 +25,9 @@ interface RssSourceDao {
@Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder")
fun liveSearch(key: String): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder")
fun liveGroupSearch(key: String): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 1 and (sourceName like '%'||:searchKey||'%' or sourceGroup like '%'||:searchKey||'%' or sourceUrl like '%'||:searchKey||'%') order by customOrder")
fun liveEnabled(searchKey: String): LiveData<List<RssSource>>

@ -46,14 +46,15 @@ data class BookSource(
var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则
var ruleToc: TocRule? = null, // 目录页规则
var ruleContent: ContentRule? = null // 正文页规则
): Parcelable, JsExtensions {
) : Parcelable, JsExtensions {
override fun hashCode(): Int {
return bookSourceUrl.hashCode()
}
override fun equals(other: Any?) = if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false
override fun equals(other: Any?) =
if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false
@Throws(Exception::class)
fun getHeaderMap() = (HashMap<String, String>().apply {
this[AppConst.UA_NAME] = AppConfig.userAgent
@ -71,17 +72,17 @@ data class BookSource(
}
}
}) as Map<String, String>
fun getSearchRule() = ruleSearch ?: SearchRule()
fun getExploreRule() = ruleExplore ?: ExploreRule()
fun getBookInfoRule() = ruleBookInfo ?: BookInfoRule()
fun getTocRule() = ruleToc ?: TocRule()
fun getContentRule() = ruleContent ?: ContentRule()
fun addGroup(group: String) {
bookSourceGroup?.let {
if (!it.contains(group)) {
@ -91,20 +92,20 @@ data class BookSource(
bookSourceGroup = group
}
}
fun removeGroup(group: String) {
bookSourceGroup?.splitNotBlank("[,;,;]".toRegex())?.toHashSet()?.let {
it.remove(group)
bookSourceGroup = TextUtils.join(",", it)
}
}
fun getExploreKinds() = arrayListOf<ExploreKind>().apply {
exploreUrl?.let {
var a = it
exploreUrl?.let { urlRule ->
var a = urlRule
if (a.isNotBlank()) {
try {
if (it.startsWith("<js>", false)) {
kotlin.runCatching {
if (urlRule.startsWith("<js>", false)) {
val aCache = ACache.get(App.INSTANCE, "explore")
a = aCache.getAsString(bookSourceUrl) ?: ""
if (a.isBlank()) {
@ -114,7 +115,7 @@ data class BookSource(
bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager
a = AppConst.SCRIPT_ENGINE.eval(
it.substring(4, it.lastIndexOf("<")),
urlRule.substring(4, urlRule.lastIndexOf("<")),
bindings
).toString()
aCache.put(bookSourceUrl, a)
@ -126,13 +127,13 @@ data class BookSource(
if (d.size > 1)
add(ExploreKind(d[0], d[1]))
}
} catch (e: Exception) {
add(ExploreKind(e.localizedMessage ?: ""))
}.onFailure {
add(ExploreKind(it.localizedMessage ?: ""))
}
}
}
}
/**
* 执行JS
*/
@ -147,60 +148,60 @@ data class BookSource(
fun equal(source: BookSource) =
equal(bookSourceName, source.bookSourceName)
&& equal(bookSourceUrl, source.bookSourceUrl)
&& equal(bookSourceGroup, source.bookSourceGroup)
&& bookSourceType == source.bookSourceType
&& equal(bookUrlPattern, source.bookUrlPattern)
&& equal(bookSourceComment, source.bookSourceComment)
&& enabled == source.enabled
&& enabledExplore == source.enabledExplore
&& equal(header, source.header)
&& equal(loginUrl, source.loginUrl)
&& equal(exploreUrl, source.exploreUrl)
&& equal(searchUrl, source.searchUrl)
&& getSearchRule() == source.getSearchRule()
&& getExploreRule() == source.getExploreRule()
&& getBookInfoRule() == source.getBookInfoRule()
&& getTocRule() == source.getTocRule()
&& getContentRule() == source.getContentRule()
&& equal(bookSourceUrl, source.bookSourceUrl)
&& equal(bookSourceGroup, source.bookSourceGroup)
&& bookSourceType == source.bookSourceType
&& equal(bookUrlPattern, source.bookUrlPattern)
&& equal(bookSourceComment, source.bookSourceComment)
&& enabled == source.enabled
&& enabledExplore == source.enabledExplore
&& equal(header, source.header)
&& equal(loginUrl, source.loginUrl)
&& equal(exploreUrl, source.exploreUrl)
&& equal(searchUrl, source.searchUrl)
&& getSearchRule() == source.getSearchRule()
&& getExploreRule() == source.getExploreRule()
&& getBookInfoRule() == source.getBookInfoRule()
&& getTocRule() == source.getTocRule()
&& getContentRule() == source.getContentRule()
private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
data class ExploreKind(
var title: String,
var url: String? = null
)
class Converters {
@TypeConverter
fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule)
@TypeConverter
fun stringToExploreRule(json: String?) = GSON.fromJsonObject<ExploreRule>(json)
@TypeConverter
fun searchRuleToString(searchRule: SearchRule?): String = GSON.toJson(searchRule)
@TypeConverter
fun stringToSearchRule(json: String?) = GSON.fromJsonObject<SearchRule>(json)
@TypeConverter
fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String = GSON.toJson(bookInfoRule)
@TypeConverter
fun stringToBookInfoRule(json: String?) = GSON.fromJsonObject<BookInfoRule>(json)
@TypeConverter
fun tocRuleToString(tocRule: TocRule?): String = GSON.toJson(tocRule)
@TypeConverter
fun stringToTocRule(json: String?) = GSON.fromJsonObject<TocRule>(json)
@TypeConverter
fun contentRuleToString(contentRule: ContentRule?): String = GSON.toJson(contentRule)
@TypeConverter
fun stringToContentRule(json: String?) = GSON.fromJsonObject<ContentRule>(json)
}
}

@ -49,8 +49,7 @@ data class ReplaceRule(
if (isRegex){
try {
Pattern.compile(pattern)
}
catch (ex: PatternSyntaxException){
} catch (ex: PatternSyntaxException) {
return false
}
}

@ -149,6 +149,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val syncBookProgress get() = context.getPrefBoolean(PreferKey.syncBookProgress, true)
val preDownload get() = context.getPrefBoolean(PreferKey.preDownload, true)
private fun getPrefUserAgent(): String {
val ua = context.getPrefString(PreferKey.userAgent)
if (ua.isNullOrBlank()) {

@ -45,6 +45,13 @@ object DefaultData {
GSON.fromJsonArray<ThemeConfig.Config>(json)!!
}
fun importDefaultHttpTTS() {
App.db.httpTTSDao.deleteDefault()
httpTTS.let {
App.db.httpTTSDao.insert(*it.toTypedArray())
}
}
fun importDefaultTocRules() {
App.db.txtTocRule.deleteDefault()
txtTocRules.let {

@ -22,9 +22,9 @@ object ImageLoader {
path.isNullOrEmpty() -> Glide.with(context).load(path)
path.isAbsUrl() -> Glide.with(context).load(AnalyzeUrl(path).getGlideUrl())
path.isContentScheme() -> Glide.with(context).load(Uri.parse(path))
else -> try {
else -> kotlin.runCatching {
Glide.with(context).load(File(path))
} catch (e: Exception) {
}.getOrElse {
Glide.with(context).load(path)
}
}

@ -13,23 +13,23 @@ object IntentHelp {
fun toTTSSetting(context: Context) {
//跳转到文字转语音设置界面
try {
kotlin.runCatching {
val intent = Intent()
intent.action = "com.android.settings.TTS_SETTINGS"
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
} catch (ignored: Exception) {
}.onFailure {
context.toast(R.string.tip_cannot_jump_setting_page)
}
}
fun toInstallUnknown(context: Context) {
try {
kotlin.runCatching {
val intent = Intent()
intent.action = "android.settings.MANAGE_UNKNOWN_APP_SOURCES"
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
} catch (ignored: Exception) {
}.onFailure {
context.toast("无法打开设置")
}
}

@ -29,13 +29,15 @@ interface JsExtensions {
* 访问网络,返回String
*/
fun ajax(urlStr: String): String? {
return try {
val analyzeUrl = AnalyzeUrl(urlStr)
runBlocking {
return runBlocking {
kotlin.runCatching {
val analyzeUrl = AnalyzeUrl(urlStr)
analyzeUrl.getStrResponse(urlStr).body
}.onFailure {
it.printStackTrace()
}.getOrElse {
it.msg
}
} catch (e: Exception) {
e.msg
}
}
@ -43,13 +45,15 @@ interface JsExtensions {
* 访问网络,返回Response<String>
*/
fun connect(urlStr: String): Any {
return try {
val analyzeUrl = AnalyzeUrl(urlStr)
runBlocking {
return runBlocking {
kotlin.runCatching {
val analyzeUrl = AnalyzeUrl(urlStr)
analyzeUrl.getStrResponse(urlStr)
}.onFailure {
it.printStackTrace()
}.getOrElse {
it.msg
}
} catch (e: Exception) {
e.msg
}
}
@ -65,7 +69,7 @@ interface JsExtensions {
FileUtils.deleteFile(zipPath)
val zipFile = FileUtils.createFileIfNotExist(zipPath)
StringUtils.hexStringToByte(content).let {
if (it != null) {
if (it.isNotEmpty()) {
zipFile.writeBytes(it)
}
}

@ -63,4 +63,10 @@ object LocalConfig {
val ruleHelpVersionIsLast: Boolean
get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean
get() = isLastVersion(1, "httpTtsVersion")
val hasUpTxtTocRule: Boolean
get() = isLastVersion(1, "txtTocRuleVersion")
}

@ -61,11 +61,11 @@ object ThemeConfig {
private fun getConfigs(): List<Config>? {
val configFile = File(configFilePath)
if (configFile.exists()) {
try {
kotlin.runCatching {
val json = configFile.readText()
return GSON.fromJsonArray(json)
} catch (e: Exception) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
}
}
return null

@ -8,7 +8,7 @@ import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset
@Parser(name = "Text")
class TextParser(val encode: String? = null) : rxhttp.wrapper.parse.Parser<String> {
class TextParser(private val encode: String? = null) : rxhttp.wrapper.parse.Parser<String> {
override fun onParse(response: Response): String {

@ -15,61 +15,61 @@ object ImportOldData {
fun importUri(context: Context, uri: Uri) {
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach {
when (it.name) {
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
when (doc.name) {
"myBookShelf.json" ->
try {
DocumentUtils.readText(context, it.uri)?.let { json ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}")
}
} catch (e: java.lang.Exception) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}")
}
"myBookSource.json" ->
try {
DocumentUtils.readText(context, it.uri)?.let { json ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) {
context.toast("导入源失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入源失败\n${it.localizedMessage}")
}
"myBookReplaceRule.json" ->
try {
DocumentUtils.readText(context, it.uri)?.let { json ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri)?.let { json ->
val importCount = importOldReplaceRule(json)
context.toast("成功导入替换规则${importCount}")
}
} catch (e: Exception) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}")
}
}
}
} else {
uri.path?.let {
val file = File(it)
try {// 导入书架
uri.path?.let { path ->
val file = File(path)
kotlin.runCatching {// 导入书架
val shelfFile =
FileUtils.createFileIfNotExist(file, "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
context.toast("成功导入书籍${importCount}")
} catch (e: Exception) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入书籍失败\n${it.localizedMessage}")
}
try {// Book source
kotlin.runCatching {// Book source
val sourceFile =
FileUtils.getFile(file, "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
context.toast("成功导入书源${importCount}")
} catch (e: Exception) {
context.toast("导入源失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入源失败\n${it.localizedMessage}")
}
try {// Replace rules
kotlin.runCatching {// Replace rules
val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json")
if (ruleFile.exists()) {
val json = ruleFile.readText()
@ -78,8 +78,8 @@ object ImportOldData {
} else {
context.toast("未找到替换规则")
}
} catch (e: Exception) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}.onFailure {
context.toast("导入替换规则失败\n${it.localizedMessage}")
}
}
}

@ -12,7 +12,6 @@ import rxhttp.wrapper.param.toInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.net.MalformedURLException
import java.net.URL
import java.net.URLEncoder
@ -39,15 +38,12 @@ class WebDav(urlStr: String) {
private val url: URL = URL(urlStr)
private val httpUrl: String? by lazy {
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://")
try {
return@lazy URLEncoder.encode(raw, "UTF-8")
return@lazy kotlin.runCatching {
URLEncoder.encode(raw, "UTF-8")
.replace("\\+".toRegex(), "%20")
.replace("%3A".toRegex(), ":")
.replace("%2F".toRegex(), "/")
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
return@lazy null
}
}.getOrNull()
}
val host: String? get() = url.host
val path get() = url.toString()

@ -11,6 +11,7 @@ import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.htmlFormat
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.msg
import kotlinx.coroutines.CoroutineScope
import java.text.SimpleDateFormat
import java.util.*
@ -101,7 +102,7 @@ object Debug {
}
}
fun startDebug(webBook: WebBook, key: String) {
fun startDebug(scope: CoroutineScope, webBook: WebBook, key: String) {
cancelDebug()
debugSource = webBook.sourceUrl
startTime = System.currentTimeMillis()
@ -111,12 +112,12 @@ object Debug {
book.origin = webBook.sourceUrl
book.bookUrl = key
log(webBook.sourceUrl, "⇒开始访问详情页:$key")
infoDebug(webBook, book)
infoDebug(scope, webBook, book)
}
key.contains("::") -> {
val url = key.substring(key.indexOf("::") + 2)
log(webBook.sourceUrl, "⇒开始访问发现页:$url")
exploreDebug(webBook, url)
exploreDebug(scope, webBook, url)
}
key.startsWith("++")-> {
val url = key.substring(2)
@ -124,7 +125,7 @@ object Debug {
book.origin = webBook.sourceUrl
book.tocUrl = url
log(webBook.sourceUrl, "⇒开始访目录页:$url")
tocDebug(webBook, book)
tocDebug(scope, webBook, book)
}
key.startsWith("--")-> {
val url = key.substring(2)
@ -134,23 +135,23 @@ object Debug {
val chapter = BookChapter()
chapter.title = "调试"
chapter.url = url
contentDebug(webBook, book, chapter, null)
contentDebug(scope, webBook, book, chapter, null)
}
else -> {
log(webBook.sourceUrl, "⇒开始搜索关键字:$key")
searchDebug(webBook, key)
searchDebug(scope, webBook, key)
}
}
}
private fun exploreDebug(webBook: WebBook, url: String) {
private fun exploreDebug(scope: CoroutineScope, webBook: WebBook, url: String) {
log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1)
val explore = webBook.exploreBook(scope, url, 1)
.onSuccess { exploreBooks ->
if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook())
infoDebug(scope, webBook, exploreBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
@ -161,14 +162,14 @@ object Debug {
tasks.add(explore)
}
private fun searchDebug(webBook: WebBook, key: String) {
private fun searchDebug(scope: CoroutineScope, webBook: WebBook, key: String) {
log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1)
val search = webBook.searchBook(scope, key, 1)
.onSuccess { searchBooks ->
if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook())
infoDebug(scope, webBook, searchBooks[0].toBook())
} else {
log(debugSource, "︽未获取到书籍", state = -1)
}
@ -179,13 +180,13 @@ object Debug {
tasks.add(search)
}
private fun infoDebug(webBook: WebBook, book: Book) {
private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析详情页")
val info = webBook.getBookInfo(book)
val info = webBook.getBookInfo(scope, book)
.onSuccess {
log(debugSource, "︽详情页解析完成")
log(debugSource, showTime = false)
tocDebug(webBook, book)
tocDebug(scope, webBook, book)
}
.onError {
log(debugSource, it.msg, state = -1)
@ -193,15 +194,15 @@ object Debug {
tasks.add(info)
}
private fun tocDebug(webBook: WebBook, book: Book) {
private fun tocDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book)
val chapterList = webBook.getChapterList(scope, book)
.onSuccess {
if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(webBook, book, it[0], nextChapterUrl)
contentDebug(scope, webBook, book, it[0], nextChapterUrl)
} else {
log(debugSource, "︽目录列表为空", state = -1)
}
@ -213,13 +214,14 @@ object Debug {
}
private fun contentDebug(
scope: CoroutineScope,
webBook: WebBook,
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String?
) {
log(debugSource, "︾开始解析正文页")
val content = webBook.getContent(book, bookChapter, nextChapterUrl)
val content = webBook.getContent(scope, book, bookChapter, nextChapterUrl)
.onSuccess {
log(debugSource, "︽正文页解析完成", state = 1000)
}

@ -62,10 +62,10 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
fun setBaseUrl(baseUrl: String?): AnalyzeRule {
baseUrl?.let {
this.baseUrl = baseUrl
try {
kotlin.runCatching {
baseURL = URL(baseUrl.substringBefore(","))
} catch (e: Exception) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
}
}
return this
@ -229,9 +229,9 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
}
}
if (result == null) result = ""
val str = try {
val str = kotlin.runCatching {
Entities.unescape(result.toString())
} catch (e: Exception) {
}.getOrElse {
result.toString()
}
if (isUrl) {
@ -653,13 +653,15 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
* js实现跨域访问,不能删
*/
override fun ajax(urlStr: String): String? {
return try {
val analyzeUrl = AnalyzeUrl(urlStr, book = book)
runBlocking {
return runBlocking {
kotlin.runCatching {
val analyzeUrl = AnalyzeUrl(urlStr, book = book)
analyzeUrl.getStrResponse(urlStr).body
}.onFailure {
it.printStackTrace()
}.getOrElse {
it.msg
}
} catch (e: Exception) {
e.localizedMessage
}
}

@ -358,6 +358,7 @@ class AnalyzeUrl(
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
}
}

@ -12,6 +12,7 @@ import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -19,13 +20,13 @@ import kotlin.coroutines.resumeWithException
object BookChapterList {
suspend fun analyzeChapterList(
coroutineScope: CoroutineScope,
scope: CoroutineScope,
book: Book,
body: String?,
bookSource: BookSource,
baseUrl: String
): List<BookChapter> = suspendCancellableCoroutine { block ->
try {
kotlin.runCatching {
val chapterList = ArrayList<BookChapter>()
body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
@ -45,7 +46,7 @@ object BookChapterList {
}
var chapterData =
analyzeChapterList(
book, baseUrl, body, tocRule, listRule, bookSource, log = true
scope, book, baseUrl, body, tocRule, listRule, bookSource, log = true
)
chapterData.chapterList?.let {
chapterList.addAll(it)
@ -55,7 +56,7 @@ object BookChapterList {
block.resume(finish(book, chapterList, reverse))
}
1 -> {
Coroutine.async(scope = coroutineScope) {
Coroutine.async(scope = scope) {
var nextUrl = chapterData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
nextUrlList.add(nextUrl)
@ -63,18 +64,15 @@ object BookChapterList {
ruleUrl = nextUrl,
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl)
.body?.let { nextBody ->
chapterData = analyzeChapterList(
book, nextUrl, nextBody, tocRule, listRule, bookSource
)
nextUrl = if (chapterData.nextUrl.isNotEmpty()) {
chapterData.nextUrl[0]
} else ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
chapterData = analyzeChapterList(
this, book, nextUrl, nextBody, tocRule, listRule, bookSource
)
nextUrl = chapterData.nextUrl.firstOrNull() ?: ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
block.resume(finish(book, chapterList, reverse))
@ -94,29 +92,21 @@ object BookChapterList {
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
for (item in chapterDataList) {
downloadToc(
coroutineScope,
item,
book,
bookSource,
tocRule,
listRule,
chapterList,
chapterDataList,
{
block.resume(finish(book, chapterList, reverse))
}, {
block.cancel(it)
})
scope, item, book, bookSource,
tocRule, listRule, chapterList, chapterDataList
) {
block.resume(finish(book, chapterList, reverse))
}
}
}
}
} catch (e: Exception) {
block.resumeWithException(e)
}.onFailure {
block.resumeWithException(it)
}
}
private fun downloadToc(
coroutineScope: CoroutineScope,
scope: CoroutineScope,
chapterData: ChapterData<String>,
book: Book,
bookSource: BookSource,
@ -124,10 +114,9 @@ object BookChapterList {
listRule: String,
chapterList: ArrayList<BookChapter>,
chapterDataList: ArrayList<ChapterData<String>>,
onFinish: () -> Unit,
onError: (e: Throwable) -> Unit
onFinish: () -> Unit
) {
Coroutine.async(scope = coroutineScope) {
Coroutine.async(scope = scope) {
val nextBody = AnalyzeUrl(
ruleUrl = chapterData.nextUrl,
book = book,
@ -135,8 +124,7 @@ object BookChapterList {
).getStrResponse(bookSource.bookSourceUrl).body
?: throw Exception("${chapterData.nextUrl}, 下载失败")
val nextChapterData = analyzeChapterList(
book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource,
false
this, book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource, false
)
synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish(
@ -154,7 +142,7 @@ object BookChapterList {
}
}
}.onError {
onError(it)
throw it
}
}
@ -200,6 +188,7 @@ object BookChapterList {
}
private fun analyzeChapterList(
scope: CoroutineScope,
book: Book,
baseUrl: String,
body: String,
@ -232,6 +221,7 @@ object BookChapterList {
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
val elements = analyzeRule.getElements(listRule)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
scope.ensureActive()
if (elements.isNotEmpty()) {
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
@ -239,6 +229,7 @@ object BookChapterList {
val update = analyzeRule.splitSourceRule(tocRule.updateTime)
var isVip: String?
for (item in elements) {
scope.ensureActive()
analyzeRule.setContent(item)
val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = baseUrl)
analyzeRule.chapter = bookChapter

@ -19,7 +19,7 @@ object BookContent {
@Throws(Exception::class)
suspend fun analyzeContent(
coroutineScope: CoroutineScope,
scope: CoroutineScope,
body: String?,
book: Book,
bookChapter: BookChapter,
@ -73,7 +73,7 @@ object BookContent {
contentDataList.add(ContentData(nextUrl = item))
}
for (item in contentDataList) {
withContext(coroutineScope.coroutineContext) {
withContext(scope.coroutineContext) {
AnalyzeUrl(
ruleUrl = item.nextUrl,
book = book,

@ -10,11 +10,14 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
object BookInfo {
@Throws(Exception::class)
fun analyzeBookInfo(
scope: CoroutineScope,
book: Book,
body: String?,
bookSource: BookSource,
@ -30,11 +33,13 @@ object BookInfo {
analyzeRule.setContent(body).setBaseUrl(baseUrl)
infoRule.init?.let {
if (it.isNotBlank()) {
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it))
}
}
val mCanReName = canReName && !infoRule.canReName.isNullOrBlank()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let {
if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) {
@ -42,6 +47,7 @@ object BookInfo {
}
Debug.log(bookSource.bookSourceUrl, "${it}")
}
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let {
if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) {
@ -49,6 +55,7 @@ object BookInfo {
}
Debug.log(bookSource.bookSourceUrl, "${it}")
}
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
analyzeRule.getStringList(infoRule.kind)
?.joinToString(",")
@ -56,27 +63,31 @@ object BookInfo {
if (it.isNotEmpty()) book.kind = it
}
Debug.log(bookSource.bookSourceUrl, "${book.kind}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let {
if (it.isNotEmpty()) book.wordCount = it
}
Debug.log(bookSource.bookSourceUrl, "${book.wordCount}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
analyzeRule.getString(infoRule.lastChapter).let {
if (it.isNotEmpty()) book.latestChapterTitle = it
}
Debug.log(bookSource.bookSourceUrl, "${book.latestChapterTitle}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
analyzeRule.getString(infoRule.intro).let {
if (it.isNotEmpty()) book.intro = it.htmlFormat()
}
Debug.log(bookSource.bookSourceUrl, "${book.intro}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
analyzeRule.getString(infoRule.coverUrl).let {
if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
}
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true)
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl

@ -12,9 +12,8 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.ensureActive
object BookList {
@ -36,10 +35,10 @@ object BookList {
)
)
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(variableBook)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
bookSource.bookUrlPattern?.let {
scope.ensureActive()
if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable)
@ -67,6 +66,7 @@ object BookList {
}
Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表")
collections = analyzeRule.getElements(ruleList)
scope.ensureActive()
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable)
@ -85,7 +85,6 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem(
scope,
item,
@ -134,37 +133,37 @@ object BookList {
with(bookSource.getBookInfoRule()) {
init?.let {
if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it))
}
}
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(name))
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = wordCountFormat(analyzeRule.getString(wordCount))
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}")
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -199,38 +198,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook
analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName))
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount))
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
}
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) {

@ -0,0 +1,38 @@
package io.legado.app.model.webBook
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
/**
* 精准搜索
*/
object PreciseSearch {
suspend fun searchFirstBook(
scope: CoroutineScope,
bookSources: List<BookSource>,
name: String,
author: String
): Book? {
bookSources.forEach { bookSource ->
val webBook = WebBook(bookSource)
kotlin.runCatching {
if (!scope.isActive) return null
webBook.searchBookAwait(scope, name).firstOrNull {
it.name == name && it.author == author
}?.let {
return if (it.tocUrl.isBlank()) {
if (!scope.isActive) return null
webBook.getBookInfoAwait(scope, it.toBook())
} else {
it.toBook()
}
}
}
}
return null
}
}

@ -68,9 +68,9 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C
searchIndex++
val source = bookSourceList[searchIndex]
val task = WebBook(source).searchBook(
scope,
searchKey,
searchPage,
scope = scope,
context = searchPool!!
).timeout(30000L)
.onSuccess(IO) {

@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
@Suppress("MemberVisibilityCanBePrivate")
class WebBook(val bookSource: BookSource) {
val sourceUrl: String
@ -20,17 +21,17 @@ class WebBook(val bookSource: BookSource) {
* 搜索
*/
fun searchBook(
scope: CoroutineScope,
key: String,
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO,
): Coroutine<ArrayList<SearchBook>> {
return Coroutine.async(scope, context) {
searchBookSuspend(scope, key, page)
searchBookAwait(scope, key, page)
}
}
suspend fun searchBookSuspend(
suspend fun searchBookAwait(
scope: CoroutineScope,
key: String,
page: Int? = 1,
@ -63,94 +64,111 @@ class WebBook(val bookSource: BookSource) {
* 发现
*/
fun exploreBook(
scope: CoroutineScope,
url: String,
page: Int? = 1,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO,
): Coroutine<List<SearchBook>> {
val variableBook = SearchBook()
return Coroutine.async(scope, context) {
val analyzeUrl = AnalyzeUrl(
ruleUrl = url,
page = page,
baseUrl = sourceUrl,
book = variableBook,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
variableBook,
false
)
exploreBookAwait(scope, url, page)
}
}
suspend fun exploreBookAwait(
scope: CoroutineScope = Coroutine.DEFAULT,
url: String,
page: Int? = 1,
): ArrayList<SearchBook> {
val variableBook = SearchBook()
val analyzeUrl = AnalyzeUrl(
ruleUrl = url,
page = page,
baseUrl = sourceUrl,
book = variableBook,
headerMapF = bookSource.getHeaderMap()
)
val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
return BookList.analyzeBookList(
scope,
res.body,
bookSource,
analyzeUrl,
res.url,
variableBook,
false
)
}
/**
* 书籍信息
*/
fun getBookInfo(
scope: CoroutineScope,
book: Book,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO,
canReName: Boolean = true,
): Coroutine<Book> {
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml
BookInfo.analyzeBookInfo(book, book.infoHtml, bookSource, book.bookUrl, canReName)
} else {
val res = AnalyzeUrl(
ruleUrl = book.bookUrl,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap(),
book = book
).getStrResponse(bookSource.bookSourceUrl)
BookInfo.analyzeBookInfo(book, res.body, bookSource, book.bookUrl, canReName)
}
book
getBookInfoAwait(scope, book, canReName)
}
}
suspend fun getBookInfoAwait(
scope: CoroutineScope = Coroutine.DEFAULT,
book: Book,
canReName: Boolean = true,
): Book {
book.type = bookSource.bookSourceType
if (!book.infoHtml.isNullOrEmpty()) {
book.infoHtml
BookInfo.analyzeBookInfo(
scope,
book,
book.infoHtml,
bookSource,
book.bookUrl,
canReName
)
} else {
val res = AnalyzeUrl(
ruleUrl = book.bookUrl,
baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap(),
book = book
).getStrResponse(bookSource.bookSourceUrl)
BookInfo.analyzeBookInfo(scope, book, res.body, bookSource, book.bookUrl, canReName)
}
return book
}
/**
* 目录
*/
fun getChapterList(
scope: CoroutineScope,
book: Book,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<List<BookChapter>> {
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
BookChapterList.analyzeChapterList(
this,
book,
book.tocHtml,
bookSource,
book.tocUrl
)
} else {
val res = AnalyzeUrl(
book = book,
ruleUrl = book.tocUrl,
baseUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl)
BookChapterList.analyzeChapterList(
this,
book,
res.body,
bookSource,
book.tocUrl
)
}
getChapterListAwait(scope, book)
}
}
suspend fun getChapterListAwait(
scope: CoroutineScope = Coroutine.DEFAULT,
book: Book,
): List<BookChapter> {
book.type = bookSource.bookSourceType
return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
BookChapterList.analyzeChapterList(scope, book, book.tocHtml, bookSource, book.tocUrl)
} else {
val res = AnalyzeUrl(
book = book,
ruleUrl = book.tocUrl,
baseUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl)
BookChapterList.analyzeChapterList(scope, book, res.body, bookSource, book.tocUrl)
}
}
@ -158,27 +176,22 @@ class WebBook(val bookSource: BookSource) {
* 章节内容
*/
fun getContent(
scope: CoroutineScope,
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
getContentSuspend(
book, bookChapter, nextChapterUrl, scope
)
getContentAwait(scope, book, bookChapter, nextChapterUrl)
}
}
/**
* 章节内容
*/
suspend fun getContentSuspend(
suspend fun getContentAwait(
scope: CoroutineScope,
book: Book,
bookChapter: BookChapter,
nextChapterUrl: String? = null,
scope: CoroutineScope = Coroutine.DEFAULT
): String {
if (bookSource.getContentRule().content.isNullOrEmpty()) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")

@ -120,7 +120,7 @@ class AudioPlayService : BaseService(),
private fun play() {
upNotification()
if (requestFocus()) {
try {
kotlin.runCatching {
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
mediaPlayer.reset()
@ -130,10 +130,10 @@ class AudioPlayService : BaseService(),
mediaPlayer.setDataSource(this, uri, analyzeUrl.headerMap)
mediaPlayer.prepareAsync()
handler.removeCallbacks(mpRunnable)
} catch (e: Exception) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
launch {
toast("$url ${e.localizedMessage}")
toast("$url ${it.localizedMessage}")
stopSelf()
}
}
@ -270,7 +270,7 @@ class AudioPlayService : BaseService(),
val book = AudioPlay.book
val webBook = AudioPlay.webBook
if (book != null && webBook != null) {
webBook.getContent(book, chapter, scope = this@AudioPlayService)
webBook.getContent(this@AudioPlayService, book, chapter)
.onSuccess { content ->
if (content.isEmpty()) {
withContext(Main) {

@ -184,12 +184,8 @@ class CacheBookService : BaseService() {
return@async
}
if (!BookHelp.hasContent(book, bookChapter)) {
webBook.getContent(
book,
bookChapter,
scope = this,
context = searchPool
).timeout(60000L)
webBook.getContent(this, book, bookChapter, context = searchPool)
.timeout(60000L)
.onError {
synchronized(this) {
downloadingList.remove(bookChapter.url)

@ -10,6 +10,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService
import io.legado.app.utils.msg
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
@ -61,6 +62,7 @@ object CacheBook {
}
fun download(
scope: CoroutineScope,
webBook: WebBook,
book: Book,
chapter: BookChapter,
@ -73,7 +75,7 @@ object CacheBook {
downloadMap[book.bookUrl] = CopyOnWriteArraySet()
}
downloadMap[book.bookUrl]?.add(chapter.index)
webBook.getContent(book, chapter)
webBook.getContent(scope, book, chapter)
.onSuccess { content ->
if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.contentLoadFinish(

@ -50,7 +50,7 @@ class CheckSource(val source: BookSource) {
): Coroutine<*> {
val webBook = WebBook(source)
return webBook
.searchBook(keyword, scope = scope, context = context)
.searchBook(scope, keyword, context = context)
.timeout(60000L)
.onError(Dispatchers.IO) {
source.addGroup("失效")

@ -133,10 +133,12 @@ object ReadBook {
callBack?.upContent()
}
loadContent(durChapterIndex.plus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) {
for (i in 2..10) {
delay(100)
download(durChapterIndex + i)
if (AppConfig.preDownload) {
GlobalScope.launch(Dispatchers.IO) {
for (i in 2..9) {
delay(1000)
download(durChapterIndex + i)
}
}
}
}
@ -163,10 +165,12 @@ object ReadBook {
callBack?.upContent()
}
loadContent(durChapterIndex.minus(1), upContent, false)
GlobalScope.launch(Dispatchers.IO) {
for (i in -5..-2) {
delay(100)
download(durChapterIndex + i)
if (AppConfig.preDownload) {
GlobalScope.launch(Dispatchers.IO) {
for (i in 2..9) {
delay(1000)
download(durChapterIndex - i)
}
}
}
}
@ -282,7 +286,7 @@ object ReadBook {
val book = book
val webBook = webBook
if (book != null && webBook != null) {
CacheBook.download(webBook, book, chapter)
CacheBook.download(Coroutine.DEFAULT, webBook, book, chapter)
} else if (book != null) {
contentLoadFinish(
book, chapter, "没有书源", resetPageOffset = resetPageOffset

@ -30,7 +30,7 @@ class AboutActivity : BaseActivity<ActivityAboutBinding>() {
.replace(R.id.fl_fragment, aboutFragment, fTag)
.commit()
binding.tvAppSummary.post {
try {
kotlin.runCatching {
val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(binding.tvAppSummary.text)
val gzh = getString(R.string.legado_gzh)
@ -40,8 +40,6 @@ class AboutActivity : BaseActivity<ActivityAboutBinding>() {
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
binding.tvAppSummary.text = spannableString
} catch (e: Exception) {
e.printStackTrace()
}
}
}

@ -14,6 +14,7 @@ import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendMail
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
class AboutFragment : PreferenceFragmentCompat() {
@ -93,12 +94,13 @@ class AboutFragment : PreferenceFragmentCompat() {
Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D$key")
// 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return try {
kotlin.runCatching {
startActivity(intent)
true
} catch (e: java.lang.Exception) {
false
return true
}.onFailure {
toast("添加失败,请手动添加")
}
return false
}
}

@ -53,7 +53,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) {
execute {
AudioPlay.webBook?.getBookInfo(book, this)
AudioPlay.webBook?.getBookInfo(this, book)
?.onSuccess {
loadChapterList(book, changeDruChapterIndex)
}
@ -65,7 +65,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) {
execute {
AudioPlay.webBook?.getChapterList(book, this)
AudioPlay.webBook?.getChapterList(this, book)
?.onSuccess(Dispatchers.IO) { cList ->
if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) {

@ -82,7 +82,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_download -> launch(IO) {
R.id.menu_download -> {
if (adapter.downloadMap.isNullOrEmpty()) {
adapter.getItems().forEach { book ->
CacheBook.start(

@ -98,7 +98,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
searchIndex++
val source = bookSourceList[searchIndex]
val task = WebBook(source)
.searchBook(name, scope = this, context = searchPool!!)
.searchBook(this, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(Dispatchers.IO) {
if (it.isNotEmpty()) {

@ -123,7 +123,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
val source = bookSourceList[searchIndex]
val webBook = WebBook(source)
val task = webBook
.searchBook(name, scope = this, context = searchPool!!)
.searchBook(this, name, context = searchPool!!)
.timeout(60000L)
.onSuccess(IO) {
it.forEach { searchBook ->
@ -159,7 +159,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
private fun loadBookInfo(webBook: WebBook, book: Book) {
webBook.getBookInfo(book, this)
webBook.getBookInfo(this, book)
.onSuccess {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadBookToc(webBook, book)
@ -175,7 +175,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
private fun loadBookToc(webBook: WebBook, book: Book) {
webBook.getChapterList(book, this)
webBook.getChapterList(this, book)
.onSuccess(IO) { chapters ->
if (chapters.isNotEmpty()) {
book.latestChapterTitle = chapters.last().title

@ -34,7 +34,7 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
val source = bookSource
val url = exploreUrl
if (source != null && url != null) {
WebBook(source).exploreBook(url, page, this)
WebBook(source).exploreBook(this, url, page)
.timeout(30000L)
.onSuccess(IO) { searchBooks ->
booksData.postValue(searchBooks)

@ -57,7 +57,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
loadChapter(book, changeDruChapterIndex)
} else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this, canReName = canReName)
WebBook(bookSource).getBookInfo(this, book, canReName = canReName)
.onSuccess(IO) {
bookData.postValue(book)
if (inBookshelf) {
@ -88,7 +88,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
} else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this)
WebBook(bookSource).getChapterList(this, book)
.onSuccess(IO) {
if (it.isNotEmpty()) {
if (inBookshelf) {

@ -21,6 +21,7 @@ import io.legado.app.constant.Status
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress
import io.legado.app.data.entities.Bookmark
import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig
import io.legado.app.help.storage.Backup
@ -204,6 +205,21 @@ class ReadBookActivity : ReadBookBaseActivity(),
}
}
R.id.menu_download -> showDownloadDialog()
R.id.menu_add_bookmark -> {
val book = ReadBook.book
val page = ReadBook.curTextChapter?.page(ReadBook.durPageIndex())
if (book != null && page != null) {
val bookmark = Bookmark(
bookUrl = book.bookUrl,
bookName = book.name,
chapterIndex = ReadBook.durChapterIndex,
chapterPos = ReadBook.durChapterPos,
chapterName = page.title,
bookText = page.text.trim()
)
showBookMark(bookmark)
}
}
R.id.menu_copy_text ->
TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent())
R.id.menu_update_toc -> ReadBook.book?.let {

@ -13,6 +13,7 @@ import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.ActivityBookReadBinding
import io.legado.app.databinding.DialogBookmarkBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig
@ -200,18 +201,18 @@ abstract class ReadBookBaseActivity :
@SuppressLint("InflateParams")
fun showBookMark(bookmark: Bookmark) {
val book = ReadBook.book ?: return
val textChapter = ReadBook.curTextChapter ?: return
alert(title = getString(R.string.bookmark_add)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.note_content)
message = bookmark.chapterName
val alertBinding = DialogBookmarkBinding.inflate(layoutInflater).apply {
editBookText.setText(bookmark.bookText)
editView.setText(bookmark.content)
}
message = bookmark.bookText
customView = alertBinding.root
yesButton {
alertBinding.editView.text?.toString()?.let { editContent ->
alertBinding.apply {
Coroutine.async {
bookmark.content = editContent
bookmark.bookText = editBookText.text.toString()
bookmark.content = editView.text.toString()
App.db.bookmarkDao.insert(bookmark)
}
}

@ -13,10 +13,12 @@ import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.storage.BookWebDav
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.PreciseSearch
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.msg
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
@ -109,7 +111,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (book.isLocalBook()) {
loadChapterList(book, changeDruChapterIndex)
} else {
ReadBook.webBook?.getBookInfo(book, this, canReName = false)
ReadBook.webBook?.getBookInfo(this, book, canReName = false)
?.onSuccess {
loadChapterList(book, changeDruChapterIndex)
}
@ -138,7 +140,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
}
} else {
ReadBook.webBook?.getChapterList(book, this)
ReadBook.webBook?.getChapterList(this, book)
?.onSuccess(IO) { cList ->
if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) {
@ -213,24 +215,18 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun autoChangeSource(name: String, author: String) {
if (!AppConfig.autoChangeSource) return
execute {
App.db.bookSourceDao.allTextEnabled.forEach { source ->
try {
WebBook(source)
.searchBookSuspend(this, name)
.getOrNull(0)?.let {
if (it.name == name && (it.author == author || author == "")) {
val book = it.toBook()
book.upInfoFromOld(ReadBook.book)
changeTo(book)
return@execute
}
}
} catch (e: Exception) {
//nothing
}
val sources = App.db.bookSourceDao.allTextEnabled
val book = PreciseSearch.searchFirstBook(this, sources, name, author)
if (book != null) {
book.upInfoFromOld(ReadBook.book)
changeTo(book)
} else {
throw Exception("自动换源失败")
}
}.onStart {
ReadBook.upMsg(context.getString(R.string.source_auto_changing))
}.onError {
toast(it.msg)
}.onFinally {
ReadBook.upMsg(null)
}

@ -130,7 +130,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
readAloud(callBack.selectedText)
}
R.id.menu_browser -> {
try {
kotlin.runCatching {
val intent = if (callBack.selectedText.isAbsUrl()) {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(callBack.selectedText)
@ -141,9 +141,9 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
}
}
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
context.toast(e.localizedMessage ?: "ERROR")
}.onFailure {
it.printStackTrace()
context.toast(it.localizedMessage ?: "ERROR")
}
}
else -> item.intent?.let {
@ -210,7 +210,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
*/
@RequiresApi(Build.VERSION_CODES.M)
private fun onInitializeMenu(menu: Menu) {
try {
kotlin.runCatching {
var menuItemOrder = 100
for (resolveInfo in getSupportedActivities()) {
menu.add(
@ -218,8 +218,8 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
menuItemOrder++, resolveInfo.loadLabel(context.packageManager)
).intent = createProcessTextIntentForResolveInfo(resolveInfo)
}
} catch (e: Exception) {
context.toast("获取文字操作菜单出错:${e.localizedMessage}")
}.onFailure {
context.toast("获取文字操作菜单出错:${it.localizedMessage}")
}
}

@ -21,6 +21,7 @@ import io.legado.app.databinding.DialogHttpTtsEditBinding
import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemHttpTtsBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryColor
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.widget.dialog.TextDialog
@ -59,6 +60,7 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
private fun initView() = with(binding) {
toolBar.setBackgroundColor(primaryColor)
toolBar.setTitle(R.string.speak_engine)
ATH.applyEdgeEffectColor(recyclerView)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
adapter = Adapter(requireContext())
recyclerView.adapter = adapter

@ -14,9 +14,7 @@ class SpeakEngineViewModel(application: Application) : BaseViewModel(application
fun importDefault() {
execute {
DefaultData.httpTTS.let {
App.db.httpTTSDao.insert(*it.toTypedArray())
}
DefaultData.importDefaultHttpTTS()
}
}

@ -338,10 +338,10 @@ class ReadView(context: Context, attrs: AttributeSet) :
}
end -= 1
}
try {
kotlin.runCatching {
curPage.selectStartMoveIndex(firstRelativePage, lineStart, start)
curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end)
} catch (e: Exception) {
}.onFailure {
print(
"""
curPage.selectStartMoveIndex($firstRelativePage, $lineStart, $start)

@ -13,6 +13,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine
@ -118,7 +119,7 @@ object ChapterProvider {
return TextChapter(
bookChapter.index, bookChapter.title,
bookChapter.getAbsoluteURL().substringBefore(","),
bookChapter.getAbsoluteURL().split(AnalyzeUrl.splitUrlRegex)[0],
textPages, chapterSize
)
}
@ -351,7 +352,7 @@ object ChapterProvider {
}
private fun getTypeface(fontPath: String): Typeface {
return try {
return kotlin.runCatching {
when {
fontPath.isContentScheme() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
val fd = App.INSTANCE.contentResolver
@ -369,7 +370,7 @@ object ChapterProvider {
else -> Typeface.SANS_SERIF
}
}
} catch (e: Exception) {
}.getOrElse {
ReadBookConfig.textFont = ""
ReadBookConfig.save()
Typeface.SANS_SERIF

@ -160,6 +160,13 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
binding.recyclerView.scrollToPosition(0)
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
if (toPosition == 0) {
binding.recyclerView.scrollToPosition(0)
}
}
})
loadMoreView = LoadMoreView(this)
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

@ -28,11 +28,14 @@ class BookSourceDebugModel(application: Application) : BaseViewModel(application
}
fun startDebug(key: String, start: (() -> Unit)? = null, error: (() -> Unit)? = null) {
webBook?.let {
execute {
Debug.callback = this@BookSourceDebugModel
Debug.startDebug(this, webBook!!, key)
}.onStart {
start?.invoke()
Debug.callback = this
Debug.startDebug(it, key)
} ?: error?.invoke()
}.onError {
error?.invoke()
}
}
override fun printLog(state: Int, msg: String) {

@ -12,7 +12,6 @@ import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import io.legado.app.App
import io.legado.app.R
@ -140,14 +139,13 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
R.id.menu_help -> showHelp()
}
if (item.groupId == R.id.source_group) {
searchView.setQuery(item.title, true)
searchView.setQuery("group:${item.title}", true)
}
return super.onCompatOptionsItemSelected(item)
}
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(binding.recyclerView)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.addItemDecoration(VerticalDivider(this))
adapter = BookSourceAdapter(this, this)
binding.recyclerView.adapter = adapter
@ -182,6 +180,10 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
searchKey == getString(R.string.disabled) -> {
App.db.bookSourceDao.liveDataDisabled()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.bookSourceDao.liveDataGroupSearch("%$key%")
}
else -> {
App.db.bookSourceDao.liveDataSearch("%$searchKey%")
}
@ -206,7 +208,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
Sort.Update -> data.sortedBy { it.lastUpdateTime }
else -> data.reversed()
}
adapter.setItems(sourceList)
adapter.setItems(sourceList, adapter.diffItemCallback)
})
}

@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
@ -27,6 +28,51 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
private val selected = linkedSetOf<BookSource>()
val diffItemCallback: DiffUtil.ItemCallback<BookSource>
get() = object : DiffUtil.ItemCallback<BookSource>() {
override fun areItemsTheSame(oldItem: BookSource, newItem: BookSource): Boolean {
return oldItem.bookSourceUrl == newItem.bookSourceUrl
}
override fun areContentsTheSame(oldItem: BookSource, newItem: BookSource): Boolean {
if (oldItem.bookSourceName != newItem.bookSourceName)
return false
if (oldItem.bookSourceGroup != newItem.bookSourceGroup)
return false
if (oldItem.enabled != newItem.enabled)
return false
if (oldItem.enabledExplore != newItem.enabledExplore
|| oldItem.exploreUrl != newItem.exploreUrl
) {
return false
}
return true
}
override fun getChangePayload(oldItem: BookSource, newItem: BookSource): Any? {
val payload = Bundle()
if (oldItem.bookSourceName != newItem.bookSourceName) {
payload.putString("name", newItem.bookSourceName)
}
if (oldItem.bookSourceGroup != newItem.bookSourceGroup) {
payload.putString("group", newItem.bookSourceGroup)
}
if (oldItem.enabled != newItem.enabled) {
payload.putBoolean("enabled", newItem.enabled)
}
if (oldItem.enabledExplore != newItem.enabledExplore
|| oldItem.exploreUrl != newItem.exploreUrl
) {
payload.putBoolean("showExplore", true)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
override fun getViewBinding(parent: ViewGroup): ItemBookSourceBinding {
return ItemBookSourceBinding.inflate(inflater, parent, false)
}

@ -13,8 +13,9 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogBookmarkBinding
import io.legado.app.databinding.FragmentBookmarkBinding
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.widget.recycler.VerticalDivider
@ -84,18 +85,19 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
@SuppressLint("InflateParams")
override fun onLongClick(bookmark: Bookmark) {
requireContext().alert(R.string.bookmark) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
message = bookmark.bookText
customView {
alertBinding.apply {
editView.setHint(R.string.note_content)
editView.setText(bookmark.content)
}.root
message = bookmark.chapterName
val alertBinding = DialogBookmarkBinding.inflate(layoutInflater).apply {
editBookText.setText(bookmark.bookText)
editView.setText(bookmark.content)
}
customView = alertBinding.root
yesButton {
alertBinding.editView.text?.toString()?.let { editContent ->
bookmark.content = editContent
App.db.bookmarkDao.update(bookmark)
alertBinding.apply {
Coroutine.async {
bookmark.bookText = editBookText.text.toString()
bookmark.content = editView.text.toString()
App.db.bookmarkDao.insert(bookmark)
}
}
}
noButton()

@ -30,11 +30,10 @@ object FilePicker {
items(selectList) { _, index ->
when (index) {
0 -> {
try {
kotlin.runCatching {
val intent = createSelectDirIntent()
activity.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}.onFailure {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
@ -81,11 +80,10 @@ object FilePicker {
items(selectList) { _, index ->
when (index) {
0 -> {
try {
kotlin.runCatching {
val intent = createSelectDirIntent()
fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}.onFailure {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,
@ -133,15 +131,14 @@ object FilePicker {
items(selectList) { _, index ->
when (index) {
0 -> {
try {
kotlin.runCatching {
val intent = createSelectFileIntent()
intent.putExtra(
Intent.EXTRA_MIME_TYPES,
typesOfExtensions(allowExtensions)
)
activity.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}.onFailure {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
@ -191,15 +188,14 @@ object FilePicker {
items(selectList) { _, index ->
when (index) {
0 -> {
try {
kotlin.runCatching {
val intent = createSelectFileIntent()
intent.putExtra(
Intent.EXTRA_MIME_TYPES,
typesOfExtensions(allowExtensions)
)
fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}.onFailure {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,

@ -34,15 +34,13 @@ open class JavaBean : Serializable {
val fields = list.toTypedArray()
for (field in fields) {
val fieldName = field.name
try {
kotlin.runCatching {
val obj = field.get(this)
sb.append(fieldName)
sb.append("=")
sb.append(obj)
sb.append("\n")
} catch (ignored: IllegalAccessException) {
}
}
return sb.toString()
}

@ -6,7 +6,6 @@ import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.text.DecimalFormat
@ -24,11 +23,9 @@ object ConvertUtils {
const val KB: Long = 1024
fun toInt(obj: Any): Int {
return try {
return kotlin.runCatching {
Integer.parseInt(obj.toString())
} catch (e: NumberFormatException) {
-1
}
}.getOrDefault(-1)
}
fun toInt(bytes: ByteArray): Int {
@ -42,11 +39,9 @@ object ConvertUtils {
}
fun toFloat(obj: Any): Float {
return try {
return kotlin.runCatching {
java.lang.Float.parseFloat(obj.toString())
} catch (e: NumberFormatException) {
-1f
}
}.getOrDefault(-1f)
}
fun toString(objects: Array<Any>, tag: String): String {
@ -62,7 +57,7 @@ object ConvertUtils {
fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? {
var bitmap: Bitmap? = null
if (bytes.isNotEmpty()) {
try {
kotlin.runCatching {
val options = BitmapFactory.Options()
// 设置让解码器以最佳方式解码
options.inPreferredConfig = null
@ -72,7 +67,6 @@ object ConvertUtils {
}
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
bitmap!!.density = 96// 96 dpi
} catch (e: Exception) {
}
}
return bitmap
@ -101,7 +95,7 @@ object ConvertUtils {
@JvmOverloads
fun toString(`is`: InputStream, charset: String = "utf-8"): String {
val sb = StringBuilder()
try {
kotlin.runCatching {
val reader = BufferedReader(InputStreamReader(`is`, charset))
while (true) {
val line = reader.readLine()
@ -113,9 +107,7 @@ object ConvertUtils {
}
reader.close()
`is`.close()
} catch (e: IOException) {
}
return sb.toString()
}

@ -9,6 +9,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.DefaultData
import io.legado.app.help.LocalConfig
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.CacheBook
import io.legado.app.utils.FileUtils
@ -74,13 +75,15 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
}
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
val webBook = WebBook(bookSource)
webBook.getChapterList(book, context = upTocPool)
webBook.getChapterList(this, book, context = upTocPool)
.timeout(300000)
.onSuccess(IO) {
App.db.bookDao.update(book)
App.db.bookChapterDao.delByBook(book.bookUrl)
App.db.bookChapterDao.insert(*it.toTypedArray())
cacheBook(webBook, book)
if (AppConfig.preDownload) {
cacheBook(webBook, book)
}
}
.onError {
it.printStackTrace()
@ -114,7 +117,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
var addToCache = false
while (!addToCache) {
if (CacheBook.downloadCount() < 5) {
CacheBook.download(webBook, book, chapter)
CacheBook.download(this, webBook, book, chapter)
addToCache = true
} else {
delay(1000)
@ -148,7 +151,12 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
fun upVersion() {
execute {
DefaultData.importDefaultTocRules()
if (LocalConfig.hasUpHttpTTS) {
DefaultData.importDefaultHttpTTS()
}
if (LocalConfig.hasUpTxtTocRule) {
DefaultData.importDefaultTocRules()
}
}
}
}

@ -1,6 +1,8 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -29,17 +31,23 @@ import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import org.jetbrains.anko.share
/**
* 书架界面
*/
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener,
FilePickerDialog.CallBack,
SearchView.OnQueryTextListener {
private val requestCodeImportBookshelf = 312
private val binding by viewBinding(FragmentBookshelfBinding::bind)
override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java)
@ -67,8 +75,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_update_toc -> {
val group = bookGroups[tabLayout.selectedTabPosition]
val fragment = fragmentMap[group.groupId]
val fragment = fragmentMap[selectedGroup.groupId]
fragment?.getBooks()?.let {
activityViewModel.upToc(it)
}
@ -79,18 +86,25 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
R.id.menu_add_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> addBookByUrl()
R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity>(
Pair("groupId", selectedGroup?.groupId ?: 0),
Pair("groupName", selectedGroup?.groupName ?: 0)
Pair("groupId", selectedGroup.groupId ?: 0),
Pair("groupName", selectedGroup.groupName ?: 0)
)
R.id.menu_download -> startActivity<CacheActivity>(
Pair("groupId", selectedGroup?.groupId ?: 0),
Pair("groupName", selectedGroup?.groupName ?: 0)
Pair("groupId", selectedGroup.groupId ?: 0),
Pair("groupName", selectedGroup.groupName ?: 0)
)
R.id.menu_export_bookshelf -> {
val fragment = fragmentMap[selectedGroup.groupId]
viewModel.exportBookshelf(fragment?.getBooks()) {
activity?.share(it)
}
}
R.id.menu_import_bookshelf -> importBookshelfAlert()
}
}
private val selectedGroup: BookGroup?
get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem)
private val selectedGroup: BookGroup
get() = bookGroups[tabLayout.selectedTabPosition]
private fun initView() {
ATH.applyEdgeEffectColor(binding.viewPagerBookshelf)
@ -190,8 +204,8 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
override fun onTabReselected(tab: TabLayout.Tab) {
fragmentMap[selectedGroup?.groupId]?.let {
toast("${selectedGroup?.groupName}(${it.getBooksCount()})")
fragmentMap[selectedGroup.groupId]?.let {
toast("${selectedGroup.groupName}(${it.getBooksCount()})")
}
}
@ -202,7 +216,42 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
fun gotoTop() {
fragmentMap[selectedGroup?.groupId]?.gotoTop()
fragmentMap[selectedGroup.groupId]?.gotoTop()
}
private fun importBookshelfAlert() {
alert(titleResource = R.string.import_bookshelf) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "url/json"
}
customView = alertBinding.root
okButton {
alertBinding.editView.text?.toString()?.let {
viewModel.importBookshelf(it, selectedGroup.groupId)
}
}
noButton()
neutralButton(R.string.select_file) {
FilePicker.selectFile(
this@BookshelfFragment,
requestCodeImportBookshelf,
allowExtensions = arrayOf("txt", "json")
)
}
}.show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeImportBookshelf -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
uri.readText(requireContext())?.let {
viewModel.importBookshelf(it, selectedGroup.groupId)
}
}
}
}
}
private inner class TabFragmentPageAdapter(fm: FragmentManager) :

@ -7,9 +7,13 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource
import io.legado.app.model.webBook.PreciseSearch
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.isActive
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -41,7 +45,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
origin = bookSource.bookSourceUrl,
originName = bookSource.bookSourceName
)
WebBook(bookSource).getBookInfo(book, this)
WebBook(bookSource).getBookInfo(this, book)
.onSuccess(IO) {
it.order = App.db.bookDao.maxOrder + 1
App.db.bookDao.insert(it)
@ -62,6 +66,65 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
}
}
fun exportBookshelf(books: List<Book>?, success: (json: String) -> Unit) {
execute {
val exportList = arrayListOf<Map<String, String?>>()
books?.forEach {
val bookMap = hashMapOf<String, String?>()
bookMap["name"] = it.name
bookMap["author"] = it.author
exportList.add(bookMap)
}
GSON.toJson(exportList)
}.onSuccess {
success(it)
}
}
fun importBookshelf(str: String, groupId: Long) {
execute {
val text = str.trim()
when {
text.isAbsUrl() -> {
RxHttp.get(text).toText().await().let {
importBookshelf(it, groupId)
}
}
text.isJsonArray() -> {
importBookshelfByJson(text, groupId)
}
else -> {
throw Exception("格式不对")
}
}
}.onError {
toast(it.localizedMessage ?: "ERROR")
}
}
private fun importBookshelfByJson(json: String, groupId: Long) {
execute {
val bookSources = App.db.bookSourceDao.allEnabled
GSON.fromJsonArray<Map<String, String?>>(json)?.forEach {
if (!isActive) return@execute
val name = it["name"] ?: ""
val author = it["author"] ?: ""
if (name.isNotEmpty() && App.db.bookDao.getBook(name, author) == null) {
val book = PreciseSearch
.searchFirstBook(this, bookSources, name, author)
book?.let {
if (groupId > 0) {
book.group = groupId
}
App.db.bookDao.insert(book)
}
}
}
}.onFinally {
toast(R.string.success)
}
}
fun checkGroup(groups: List<BookGroup>) {
groups.forEach { group ->
if (group.groupId >= 0 && group.groupId and (group.groupId - 1) != 0L) {

@ -97,7 +97,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
private fun initGroupData() {
liveGroup?.removeObservers(viewLifecycleOwner)
liveGroup = App.db.bookSourceDao.liveGroupExplore()
liveGroup = App.db.bookSourceDao.liveExploreGroup()
liveGroup?.observe(viewLifecycleOwner, {
groups.clear()
it.map { group ->
@ -107,12 +107,19 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
})
}
private fun initExploreData(key: String? = null) {
private fun initExploreData(searchKey: String? = null) {
liveExplore?.removeObservers(viewLifecycleOwner)
liveExplore = if (key.isNullOrBlank()) {
App.db.bookSourceDao.liveExplore()
} else {
App.db.bookSourceDao.liveExplore("%$key%")
liveExplore = when {
searchKey.isNullOrBlank() -> {
App.db.bookSourceDao.liveExplore()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.bookSourceDao.liveGroupExplore("%$key%")
}
else -> {
App.db.bookSourceDao.liveExplore("%$searchKey%")
}
}
liveExplore?.observe(viewLifecycleOwner, {
binding.tvEmptyMsg.isGone = it.isNotEmpty() || searchView.query.isNotEmpty()
@ -135,7 +142,7 @@ class ExploreFragment : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explo
override fun onCompatOptionsItemSelected(item: MenuItem) {
super.onCompatOptionsItemSelected(item)
if (item.groupId == R.id.menu_group_text) {
searchView.setQuery(item.title, true)
searchView.setQuery("group:${item.title}", true)
}
}

@ -1,42 +0,0 @@
package io.legado.app.ui.replace
import android.os.Bundle
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.data.entities.ReplaceRule
class DiffCallBack : DiffUtil.ItemCallback<ReplaceRule>() {
override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
if (oldItem.name != newItem.name) {
return false
}
if (oldItem.group != newItem.group) {
return false
}
if (oldItem.isEnabled != newItem.isEnabled) {
return false
}
return true
}
override fun getChangePayload(oldItem: ReplaceRule, newItem: ReplaceRule): Any? {
val payload = Bundle()
if (oldItem.name != newItem.name) {
payload.putString("name", newItem.name)
}
if (oldItem.group != newItem.group) {
payload.putString("group", newItem.group)
}
if (oldItem.isEnabled != newItem.isEnabled) {
payload.putBoolean("enabled", newItem.isEnabled)
}
if (payload.isEmpty) {
return null
}
return payload
}
}

@ -54,6 +54,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
private val importRequestCode = 132
private val exportRequestCode = 65
private lateinit var adapter: ReplaceRuleAdapter
private lateinit var searchView: SearchView
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null
@ -64,6 +65,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
searchView = binding.titleBar.findViewById(R.id.search_view)
initRecyclerView()
initSearchView()
initSelectActionView()
@ -101,13 +103,11 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}
private fun initSearchView() {
binding.titleBar.findViewById<SearchView>(R.id.search_view).let {
ATH.setTint(it, primaryTextColor)
it.onActionViewExpanded()
it.queryHint = getString(R.string.replace_purify_search)
it.clearFocus()
it.setOnQueryTextListener(this)
}
ATH.setTint(searchView, primaryTextColor)
searchView.onActionViewExpanded()
searchView.queryHint = getString(R.string.replace_purify_search)
searchView.clearFocus()
searchView.setOnQueryTextListener(this)
}
override fun selectAll(selectAll: Boolean) {
@ -140,19 +140,26 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}.show()
}
private fun observeReplaceRuleData(key: String? = null) {
private fun observeReplaceRuleData(searchKey: String? = null) {
dataInit = false
replaceRuleLiveData?.removeObservers(this)
replaceRuleLiveData = if (key.isNullOrEmpty()) {
App.db.replaceRuleDao.liveDataAll()
} else {
App.db.replaceRuleDao.liveDataSearch(key)
replaceRuleLiveData = when {
searchKey.isNullOrEmpty() -> {
App.db.replaceRuleDao.liveDataAll()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.replaceRuleDao.liveDataGroupSearch("%$key%")
}
else -> {
App.db.replaceRuleDao.liveDataSearch("%$searchKey%")
}
}
replaceRuleLiveData?.observe(this, {
if (dataInit) {
setResult(Activity.RESULT_OK)
}
adapter.setItems(it)
adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true
})
}
@ -179,8 +186,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json"))
else -> if (item.groupId == R.id.replace_group) {
binding.titleBar.findViewById<SearchView>(R.id.search_view)
.setQuery(item.title, true)
searchView.setQuery("group:${item.title}", true)
}
}
return super.onCompatOptionsItemSelected(item)
@ -246,13 +252,13 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
when (requestCode) {
importRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
try {
kotlin.runCatching {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportReplaceRuleActivity>("dataKey" to dataKey)
}
} catch (e: Exception) {
toast("readTextError:${e.localizedMessage}")
}.onFailure {
toast("readTextError:${it.localizedMessage}")
}
}
}

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
@ -25,6 +26,43 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) :
private val selected = linkedSetOf<ReplaceRule>()
val diffItemCallBack = object : DiffUtil.ItemCallback<ReplaceRule>() {
override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
if (oldItem.name != newItem.name) {
return false
}
if (oldItem.group != newItem.group) {
return false
}
if (oldItem.isEnabled != newItem.isEnabled) {
return false
}
return true
}
override fun getChangePayload(oldItem: ReplaceRule, newItem: ReplaceRule): Any? {
val payload = Bundle()
if (oldItem.name != newItem.name) {
payload.putString("name", newItem.name)
}
if (oldItem.group != newItem.group) {
payload.putString("group", newItem.group)
}
if (oldItem.isEnabled != newItem.isEnabled) {
payload.putBoolean("enabled", newItem.isEnabled)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
fun selectAll() {
getItems().forEach {
selected.add(it)

@ -12,7 +12,6 @@ import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
@ -96,7 +95,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
R.id.menu_help -> showHelp()
else -> if (item.groupId == R.id.source_group) {
binding.titleBar.findViewById<SearchView>(R.id.search_view)
.setQuery(item.title, true)
.setQuery("group:${item.title}", true)
}
}
return super.onCompatOptionsItemSelected(item)
@ -116,7 +115,6 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
private fun initRecyclerView() {
ATH.applyEdgeEffectColor(binding.recyclerView)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.addItemDecoration(VerticalDivider(this))
adapter = RssSourceAdapter(this, this)
binding.recyclerView.adapter = adapter
@ -199,16 +197,23 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
}
}
private fun initLiveDataSource(key: String? = null) {
private fun initLiveDataSource(searchKey: String? = null) {
sourceLiveData?.removeObservers(this)
sourceLiveData =
if (key.isNullOrBlank()) {
App.db.rssSourceDao.liveAll()
} else {
App.db.rssSourceDao.liveSearch("%$key%")
when {
searchKey.isNullOrBlank() -> {
App.db.rssSourceDao.liveAll()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.rssSourceDao.liveGroupSearch("%$key%")
}
else -> {
App.db.rssSourceDao.liveSearch("%$searchKey%")
}
}
sourceLiveData?.observe(this, {
adapter.setItems(it)
adapter.setItems(it, adapter.diffItemCallback)
})
}
@ -259,13 +264,13 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
when (requestCode) {
importRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
try {
kotlin.runCatching {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportRssSourceActivity>("dataKey" to dataKey)
}
} catch (e: Exception) {
toast("readTextError:${e.localizedMessage}")
}.onFailure {
toast("readTextError:${it.localizedMessage}")
}
}
}

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
@ -23,6 +24,37 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) :
private val selected = linkedSetOf<RssSource>()
val diffItemCallback: DiffUtil.ItemCallback<RssSource>
get() = object : DiffUtil.ItemCallback<RssSource>() {
override fun areItemsTheSame(oldItem: RssSource, newItem: RssSource): Boolean {
return oldItem.sourceUrl == newItem.sourceUrl
}
override fun areContentsTheSame(oldItem: RssSource, newItem: RssSource): Boolean {
return oldItem.sourceName == newItem.sourceName
&& oldItem.sourceGroup == newItem.sourceGroup
&& oldItem.enabled == newItem.enabled
}
override fun getChangePayload(oldItem: RssSource, newItem: RssSource): Any? {
val payload = Bundle()
if (oldItem.sourceName != newItem.sourceName) {
payload.putString("name", newItem.sourceName)
}
if (oldItem.sourceGroup != newItem.sourceGroup) {
payload.putString("group", newItem.sourceGroup)
}
if (oldItem.enabled != newItem.enabled) {
payload.putBoolean("enabled", newItem.enabled)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
override fun getViewBinding(parent: ViewGroup): ItemRssSourceBinding {
return ItemRssSourceBinding.inflate(inflater, parent, false)
}

@ -12,6 +12,8 @@ import io.legado.app.utils.getSize
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.noties.markwon.Markwon
import io.noties.markwon.image.glide.GlideImagesPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.html.HtmlPlugin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -66,6 +68,8 @@ class TextDialog : BaseDialogFragment() {
MD -> binding.textView.post {
Markwon.builder(requireContext())
.usePlugin(GlideImagesPlugin.create(requireContext()))
.usePlugin(HtmlPlugin.create())
.usePlugin(TablePlugin.create(requireContext()))
.build()
.setMarkdown(binding.textView, content)
}

@ -29,7 +29,7 @@ class FontAdapter(context: Context, val callBack: CallBack) :
payloads: MutableList<Any>
) {
with(binding) {
try {
kotlin.runCatching {
val typeface: Typeface? = if (item.isContentPath) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.contentResolver
@ -44,9 +44,9 @@ class FontAdapter(context: Context, val callBack: CallBack) :
Typeface.createFromFile(item.uri.toString())
}
tvFont.typeface = typeface
} catch (e: Exception) {
e.printStackTrace()
context.toast("Read ${item.name} Error: ${e.localizedMessage}")
}.onFailure {
it.printStackTrace()
context.toast("Read ${item.name} Error: ${it.localizedMessage}")
}
tvFont.text = item.name
root.onClick { callBack.onClick(item) }

@ -43,12 +43,9 @@ class BadgeView @JvmOverloads constructor(
return null
}
val text = text.toString()
return try {
return kotlin.runCatching {
Integer.parseInt(text)
} catch (e: NumberFormatException) {
null
}
}.getOrNull()
}
var badgeGravity: Int

@ -69,11 +69,10 @@ inline fun <reified T : Activity> Fragment.startActivity(vararg params: Pair<Str
inline fun <reified T : Activity> Fragment.startActivityForResult(
requestCode: Int,
vararg params: Pair<String, Any?>
) =
startActivityForResult(
AnkoInternals.createIntent(requireActivity(), T::class.java, params),
requestCode
)
) = startActivityForResult(
AnkoInternals.createIntent(requireActivity(), T::class.java, params),
requestCode
)
inline fun <reified T : Service> Fragment.startService(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartService(requireActivity(), T::class.java, params)

@ -3,7 +3,6 @@ package io.legado.app.utils
import android.annotation.SuppressLint
import android.text.TextUtils.isEmpty
import java.text.DecimalFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Matcher
@ -56,7 +55,7 @@ object StringUtils {
@SuppressLint("SimpleDateFormat")
val format = SimpleDateFormat(pattern)
val calendar = Calendar.getInstance()
try {
kotlin.runCatching {
val date = format.parse(source) ?: return ""
val curTime = calendar.timeInMillis
calendar.time = date
@ -95,8 +94,8 @@ object StringUtils {
convertFormat.format(date)
}
}
} catch (e: ParseException) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
}
return ""
@ -106,11 +105,9 @@ object StringUtils {
if (length <= 0) return "0"
val units = arrayOf("b", "kb", "M", "G", "T")
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
val digitGroups =
(log10(length.toDouble()) / log10(1024.0)).toInt()
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
return DecimalFormat("#,##0.##")
.format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
}
@ -175,7 +172,7 @@ object StringUtils {
}
// "一千零二十五", "一千二" 形式
try {
return kotlin.runCatching {
for (i in cn.indices) {
val tmpNum = ChnMap[cn[i]]!!
when {
@ -206,22 +203,18 @@ object StringUtils {
}
}
result += tmp + billion
return result
} catch (e: Exception) {
return -1
}
result
}.getOrDefault(-1)
}
fun stringToInt(str: String?): Int {
if (str != null) {
val num = fullToHalf(str).replace("\\s+".toRegex(), "")
return try {
return kotlin.runCatching {
Integer.parseInt(num)
} catch (e: Exception) {
}.getOrElse {
chineseNumToInt(num)
}
}
return -1
}
@ -306,7 +299,7 @@ object StringUtils {
return sb.toString()
}
fun hexStringToByte(hexString: String): ByteArray? {
fun hexStringToByte(hexString: String): ByteArray {
val hexStr = hexString.replace(" ", "")
val len = hexStr.length
val bytes = ByteArray(len / 2)

@ -19,13 +19,13 @@ object SystemUtils {
fun getScreenOffTime(context: Context): Int {
var screenOffTime = 0
try {
kotlin.runCatching {
screenOffTime = Settings.System.getInt(
context.contentResolver,
Settings.System.SCREEN_OFF_TIMEOUT
)
} catch (e: Exception) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
}
return screenOffTime

@ -0,0 +1,19 @@
package io.legado.app.utils
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Build
import android.widget.Toolbar
import androidx.core.content.ContextCompat
import io.legado.app.R
/**
* 设置toolBar更多图标颜色
*/
fun Toolbar.setMoreIconColor(color: Int) {
val moreIcon = ContextCompat.getDrawable(context, R.drawable.ic_more)
if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
overflowIcon = moreIcon
}
}

@ -1,11 +1,6 @@
package io.legado.app.utils
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Build
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.primaryTextColor
@ -13,19 +8,6 @@ import io.legado.app.lib.theme.primaryTextColor
@Suppress("unused")
object UIUtils {
/** 设置更多工具条图标和颜色 */
fun setToolbarMoreIconCustomColor(toolbar: Toolbar?, color: Int? = null) {
toolbar ?: return
val moreIcon = ContextCompat.getDrawable(toolbar.context, R.drawable.ic_more)
if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (color != null) {
moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
}
toolbar.overflowIcon = moreIcon
}
}
fun getMenuColor(
context: Context,
theme: Theme = Theme.Auto,

@ -107,13 +107,11 @@ fun RadioGroup.checkByIndex(index: Int) {
@SuppressLint("RestrictedApi")
fun PopupMenu.show(x: Int, y: Int) {
try {
kotlin.runCatching {
val field: Field = this.javaClass.getDeclaredField("mPopup")
field.isAccessible = true
(field.get(this) as MenuPopupHelper).show(x, y)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}.onFailure {
it.printStackTrace()
}
}

@ -1,13 +1,15 @@
package io.legado.app.utils
import android.util.Log
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.io.*
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
@Suppress("unused")
@Suppress("unused", "BlockingMethodInNonBlockingContext")
object ZipUtils {
/**
@ -38,20 +40,14 @@ object ZipUtils {
srcFilePaths: Collection<String>?,
zipFilePath: String?,
comment: String?
): Boolean {
if (srcFilePaths == null || zipFilePath == null) return false
var zos: ZipOutputStream? = null
try {
zos = ZipOutputStream(FileOutputStream(zipFilePath))
): Boolean = withContext(IO) {
if (srcFilePaths == null || zipFilePath == null) return@withContext false
ZipOutputStream(FileOutputStream(zipFilePath)).use {
for (srcFile in srcFilePaths) {
if (!zipFile(getFileByPath(srcFile)!!, "", zos, comment)) return false
}
return true
} finally {
zos?.let {
zos.finish()
zos.close()
if (!zipFile(getFileByPath(srcFile)!!, "", it, comment))
return@withContext false
}
return@withContext true
}
}
@ -72,18 +68,11 @@ object ZipUtils {
comment: String? = null
): Boolean {
if (srcFiles == null || zipFile == null) return false
var zos: ZipOutputStream? = null
try {
zos = ZipOutputStream(FileOutputStream(zipFile))
ZipOutputStream(FileOutputStream(zipFile)).use {
for (srcFile in srcFiles) {
if (!zipFile(srcFile, "", zos, comment)) return false
if (!zipFile(srcFile, "", it, comment)) return false
}
return true
} finally {
zos?.let {
zos.finish()
zos.close()
}
}
}
@ -139,12 +128,7 @@ object ZipUtils {
): Boolean {
if (srcFile == null || zipFile == null) return false
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
return zipFile(
srcFile,
"",
zos,
comment
)
return zipFile(srcFile, "", zos, comment)
}
}

@ -2,10 +2,10 @@ package io.legado.app.web
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
import io.legado.app.api.ReturnData
import io.legado.app.api.controller.BookshelfController
import io.legado.app.api.controller.SourceController
import io.legado.app.web.utils.AssetsWeb
import io.legado.app.web.utils.ReturnData
import java.util.*
class HttpServer(port: Int) : NanoHTTPD(port) {

@ -1,4 +1,4 @@
package io.legado.app.web.utils
package io.legado.app.web
import fi.iki.elonen.NanoHTTPD
@ -20,13 +20,12 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
CoroutineScope by MainScope(),
Debug.Callback {
override fun onOpen() {
launch(IO) {
do {
delay(30000)
runCatching {
ping(byteArrayOf("ping".toByte()))
ping("ping".toByteArray())
}
} while (isOpen)
}
@ -57,7 +56,7 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
}
App.db.bookSourceDao.getBookSource(tag)?.let {
Debug.callback = this
Debug.startDebug(WebBook(it), key)
Debug.startDebug(this, WebBook(it), key)
}
}
}

@ -1,7 +1,6 @@
package io.legado.app.web
import fi.iki.elonen.NanoWSD
import io.legado.app.web.utils.SourceDebugWebSocket
class WebSocketServer(port: Int) : NanoWSD(port) {

@ -22,7 +22,8 @@
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</FrameLayout>

@ -22,7 +22,8 @@
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</FrameLayout>

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="3dp"
android:paddingRight="16dp">
<io.legado.app.ui.widget.text.EditText
android:id="@+id/edit_book_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content" />
</io.legado.app.ui.widget.text.TextInputLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="3dp"
android:paddingRight="16dp">
<io.legado.app.ui.widget.text.EditText
android:id="@+id/edit_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/note_content" />
</io.legado.app.ui.widget.text.TextInputLayout>
</LinearLayout>
</ScrollView>

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarStyle"
app:titleTextAppearance="@style/ToolbarTitle"
app:popupTheme="@style/AppTheme.PopupOverlay" />
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/ToolbarTitle" />
<FrameLayout
android:layout_width="match_parent"
@ -22,7 +22,8 @@
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:scrollbars="none" />
</FrameLayout>

@ -34,4 +34,9 @@
android:src="@drawable/ic_clear_all"
android:tint="@color/primaryText"
tools:ignore="RtlHardcoded" />
<View
android:layout_width="12dp"
android:layout_height="36dp" />
</LinearLayout>

@ -44,6 +44,11 @@
</group>
<item
android:id="@+id/menu_add_bookmark"
android:title="@string/bookmark_add"
app:showAsAction="never" />
<group android:id="@+id/menu_group_login">
<item
android:id="@+id/menu_login"

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_search"
android:title="@string/search"
app:showAsAction="ifRoom" />
app:showAsAction="always" />
<item
android:id="@+id/menu_update_toc"
@ -49,4 +51,17 @@
android:icon="@drawable/ic_arrange"
android:title="@string/arrange_bookshelf"
app:showAsAction="never" />
<item
android:id="@+id/menu_export_bookshelf"
android:icon="@drawable/ic_export"
android:title="@string/export_bookshelf"
app:showAsAction="never" />
<item
android:id="@+id/menu_import_bookshelf"
android:icon="@drawable/ic_import"
android:title="@string/import_bookshelf"
app:showAsAction="never" />
</menu>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/md_grey_100" />
<background android:drawable="@color/md_grey_50" />
<foreground android:drawable="@drawable/ic_launcher3" />
</adaptive-icon>

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

Loading…
Cancel
Save