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. 29
      .github/workflows/legado.yml
  2. 17
      README.md
  3. 3
      app/build.gradle
  4. 2
      app/src/main/AndroidManifest.xml
  5. 146
      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. 116
      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. 17
      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. 22
      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. 20
      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. 53
      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. 91
      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. 16
      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. 26
      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. 22
      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. 12
      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. 19
      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. 40
      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. 27
      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. 3
      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测试版' LANZOU_FOLDER: '阅读3测试版'
# 是否上传到artifact # 是否上传到artifact
UPLOAD_ARTIFACT: 'false' UPLOAD_ARTIFACT: 'true'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: set up JDK 1.8 - name: Set Up JDK
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 14
- name: clear 18PlusList.txt - name: Clear 18PlusList.txt
run: | run: |
echo "清空18PlusList.txt" echo "清空18PlusList.txt"
echo "">$GITHUB_WORKSPACE/app/src/main/assets/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: | run: |
echo "给apk增加签名" echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks 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_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_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 sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: apk live together - name: Apk Live Together
run: | run: |
echo "设置apk共存" echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i
- name: build with gradle - name: Build With Gradle
run: | run: |
echo "开始进行release构建" echo "开始进行release构建"
chmod +x gradlew chmod +x gradlew
@ -63,12 +68,12 @@ jobs:
with: with:
name: legado apk name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.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 }} if: ${{ env.LANZOU_PSD }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.x
- name: Set up lanzou-api - name: Dependence Install
if: ${{ env.LANZOU_PSD }} if: ${{ env.LANZOU_PSD }}
run: | run: |
pip install requests pip install requests
@ -79,10 +84,10 @@ jobs:
run: | run: |
import requests,os import requests,os
res = requests.get('https://raw.githubusercontent.com/Celeter/build/master/.github/scripts/API_lanzou.py') 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) f.write(res.content)
env: env:
FILEPATH: ${{ github.workspace }}/.github/lzy.py FILE_PATH: ${{ github.workspace }}/.github/lzy.py
- name: Upload App To Lanzou - name: Upload App To Lanzou
if: ${{ env.LANZOU_PSD }} if: ${{ env.LANZOU_PSD }}
run: python $GITHUB_WORKSPACE/.github/lzy.py run: python $GITHUB_WORKSPACE/.github/lzy.py

@ -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 ## 阅读3.0
* [书源规则](https://alanskycn.gitee.io/teachme/) * [书源规则](https://alanskycn.gitee.io/teachme/)
@ -8,9 +17,9 @@
* [帮助文档](/app/src/main/assets/help/appHelp.md) * [帮助文档](/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)根据需要自行调用。 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
## 感谢 ## 感谢

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

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

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

@ -3,6 +3,25 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(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** **2020/12/19**
* 书签转移到文本菜单里,会记录选择的文本和位置 * 书签转移到文本菜单里,会记录选择的文本和位置
* 订阅源添加单url选项,直接打开url * 订阅源添加单url选项,直接打开url

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

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

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

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

@ -32,13 +32,10 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun show(manager: FragmentManager, tag: String?) { override fun show(manager: FragmentManager, tag: String?) {
try { kotlin.runCatching {
//在每个add事务前增加一个remove事务,防止连续的add //在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit() manager.beginTransaction().remove(this).commit()
super.show(manager, tag) 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 itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = 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 recyclerView.adapter = this
} }
@Synchronized
fun setItems(items: List<ITEM>?) { fun setItems(items: List<ITEM>?) {
synchronized(lock) { kotlin.runCatching {
asyncListDiffer.submitList(items) asyncListDiffer.submitList(items)
} }
} }
@Synchronized
fun setItem(position: Int, item: ITEM) { fun setItem(position: Int, item: ITEM) {
synchronized(lock) { kotlin.runCatching {
val list = ArrayList(asyncListDiffer.currentList) val list = ArrayList(asyncListDiffer.currentList)
list[position] = item list[position] = item
asyncListDiffer.submitList(list) asyncListDiffer.submitList(list)
} }
} }
fun updateItem(item: ITEM) = @Synchronized
synchronized(lock) { fun updateItem(item: ITEM) {
kotlin.runCatching {
val index = asyncListDiffer.currentList.indexOf(item) val index = asyncListDiffer.currentList.indexOf(item)
if (index >= 0) { if (index >= 0) {
asyncListDiffer.currentList[index] = item asyncListDiffer.currentList[index] = item
notifyItemChanged(index) notifyItemChanged(index)
} }
} }
}
fun updateItem(position: Int, payload: Any) = @Synchronized
synchronized(lock) { fun updateItem(position: Int, payload: Any) {
kotlin.runCatching {
val size = itemCount val size = itemCount
if (position in 0 until size) { if (position in 0 until size) {
notifyItemChanged(position, payload) notifyItemChanged(position, payload)
} }
} }
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) = @Synchronized
synchronized(lock) { fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
kotlin.runCatching {
val size = itemCount val size = itemCount
if (fromPosition in 0 until size && toPosition in 0 until size) { if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged( notifyItemRangeChanged(
@ -89,6 +94,7 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
) )
} }
} }
}
fun isEmpty() = asyncListDiffer.currentList.isEmpty() 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 items: MutableList<ITEM> = mutableListOf()
private val lock = Object()
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = 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 recyclerView.adapter = this
} }
@Synchronized
fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
synchronized(lock) { kotlin.runCatching {
val index = headerItems.size() val index = headerItems.size()
headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header) headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header)
notifyItemInserted(index) notifyItemInserted(index)
} }
} }
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) = @Synchronized
synchronized(lock) { fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching {
val index = getActualItemCount() + footerItems.size() val index = getActualItemCount() + footerItems.size()
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer) footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
notifyItemInserted(index) notifyItemInserted(index)
} }
}
@Synchronized
fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) = fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
synchronized(lock) { kotlin.runCatching {
val index = headerItems.indexOfValue(header) val index = headerItems.indexOfValue(header)
if (index >= 0) { if (index >= 0) {
headerItems.remove(index) headerItems.remove(index)
notifyItemRemoved(index) notifyItemRemoved(index)
} }
} }
}
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) = @Synchronized
synchronized(lock) { fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching {
val index = footerItems.indexOfValue(footer) val index = footerItems.indexOfValue(footer)
if (index >= 0) { if (index >= 0) {
footerItems.remove(index) footerItems.remove(index)
notifyItemRemoved(getActualItemCount() + index - 2) notifyItemRemoved(getActualItemCount() + index - 2)
} }
} }
}
@Synchronized
fun setItems(items: List<ITEM>?) { fun setItems(items: List<ITEM>?) {
synchronized(lock) { kotlin.runCatching {
if (this.items.isNotEmpty()) { if (this.items.isNotEmpty()) {
this.items.clear() 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
synchronized(lock) { 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()) { if (this.items.isNotEmpty()) {
this.items.clear() this.items.clear()
} }
@ -105,8 +148,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@Synchronized
fun setItem(position: Int, item: ITEM) { fun setItem(position: Int, item: ITEM) {
synchronized(lock) { kotlin.runCatching {
val oldSize = getActualItemCount() val oldSize = getActualItemCount()
if (position in 0 until oldSize) { if (position in 0 until oldSize) {
this.items[position] = item this.items[position] = item
@ -116,8 +160,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@Synchronized
fun addItem(item: ITEM) { fun addItem(item: ITEM) {
synchronized(lock) { kotlin.runCatching {
val oldSize = getActualItemCount() val oldSize = getActualItemCount()
if (this.items.add(item)) { if (this.items.add(item)) {
notifyItemInserted(oldSize + getHeaderCount()) 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>) { fun addItems(position: Int, newItems: List<ITEM>) {
synchronized(lock) { kotlin.runCatching {
if (this.items.addAll(position, newItems)) { if (this.items.addAll(position, newItems)) {
notifyItemRangeInserted(position + getHeaderCount(), newItems.size) 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>) { fun addItems(newItems: List<ITEM>) {
synchronized(lock) { kotlin.runCatching {
val oldSize = getActualItemCount() val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) { if (this.items.addAll(newItems)) {
if (oldSize == 0 && getHeaderCount() == 0) { if (oldSize == 0 && getHeaderCount() == 0) {
@ -149,8 +196,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@Synchronized
fun removeItem(position: Int) { fun removeItem(position: Int) {
synchronized(lock) { kotlin.runCatching {
if (this.items.removeAt(position) != null) { if (this.items.removeAt(position) != null) {
notifyItemRemoved(position + getHeaderCount()) notifyItemRemoved(position + getHeaderCount())
} }
@ -158,8 +206,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@Synchronized
fun removeItem(item: ITEM) { fun removeItem(item: ITEM) {
synchronized(lock) { kotlin.runCatching {
if (this.items.remove(item)) { if (this.items.remove(item)) {
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount()) 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>) { fun removeItems(items: List<ITEM>) {
synchronized(lock) { kotlin.runCatching {
if (this.items.removeAll(items)) { if (this.items.removeAll(items)) {
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -176,8 +226,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
@Synchronized
fun swapItem(oldPosition: Int, newPosition: Int) { fun swapItem(oldPosition: Int, newPosition: Int) {
synchronized(lock) { kotlin.runCatching {
val size = getActualItemCount() val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) { if (oldPosition in 0 until size && newPosition in 0 until size) {
val srcPosition = oldPosition + getHeaderCount() val srcPosition = oldPosition + getHeaderCount()
@ -189,8 +240,9 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
fun updateItem(item: ITEM) = @Synchronized
synchronized(lock) { fun updateItem(item: ITEM) {
kotlin.runCatching {
val index = this.items.indexOf(item) val index = this.items.indexOf(item)
if (index >= 0) { if (index >= 0) {
this.items[index] = item this.items[index] = item
@ -198,17 +250,21 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
onCurrentListChanged() onCurrentListChanged()
} }
}
fun updateItem(position: Int, payload: Any) = @Synchronized
synchronized(lock) { fun updateItem(position: Int, payload: Any) {
kotlin.runCatching {
val size = getActualItemCount() val size = getActualItemCount()
if (position in 0 until size) { if (position in 0 until size) {
notifyItemChanged(position + getHeaderCount(), payload) notifyItemChanged(position + getHeaderCount(), payload)
} }
} }
}
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) = @Synchronized
synchronized(lock) { fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
kotlin.runCatching {
val size = getActualItemCount() val size = getActualItemCount()
if (fromPosition in 0 until size && toPosition in 0 until size) { if (fromPosition in 0 until size && toPosition in 0 until size) {
notifyItemRangeChanged( notifyItemRangeChanged(
@ -218,12 +274,16 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
) )
} }
} }
}
fun clearItems() = synchronized(lock) { @Synchronized
fun clearItems() {
kotlin.runCatching {
this.items.clear() this.items.clear()
notifyDataSetChanged() notifyDataSetChanged()
onCurrentListChanged() onCurrentListChanged()
} }
}
fun isEmpty() = items.isEmpty() fun isEmpty() = items.isEmpty()

@ -64,6 +64,7 @@ object PreferKey {
const val importKeepName = "importKeepName" const val importKeepName = "importKeepName"
const val screenOrientation = "screenOrientation" const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress" const val syncBookProgress = "syncBookProgress"
const val preDownload = "preDownload"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" 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") @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>> 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") @Query("select * from book_sources where enabled = 1 order by customOrder asc")
fun liveDataEnabled(): LiveData<List<BookSource>> 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") @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>> 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) <> ''") @Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>> fun liveGroup(): LiveData<List<String>>
@ -33,7 +39,7 @@ interface BookSourceDao {
fun liveGroupEnabled(): LiveData<List<String>> fun liveGroupEnabled(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''") @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") @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> fun searchIsEnable(searchKey: String = ""): List<Boolean>

@ -28,4 +28,6 @@ interface HttpTTSDao {
@Update @Update
fun update(vararg httpTTS: HttpTTS) 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") @Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
fun liveDataSearch(key: String): LiveData<List<ReplaceRule>> 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") @get:Query("SELECT MIN(sortOrder) FROM replace_rules")
val minOrder: Int 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") @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>> 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") @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>> fun liveEnabled(searchKey: String): LiveData<List<RssSource>>

@ -52,7 +52,8 @@ data class BookSource(
return bookSourceUrl.hashCode() 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) @Throws(Exception::class)
fun getHeaderMap() = (HashMap<String, String>().apply { fun getHeaderMap() = (HashMap<String, String>().apply {
@ -100,11 +101,11 @@ data class BookSource(
} }
fun getExploreKinds() = arrayListOf<ExploreKind>().apply { fun getExploreKinds() = arrayListOf<ExploreKind>().apply {
exploreUrl?.let { exploreUrl?.let { urlRule ->
var a = it var a = urlRule
if (a.isNotBlank()) { if (a.isNotBlank()) {
try { kotlin.runCatching {
if (it.startsWith("<js>", false)) { if (urlRule.startsWith("<js>", false)) {
val aCache = ACache.get(App.INSTANCE, "explore") val aCache = ACache.get(App.INSTANCE, "explore")
a = aCache.getAsString(bookSourceUrl) ?: "" a = aCache.getAsString(bookSourceUrl) ?: ""
if (a.isBlank()) { if (a.isBlank()) {
@ -114,7 +115,7 @@ data class BookSource(
bindings["cookie"] = CookieStore bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager bindings["cache"] = CacheManager
a = AppConst.SCRIPT_ENGINE.eval( a = AppConst.SCRIPT_ENGINE.eval(
it.substring(4, it.lastIndexOf("<")), urlRule.substring(4, urlRule.lastIndexOf("<")),
bindings bindings
).toString() ).toString()
aCache.put(bookSourceUrl, a) aCache.put(bookSourceUrl, a)
@ -126,8 +127,8 @@ data class BookSource(
if (d.size > 1) if (d.size > 1)
add(ExploreKind(d[0], d[1])) add(ExploreKind(d[0], d[1]))
} }
} catch (e: Exception) { }.onFailure {
add(ExploreKind(e.localizedMessage ?: "")) add(ExploreKind(it.localizedMessage ?: ""))
} }
} }
} }

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

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

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

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

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

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

@ -63,4 +63,10 @@ object LocalConfig {
val ruleHelpVersionIsLast: Boolean val ruleHelpVersionIsLast: Boolean
get() = isLastVersion(1, "ruleHelpVersion") 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>? { private fun getConfigs(): List<Config>? {
val configFile = File(configFilePath) val configFile = File(configFilePath)
if (configFile.exists()) { if (configFile.exists()) {
try { kotlin.runCatching {
val json = configFile.readText() val json = configFile.readText()
return GSON.fromJsonArray(json) return GSON.fromJsonArray(json)
} catch (e: Exception) { }.onFailure {
e.printStackTrace() it.printStackTrace()
} }
} }
return null return null

@ -8,7 +8,7 @@ import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset import java.nio.charset.Charset
@Parser(name = "Text") @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 { override fun onParse(response: Response): String {

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

@ -12,7 +12,6 @@ import rxhttp.wrapper.param.toInputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.net.URLEncoder import java.net.URLEncoder
@ -39,15 +38,12 @@ class WebDav(urlStr: String) {
private val url: URL = URL(urlStr) private val url: URL = URL(urlStr)
private val httpUrl: String? by lazy { private val httpUrl: String? by lazy {
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://")
try { return@lazy kotlin.runCatching {
return@lazy URLEncoder.encode(raw, "UTF-8") URLEncoder.encode(raw, "UTF-8")
.replace("\\+".toRegex(), "%20") .replace("\\+".toRegex(), "%20")
.replace("%3A".toRegex(), ":") .replace("%3A".toRegex(), ":")
.replace("%2F".toRegex(), "/") .replace("%2F".toRegex(), "/")
} catch (e: UnsupportedEncodingException) { }.getOrNull()
e.printStackTrace()
return@lazy null
}
} }
val host: String? get() = url.host val host: String? get() = url.host
val path get() = url.toString() 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.htmlFormat
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.msg import io.legado.app.utils.msg
import kotlinx.coroutines.CoroutineScope
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -101,7 +102,7 @@ object Debug {
} }
} }
fun startDebug(webBook: WebBook, key: String) { fun startDebug(scope: CoroutineScope, webBook: WebBook, key: String) {
cancelDebug() cancelDebug()
debugSource = webBook.sourceUrl debugSource = webBook.sourceUrl
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
@ -111,12 +112,12 @@ object Debug {
book.origin = webBook.sourceUrl book.origin = webBook.sourceUrl
book.bookUrl = key book.bookUrl = key
log(webBook.sourceUrl, "⇒开始访问详情页:$key") log(webBook.sourceUrl, "⇒开始访问详情页:$key")
infoDebug(webBook, book) infoDebug(scope, webBook, book)
} }
key.contains("::") -> { key.contains("::") -> {
val url = key.substring(key.indexOf("::") + 2) val url = key.substring(key.indexOf("::") + 2)
log(webBook.sourceUrl, "⇒开始访问发现页:$url") log(webBook.sourceUrl, "⇒开始访问发现页:$url")
exploreDebug(webBook, url) exploreDebug(scope, webBook, url)
} }
key.startsWith("++")-> { key.startsWith("++")-> {
val url = key.substring(2) val url = key.substring(2)
@ -124,7 +125,7 @@ object Debug {
book.origin = webBook.sourceUrl book.origin = webBook.sourceUrl
book.tocUrl = url book.tocUrl = url
log(webBook.sourceUrl, "⇒开始访目录页:$url") log(webBook.sourceUrl, "⇒开始访目录页:$url")
tocDebug(webBook, book) tocDebug(scope, webBook, book)
} }
key.startsWith("--")-> { key.startsWith("--")-> {
val url = key.substring(2) val url = key.substring(2)
@ -134,23 +135,23 @@ object Debug {
val chapter = BookChapter() val chapter = BookChapter()
chapter.title = "调试" chapter.title = "调试"
chapter.url = url chapter.url = url
contentDebug(webBook, book, chapter, null) contentDebug(scope, webBook, book, chapter, null)
} }
else -> { else -> {
log(webBook.sourceUrl, "⇒开始搜索关键字:$key") 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, "︾开始解析发现页") log(debugSource, "︾开始解析发现页")
val explore = webBook.exploreBook(url, 1) val explore = webBook.exploreBook(scope, url, 1)
.onSuccess { exploreBooks -> .onSuccess { exploreBooks ->
if (exploreBooks.isNotEmpty()) { if (exploreBooks.isNotEmpty()) {
log(debugSource, "︽发现页解析完成") log(debugSource, "︽发现页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
infoDebug(webBook, exploreBooks[0].toBook()) infoDebug(scope, webBook, exploreBooks[0].toBook())
} else { } else {
log(debugSource, "︽未获取到书籍", state = -1) log(debugSource, "︽未获取到书籍", state = -1)
} }
@ -161,14 +162,14 @@ object Debug {
tasks.add(explore) tasks.add(explore)
} }
private fun searchDebug(webBook: WebBook, key: String) { private fun searchDebug(scope: CoroutineScope, webBook: WebBook, key: String) {
log(debugSource, "︾开始解析搜索页") log(debugSource, "︾开始解析搜索页")
val search = webBook.searchBook(key, 1) val search = webBook.searchBook(scope, key, 1)
.onSuccess { searchBooks -> .onSuccess { searchBooks ->
if (searchBooks.isNotEmpty()) { if (searchBooks.isNotEmpty()) {
log(debugSource, "︽搜索页解析完成") log(debugSource, "︽搜索页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
infoDebug(webBook, searchBooks[0].toBook()) infoDebug(scope, webBook, searchBooks[0].toBook())
} else { } else {
log(debugSource, "︽未获取到书籍", state = -1) log(debugSource, "︽未获取到书籍", state = -1)
} }
@ -179,13 +180,13 @@ object Debug {
tasks.add(search) tasks.add(search)
} }
private fun infoDebug(webBook: WebBook, book: Book) { private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析详情页") log(debugSource, "︾开始解析详情页")
val info = webBook.getBookInfo(book) val info = webBook.getBookInfo(scope, book)
.onSuccess { .onSuccess {
log(debugSource, "︽详情页解析完成") log(debugSource, "︽详情页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
tocDebug(webBook, book) tocDebug(scope, webBook, book)
} }
.onError { .onError {
log(debugSource, it.msg, state = -1) log(debugSource, it.msg, state = -1)
@ -193,15 +194,15 @@ object Debug {
tasks.add(info) tasks.add(info)
} }
private fun tocDebug(webBook: WebBook, book: Book) { private fun tocDebug(scope: CoroutineScope, webBook: WebBook, book: Book) {
log(debugSource, "︾开始解析目录页") log(debugSource, "︾开始解析目录页")
val chapterList = webBook.getChapterList(book) val chapterList = webBook.getChapterList(scope, book)
.onSuccess { .onSuccess {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
log(debugSource, "︽目录页解析完成") log(debugSource, "︽目录页解析完成")
log(debugSource, showTime = false) log(debugSource, showTime = false)
val nextChapterUrl = if (it.size > 1) it[1].url else null 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 { } else {
log(debugSource, "︽目录列表为空", state = -1) log(debugSource, "︽目录列表为空", state = -1)
} }
@ -213,13 +214,14 @@ object Debug {
} }
private fun contentDebug( private fun contentDebug(
scope: CoroutineScope,
webBook: WebBook, webBook: WebBook,
book: Book, book: Book,
bookChapter: BookChapter, bookChapter: BookChapter,
nextChapterUrl: String? nextChapterUrl: String?
) { ) {
log(debugSource, "︾开始解析正文页") log(debugSource, "︾开始解析正文页")
val content = webBook.getContent(book, bookChapter, nextChapterUrl) val content = webBook.getContent(scope, book, bookChapter, nextChapterUrl)
.onSuccess { .onSuccess {
log(debugSource, "︽正文页解析完成", state = 1000) log(debugSource, "︽正文页解析完成", state = 1000)
} }

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

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

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

@ -10,11 +10,14 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
object BookInfo { object BookInfo {
@Throws(Exception::class) @Throws(Exception::class)
fun analyzeBookInfo( fun analyzeBookInfo(
scope: CoroutineScope,
book: Book, book: Book,
body: String?, body: String?,
bookSource: BookSource, bookSource: BookSource,
@ -30,11 +33,13 @@ object BookInfo {
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
infoRule.init?.let { infoRule.init?.let {
if (it.isNotBlank()) { if (it.isNotBlank()) {
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it)) analyzeRule.setContent(analyzeRule.getElement(it))
} }
} }
val mCanReName = canReName && !infoRule.canReName.isNullOrBlank() val mCanReName = canReName && !infoRule.canReName.isNullOrBlank()
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名") Debug.log(bookSource.bookSourceUrl, "┌获取书名")
BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let { BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let {
if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) { if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) {
@ -42,6 +47,7 @@ object BookInfo {
} }
Debug.log(bookSource.bookSourceUrl, "${it}") Debug.log(bookSource.bookSourceUrl, "${it}")
} }
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let { BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let {
if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) { if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) {
@ -49,6 +55,7 @@ object BookInfo {
} }
Debug.log(bookSource.bookSourceUrl, "${it}") Debug.log(bookSource.bookSourceUrl, "${it}")
} }
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
analyzeRule.getStringList(infoRule.kind) analyzeRule.getStringList(infoRule.kind)
?.joinToString(",") ?.joinToString(",")
@ -56,27 +63,31 @@ object BookInfo {
if (it.isNotEmpty()) book.kind = it if (it.isNotEmpty()) book.kind = it
} }
Debug.log(bookSource.bookSourceUrl, "${book.kind}") Debug.log(bookSource.bookSourceUrl, "${book.kind}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let { wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let {
if (it.isNotEmpty()) book.wordCount = it if (it.isNotEmpty()) book.wordCount = it
} }
Debug.log(bookSource.bookSourceUrl, "${book.wordCount}") Debug.log(bookSource.bookSourceUrl, "${book.wordCount}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
analyzeRule.getString(infoRule.lastChapter).let { analyzeRule.getString(infoRule.lastChapter).let {
if (it.isNotEmpty()) book.latestChapterTitle = it if (it.isNotEmpty()) book.latestChapterTitle = it
} }
Debug.log(bookSource.bookSourceUrl, "${book.latestChapterTitle}") Debug.log(bookSource.bookSourceUrl, "${book.latestChapterTitle}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
analyzeRule.getString(infoRule.intro).let { analyzeRule.getString(infoRule.intro).let {
if (it.isNotEmpty()) book.intro = it.htmlFormat() if (it.isNotEmpty()) book.intro = it.htmlFormat()
} }
Debug.log(bookSource.bookSourceUrl, "${book.intro}") Debug.log(bookSource.bookSourceUrl, "${book.intro}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
analyzeRule.getString(infoRule.coverUrl).let { analyzeRule.getString(infoRule.coverUrl).let {
if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${book.coverUrl}")
scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true) book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true)
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl 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.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive import kotlinx.coroutines.ensureActive
object BookList { object BookList {
@ -36,10 +35,10 @@ object BookList {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(variableBook) val analyzeRule = AnalyzeRule(variableBook)
analyzeRule.setContent(body).setBaseUrl(baseUrl) analyzeRule.setContent(body).setBaseUrl(baseUrl)
bookSource.bookUrlPattern?.let { bookSource.bookUrlPattern?.let {
scope.ensureActive()
if (baseUrl.matches(it.toRegex())) { if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable)
@ -67,6 +66,7 @@ object BookList {
} }
Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表") Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表")
collections = analyzeRule.getElements(ruleList) collections = analyzeRule.getElements(ruleList)
scope.ensureActive()
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable)
@ -85,7 +85,6 @@ object BookList {
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
for ((index, item) in collections.withIndex()) { for ((index, item) in collections.withIndex()) {
if (!scope.isActive) throw CancellationException()
getSearchItem( getSearchItem(
scope, scope,
item, item,
@ -134,37 +133,37 @@ object BookList {
with(bookSource.getBookInfoRule()) { with(bookSource.getBookInfoRule()) {
init?.let { init?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
analyzeRule.setContent(analyzeRule.getElement(it)) analyzeRule.setContent(analyzeRule.getElement(it))
} }
} }
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名") Debug.log(bookSource.bookSourceUrl, "┌获取书名")
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(name)) searchBook.name = BookHelp.formatBookName(analyzeRule.getString(name))
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}") Debug.log(bookSource.bookSourceUrl, "${searchBook.name}")
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者") Debug.log(bookSource.bookSourceUrl, "┌获取作者")
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(author)) searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(author))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "${searchBook.author}")
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类") Debug.log(bookSource.bookSourceUrl, "┌获取分类")
searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}") Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}")
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数") Debug.log(bookSource.bookSourceUrl, "┌获取字数")
searchBook.wordCount = wordCountFormat(analyzeRule.getString(wordCount)) searchBook.wordCount = wordCountFormat(analyzeRule.getString(wordCount))
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}") Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}")
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
searchBook.latestChapterTitle = analyzeRule.getString(lastChapter) searchBook.latestChapterTitle = analyzeRule.getString(lastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}") Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介") Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro).htmlFormat() searchBook.intro = analyzeRule.getString(intro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}") Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}")
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
searchBook.coverUrl = analyzeRule.getString(coverUrl, true) searchBook.coverUrl = analyzeRule.getString(coverUrl, true)
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}") Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}")
@ -199,38 +198,38 @@ object BookList {
searchBook.originOrder = bookSource.customOrder searchBook.originOrder = bookSource.customOrder
analyzeRule.book = searchBook analyzeRule.book = searchBook
analyzeRule.setContent(item) analyzeRule.setContent(item)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName)) searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName))
Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.name}", log)
if (searchBook.name.isNotEmpty()) { if (searchBook.name.isNotEmpty()) {
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor)) searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor))
Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.author}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.kind}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount)) searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount))
Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.wordCount}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat() searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
analyzeRule.getString(ruleCoverUrl).let { analyzeRule.getString(ruleCoverUrl).let {
if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
} }
Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log) Debug.log(bookSource.bookSourceUrl, "${searchBook.coverUrl}", log)
if (!scope.isActive) throw CancellationException() scope.ensureActive()
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true) searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true)
if (searchBook.bookUrl.isEmpty()) { 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++ searchIndex++
val source = bookSourceList[searchIndex] val source = bookSourceList[searchIndex]
val task = WebBook(source).searchBook( val task = WebBook(source).searchBook(
scope,
searchKey, searchKey,
searchPage, searchPage,
scope = scope,
context = searchPool!! context = searchPool!!
).timeout(30000L) ).timeout(30000L)
.onSuccess(IO) { .onSuccess(IO) {

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

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

@ -184,12 +184,8 @@ class CacheBookService : BaseService() {
return@async return@async
} }
if (!BookHelp.hasContent(book, bookChapter)) { if (!BookHelp.hasContent(book, bookChapter)) {
webBook.getContent( webBook.getContent(this, book, bookChapter, context = searchPool)
book, .timeout(60000L)
bookChapter,
scope = this,
context = searchPool
).timeout(60000L)
.onError { .onError {
synchronized(this) { synchronized(this) {
downloadingList.remove(bookChapter.url) 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.model.webBook.WebBook
import io.legado.app.service.CacheBookService import io.legado.app.service.CacheBookService
import io.legado.app.utils.msg import io.legado.app.utils.msg
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
@ -61,6 +62,7 @@ object CacheBook {
} }
fun download( fun download(
scope: CoroutineScope,
webBook: WebBook, webBook: WebBook,
book: Book, book: Book,
chapter: BookChapter, chapter: BookChapter,
@ -73,7 +75,7 @@ object CacheBook {
downloadMap[book.bookUrl] = CopyOnWriteArraySet() downloadMap[book.bookUrl] = CopyOnWriteArraySet()
} }
downloadMap[book.bookUrl]?.add(chapter.index) downloadMap[book.bookUrl]?.add(chapter.index)
webBook.getContent(book, chapter) webBook.getContent(scope, book, chapter)
.onSuccess { content -> .onSuccess { content ->
if (ReadBook.book?.bookUrl == book.bookUrl) { if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.contentLoadFinish( ReadBook.contentLoadFinish(

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

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

@ -30,7 +30,7 @@ class AboutActivity : BaseActivity<ActivityAboutBinding>() {
.replace(R.id.fl_fragment, aboutFragment, fTag) .replace(R.id.fl_fragment, aboutFragment, fTag)
.commit() .commit()
binding.tvAppSummary.post { binding.tvAppSummary.post {
try { kotlin.runCatching {
val span = ForegroundColorSpan(accentColor) val span = ForegroundColorSpan(accentColor)
val spannableString = SpannableString(binding.tvAppSummary.text) val spannableString = SpannableString(binding.tvAppSummary.text)
val gzh = getString(R.string.legado_gzh) val gzh = getString(R.string.legado_gzh)
@ -40,8 +40,6 @@ class AboutActivity : BaseActivity<ActivityAboutBinding>() {
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
) )
binding.tvAppSummary.text = spannableString 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.openUrl
import io.legado.app.utils.sendMail import io.legado.app.utils.sendMail
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
class AboutFragment : PreferenceFragmentCompat() { 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") 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主界面,不设置,按返回会返回到呼起产品界面 // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return try { kotlin.runCatching {
startActivity(intent) startActivity(intent)
true return true
} catch (e: java.lang.Exception) { }.onFailure {
false toast("添加失败,请手动添加")
} }
return false
} }
} }

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

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

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

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

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

@ -57,7 +57,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
loadChapter(book, changeDruChapterIndex) loadChapter(book, changeDruChapterIndex)
} else { } else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this, canReName = canReName) WebBook(bookSource).getBookInfo(this, book, canReName = canReName)
.onSuccess(IO) { .onSuccess(IO) {
bookData.postValue(book) bookData.postValue(book)
if (inBookshelf) { if (inBookshelf) {
@ -88,7 +88,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
} else { } else {
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(this, book)
.onSuccess(IO) { .onSuccess(IO) {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (inBookshelf) { 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.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress 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.ReadBookConfig
import io.legado.app.help.ReadTipConfig import io.legado.app.help.ReadTipConfig
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
@ -204,6 +205,21 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
} }
R.id.menu_download -> showDownloadDialog() 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 -> R.id.menu_copy_text ->
TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent())
R.id.menu_update_toc -> ReadBook.book?.let { 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.constant.PreferKey
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.ActivityBookReadBinding import io.legado.app.databinding.ActivityBookReadBinding
import io.legado.app.databinding.DialogBookmarkBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding import io.legado.app.databinding.DialogDownloadChoiceBinding
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -200,18 +201,18 @@ abstract class ReadBookBaseActivity :
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
fun showBookMark(bookmark: Bookmark) { fun showBookMark(bookmark: Bookmark) {
val book = ReadBook.book ?: return
val textChapter = ReadBook.curTextChapter ?: return
alert(title = getString(R.string.bookmark_add)) { alert(title = getString(R.string.bookmark_add)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { message = bookmark.chapterName
editView.setHint(R.string.note_content) val alertBinding = DialogBookmarkBinding.inflate(layoutInflater).apply {
editBookText.setText(bookmark.bookText)
editView.setText(bookmark.content)
} }
message = bookmark.bookText
customView = alertBinding.root customView = alertBinding.root
yesButton { yesButton {
alertBinding.editView.text?.toString()?.let { editContent -> alertBinding.apply {
Coroutine.async { Coroutine.async {
bookmark.content = editContent bookmark.bookText = editBookText.text.toString()
bookmark.content = editView.text.toString()
App.db.bookmarkDao.insert(bookmark) 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.IntentDataHelp
import io.legado.app.help.storage.BookWebDav import io.legado.app.help.storage.BookWebDav
import io.legado.app.model.localBook.LocalBook 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.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.msg
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -109,7 +111,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (book.isLocalBook()) { if (book.isLocalBook()) {
loadChapterList(book, changeDruChapterIndex) loadChapterList(book, changeDruChapterIndex)
} else { } else {
ReadBook.webBook?.getBookInfo(book, this, canReName = false) ReadBook.webBook?.getBookInfo(this, book, canReName = false)
?.onSuccess { ?.onSuccess {
loadChapterList(book, changeDruChapterIndex) loadChapterList(book, changeDruChapterIndex)
} }
@ -138,7 +140,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.upMsg("LoadTocError:${it.localizedMessage}") ReadBook.upMsg("LoadTocError:${it.localizedMessage}")
} }
} else { } else {
ReadBook.webBook?.getChapterList(book, this) ReadBook.webBook?.getChapterList(this, book)
?.onSuccess(IO) { cList -> ?.onSuccess(IO) { cList ->
if (cList.isNotEmpty()) { if (cList.isNotEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
@ -213,24 +215,18 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
private fun autoChangeSource(name: String, author: String) { private fun autoChangeSource(name: String, author: String) {
if (!AppConfig.autoChangeSource) return if (!AppConfig.autoChangeSource) return
execute { execute {
App.db.bookSourceDao.allTextEnabled.forEach { source -> val sources = App.db.bookSourceDao.allTextEnabled
try { val book = PreciseSearch.searchFirstBook(this, sources, name, author)
WebBook(source) if (book != null) {
.searchBookSuspend(this, name)
.getOrNull(0)?.let {
if (it.name == name && (it.author == author || author == "")) {
val book = it.toBook()
book.upInfoFromOld(ReadBook.book) book.upInfoFromOld(ReadBook.book)
changeTo(book) changeTo(book)
return@execute } else {
} throw Exception("自动换源失败")
}
} catch (e: Exception) {
//nothing
}
} }
}.onStart { }.onStart {
ReadBook.upMsg(context.getString(R.string.source_auto_changing)) ReadBook.upMsg(context.getString(R.string.source_auto_changing))
}.onError {
toast(it.msg)
}.onFinally { }.onFinally {
ReadBook.upMsg(null) ReadBook.upMsg(null)
} }

@ -130,7 +130,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
readAloud(callBack.selectedText) readAloud(callBack.selectedText)
} }
R.id.menu_browser -> { R.id.menu_browser -> {
try { kotlin.runCatching {
val intent = if (callBack.selectedText.isAbsUrl()) { val intent = if (callBack.selectedText.isAbsUrl()) {
Intent(Intent.ACTION_VIEW).apply { Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(callBack.selectedText) data = Uri.parse(callBack.selectedText)
@ -141,9 +141,9 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
} }
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { }.onFailure {
e.printStackTrace() it.printStackTrace()
context.toast(e.localizedMessage ?: "ERROR") context.toast(it.localizedMessage ?: "ERROR")
} }
} }
else -> item.intent?.let { else -> item.intent?.let {
@ -210,7 +210,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
*/ */
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun onInitializeMenu(menu: Menu) { private fun onInitializeMenu(menu: Menu) {
try { kotlin.runCatching {
var menuItemOrder = 100 var menuItemOrder = 100
for (resolveInfo in getSupportedActivities()) { for (resolveInfo in getSupportedActivities()) {
menu.add( menu.add(
@ -218,8 +218,8 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
menuItemOrder++, resolveInfo.loadLabel(context.packageManager) menuItemOrder++, resolveInfo.loadLabel(context.packageManager)
).intent = createProcessTextIntentForResolveInfo(resolveInfo) ).intent = createProcessTextIntentForResolveInfo(resolveInfo)
} }
} catch (e: Exception) { }.onFailure {
context.toast("获取文字操作菜单出错:${e.localizedMessage}") 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.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemHttpTtsBinding import io.legado.app.databinding.ItemHttpTtsBinding
import io.legado.app.lib.dialogs.alert 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.lib.theme.primaryColor
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -59,6 +60,7 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
private fun initView() = with(binding) { private fun initView() = with(binding) {
toolBar.setBackgroundColor(primaryColor) toolBar.setBackgroundColor(primaryColor)
toolBar.setTitle(R.string.speak_engine) toolBar.setTitle(R.string.speak_engine)
ATH.applyEdgeEffectColor(recyclerView)
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
adapter = Adapter(requireContext()) adapter = Adapter(requireContext())
recyclerView.adapter = adapter recyclerView.adapter = adapter

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

@ -338,10 +338,10 @@ class ReadView(context: Context, attrs: AttributeSet) :
} }
end -= 1 end -= 1
} }
try { kotlin.runCatching {
curPage.selectStartMoveIndex(firstRelativePage, lineStart, start) curPage.selectStartMoveIndex(firstRelativePage, lineStart, start)
curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end) curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end)
} catch (e: Exception) { }.onFailure {
print( print(
""" """
curPage.selectStartMoveIndex($firstRelativePage, $lineStart, $start) 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.data.entities.BookChapter
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig 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.TextChapter
import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
@ -118,7 +119,7 @@ object ChapterProvider {
return TextChapter( return TextChapter(
bookChapter.index, bookChapter.title, bookChapter.index, bookChapter.title,
bookChapter.getAbsoluteURL().substringBefore(","), bookChapter.getAbsoluteURL().split(AnalyzeUrl.splitUrlRegex)[0],
textPages, chapterSize textPages, chapterSize
) )
} }
@ -351,7 +352,7 @@ object ChapterProvider {
} }
private fun getTypeface(fontPath: String): Typeface { private fun getTypeface(fontPath: String): Typeface {
return try { return kotlin.runCatching {
when { when {
fontPath.isContentScheme() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { fontPath.isContentScheme() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
val fd = App.INSTANCE.contentResolver val fd = App.INSTANCE.contentResolver
@ -369,7 +370,7 @@ object ChapterProvider {
else -> Typeface.SANS_SERIF else -> Typeface.SANS_SERIF
} }
} }
} catch (e: Exception) { }.getOrElse {
ReadBookConfig.textFont = "" ReadBookConfig.textFont = ""
ReadBookConfig.save() ReadBookConfig.save()
Typeface.SANS_SERIF Typeface.SANS_SERIF

@ -160,6 +160,13 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
binding.recyclerView.scrollToPosition(0) 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) loadMoreView = LoadMoreView(this)
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { 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) { 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() start?.invoke()
Debug.callback = this }.onError {
Debug.startDebug(it, key) error?.invoke()
} ?: error?.invoke() }
} }
override fun printLog(state: Int, msg: String) { override fun printLog(state: Int, msg: String) {

@ -12,7 +12,6 @@ import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
@ -140,14 +139,13 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
R.id.menu_help -> showHelp() R.id.menu_help -> showHelp()
} }
if (item.groupId == R.id.source_group) { if (item.groupId == R.id.source_group) {
searchView.setQuery(item.title, true) searchView.setQuery("group:${item.title}", true)
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(binding.recyclerView) ATH.applyEdgeEffectColor(binding.recyclerView)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.addItemDecoration(VerticalDivider(this)) binding.recyclerView.addItemDecoration(VerticalDivider(this))
adapter = BookSourceAdapter(this, this) adapter = BookSourceAdapter(this, this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
@ -182,6 +180,10 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
searchKey == getString(R.string.disabled) -> { searchKey == getString(R.string.disabled) -> {
App.db.bookSourceDao.liveDataDisabled() App.db.bookSourceDao.liveDataDisabled()
} }
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.bookSourceDao.liveDataGroupSearch("%$key%")
}
else -> { else -> {
App.db.bookSourceDao.liveDataSearch("%$searchKey%") App.db.bookSourceDao.liveDataSearch("%$searchKey%")
} }
@ -206,7 +208,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
Sort.Update -> data.sortedBy { it.lastUpdateTime } Sort.Update -> data.sortedBy { it.lastUpdateTime }
else -> data.reversed() 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.ImageView
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
@ -27,6 +28,51 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
private val selected = linkedSetOf<BookSource>() 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 { override fun getViewBinding(parent: ViewGroup): ItemBookSourceBinding {
return ItemBookSourceBinding.inflate(inflater, parent, false) return ItemBookSourceBinding.inflate(inflater, parent, false)
} }

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

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

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

@ -6,7 +6,6 @@ import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import java.io.BufferedReader import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.text.DecimalFormat import java.text.DecimalFormat
@ -24,11 +23,9 @@ object ConvertUtils {
const val KB: Long = 1024 const val KB: Long = 1024
fun toInt(obj: Any): Int { fun toInt(obj: Any): Int {
return try { return kotlin.runCatching {
Integer.parseInt(obj.toString()) Integer.parseInt(obj.toString())
} catch (e: NumberFormatException) { }.getOrDefault(-1)
-1
}
} }
fun toInt(bytes: ByteArray): Int { fun toInt(bytes: ByteArray): Int {
@ -42,11 +39,9 @@ object ConvertUtils {
} }
fun toFloat(obj: Any): Float { fun toFloat(obj: Any): Float {
return try { return kotlin.runCatching {
java.lang.Float.parseFloat(obj.toString()) java.lang.Float.parseFloat(obj.toString())
} catch (e: NumberFormatException) { }.getOrDefault(-1f)
-1f
}
} }
fun toString(objects: Array<Any>, tag: String): String { 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? { fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? {
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
if (bytes.isNotEmpty()) { if (bytes.isNotEmpty()) {
try { kotlin.runCatching {
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
// 设置让解码器以最佳方式解码 // 设置让解码器以最佳方式解码
options.inPreferredConfig = null options.inPreferredConfig = null
@ -72,7 +67,6 @@ object ConvertUtils {
} }
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
bitmap!!.density = 96// 96 dpi bitmap!!.density = 96// 96 dpi
} catch (e: Exception) {
} }
} }
return bitmap return bitmap
@ -101,7 +95,7 @@ object ConvertUtils {
@JvmOverloads @JvmOverloads
fun toString(`is`: InputStream, charset: String = "utf-8"): String { fun toString(`is`: InputStream, charset: String = "utf-8"): String {
val sb = StringBuilder() val sb = StringBuilder()
try { kotlin.runCatching {
val reader = BufferedReader(InputStreamReader(`is`, charset)) val reader = BufferedReader(InputStreamReader(`is`, charset))
while (true) { while (true) {
val line = reader.readLine() val line = reader.readLine()
@ -113,9 +107,7 @@ object ConvertUtils {
} }
reader.close() reader.close()
`is`.close() `is`.close()
} catch (e: IOException) {
} }
return sb.toString() 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.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.LocalConfig
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.CacheBook import io.legado.app.service.help.CacheBook
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
@ -74,14 +75,16 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
} }
App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao.getBookSource(book.origin)?.let { bookSource ->
val webBook = WebBook(bookSource) val webBook = WebBook(bookSource)
webBook.getChapterList(book, context = upTocPool) webBook.getChapterList(this, book, context = upTocPool)
.timeout(300000) .timeout(300000)
.onSuccess(IO) { .onSuccess(IO) {
App.db.bookDao.update(book) App.db.bookDao.update(book)
App.db.bookChapterDao.delByBook(book.bookUrl) App.db.bookChapterDao.delByBook(book.bookUrl)
App.db.bookChapterDao.insert(*it.toTypedArray()) App.db.bookChapterDao.insert(*it.toTypedArray())
if (AppConfig.preDownload) {
cacheBook(webBook, book) cacheBook(webBook, book)
} }
}
.onError { .onError {
it.printStackTrace() it.printStackTrace()
} }
@ -114,7 +117,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
var addToCache = false var addToCache = false
while (!addToCache) { while (!addToCache) {
if (CacheBook.downloadCount() < 5) { if (CacheBook.downloadCount() < 5) {
CacheBook.download(webBook, book, chapter) CacheBook.download(this, webBook, book, chapter)
addToCache = true addToCache = true
} else { } else {
delay(1000) delay(1000)
@ -148,7 +151,12 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
fun upVersion() { fun upVersion() {
execute { execute {
if (LocalConfig.hasUpHttpTTS) {
DefaultData.importDefaultHttpTTS()
}
if (LocalConfig.hasUpTxtTocRule) {
DefaultData.importDefaultTocRules() DefaultData.importDefaultTocRules()
} }
} }
} }
}

@ -1,6 +1,8 @@
package io.legado.app.ui.main.bookshelf package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem 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.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity 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.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import org.jetbrains.anko.share
/** /**
* 书架界面 * 书架界面
*/ */
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf), class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener, TabLayout.OnTabSelectedListener,
FilePickerDialog.CallBack,
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
private val requestCodeImportBookshelf = 312
private val binding by viewBinding(FragmentBookshelfBinding::bind) private val binding by viewBinding(FragmentBookshelfBinding::bind)
override val viewModel: BookshelfViewModel override val viewModel: BookshelfViewModel
get() = getViewModel(BookshelfViewModel::class.java) get() = getViewModel(BookshelfViewModel::class.java)
@ -67,8 +75,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
when (item.itemId) { when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>() R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_update_toc -> { R.id.menu_update_toc -> {
val group = bookGroups[tabLayout.selectedTabPosition] val fragment = fragmentMap[selectedGroup.groupId]
val fragment = fragmentMap[group.groupId]
fragment?.getBooks()?.let { fragment?.getBooks()?.let {
activityViewModel.upToc(it) 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_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> addBookByUrl() R.id.menu_add_url -> addBookByUrl()
R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity>( R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity>(
Pair("groupId", selectedGroup?.groupId ?: 0), Pair("groupId", selectedGroup.groupId ?: 0),
Pair("groupName", selectedGroup?.groupName ?: 0) Pair("groupName", selectedGroup.groupName ?: 0)
) )
R.id.menu_download -> startActivity<CacheActivity>( R.id.menu_download -> startActivity<CacheActivity>(
Pair("groupId", selectedGroup?.groupId ?: 0), Pair("groupId", selectedGroup.groupId ?: 0),
Pair("groupName", selectedGroup?.groupName ?: 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? private val selectedGroup: BookGroup
get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem) get() = bookGroups[tabLayout.selectedTabPosition]
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(binding.viewPagerBookshelf) ATH.applyEdgeEffectColor(binding.viewPagerBookshelf)
@ -190,8 +204,8 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
override fun onTabReselected(tab: TabLayout.Tab) { override fun onTabReselected(tab: TabLayout.Tab) {
fragmentMap[selectedGroup?.groupId]?.let { fragmentMap[selectedGroup.groupId]?.let {
toast("${selectedGroup?.groupName}(${it.getBooksCount()})") toast("${selectedGroup.groupName}(${it.getBooksCount()})")
} }
} }
@ -202,7 +216,42 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
fun gotoTop() { 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) : 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.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource 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.model.webBook.WebBook
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO 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) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -41,7 +45,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
origin = bookSource.bookSourceUrl, origin = bookSource.bookSourceUrl,
originName = bookSource.bookSourceName originName = bookSource.bookSourceName
) )
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(this, book)
.onSuccess(IO) { .onSuccess(IO) {
it.order = App.db.bookDao.maxOrder + 1 it.order = App.db.bookDao.maxOrder + 1
App.db.bookDao.insert(it) 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>) { fun checkGroup(groups: List<BookGroup>) {
groups.forEach { group -> groups.forEach { group ->
if (group.groupId >= 0 && group.groupId and (group.groupId - 1) != 0L) { 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() { private fun initGroupData() {
liveGroup?.removeObservers(viewLifecycleOwner) liveGroup?.removeObservers(viewLifecycleOwner)
liveGroup = App.db.bookSourceDao.liveGroupExplore() liveGroup = App.db.bookSourceDao.liveExploreGroup()
liveGroup?.observe(viewLifecycleOwner, { liveGroup?.observe(viewLifecycleOwner, {
groups.clear() groups.clear()
it.map { group -> 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?.removeObservers(viewLifecycleOwner)
liveExplore = if (key.isNullOrBlank()) { liveExplore = when {
searchKey.isNullOrBlank() -> {
App.db.bookSourceDao.liveExplore() App.db.bookSourceDao.liveExplore()
} else { }
App.db.bookSourceDao.liveExplore("%$key%") searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.bookSourceDao.liveGroupExplore("%$key%")
}
else -> {
App.db.bookSourceDao.liveExplore("%$searchKey%")
}
} }
liveExplore?.observe(viewLifecycleOwner, { liveExplore?.observe(viewLifecycleOwner, {
binding.tvEmptyMsg.isGone = it.isNotEmpty() || searchView.query.isNotEmpty() 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) { override fun onCompatOptionsItemSelected(item: MenuItem) {
super.onCompatOptionsItemSelected(item) super.onCompatOptionsItemSelected(item)
if (item.groupId == R.id.menu_group_text) { 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 importRequestCode = 132
private val exportRequestCode = 65 private val exportRequestCode = 65
private lateinit var adapter: ReplaceRuleAdapter private lateinit var adapter: ReplaceRuleAdapter
private lateinit var searchView: SearchView
private var groups = hashSetOf<String>() private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null
@ -64,6 +65,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
searchView = binding.titleBar.findViewById(R.id.search_view)
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initSelectActionView() initSelectActionView()
@ -101,13 +103,11 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
} }
private fun initSearchView() { private fun initSearchView() {
binding.titleBar.findViewById<SearchView>(R.id.search_view).let { ATH.setTint(searchView, primaryTextColor)
ATH.setTint(it, primaryTextColor) searchView.onActionViewExpanded()
it.onActionViewExpanded() searchView.queryHint = getString(R.string.replace_purify_search)
it.queryHint = getString(R.string.replace_purify_search) searchView.clearFocus()
it.clearFocus() searchView.setOnQueryTextListener(this)
it.setOnQueryTextListener(this)
}
} }
override fun selectAll(selectAll: Boolean) { override fun selectAll(selectAll: Boolean) {
@ -140,19 +140,26 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}.show() }.show()
} }
private fun observeReplaceRuleData(key: String? = null) { private fun observeReplaceRuleData(searchKey: String? = null) {
dataInit = false dataInit = false
replaceRuleLiveData?.removeObservers(this) replaceRuleLiveData?.removeObservers(this)
replaceRuleLiveData = if (key.isNullOrEmpty()) { replaceRuleLiveData = when {
searchKey.isNullOrEmpty() -> {
App.db.replaceRuleDao.liveDataAll() App.db.replaceRuleDao.liveDataAll()
} else { }
App.db.replaceRuleDao.liveDataSearch(key) searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.replaceRuleDao.liveDataGroupSearch("%$key%")
}
else -> {
App.db.replaceRuleDao.liveDataSearch("%$searchKey%")
}
} }
replaceRuleLiveData?.observe(this, { replaceRuleLiveData?.observe(this, {
if (dataInit) { if (dataInit) {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
} }
adapter.setItems(it) adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true dataInit = true
}) })
} }
@ -179,8 +186,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
R.id.menu_import_source_local -> FilePicker R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) .selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json"))
else -> if (item.groupId == R.id.replace_group) { else -> if (item.groupId == R.id.replace_group) {
binding.titleBar.findViewById<SearchView>(R.id.search_view) searchView.setQuery("group:${item.title}", true)
.setQuery(item.title, true)
} }
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
@ -246,13 +252,13 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
when (requestCode) { when (requestCode) {
importRequestCode -> if (resultCode == Activity.RESULT_OK) { importRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri -> data?.data?.let { uri ->
try { kotlin.runCatching {
uri.readText(this)?.let { uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it) val dataKey = IntentDataHelp.putData(it)
startActivity<ImportReplaceRuleActivity>("dataKey" to dataKey) startActivity<ImportReplaceRuleActivity>("dataKey" to dataKey)
} }
} catch (e: Exception) { }.onFailure {
toast("readTextError:${e.localizedMessage}") toast("readTextError:${it.localizedMessage}")
} }
} }
} }

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
@ -25,6 +26,43 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) :
private val selected = linkedSetOf<ReplaceRule>() 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() { fun selectAll() {
getItems().forEach { getItems().forEach {
selected.add(it) selected.add(it)

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

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
@ -23,6 +24,37 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) :
private val selected = linkedSetOf<RssSource>() 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 { override fun getViewBinding(parent: ViewGroup): ItemRssSourceBinding {
return ItemRssSourceBinding.inflate(inflater, parent, false) 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.legado.app.utils.viewbindingdelegate.viewBinding
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.image.glide.GlideImagesPlugin 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.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -66,6 +68,8 @@ class TextDialog : BaseDialogFragment() {
MD -> binding.textView.post { MD -> binding.textView.post {
Markwon.builder(requireContext()) Markwon.builder(requireContext())
.usePlugin(GlideImagesPlugin.create(requireContext())) .usePlugin(GlideImagesPlugin.create(requireContext()))
.usePlugin(HtmlPlugin.create())
.usePlugin(TablePlugin.create(requireContext()))
.build() .build()
.setMarkdown(binding.textView, content) .setMarkdown(binding.textView, content)
} }

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

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

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

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

@ -19,13 +19,13 @@ object SystemUtils {
fun getScreenOffTime(context: Context): Int { fun getScreenOffTime(context: Context): Int {
var screenOffTime = 0 var screenOffTime = 0
try { kotlin.runCatching {
screenOffTime = Settings.System.getInt( screenOffTime = Settings.System.getInt(
context.contentResolver, context.contentResolver,
Settings.System.SCREEN_OFF_TIMEOUT Settings.System.SCREEN_OFF_TIMEOUT
) )
} catch (e: Exception) { }.onFailure {
e.printStackTrace() it.printStackTrace()
} }
return screenOffTime 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 package io.legado.app.utils
import android.content.Context 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.R
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
@ -13,19 +8,6 @@ import io.legado.app.lib.theme.primaryTextColor
@Suppress("unused") @Suppress("unused")
object UIUtils { 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( fun getMenuColor(
context: Context, context: Context,
theme: Theme = Theme.Auto, theme: Theme = Theme.Auto,

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

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

@ -2,10 +2,10 @@ package io.legado.app.web
import com.google.gson.Gson import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD
import io.legado.app.api.ReturnData
import io.legado.app.api.controller.BookshelfController import io.legado.app.api.controller.BookshelfController
import io.legado.app.api.controller.SourceController import io.legado.app.api.controller.SourceController
import io.legado.app.web.utils.AssetsWeb import io.legado.app.web.utils.AssetsWeb
import io.legado.app.web.utils.ReturnData
import java.util.* import java.util.*
class HttpServer(port: Int) : NanoHTTPD(port) { 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 import fi.iki.elonen.NanoHTTPD
@ -20,13 +20,12 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
CoroutineScope by MainScope(), CoroutineScope by MainScope(),
Debug.Callback { Debug.Callback {
override fun onOpen() { override fun onOpen() {
launch(IO) { launch(IO) {
do { do {
delay(30000) delay(30000)
runCatching { runCatching {
ping(byteArrayOf("ping".toByte())) ping("ping".toByteArray())
} }
} while (isOpen) } while (isOpen)
} }
@ -57,7 +56,7 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
} }
App.db.bookSourceDao.getBookSource(tag)?.let { App.db.bookSourceDao.getBookSource(tag)?.let {
Debug.callback = this Debug.callback = this
Debug.startDebug(WebBook(it), key) Debug.startDebug(this, WebBook(it), key)
} }
} }
} }

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

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

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

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

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

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <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 <item
android:id="@+id/menu_search" android:id="@+id/menu_search"
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
android:title="@string/search" android:title="@string/search"
app:showAsAction="ifRoom" /> app:showAsAction="always" />
<item <item
android:id="@+id/menu_update_toc" android:id="@+id/menu_update_toc"
@ -49,4 +51,17 @@
android:icon="@drawable/ic_arrange" android:icon="@drawable/ic_arrange"
android:title="@string/arrange_bookshelf" android:title="@string/arrange_bookshelf"
app:showAsAction="never" /> 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> </menu>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <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" /> <foreground android:drawable="@drawable/ic_launcher3" />
</adaptive-icon> </adaptive-icon>

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

Loading…
Cancel
Save