Merge pull request #2 from gedoor/master

更新远程版本
pull/885/head
guibiaoguo 5 years ago committed by GitHub
commit bd68266fe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .github/workflows/legado.yml
  2. 8
      app/build.gradle
  3. 10
      app/src/main/AndroidManifest.xml
  4. 61
      app/src/main/assets/defaultData/httpTTS.json
  5. 21
      app/src/main/assets/defaultData/rssSources.json
  6. 17
      app/src/main/assets/help/ruleHelp.md
  7. 32
      app/src/main/assets/updateLog.md
  8. 21
      app/src/main/assets/web/index.html
  9. 6
      app/src/main/assets/web/index.js
  10. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  11. 16
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  12. 13
      app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
  13. 4
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  14. 21
      app/src/main/java/io/legado/app/help/DefaultData.kt
  15. 7
      app/src/main/java/io/legado/app/help/LocalConfig.kt
  16. 1
      app/src/main/java/io/legado/app/help/ThemeConfig.kt
  17. 4
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  18. 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  19. 17
      app/src/main/java/io/legado/app/model/Debug.kt
  20. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  21. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  22. 4
      app/src/main/java/io/legado/app/model/rss/Rss.kt
  23. 14
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  24. 38
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  25. 125
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  26. 67
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  27. 49
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  28. 11
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  29. 15
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  30. 15
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  31. 8
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  32. 5
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  33. 96
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  34. 10
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  35. 6
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  36. 8
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  37. 13
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  38. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  39. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  40. 68
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  41. 9
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  42. 110
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  43. 3
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  44. 2
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  45. 15
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  46. 17
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  47. 4
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt
  48. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  49. 2
      app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt
  50. 31
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  51. 7
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  52. 2
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  53. 58
      app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt
  54. BIN
      app/src/main/res/drawable/image_legado.png
  55. 2
      app/src/main/res/layout/dialog_bookmark.xml
  56. 10
      app/src/main/res/layout/item_bookshelf_list.xml
  57. 92
      app/src/main/res/layout/view_read_menu.xml
  58. 8
      app/src/main/res/menu/book_read.xml
  59. 6
      app/src/main/res/menu/book_source.xml
  60. 6
      app/src/main/res/menu/rss_source.xml
  61. 2
      app/src/main/res/values-zh-rHK/strings.xml
  62. 21
      app/src/main/res/values-zh-rTW/strings.xml
  63. 2
      app/src/main/res/values-zh/strings.xml
  64. 1
      app/src/main/res/values/pref_key_value.xml
  65. 2
      app/src/main/res/values/strings.xml
  66. 16
      app/src/main/res/xml/about.xml
  67. 10
      app/src/main/res/xml/pref_config_theme.xml
  68. 23
      app/src/main/res/xml/spen_remote_actions.xml
  69. 2
      build.gradle

@ -39,11 +39,6 @@ jobs:
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: 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 - name: Release Apk Sign
run: | run: |
echo "给apk增加签名" echo "给apk增加签名"

@ -142,10 +142,10 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.5' def room_version = '2.2.6'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:2.2.5" testImplementation "androidx.room:room-testing:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime-ktx:2.1.2' implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
@ -174,8 +174,8 @@ dependencies {
// //
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.ljx.rxhttp:rxhttp:2.5.1' implementation 'com.ljx.rxhttp:rxhttp:2.5.3'
kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.1' kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.3'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'

@ -150,7 +150,15 @@
<activity <activity
android:name=".ui.book.read.ReadBookActivity" android:name=".ui.book.read.ReadBookActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask" /> android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/spen_remote_actions" />
</activity>
<!-- 书籍详情页 --> <!-- 书籍详情页 -->
<activity <activity
android:name=".ui.book.info.BookInfoActivity" android:name=".ui.book.info.BookInfoActivity"

@ -2,141 +2,146 @@
{ {
"id": -1, "id": -1,
"name": "度丫丫", "name": "度丫丫",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -2, "id": -2,
"name": "度博文①", "name": "度博文①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -3, "id": -3,
"name": "度博文②", "name": "度博文②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -4, "id": -4,
"name": "度博文③", "name": "度博文③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -5, "id": -5,
"name": "度小娇", "name": "度小娇",
"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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -6, "id": -6,
"name": "度小宇", "name": "度小宇",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -7, "id": -7,
"name": "度小童", "name": "度小童",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -8, "id": -8,
"name": "度小美", "name": "度小美",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -9, "id": -9,
"name": "度小萌", "name": "度小萌",
"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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -10, "id": -10,
"name": "度小鹿①", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -11, "id": -11,
"name": "度小鹿②", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -12, "id": -12,
"name": "度小鹿③", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -13, "id": -13,
"name": "度米朵①", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -14, "id": -14,
"name": "度米朵②", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -15, "id": -15,
"name": "度逍遥-基础", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -16, "id": -16,
"name": "度逍遥-精品①", "name": "度逍遥-精品①",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -17, "id": -17,
"name": "度逍遥-精品②", "name": "度逍遥-精品②",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -18, "id": -18,
"name": "情感女声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -19, "id": -19,
"name": "情感男声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -20, "id": -20,
"name": "标准女声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -21, "id": -21,
"name": "标准男声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -22, "id": -22,
"name": "治愈女声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -23, "id": -23,
"name": "治愈男声", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -24, "id": -24,
"name": "甜美女声①", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -25, "id": -25,
"name": "甜美女声②", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -26, "id": -26,
"name": "百度主持", "name": "百度主持",
"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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -27, "id": -27,
"name": "百度评书①", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -28, "id": -28,
"name": "百度评书②", "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}" "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=301&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -29,
"name": "阿里云语音",
"url": "<js>/*播音人Aiting可改其他https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/speaker.json,详见https://ai.aliyun.com/nls/tts*/;eval(''+java.ajax('https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/ttsDemo.js'));ttsDemo(speakText,speakSpeed,'Aiting')</js>"
} }
] ]

@ -0,0 +1,21 @@
[
{
"articleStyle": 0,
"customOrder": 1,
"enableJs": true,
"enabled": true,
"loadWithBaseUrl": false,
"ruleArticles": "class.release-main-section||tbody@tr",
"ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<a href=\"([^\"]+)\".*?><.*?>([^<]+)<\\\/span><\\\/a>\/g,'<h4><a href=\"$1\" style=\"color:#000\">$2<\/a><\/h4><p style=\"font-size:10px\">$1<\/p>\\n\\n')}\nelse{\nhtml=String(java.getString(\"@@class.markdown-body@html\",false)+java.getString(\"@@class.flex-justify-between.-1@html\",false));\nhref='https:\/\/github.com'+String(java.getString(\"@@class.flex-justify-between.-1@class.Box-body.0@a@href\",false))\nhtml+'<br><p>下载地址:<\/p><a style=\"font-size:13px\">'+href+'<\/a>'\n}",
"ruleImage": "@js:if(baseUrl.match(\/legado|704\/g)){\nresult='https:\/\/urlmin.cn\/034'\n}else{result=\"https:\/\/urlmin.cn\/15c\"}",
"ruleLink": "tag.a.0@href\n@js:if(baseUrl.match(\/90pan\/)){\nresult='https:\/\/www.90pan.com\/ajax.php,{\"method\": \"POST\",\"body\": \"action=load_down_addr1&file_id='+result.match(\/b(\\d+)\/)[1]+'\"}'}else{\nresult='https:\/\/github.com'+result\n}",
"ruleNextPage": "text.Next@href",
"rulePubDate": "tag.td!0@text||class.markdown-body@text##\\n##·",
"ruleTitle": "tag.a.0@text",
"sortUrl": "阅读2.0·GitHub::https:\/\/github.com\/gedoor\/MyBookshelf\/releases\n阅读3.0·GitHub::https:\/\/github.com\/gedoor\/legado\/releases\n阅读2.0·90盘::https:\/\/www.90pan.com\/o138703\n阅读3.0·90盘::https:\/\/www.90pan.com\/o138704",
"sourceIcon": "http:\/\/ku.mumuceo.com\/static\/images\/applogo\/yuedu.png",
"sourceName": "阅读下载",
"sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases",
"style": ""
}
]

@ -78,8 +78,7 @@ variable // 自定义书籍变量信息(用于书源规则检索书籍信息)
``` ```
### chapter对象的可用属性 ### chapter对象的可用属性
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符.
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符.
``` ```
url // 章节地址 url // 章节地址
title // 章节标题 title // 章节标题
@ -93,3 +92,17 @@ variable // 自定义书籍变量信息(用于书源规则检索书籍信息)
variable //变量 variable //变量
``` ```
### 字体解析使用
> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码
```
@js:
var b64=String(src).match(/ttf;base64,([^\)]+)/);
if (b64) {
var f1 = java.queryBase64TTF(b64[1])
var f2 = java.queryTTF("/storage/emulated/0/Fonts/Source Han Sans CN Regular.ttf")
java.replaceFont(result, f1, f2)
}else{
result
}
```

@ -3,8 +3,25 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2021/01/04** **2021/01/23**
* 优化书源校验,从搜索到正文全部校验
**2021/01/20**
* 更新在线朗读
**2021/01/18**
* 增加三星 S Pen 支持 by [dacer](https://github.com/dacer)
* 订阅添加阅读下载,可以从多个渠道下载
* 修复一些BUG
**2021/01/12**
* 修复bug * 修复bug
* 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc)
* 换源刷新之前删除原搜索记录
* 优化web调试
**2021/01/05**
* 修复崩溃bug
**2021/01/03** **2021/01/03**
* 导出书单只保留书名与作者,导入时自动查找可用源 * 导出书单只保留书名与作者,导入时自动查找可用源
@ -68,18 +85,7 @@
* 阅读进度从页数改为字数,排版变化时定位更准确 * 阅读进度从页数改为字数,排版变化时定位更准确
* 修改viewBinding * 修改viewBinding
* 修复中文排序 * 修复中文排序
* 去掉FontJs规则,可以写在替换规则里,例:f2为正确的字体,根据文字数据去正确的字体里面查找,可以把错误的文字替换为正确的文字 * 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看
```
@js:
var b64=String(src).match(/ttf;base64,([^\)]+)/);
if (b64) {
var f1 = java.queryBase64TTF(b64[1])
var f2 = java.queryTTF("/storage/emulated/0/Fonts/Source Han Sans CN Regular.ttf")
java.replaceFont(result, f1, f2)
}else{
result
}
```
**2020/11/30** **2020/11/30**
* 优化阅读界面设置 * 优化阅读界面设置

@ -228,29 +228,30 @@
<div> <div>
<div>章节信息:</div> <div>章节信息:</div>
<textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime" <textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"
placeholder="选择章节信息 (规则结果为String)"></textarea> placeholder="选择章节信息 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>翻页规则:</div> <div>翻页规则:</div>
<textarea rows="1" id="ruleToc_nextTocUrl" class="ruleToc" title="nextTocUrl" <textarea rows="1" id="ruleToc_nextTocUrl" class="ruleToc" title="nextTocUrl"
placeholder="选择目录下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea> placeholder="选择目录下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea>
</div> </div>
<p></p> <p></p>
<div><b>正文</b></div> <div><b>正文</b></div>
<div>
<div>脚本注入:</div>
<textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
</div>
<div> <div>
<div>正文规则:</div> <div>正文规则:</div>
<textarea rows="1" id="ruleContent_content" class="ruleContent" title="content" <textarea rows="1" id="ruleContent_content" class="ruleContent" title="content"
placeholder="选择正文内容 (规则结果为String)"></textarea> placeholder="选择正文内容 (规则结果为String)"></textarea>
</div> </div>
<div> <div>
<div>翻页规则:</div> <div>翻页规则:</div>
<textarea rows="1" id="ruleContent_nextContentUrl" class="ruleContent" title="nextContentUrl" <textarea rows="1" id="ruleContent_nextContentUrl" class="ruleContent"
placeholder="选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"></textarea> title="nextContentUrl"
</div> placeholder="选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"></textarea>
<div>
<div>脚本注入:</div>
<textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
</div> </div>
<div> <div>
<div>资源正则:</div> <div>资源正则:</div>

@ -383,15 +383,15 @@ $('.menu').addEventListener('click', e => {
ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`); ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
}; };
ws.onmessage = (msg) => { ws.onmessage = (msg) => {
DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data); console.log('[调试]', msg);
if (msg.data == 'finish') setRule(saveRule[0]); DebugPrint(msg.data);
}; };
ws.onerror = (err) => { ws.onerror = (err) => {
throw `${err.data}`; throw `${err.data}`;
} }
ws.onclose = () => { ws.onclose = () => {
thisNode.setAttribute('class', ''); thisNode.setAttribute('class', '');
DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`); DebugPrint(`\n调试服务已关闭!`);
} }
} else throw `${sResult.errorMsg}`; } else throw `${sResult.errorMsg}`;
}).catch(err => { }).catch(err => {

@ -70,10 +70,12 @@ object PreferKey {
const val cAccent = "colorAccent" const val cAccent = "colorAccent"
const val cBackground = "colorBackground" const val cBackground = "colorBackground"
const val cBBackground = "colorBottomBackground" const val cBBackground = "colorBottomBackground"
const val bgImage = "backgroundImage"
const val cNPrimary = "colorPrimaryNight" const val cNPrimary = "colorPrimaryNight"
const val cNAccent = "colorAccentNight" const val cNAccent = "colorAccentNight"
const val cNBackground = "colorBackgroundNight" const val cNBackground = "colorBackgroundNight"
const val cNBBackground = "colorBottomBackgroundNight" const val cNBBackground = "colorBottomBackgroundNight"
const val bgImageN = "backgroundImageNight"
} }

@ -12,10 +12,10 @@ interface BookSourceDao {
fun liveDataAll(): LiveData<List<BookSource>> fun liveDataAll(): LiveData<List<BookSource>>
@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") @Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
fun liveDataGroupSearch(searchKey: String = ""): LiveData<List<BookSource>> 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>>
@ -32,18 +32,15 @@ interface BookSourceDao {
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key) order by customOrder asc") @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>> fun liveGroupExplore(key: String): LiveData<List<BookSource>>
@Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") @Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>> fun liveGroup(): LiveData<List<String>>
@Query("select bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''") @Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
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 distinct bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''")
fun liveExploreGroup(): LiveData<List<String>> fun liveExploreGroup(): LiveData<List<String>>
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("select * from book_sources where enabledExplore = 1 order by customOrder asc") @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource> fun observeFind(): DataSource.Factory<Int, BookSource>
@ -68,6 +65,9 @@ interface BookSourceDao {
@get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder") @get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
val allTextEnabled: List<BookSource> val allTextEnabled: List<BookSource>
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
val allGroup: List<String>
@Query("select * from book_sources where bookSourceUrl = :key") @Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource? fun getBookSource(key: String): BookSource?

@ -28,12 +28,21 @@ interface RssSourceDao {
@Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder") @Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder")
fun liveGroupSearch(key: String): LiveData<List<RssSource>> 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 order by customOrder")
fun liveEnabled(): LiveData<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 1 and (sourceName like :searchKey or sourceGroup like :searchKey or sourceUrl like :searchKey) order by customOrder")
fun liveEnabled(searchKey: String): LiveData<List<RssSource>> fun liveEnabled(searchKey: String): LiveData<List<RssSource>>
@Query("select sourceGroup from rssSources where trim(sourceGroup) <> ''") @Query("SELECT * FROM rssSources where enabled = 1 and sourceGroup like :searchKey order by customOrder")
fun liveEnabledByGroup(searchKey: String): LiveData<List<RssSource>>
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
fun liveGroup(): LiveData<List<String>> fun liveGroup(): LiveData<List<String>>
@get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
val allGroup: List<String>
@get:Query("select min(customOrder) from rssSources") @get:Query("select min(customOrder) from rssSources")
val minOrder: Int val minOrder: Int

@ -63,6 +63,10 @@ interface SearchBookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long> fun insert(vararg searchBook: SearchBook): List<Long>
@Query("delete from searchBooks where name = :name and author = :author")
fun clear(name: String, author: String)
@Query("delete from searchBooks where time < :time") @Query("delete from searchBooks where time < :time")
fun clearExpired(time: Long) fun clearExpired(time: Long)
} }

@ -2,6 +2,7 @@ package io.legado.app.help
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.data.entities.RssSource
import io.legado.app.data.entities.TxtTocRule import io.legado.app.data.entities.TxtTocRule
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonArray
@ -45,17 +46,25 @@ object DefaultData {
GSON.fromJsonArray<ThemeConfig.Config>(json)!! GSON.fromJsonArray<ThemeConfig.Config>(json)!!
} }
val rssSources by lazy {
val json = String(
App.INSTANCE.assets.open("defaultData${File.separator}rssSources.json")
.readBytes()
)
GSON.fromJsonArray<RssSource>(json)!!
}
fun importDefaultHttpTTS() { fun importDefaultHttpTTS() {
App.db.httpTTSDao.deleteDefault() App.db.httpTTSDao.deleteDefault()
httpTTS.let { App.db.httpTTSDao.insert(*httpTTS.toTypedArray())
App.db.httpTTSDao.insert(*it.toTypedArray())
}
} }
fun importDefaultTocRules() { fun importDefaultTocRules() {
App.db.txtTocRule.deleteDefault() App.db.txtTocRule.deleteDefault()
txtTocRules.let { App.db.txtTocRule.insert(*txtTocRules.toTypedArray())
App.db.txtTocRule.insert(*it.toTypedArray()) }
}
fun importDefaultRssSources() {
App.db.rssSourceDao.insert(*rssSources.toTypedArray())
} }
} }

@ -65,8 +65,11 @@ object LocalConfig {
get() = isLastVersion(1, "ruleHelpVersion") get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean val hasUpHttpTTS: Boolean
get() = isLastVersion(1, "httpTtsVersion") get() = !isLastVersion(2, "httpTtsVersion")
val hasUpTxtTocRule: Boolean val hasUpTxtTocRule: Boolean
get() = isLastVersion(1, "txtTocRuleVersion") get() = !isLastVersion(1, "txtTocRuleVersion")
val hasUpRssSources: Boolean
get() = !isLastVersion(1, "rssSourceVersion")
} }

@ -204,6 +204,7 @@ object ThemeConfig {
var primaryColor: String, var primaryColor: String,
var accentColor: String, var accentColor: String,
var backgroundColor: String, var backgroundColor: String,
var backgroundImage: String? = null,
var bottomBackground: String var bottomBackground: String
) )

@ -29,7 +29,7 @@ object Backup {
"bookmark.json", "bookmark.json",
"bookGroup.json", "bookGroup.json",
"bookSource.json", "bookSource.json",
"rssSource.json", "rssSources.json",
"rssStar.json", "rssStar.json",
"replaceRule.json", "replaceRule.json",
"readRecord.json", "readRecord.json",
@ -61,7 +61,7 @@ object Backup {
writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath) writeListToJson(App.db.rssSourceDao.all, "rssSources.json", backupPath)
writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath)

@ -122,7 +122,7 @@ object Restore {
fileToListT<BookSource>(path, "bookSource.json")?.let { fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao.insert(*it.toTypedArray()) App.db.bookSourceDao.insert(*it.toTypedArray())
} }
fileToListT<RssSource>(path, "rssSource.json")?.let { fileToListT<RssSource>(path, "rssSources.json")?.let {
App.db.rssSourceDao.insert(*it.toTypedArray()) App.db.rssSourceDao.insert(*it.toTypedArray())
} }
fileToListT<RssStar>(path, "rssStar.json")?.let { fileToListT<RssStar>(path, "rssStar.json")?.let {

@ -16,8 +16,8 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
object Debug { object Debug {
private var debugSource: String? = null
var callback: Callback? = null var callback: Callback? = null
private var debugSource: String? = null
private val tasks: CompositeCoroutine = CompositeCoroutine() private val tasks: CompositeCoroutine = CompositeCoroutine()
@SuppressLint("ConstantLocale") @SuppressLint("ConstantLocale")
@ -59,12 +59,12 @@ object Debug {
} }
} }
fun startDebug(rssSource: RssSource) { fun startDebug(scope: CoroutineScope, rssSource: RssSource) {
cancelDebug() cancelDebug()
debugSource = rssSource.sourceUrl debugSource = rssSource.sourceUrl
log(debugSource, "︾开始解析") log(debugSource, "︾开始解析")
val sort = rssSource.sortUrls().entries.first() val sort = rssSource.sortUrls().entries.first()
Rss.getArticles(sort.key, sort.value, rssSource, 1) Rss.getArticles(scope, sort.key, sort.value, rssSource, 1)
.onSuccess { .onSuccess {
if (it.articles.isEmpty()) { if (it.articles.isEmpty()) {
log(debugSource, "⇒列表页解析成功,为空") log(debugSource, "⇒列表页解析成功,为空")
@ -77,7 +77,7 @@ object Debug {
if (ruleContent.isNullOrEmpty()) { if (ruleContent.isNullOrEmpty()) {
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000) log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
} else { } else {
rssContentDebug(it.articles[0], ruleContent, rssSource) rssContentDebug(scope, it.articles[0], ruleContent, rssSource)
} }
} else { } else {
log(debugSource, "⇒存在描述规则,不解析内容页") log(debugSource, "⇒存在描述规则,不解析内容页")
@ -90,9 +90,14 @@ object Debug {
} }
} }
private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String, rssSource: RssSource) { private fun rssContentDebug(
scope: CoroutineScope,
rssArticle: RssArticle,
ruleContent: String,
rssSource: RssSource
) {
log(debugSource, "︾开始解析内容页") log(debugSource, "︾开始解析内容页")
Rss.getContent(rssArticle, ruleContent, rssSource) Rss.getContent(scope, rssArticle, ruleContent, rssSource)
.onSuccess { .onSuccess {
log(debugSource, it) log(debugSource, it)
log(debugSource, "︽内容页解析完成", state = 1000) log(debugSource, "︽内容页解析完成", state = 1000)

@ -646,7 +646,9 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
bindings["chapter"] = chapter bindings["chapter"] = chapter
bindings["title"] = chapter?.title bindings["title"] = chapter?.title
bindings["src"] = content bindings["src"] = content
return SCRIPT_ENGINE.eval(jsStr, bindings) return runBlocking {
SCRIPT_ENGINE.eval(jsStr, bindings)
}
} }
/** /**

@ -309,7 +309,7 @@ class AnalyzeUrl(
} }
return when (method) { return when (method) {
RequestMethod.POST -> { RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url) RxHttp.postForm(url)
.setAssemblyEnabled(false) .setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy)) .setOkClient(HttpHelper.getProxyClient(proxy))
@ -338,7 +338,7 @@ class AnalyzeUrl(
setCookie(tag) setCookie(tag)
return when (method) { return when (method) {
RequestMethod.POST -> { RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url) RxHttp.postForm(url)
.setAssemblyEnabled(false) .setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy)) .setOkClient(HttpHelper.getProxyClient(proxy))

@ -14,11 +14,11 @@ import kotlin.coroutines.CoroutineContext
object Rss { object Rss {
fun getArticles( fun getArticles(
scope: CoroutineScope,
sortName: String, sortName: String,
sortUrl: String, sortUrl: String,
rssSource: RssSource, rssSource: RssSource,
page: Int, page: Int,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO context: CoroutineContext = Dispatchers.IO
): Coroutine<RssResult> { ): Coroutine<RssResult> {
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
@ -35,10 +35,10 @@ object Rss {
} }
fun getContent( fun getContent(
scope: CoroutineScope,
rssArticle: RssArticle, rssArticle: RssArticle,
ruleContent: String, ruleContent: String,
rssSource: RssSource?, rssSource: RssSource?,
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) {

@ -93,15 +93,18 @@ object BookChapterList {
for (item in chapterDataList) { for (item in chapterDataList) {
downloadToc( downloadToc(
scope, item, book, bookSource, scope, item, book, bookSource,
tocRule, listRule, chapterList, chapterDataList tocRule, listRule, chapterList, chapterDataList,
{
block.resume(finish(book, chapterList, reverse))
}
) { ) {
block.resume(finish(book, chapterList, reverse)) block.cancel(it)
} }
} }
} }
} }
}.onFailure { }.onFailure {
block.resumeWithException(it) block.cancel(it)
} }
} }
@ -114,7 +117,8 @@ 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: (error: Throwable) -> Unit
) { ) {
Coroutine.async(scope = scope) { Coroutine.async(scope = scope) {
val nextBody = AnalyzeUrl( val nextBody = AnalyzeUrl(
@ -142,7 +146,7 @@ object BookChapterList {
} }
} }
}.onError { }.onError {
throw it onError.invoke(it)
} }
} }

@ -8,9 +8,11 @@ import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.CheckSource import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -21,7 +23,7 @@ import kotlin.math.min
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private var threadCount = AppConfig.threadCount private var threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() private var searchCoroutine = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var tasks = CompositeCoroutine() private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>() private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>() private val checkedIds = ArrayList<String>()
@ -62,7 +64,7 @@ class CheckSourceService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
tasks.clear() tasks.clear()
searchPool.close() searchCoroutine.close()
postEvent(EventBus.CHECK_SOURCE_DONE, 0) postEvent(EventBus.CHECK_SOURCE_DONE, 0)
} }
@ -96,18 +98,36 @@ class CheckSourceService : BaseService() {
if (index < allIds.size) { if (index < allIds.size) {
val sourceUrl = allIds[index] val sourceUrl = allIds[index]
App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source -> App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) { check(source)
onNext(sourceUrl, source.bookSourceName)
} else {
CheckSource(source).check(this, searchPool) {
onNext(it, source.bookSourceName)
}
}
} ?: onNext(sourceUrl, "") } ?: onNext(sourceUrl, "")
} }
} }
} }
fun check(source: BookSource) {
execute(context = searchCoroutine) {
val webBook = WebBook(source)
val books = webBook.searchBookAwait(this, CheckSource.keyword)
val book = webBook.getBookInfoAwait(this, books.first().toBook())
val toc = webBook.getChapterListAwait(this, book)
val content = webBook.getContentAwait(this, book, toc.first())
if (content.isBlank()) {
throw Exception("正文内容为空")
}
}.timeout(60000L)
.onError {
source.addGroup("失效")
source.bookSourceComment =
"error:${it.localizedMessage}\n${source.bookSourceComment}"
App.db.bookSourceDao.update(source)
}.onSuccess {
source.removeGroup("失效")
App.db.bookSourceDao.update(source)
}.onFinally {
onNext(source.bookSourceUrl, source.bookSourceName)
}
}
private fun onNext(sourceUrl: String, sourceName: String) { private fun onNext(sourceUrl: String, sourceName: String) {
synchronized(this) { synchronized(this) {
check() check()

@ -12,11 +12,17 @@ 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.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import org.jetbrains.anko.collections.forEachWithIndex
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.util.*
class HttpReadAloudService : BaseReadAloudService(), class HttpReadAloudService : BaseReadAloudService(),
MediaPlayer.OnPreparedListener, MediaPlayer.OnPreparedListener,
@ -55,12 +61,17 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun play() { override fun play() {
if (contentList.isEmpty()) return if (contentList.isEmpty()) return
if (nowSpeak == 0) { ReadAloud.httpTTS?.let {
downloadAudio() val fileName = md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), contentList[nowSpeak])
} else { if (nowSpeak == 0) {
val file = getSpeakFile(nowSpeak) downloadAudio()
if (file.exists()) { } else {
playAudio(FileInputStream(file).fd) val file = getSpeakFileAsMd5(fileName)
if (file.exists()) {
playAudio(FileInputStream(file).fd)
} else {
downloadAudio()
}
} }
} }
} }
@ -68,28 +79,53 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun downloadAudio() { private fun downloadAudio() {
task?.cancel() task?.cancel()
task = execute { task = execute {
FileUtils.deleteFile(ttsFolder) removeCacheFile()
for (index in 0 until contentList.size) { ReadAloud.httpTTS?.let {
if (isActive) { contentList.forEachWithIndex { index, item ->
ReadAloud.httpTTS?.let { if (isActive) {
AnalyzeUrl( val fileName =
it.url, md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item)
speakText = contentList[index],
speakSpeed = AppConfig.ttsSpeechRate if (hasSpeakFile(fileName)) { //已经下载好的语音缓存
).getByteArray().let { bytes -> if (index == nowSpeak) {
if (isActive) { val file = getSpeakFileAsMd5(fileName)
val file = getSpeakFile(index)
file.writeBytes(bytes) @Suppress("BlockingMethodInNonBlockingContext")
if (index == nowSpeak) { val fis = FileInputStream(file)
@Suppress("BlockingMethodInNonBlockingContext") playAudio(fis.fd)
val fis = FileInputStream(file) }
playAudio(fis.fd) } else if (hasSpeakCacheFile(fileName)) { //缓存文件还在,可能还没下载完
return@let
} else { //没有下载并且没有缓存文件
try {
createSpeakCacheFile(fileName)
AnalyzeUrl(
it.url,
speakText = item,
speakSpeed = AppConfig.ttsSpeechRate
).getByteArray().let { bytes ->
ensureActive()
val file = getSpeakFileAsMd5(fileName)
//val file = getSpeakFile(index)
file.writeBytes(bytes)
removeSpeakCacheFile(fileName)
if (index == nowSpeak) {
@Suppress("BlockingMethodInNonBlockingContext")
val fis = FileInputStream(file)
playAudio(fis.fd)
}
} }
} catch (e: SocketTimeoutException) {
removeSpeakCacheFile(fileName)
// delay(2000)
// downloadAudio()
} catch (e: ConnectException) {
removeSpeakCacheFile(fileName)
} catch (e: Exception) {
removeSpeakCacheFile(fileName)
} }
} }
} }
} else {
break
} }
} }
} }
@ -110,10 +146,47 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
private fun getSpeakFile(index: Int = nowSpeak): File { private fun speakFilePath() = ttsFolder + File.separator
return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") private fun md5SpeakFileName(url: String, ttsConfig: String, content: String): String {
return MD5Utils.md5Encode16(textChapter!!.title) + "_" + MD5Utils.md5Encode16("$url-|-$ttsConfig-|-$content")
} }
private fun hasSpeakFile(name: String) =
FileUtils.exist("${speakFilePath()}$name.mp3")
private fun hasSpeakCacheFile(name: String) =
FileUtils.exist("${speakFilePath()}$name.mp3.cache")
private fun createSpeakCacheFile(name: String): File =
FileUtils.createFileWithReplace("${speakFilePath()}$name.mp3.cache")
private fun removeSpeakCacheFile(name: String) {
FileUtils.delete("${speakFilePath()}$name.mp3.cache")
}
private fun getSpeakFileAsMd5(name: String): File =
FileUtils.createFileIfNotExist("${speakFilePath()}$name.mp3")
private fun removeCacheFile() {
FileUtils.listDirsAndFiles(speakFilePath())?.forEach {
if (it == null) {
return@forEach
}
if (Regex(""".+\.mp3$""").matches(it.name)) { //mp3缓存文件
val reg =
"""^${MD5Utils.md5Encode16(textChapter!!.title)}_[a-z0-9]{16}\.mp3$""".toRegex()
if (!reg.matches(it.name)) {
FileUtils.deleteFile(it.absolutePath)
}
} else {
if (Date().time - it.lastModified() > 30000) {
FileUtils.deleteFile(it.absolutePath)
}
}
}
}
override fun pauseReadAloud(pause: Boolean) { override fun pauseReadAloud(pause: Boolean) {
super.pauseReadAloud(pause) super.pauseReadAloud(pause)
mediaPlayer.pause() mediaPlayer.pause()

@ -2,64 +2,35 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CheckSourceService import io.legado.app.service.CheckSourceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
class CheckSource(val source: BookSource) { object CheckSource {
var keyword = "我的"
companion object { fun start(context: Context, sources: List<BookSource>) {
var keyword = "我的" if (sources.isEmpty()) {
context.toast(R.string.non_select)
fun start(context: Context, sources: List<BookSource>) { return
if (sources.isEmpty()) {
context.toast(R.string.non_select)
return
}
val selectedIds: ArrayList<String> = arrayListOf()
sources.map {
selectedIds.add(it.bookSourceUrl)
}
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.start
it.putExtra("selectIds", selectedIds)
context.startService(it)
}
} }
val selectedIds: ArrayList<String> = arrayListOf()
fun stop(context: Context) { sources.map {
Intent(context, CheckSourceService::class.java).let { selectedIds.add(it.bookSourceUrl)
it.action = IntentAction.stop }
context.startService(it) Intent(context, CheckSourceService::class.java).let {
} it.action = IntentAction.start
it.putExtra("selectIds", selectedIds)
context.startService(it)
} }
} }
fun check( fun stop(context: Context) {
scope: CoroutineScope, Intent(context, CheckSourceService::class.java).let {
context: CoroutineContext, it.action = IntentAction.stop
onNext: (sourceUrl: String) -> Unit context.startService(it)
): Coroutine<*> { }
val webBook = WebBook(source)
return webBook
.searchBook(scope, keyword, context = context)
.timeout(60000L)
.onError(Dispatchers.IO) {
source.addGroup("失效")
App.db.bookSourceDao.update(source)
}.onSuccess(Dispatchers.IO) {
source.removeGroup("失效")
App.db.bookSourceDao.update(source)
}.onFinally(Dispatchers.IO) {
onNext(source.bookSourceUrl)
}
} }
} }

@ -183,9 +183,11 @@ object ReadBook {
} }
} }
fun skipToPage(index: Int) { fun skipToPage(index: Int, success: (() -> Unit)? = null) {
durChapterPos = curTextChapter?.getReadLength(index) ?: index durChapterPos = curTextChapter?.getReadLength(index) ?: index
callBack?.upContent() callBack?.upContent() {
success?.invoke()
}
curPageChanged() curPageChanged()
saveRead() saveRead()
} }
@ -240,19 +242,28 @@ object ReadBook {
/** /**
* 加载章节内容 * 加载章节内容
*/ */
fun loadContent(resetPageOffset: Boolean) { fun loadContent(resetPageOffset: Boolean, success: (() -> Unit)? = null) {
loadContent(durChapterIndex, resetPageOffset = resetPageOffset) loadContent(durChapterIndex, resetPageOffset = resetPageOffset) {
success?.invoke()
}
loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset) loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
} }
fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) { fun loadContent(
index: Int,
upContent: Boolean = true,
resetPageOffset: Boolean,
success: (() -> Unit)? = null
) {
book?.let { book -> book?.let { book ->
if (addLoading(index)) { if (addLoading(index)) {
Coroutine.async { Coroutine.async {
App.db.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter -> App.db.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter ->
BookHelp.getContent(book, chapter)?.let { BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(book, chapter, it, upContent, resetPageOffset) contentLoadFinish(book, chapter, it, upContent, resetPageOffset) {
success?.invoke()
}
removeLoading(chapter.index) removeLoading(chapter.index)
} ?: download(chapter, resetPageOffset = resetPageOffset) } ?: download(chapter, resetPageOffset = resetPageOffset)
} ?: removeLoading(index) } ?: removeLoading(index)
@ -282,7 +293,11 @@ object ReadBook {
} }
} }
private fun download(chapter: BookChapter, resetPageOffset: Boolean) { private fun download(
chapter: BookChapter,
resetPageOffset: Boolean,
success: (() -> Unit)? = null
) {
val book = book val book = book
val webBook = webBook val webBook = webBook
if (book != null && webBook != null) { if (book != null && webBook != null) {
@ -290,7 +305,9 @@ object ReadBook {
} else if (book != null) { } else if (book != null) {
contentLoadFinish( contentLoadFinish(
book, chapter, "没有书源", resetPageOffset = resetPageOffset book, chapter, "没有书源", resetPageOffset = resetPageOffset
) ) {
success?.invoke()
}
removeLoading(chapter.index) removeLoading(chapter.index)
} else { } else {
removeLoading(chapter.index) removeLoading(chapter.index)
@ -379,7 +396,8 @@ object ReadBook {
chapter: BookChapter, chapter: BookChapter,
content: String, content: String,
upContent: Boolean = true, upContent: Boolean = true,
resetPageOffset: Boolean resetPageOffset: Boolean,
success: (() -> Unit)? = null
) { ) {
Coroutine.async { Coroutine.async {
ImageProvider.clearOut(durChapterIndex) ImageProvider.clearOut(durChapterIndex)
@ -420,6 +438,8 @@ object ReadBook {
}.onError { }.onError {
it.printStackTrace() it.printStackTrace()
App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}") App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}")
}.onSuccess {
success?.invoke()
} }
} }
@ -460,10 +480,19 @@ object ReadBook {
interface CallBack { interface CallBack {
fun loadChapterList(book: Book) fun loadChapterList(book: Book)
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upContent(
relativePosition: Int = 0,
resetPageOffset: Boolean = true,
success: (() -> Unit)? = null
)
fun upView() fun upView()
fun pageChanged() fun pageChanged()
fun contentLoadFinish() fun contentLoadFinish()
fun upPageAnim() fun upPageAnim()
} }

@ -9,6 +9,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
@ -24,19 +25,24 @@ class AboutFragment : PreferenceFragmentCompat() {
Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"), Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"),
Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"), Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"),
Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"), Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"),
Pair("(QQ群VIP4)680280282", "VF2UwvUCuaqlo6pddWTe_kw__a1_Fr8O"),
Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"), Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"),
Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"), Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"),
Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"), Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"),
Pair("(QQ群4)256929088", "czEJPLDnT4Pd9SKQ6RoRVzKhDxLchZrO"), Pair("(QQ群4)256929088", "czEJPLDnT4Pd9SKQ6RoRVzKhDxLchZrO"),
Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX"), Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX"),
Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n"), Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n"),
Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC") Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC"),
Pair("(QQ群8)1079926194", "gg2qFH8q9IPFaCHV3H7CqCN-YljvazE1"),
) )
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.about) addPreferencesFromResource(R.xml.about)
findPreference<Preference>("check_update")?.summary = findPreference<Preference>("update_log")?.summary =
"${getString(R.string.version)} ${App.versionName}" "${getString(R.string.version)} ${App.versionName}"
if (AppConfig.isGooglePlay) {
preferenceScreen.removePreferenceRecursively("check_update")
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -58,6 +64,7 @@ class AboutFragment : PreferenceFragmentCompat() {
"qq" -> showQqGroups() "qq" -> showQqGroups()
"gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh)) "gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh))
"tg" -> openUrl(R.string.tg_url) "tg" -> openUrl(R.string.tg_url)
"discord" -> openUrl(R.string.discord_url)
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }

@ -10,10 +10,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
@ -21,10 +23,8 @@ import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.*
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
/** /**
@ -85,7 +85,14 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater) val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>()
App.db.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp
}
customView = alertBinding.root customView = alertBinding.root
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> alertBinding.editView.text?.toString()?.let { group ->

@ -10,10 +10,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
@ -21,10 +23,8 @@ import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.*
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
/** /**
@ -85,7 +85,14 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater) val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>()
App.db.rssSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp
}
customView = alertBinding.root customView = alertBinding.root
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> alertBinding.editView.text?.toString()?.let { group ->

@ -84,10 +84,10 @@ class ChangeSourceDialog : BaseDialogFragment(),
binding.toolBar.inflateMenu(R.menu.change_source) binding.toolBar.inflateMenu(R.menu.change_source)
binding.toolBar.menu.applyTint(requireContext()) binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener(this) binding.toolBar.setOnMenuItemClickListener(this)
binding.toolBar.menu.findItem(R.id.menu_load_info)?.isChecked = binding.toolBar.menu.findItem(R.id.menu_load_info)
AppConfig.changeSourceLoadInfo ?.isChecked = AppConfig.changeSourceLoadInfo
binding.toolBar.menu.findItem(R.id.menu_load_toc)?.isChecked = AppConfig.changeSourceLoadToc binding.toolBar.menu.findItem(R.id.menu_load_toc)
?.isChecked = AppConfig.changeSourceLoadToc
} }
private fun initRecyclerView() { private fun initRecyclerView() {

@ -88,6 +88,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
private fun searchFinish(searchBook: SearchBook) { private fun searchFinish(searchBook: SearchBook) {
if (searchBooks.contains(searchBook)) return
App.db.searchBookDao.insert(searchBook) App.db.searchBookDao.insert(searchBook)
if (screenKey.isEmpty()) { if (screenKey.isEmpty()) {
searchBooks.add(searchBook) searchBooks.add(searchBook)
@ -99,6 +100,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
private fun startSearch() { private fun startSearch() {
execute { execute {
App.db.searchBookDao.clear(name, author)
searchBooks.clear()
upAdapter()
bookSourceList.clear() bookSourceList.clear()
if (searchGroup.isBlank()) { if (searchGroup.isBlank()) {
bookSourceList.addAll(App.db.bookSourceDao.allEnabled) bookSourceList.addAll(App.db.bookSourceDao.allEnabled)
@ -137,7 +141,6 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} else { } else {
searchFinish(searchBook) searchFinish(searchBook)
} }
return@onSuccess
} }
} }
} }

@ -51,10 +51,8 @@ import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -171,8 +169,6 @@ class ReadBookActivity : ReadBookBaseActivity(),
R.id.menu_group_on_line_ns -> item.isVisible = onLine R.id.menu_group_on_line_ns -> item.isVisible = onLine
R.id.menu_group_local -> item.isVisible = !onLine R.id.menu_group_local -> item.isVisible = !onLine
R.id.menu_group_text -> item.isVisible = book.isLocalTxt() R.id.menu_group_text -> item.isVisible = book.isLocalTxt()
R.id.menu_group_login ->
item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
else -> when (item.itemId) { else -> when (item.itemId) {
R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule()
R.id.menu_re_segment -> item.isChecked = book.getReSegment() R.id.menu_re_segment -> item.isChecked = book.getReSegment()
@ -245,12 +241,6 @@ class ReadBookActivity : ReadBookBaseActivity(),
supportFragmentManager, supportFragmentManager,
ReadBook.book?.tocUrl ReadBook.book?.tocUrl
) )
R.id.menu_login -> ReadBook.webBook?.bookSource?.let {
startActivity<SourceLogin>(
Pair("sourceUrl", it.bookSourceUrl),
Pair("loginUrl", it.loginUrl)
)
}
R.id.menu_set_charset -> showCharsetConfig() R.id.menu_set_charset -> showCharsetConfig()
R.id.menu_get_progress -> ReadBook.book?.let { R.id.menu_get_progress -> ReadBook.book?.let {
viewModel.syncBookProgress(it) { progress -> viewModel.syncBookProgress(it) { progress ->
@ -310,6 +300,14 @@ class ReadBookActivity : ReadBookBaseActivity(),
return true return true
} }
} }
keyCode == KeyEvent.KEYCODE_PAGE_UP -> {
binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV)
return true
}
keyCode == KeyEvent.KEYCODE_PAGE_DOWN -> {
binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
return true
}
keyCode == KeyEvent.KEYCODE_SPACE -> { keyCode == KeyEvent.KEYCODE_SPACE -> {
binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
return true return true
@ -531,13 +529,18 @@ class ReadBookActivity : ReadBookBaseActivity(),
/** /**
* 更新内容 * 更新内容
*/ */
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { override fun upContent(
autoPageProgress = 0 relativePosition: Int,
resetPageOffset: Boolean,
success: (() -> Unit)?
) {
launch { launch {
autoPageProgress = 0
binding.readView.upContent(relativePosition, resetPageOffset) binding.readView.upContent(relativePosition, resetPageOffset)
binding.readMenu.setSeekPage(ReadBook.durPageIndex()) binding.readMenu.setSeekPage(ReadBook.durPageIndex())
loadStates = false
success?.invoke()
} }
loadStates = false
} }
/** /**
@ -715,6 +718,15 @@ class ReadBookActivity : ReadBookBaseActivity(),
upNavigationBarColor() upNavigationBarColor()
} }
override fun showLogin() {
ReadBook.webBook?.bookSource?.let {
startActivity<SourceLogin>(
Pair("sourceUrl", it.bookSourceUrl),
Pair("loginUrl", it.loginUrl)
)
}
}
/** /**
* 朗读按钮 * 朗读按钮
*/ */
@ -775,7 +787,9 @@ class ReadBookActivity : ReadBookBaseActivity(),
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
when (requestCode) { when (requestCode) {
requestCodeEditSource -> viewModel.upBookSource() requestCodeEditSource -> viewModel.upBookSource {
upView()
}
requestCodeChapterList -> requestCodeChapterList ->
data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index ->
if (index != ReadBook.durChapterIndex) { if (index != ReadBook.durChapterIndex) {
@ -795,43 +809,35 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
private fun skipToSearch(index: Int, indexWithinChapter: Int) { private fun skipToSearch(index: Int, indexWithinChapter: Int) {
launch(IO) { viewModel.openChapter(index) {
viewModel.openChapter(index) val pages = ReadBook.curTextChapter?.pages ?: return@openChapter
// block until load correct chapter and pages
var pages = ReadBook.curTextChapter?.pages
while (ReadBook.durChapterIndex != index || pages == null) {
delay(100L)
pages = ReadBook.curTextChapter?.pages
}
val positions = ReadBook.searchResultPositions( val positions = ReadBook.searchResultPositions(
pages, pages,
indexWithinChapter, indexWithinChapter,
viewModel.searchContentQuery viewModel.searchContentQuery
) )
while (ReadBook.durPageIndex() != positions[0]) { ReadBook.skipToPage(positions[0]) {
delay(100L) launch {
ReadBook.skipToPage(positions[0]) binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
} delay(20L)
withContext(Main) { when (positions[3]) {
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) 0 -> binding.readView.curPage.selectEndMoveIndex(
delay(20L) 0,
when (positions[3]) { positions[1],
0 -> binding.readView.curPage.selectEndMoveIndex( positions[2] + viewModel.searchContentQuery.length - 1
0, )
positions[1], 1 -> binding.readView.curPage.selectEndMoveIndex(
positions[2] + viewModel.searchContentQuery.length - 1 0,
) positions[1] + 1,
1 -> binding.readView.curPage.selectEndMoveIndex( positions[4]
0, )
positions[1] + 1, //consider change page, jump to scroll position
positions[4] -1 -> binding.readView.curPage
) .selectEndMoveIndex(1, 0, positions[4])
//consider change page, jump to scroll position }
-1 -> binding.readView.curPage binding.readView.isTextSelected = true
.selectEndMoveIndex(1, 0, positions[4]) delay(100L)
} }
binding.readView.isTextSelected = true
delay(100L)
} }
} }
} }

@ -250,7 +250,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun openChapter(index: Int, durChapterPos: Int = 0) { fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
ReadBook.prevTextChapter = null ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null ReadBook.curTextChapter = null
ReadBook.nextTextChapter = null ReadBook.nextTextChapter = null
@ -260,7 +260,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durChapterPos = durChapterPos ReadBook.durChapterPos = durChapterPos
} }
ReadBook.saveRead() ReadBook.saveRead()
ReadBook.loadContent(resetPageOffset = true) ReadBook.loadContent(resetPageOffset = true) {
success?.invoke()
}
} }
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
@ -271,13 +273,15 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun upBookSource() { fun upBookSource(success: (() -> Unit)?) {
execute { execute {
ReadBook.book?.let { book -> ReadBook.book?.let { book ->
App.db.bookSourceDao.getBookSource(book.origin)?.let { App.db.bookSourceDao.getBookSource(book.origin)?.let {
ReadBook.webBook = WebBook(it) ReadBook.webBook = WebBook(it)
} }
} }
}.onSuccess {
success?.invoke()
} }
} }

@ -9,6 +9,7 @@ import android.view.WindowManager
import android.view.animation.Animation import android.view.animation.Animation
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.SeekBar import android.widget.SeekBar
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
@ -140,6 +141,9 @@ class ReadMenu @JvmOverloads constructor(
tvChapterUrl.onClick { tvChapterUrl.onClick {
context.openUrl(binding.tvChapterUrl.text.toString()) context.openUrl(binding.tvChapterUrl.text.toString())
} }
tvLogin.onClick {
callBack.showLogin()
}
ivBrightnessAuto.onClick { ivBrightnessAuto.onClick {
context.putPrefBoolean("brightnessAuto", !brightnessAuto()) context.putPrefBoolean("brightnessAuto", !brightnessAuto())
upBrightnessState() upBrightnessState()
@ -277,6 +281,7 @@ class ReadMenu @JvmOverloads constructor(
} }
fun upBookView() { fun upBookView() {
binding.tvLogin.isGone = ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
ReadBook.curTextChapter?.let { ReadBook.curTextChapter?.let {
binding.tvChapterName.text = it.title binding.tvChapterName.text = it.title
binding.tvChapterName.visible() binding.tvChapterName.visible()
@ -323,6 +328,7 @@ class ReadMenu @JvmOverloads constructor(
fun upSystemUiVisibility() fun upSystemUiVisibility()
fun onClickReadAloud() fun onClickReadAloud()
fun showReadMenuHelp() fun showReadMenuHelp()
fun showLogin()
} }
} }

@ -347,8 +347,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[0] = relativePage selectStart[0] = relativePage
selectStart[1] = lineIndex selectStart[1] = lineIndex
selectStart[2] = charIndex selectStart[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex] val textLine = relativePage(relativePage).getLine(lineIndex)
val textChar = textLine.textChars[charIndex] val textChar = textLine.getTextChar(charIndex)
upSelectedStart( upSelectedStart(
textChar.start, textChar.start,
textLine.lineBottom + relativeOffset(relativePage), textLine.lineBottom + relativeOffset(relativePage),
@ -364,8 +364,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectEnd[0] = relativePage selectEnd[0] = relativePage
selectEnd[1] = lineIndex selectEnd[1] = lineIndex
selectEnd[2] = charIndex selectEnd[2] = charIndex
val textLine = relativePage(relativePage).textLines[lineIndex] val textLine = relativePage(relativePage).getLine(lineIndex)
val textChar = textLine.textChars[charIndex] val textChar = textLine.getTextChar(charIndex)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage)) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage))
upSelectChars() upSelectChars()
} }

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

@ -28,8 +28,10 @@ data class TextLine(
textChars.add(TextChar(charData, start = start, end = end)) textChars.add(TextChar(charData, start = start, end = end))
} }
fun getTextCharAt(index: Int): TextChar { fun getTextChar(index: Int): TextChar {
return textChars[index] return textChars.getOrElse(index) {
textChars.last()
}
} }
fun getTextCharReverseAt(index: Int): TextChar { fun getTextCharReverseAt(index: Int): TextChar {

@ -25,6 +25,12 @@ data class TextPage(
val lineSize get() = textLines.size val lineSize get() = textLines.size
val charSize get() = text.length val charSize get() = text.length
fun getLine(index: Int): TextLine {
return textLines.getOrElse(index) {
textLines.last()
}
}
fun upLinesPosition() = ChapterProvider.apply { fun upLinesPosition() = ChapterProvider.apply {
if (!ReadBookConfig.textBottomJustify) return@apply if (!ReadBookConfig.textBottomJustify) return@apply
if (textLines.size <= 1) return@apply if (textLines.size <= 1) return@apply

@ -58,7 +58,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private lateinit var adapter: BookSourceAdapter private lateinit var adapter: BookSourceAdapter
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private var bookSourceLiveDate: LiveData<List<BookSource>>? = null private var bookSourceLiveDate: LiveData<List<BookSource>>? = null
private var groups = linkedSetOf<String>() private val groups = linkedSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private var sort = Sort.Default private var sort = Sort.Default
private var sortAscending = true private var sortAscending = true
@ -130,6 +130,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
sortCheck(Sort.Update) sortCheck(Sort.Update)
initLiveDataBookSource(searchView.query?.toString()) initLiveDataBookSource(searchView.query?.toString())
} }
R.id.menu_sort_enable -> {
item.isChecked = true
sortCheck(Sort.Enable)
initLiveDataBookSource(searchView.query?.toString())
}
R.id.menu_enabled_group -> { R.id.menu_enabled_group -> {
searchView.setQuery(getString(R.string.enabled), true) searchView.setQuery(getString(R.string.enabled), true)
} }
@ -187,29 +192,44 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
else -> { else -> {
App.db.bookSourceDao.liveDataSearch("%$searchKey%") App.db.bookSourceDao.liveDataSearch("%$searchKey%")
} }
} }.apply {
bookSourceLiveDate?.observe(this, { data -> observe(this@BookSourceActivity, { data ->
val sourceList = val sourceList =
if (sortAscending) when (sort) { if (sortAscending) when (sort) {
Sort.Weight -> data.sortedBy { it.weight } Sort.Weight -> data.sortedBy { it.weight }
Sort.Name -> data.sortedWith { o1, o2 -> Sort.Name -> data.sortedWith { o1, o2 ->
o1.bookSourceName.cnCompare(o2.bookSourceName) o1.bookSourceName.cnCompare(o2.bookSourceName)
}
Sort.Url -> data.sortedBy { it.bookSourceUrl }
Sort.Update -> data.sortedByDescending { it.lastUpdateTime }
Sort.Enable -> data.sortedWith { o1, o2 ->
var sort = -o1.enabled.compareTo(o2.enabled)
if (sort == 0) {
sort = o1.bookSourceName.cnCompare(o2.bookSourceName)
}
sort
}
else -> data
} }
Sort.Url -> data.sortedBy { it.bookSourceUrl } else when (sort) {
Sort.Update -> data.sortedByDescending { it.lastUpdateTime } Sort.Weight -> data.sortedByDescending { it.weight }
else -> data Sort.Name -> data.sortedWith { o1, o2 ->
} o2.bookSourceName.cnCompare(o1.bookSourceName)
else when (sort) { }
Sort.Weight -> data.sortedByDescending { it.weight } Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
Sort.Name -> data.sortedWith { o1, o2 -> Sort.Update -> data.sortedBy { it.lastUpdateTime }
o2.bookSourceName.cnCompare(o1.bookSourceName) Sort.Enable -> data.sortedWith { o1, o2 ->
var sort = o1.enabled.compareTo(o2.enabled)
if (sort == 0) {
sort = o1.bookSourceName.cnCompare(o2.bookSourceName)
}
sort
}
else -> data.reversed()
} }
Sort.Url -> data.sortedByDescending { it.bookSourceUrl } adapter.setItems(sourceList, adapter.diffItemCallback)
Sort.Update -> data.sortedBy { it.lastUpdateTime } })
else -> data.reversed() }
}
adapter.setItems(sourceList, adapter.diffItemCallback)
})
} }
private fun showHelp() { private fun showHelp() {
@ -229,7 +249,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun initLiveDataGroup() { private fun initLiveDataGroup() {
App.db.bookSourceDao.liveGroup().observe(this, { App.db.bookSourceDao.liveGroup().observe(this, {
groups.clear() groups.clear()
it.map { group -> it.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
} }
upGroupMenu() upGroupMenu()
@ -483,6 +503,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
enum class Sort { enum class Sort {
Default, Name, Url, Weight, Update Default, Name, Url, Weight, Update, Enable
} }
} }

@ -88,13 +88,13 @@ class OtherConfigFragment : BasePreferenceFragment(),
} }
PreferKey.cleanCache -> clearCache() PreferKey.cleanCache -> clearCache()
PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectDefaultCover() selectImage(requestCodeCover)
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.defaultCover) removePref(PreferKey.defaultCover)
} else { } else {
selectDefaultCover() selectImage(requestCodeCover)
} }
} }
} }
@ -183,11 +183,12 @@ class OtherConfigFragment : BasePreferenceFragment(),
}.show() }.show()
} }
private fun selectDefaultCover() { @Suppress("SameParameterValue")
private fun selectImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*" intent.type = "image/*"
startActivityForResult(intent, requestCodeCover) startActivityForResult(intent, requestCode)
} }
private fun isProcessTextEnabled(): Boolean { private fun isProcessTextEnabled(): Boolean {

@ -1,13 +1,17 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
@ -19,24 +23,38 @@ import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.prefs.ColorPreference import io.legado.app.ui.widget.prefs.ColorPreference
import io.legado.app.ui.widget.prefs.IconListPreference import io.legado.app.ui.widget.prefs.PreferenceCategory
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
class ThemeConfigFragment : BasePreferenceFragment(), class ThemeConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeBgImage = 234
private val requestCodeBgImageN = 342
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme) addPreferencesFromResource(R.xml.pref_config_theme)
if (Build.VERSION.SDK_INT < 26) { if (Build.VERSION.SDK_INT < 26) {
findPreference<IconListPreference>(PreferKey.launcherIcon)?.let { preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon)
preferenceScreen.removePreference(it) }
} if (AppConfig.isGooglePlay) {
upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage))
upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
} else {
findPreference<PreferenceCategory>("dayThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImage)
findPreference<PreferenceCategory>("nightThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImageN)
} }
upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString()) upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString())
findPreference<ColorPreference>(PreferKey.cBackground)?.let { findPreference<ColorPreference>(PreferKey.cBackground)?.let {
@ -168,10 +186,39 @@ class ThemeConfigFragment : BasePreferenceFragment(),
} }
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key)
PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) {
selectImage(requestCodeBgImage)
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImage)
} else {
selectImage(requestCodeBgImage)
}
}
}
PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) {
selectImage(requestCodeBgImageN)
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImageN)
} else {
selectImage(requestCodeBgImageN)
}
}
}
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
private fun selectImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun saveThemeAlert(key: String) { private fun saveThemeAlert(key: String) {
alert(R.string.theme_name) { alert(R.string.theme_name) {
@ -211,6 +258,61 @@ class ThemeConfigFragment : BasePreferenceFragment(),
when (preferenceKey) { when (preferenceKey) {
PreferKey.barElevation -> preference.summary = PreferKey.barElevation -> preference.summary =
getString(R.string.bar_elevation_s, value) getString(R.string.bar_elevation_s, value)
else -> preference.summary = value
}
}
private fun setBgFromUri(uri: Uri, preferenceKey: String) {
if (uri.isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.name?.let {
var file = requireContext().externalFilesDir
file = FileUtils.createFileIfNotExist(file, preferenceKey, it)
kotlin.runCatching {
DocumentUtils.readBytes(requireContext(), doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
putPrefString(preferenceKey, file.absolutePath)
} ?: toast("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = requireContext().externalFilesDir
file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name)
file.writeBytes(imgFile.readBytes())
putPrefString(preferenceKey, file.absolutePath)
}
}
}
.request()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeBgImage -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri, PreferKey.bgImage)
}
}
requestCodeBgImageN -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri, PreferKey.bgImageN)
}
}
} }
} }
} }

@ -157,6 +157,9 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
if (LocalConfig.hasUpTxtTocRule) { if (LocalConfig.hasUpTxtTocRule) {
DefaultData.importDefaultTocRules() DefaultData.importDefaultTocRules()
} }
if (LocalConfig.hasUpRssSources) {
DefaultData.importDefaultRssSources()
}
} }
} }
} }

@ -96,7 +96,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FilePickerDialog.C
} }
if (AppConfig.isGooglePlay) { if (AppConfig.isGooglePlay) {
findPreference<PreferenceCategory>("aboutCategory") findPreference<PreferenceCategory>("aboutCategory")
?.removePreference(findPreference("donate")) ?.removePreferenceRecursively("donate")
} }
} }

@ -92,7 +92,7 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
} }
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
initData(newText ?: "") initData(newText)
return false return false
} }
}) })
@ -105,7 +105,7 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
adapter.addHeaderView { adapter.addHeaderView {
ItemRssBinding.inflate(layoutInflater, it, false).apply { ItemRssBinding.inflate(layoutInflater, it, false).apply {
tvName.setText(R.string.rule_subscription) tvName.setText(R.string.rule_subscription)
ivIcon.setImageResource(R.mipmap.ic_launcher) ivIcon.setImageResource(R.drawable.image_legado)
root.onClick { root.onClick {
startActivity<RuleSubActivity>() startActivity<RuleSubActivity>()
} }
@ -113,9 +113,16 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
} }
} }
private fun initData(searchKey: String = "") { private fun initData(searchKey: String? = null) {
liveRssData?.removeObservers(this) liveRssData?.removeObservers(this)
liveRssData = App.db.rssSourceDao.liveEnabled(searchKey).apply { liveRssData = when {
searchKey.isNullOrEmpty() -> App.db.rssSourceDao.liveEnabled()
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.rssSourceDao.liveEnabledByGroup("%$key%")
}
else -> App.db.rssSourceDao.liveEnabled("%$searchKey%")
}.apply {
observe(viewLifecycleOwner, { observe(viewLifecycleOwner, {
adapter.setItems(it) adapter.setItems(it)
}) })

@ -154,14 +154,15 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
else -> { else -> {
App.db.replaceRuleDao.liveDataSearch("%$searchKey%") App.db.replaceRuleDao.liveDataSearch("%$searchKey%")
} }
}.apply {
observe(this@ReplaceRuleActivity, {
if (dataInit) {
setResult(Activity.RESULT_OK)
}
adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true
})
} }
replaceRuleLiveData?.observe(this, {
if (dataInit) {
setResult(Activity.RESULT_OK)
}
adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true
})
} }
private fun observeGroupData() { private fun observeGroupData() {
@ -239,7 +240,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
} }
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
observeReplaceRuleData("%$newText%") observeReplaceRuleData(newText)
return false return false
} }

@ -30,7 +30,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
fun loadContent(rssSource: RssSource) { fun loadContent(rssSource: RssSource) {
isLoading = true isLoading = true
page = 1 page = 1
Rss.getArticles(sortName, sortUrl, rssSource, page) Rss.getArticles(this, sortName, sortUrl, rssSource, page)
.onSuccess(Dispatchers.IO) { .onSuccess(Dispatchers.IO) {
nextPageUrl = it.nextPageUrl nextPageUrl = it.nextPageUrl
it.articles.let { list -> it.articles.let { list ->
@ -60,7 +60,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
page++ page++
val pageUrl = nextPageUrl val pageUrl = nextPageUrl
if (!pageUrl.isNullOrEmpty()) { if (!pageUrl.isNullOrEmpty()) {
Rss.getArticles(sortName, pageUrl, rssSource, page) Rss.getArticles(this, sortName, pageUrl, rssSource, page)
.onSuccess(Dispatchers.IO) { .onSuccess(Dispatchers.IO) {
nextPageUrl = it.nextPageUrl nextPageUrl = it.nextPageUrl
loadMoreSuccess(it.articles) loadMoreSuccess(it.articles)

@ -94,7 +94,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
} }
private fun loadContent(rssArticle: RssArticle, ruleContent: String) { private fun loadContent(rssArticle: RssArticle, ruleContent: String) {
Rss.getContent(rssArticle, ruleContent, rssSource, this) Rss.getContent(this, rssArticle, ruleContent, rssSource)
.onSuccess(IO) { body -> .onSuccess(IO) { body ->
rssArticle.description = body rssArticle.description = body
App.db.rssArticleDao.insert(rssArticle) App.db.rssArticleDao.insert(rssArticle)

@ -31,7 +31,7 @@ class RssSourceDebugModel(application: Application) : BaseViewModel(application)
rssSource?.let { rssSource?.let {
start?.invoke() start?.invoke()
Debug.callback = this Debug.callback = this
Debug.startDebug(it) Debug.startDebug(this, it)
} ?: error?.invoke() } ?: error?.invoke()
} }

@ -92,6 +92,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) { R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) {
startActivity(Intent.createChooser(it, getString(R.string.share_selected_source))) startActivity(Intent.createChooser(it, getString(R.string.share_selected_source)))
} }
R.id.menu_import_default -> viewModel.importDefault()
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)
@ -199,22 +200,22 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
private fun initLiveDataSource(searchKey: String? = null) { private fun initLiveDataSource(searchKey: String? = null) {
sourceLiveData?.removeObservers(this) sourceLiveData?.removeObservers(this)
sourceLiveData = sourceLiveData = when {
when { searchKey.isNullOrBlank() -> {
searchKey.isNullOrBlank() -> { App.db.rssSourceDao.liveAll()
App.db.rssSourceDao.liveAll()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
App.db.rssSourceDao.liveGroupSearch("%$key%")
}
else -> {
App.db.rssSourceDao.liveSearch("%$searchKey%")
}
} }
sourceLiveData?.observe(this, { searchKey.startsWith("group:") -> {
adapter.setItems(it, adapter.diffItemCallback) val key = searchKey.substringAfter("group:")
}) App.db.rssSourceDao.liveGroupSearch("%$key%")
}
else -> {
App.db.rssSourceDao.liveSearch("%$searchKey%")
}
}.apply {
observe(this@RssSourceActivity, {
adapter.setItems(it, adapter.diffItemCallback)
})
}
} }
private fun showHelp() { private fun showHelp() {

@ -9,6 +9,7 @@ import io.legado.app.App
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.DefaultData
import io.legado.app.utils.* import io.legado.app.utils.*
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.io.File import java.io.File
@ -166,4 +167,10 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun importDefault() {
execute {
DefaultData.importDefaultRssSources()
}
}
} }

@ -10,7 +10,7 @@ import java.util.*
val removeHtmlRegex = "</?(?:div|p|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex() val removeHtmlRegex = "</?(?:div|p|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex()
val imgRegex = "<img[^>]*>".toRegex() val imgRegex = "<img[^>]*>".toRegex()
val notImgHtmlRegex = "</?(?!img)\\w+[^>]*>".toRegex() val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()

@ -1,6 +1,7 @@
package io.legado.app.web package io.legado.app.web
import android.os.Looper
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoWSD import fi.iki.elonen.NanoWSD
import io.legado.app.App import io.legado.app.App
@ -22,12 +23,12 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
override fun onOpen() { override fun onOpen() {
launch(IO) { launch(IO) {
do { kotlin.runCatching {
delay(30000) while (isOpen) {
runCatching {
ping("ping".toByteArray()) ping("ping".toByteArray())
delay(30000)
} }
} while (isOpen) }
} }
} }
@ -42,21 +43,21 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
override fun onMessage(message: NanoWSD.WebSocketFrame) { override fun onMessage(message: NanoWSD.WebSocketFrame) {
if (!message.textPayload.isJson()) return if (!message.textPayload.isJson()) return
kotlin.runCatching { launch(IO) {
val debugBean = GSON.fromJsonObject<Map<String, String>>(message.textPayload) kotlin.runCatching {
if (debugBean != null) { val debugBean = GSON.fromJsonObject<Map<String, String>>(message.textPayload)
val tag = debugBean["tag"] if (debugBean != null) {
val key = debugBean["key"] val tag = debugBean["tag"]
if (tag.isNullOrBlank() || key.isNullOrBlank()) { val key = debugBean["key"]
kotlin.runCatching { if (tag.isNullOrBlank() || key.isNullOrBlank()) {
send(App.INSTANCE.getString(R.string.cannot_empty)) send(App.INSTANCE.getString(R.string.cannot_empty))
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
return@launch
}
App.db.bookSourceDao.getBookSource(tag)?.let {
Debug.callback = this@SourceDebugWebSocket
Debug.startDebug(this, WebBook(it), key)
} }
return
}
App.db.bookSourceDao.getBookSource(tag)?.let {
Debug.callback = this
Debug.startDebug(this, WebBook(it), key)
} }
} }
} }
@ -71,10 +72,27 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
} }
override fun printLog(state: Int, msg: String) { override fun printLog(state: Int, msg: String) {
kotlin.runCatching { if (Looper.getMainLooper() == Looper.myLooper()) {
send(msg) launch(IO) {
if (state == -1 || state == 1000) { runCatching {
Debug.cancelDebug(true) send(msg)
if (state == -1 || state == 1000) {
Debug.cancelDebug(true)
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
}
}.onFailure {
it.printStackTrace()
}
}
} else {
runCatching {
send(msg)
if (state == -1 || state == 1000) {
Debug.cancelDebug(true)
close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false)
}
}.onFailure {
it.printStackTrace()
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -22,6 +22,7 @@
android:id="@+id/edit_book_text" android:id="@+id/edit_book_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="6"
android:hint="@string/content" /> android:hint="@string/content" />
</io.legado.app.ui.widget.text.TextInputLayout> </io.legado.app.ui.widget.text.TextInputLayout>
@ -37,6 +38,7 @@
android:id="@+id/edit_view" android:id="@+id/edit_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="6"
android:hint="@string/note_content" /> android:hint="@string/note_content" />
</io.legado.app.ui.widget.text.TextInputLayout> </io.legado.app.ui.widget.text.TextInputLayout>

@ -11,8 +11,8 @@
<io.legado.app.ui.widget.image.CoverImageView <io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover" android:id="@+id/iv_cover"
android:layout_width="60dp" android:layout_width="66dp"
android:layout_height="80dp" android:layout_height="90dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
@ -97,7 +97,7 @@
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@+id/tv_read" app:layout_constraintBottom_toTopOf="@+id/tv_read"
app:layout_constraintLeft_toRightOf="@+id/iv_author" app:layout_constraintLeft_toRightOf="@+id/iv_author"
app:layout_constraintRight_toLeftOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_name" app:layout_constraintTop_toBottomOf="@+id/tv_name"
tools:ignore="RtlSymmetry" /> tools:ignore="RtlSymmetry" />
@ -126,7 +126,7 @@
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@id/tv_last" app:layout_constraintBottom_toTopOf="@id/tv_last"
app:layout_constraintLeft_toRightOf="@+id/iv_read" app:layout_constraintLeft_toRightOf="@+id/iv_read"
app:layout_constraintRight_toLeftOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@+id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_author" /> app:layout_constraintTop_toBottomOf="@+id/tv_author" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
@ -154,7 +154,7 @@
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/iv_last" app:layout_constraintLeft_toRightOf="@+id/iv_last"
app:layout_constraintRight_toLeftOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_read" /> app:layout_constraintTop_toBottomOf="@+id/tv_read" />
<View <View

@ -21,25 +21,49 @@
android:theme="?attr/actionBarStyle" android:theme="?attr/actionBarStyle"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/tv_chapter_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:paddingLeft="10dp"
android:paddingRight="10dp" <TextView
android:singleLine="true" android:id="@+id/tv_chapter_name"
android:textSize="12sp" android:layout_width="0dp"
android:visibility="gone" /> android:layout_height="wrap_content"
android:paddingLeft="10dp"
<TextView android:paddingRight="10dp"
android:id="@+id/tv_chapter_url" android:singleLine="true"
android:layout_width="match_parent" android:textSize="12sp"
android:layout_height="wrap_content" android:visibility="gone"
android:paddingLeft="10dp" app:layout_constraintLeft_toLeftOf="parent"
android:paddingRight="10dp" app:layout_constraintRight_toLeftOf="@+id/tv_login"
android:singleLine="true" app:layout_constraintTop_toTopOf="parent" />
android:textSize="12sp"
android:visibility="gone" /> <TextView
android:id="@+id/tv_chapter_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:singleLine="true"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/tv_login"
app:layout_constraintTop_toBottomOf="@+id/tv_chapter_name" />
<io.legado.app.ui.widget.text.AccentBgTextView
android:id="@+id/tv_login"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="@string/login"
app:layout_constraintBottom_toBottomOf="@+id/tv_chapter_url"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</io.legado.app.ui.widget.TitleBar> </io.legado.app.ui.widget.TitleBar>
@ -48,19 +72,19 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_margin="8dp" android:layout_margin="8dp"
android:orientation="vertical"
android:gravity="center_horizontal" android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@+id/title_bar" android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/bottom_menu"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottom_menu"> app:layout_constraintTop_toBottomOf="@+id/title_bar">
<ImageView <ImageView
android:id="@+id/iv_brightness_auto" android:id="@+id/iv_brightness_auto"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_margin="8dp" android:layout_margin="8dp"
android:src="@drawable/ic_brightness_auto" android:contentDescription="@string/brightness_auto"
android:contentDescription="@string/brightness_auto" /> android:src="@drawable/ic_brightness_auto" />
<io.legado.app.ui.widget.seekbar.VerticalSeekBarWrapper <io.legado.app.ui.widget.seekbar.VerticalSeekBarWrapper
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -88,8 +112,8 @@
android:id="@+id/ll_floating_button" android:id="@+id/ll_floating_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="32dp" android:paddingStart="32dp"
android:paddingEnd="32dp"> android:paddingEnd="32dp">
@ -187,8 +211,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/background_menu" android:background="@color/background_menu"
android:orientation="vertical" android:importantForAccessibility="no"
android:importantForAccessibility="no"> android:orientation="vertical">
<!--章节设置--> <!--章节设置-->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -197,8 +221,8 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginRight="20dp" android:layout_marginRight="20dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:orientation="horizontal" android:importantForAccessibility="no"
android:importantForAccessibility="no"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/tv_pre" android:id="@+id/tv_pre"
@ -244,8 +268,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal" android:importantForAccessibility="no"
android:importantForAccessibility="no"> android:orientation="horizontal">
<View <View
android:layout_width="0dp" android:layout_width="0dp"
@ -281,8 +305,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:singleLine="true"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true"
android:text="@string/chapter_list" android:text="@string/chapter_list"
android:textColor="@color/primaryText" android:textColor="@color/primaryText"
android:textSize="12sp" /> android:textSize="12sp" />
@ -322,8 +346,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:singleLine="true"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true"
android:text="@string/read_aloud" android:text="@string/read_aloud"
android:textColor="@color/primaryText" android:textColor="@color/primaryText"
android:textSize="12sp" /> android:textSize="12sp" />
@ -363,8 +387,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:singleLine="true"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true"
android:text="@string/interface_setting" android:text="@string/interface_setting"
android:textColor="@color/primaryText" android:textColor="@color/primaryText"
android:textSize="12sp" /> android:textSize="12sp" />
@ -404,8 +428,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:singleLine="true"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true"
android:text="@string/setting" android:text="@string/setting"
android:textColor="@color/primaryText" android:textColor="@color/primaryText"
android:textSize="12sp" /> android:textSize="12sp" />

@ -49,14 +49,6 @@
android:title="@string/bookmark_add" android:title="@string/bookmark_add"
app:showAsAction="never" /> app:showAsAction="never" />
<group android:id="@+id/menu_group_login">
<item
android:id="@+id/menu_login"
android:visible="false"
android:title="@string/login"
app:showAsAction="never" />
</group>
<group android:id="@+id/menu_group_on_line_ns"> <group android:id="@+id/menu_group_on_line_ns">
<item <item

@ -50,6 +50,12 @@
android:id="@+id/menu_sort_time" android:id="@+id/menu_sort_time"
android:checkable="true" android:checkable="true"
android:title="@string/sort_by_lastUpdateTime" /> android:title="@string/sort_by_lastUpdateTime" />
<item
android:id="@+id/menu_sort_enable"
android:checkable="true"
android:title="@string/sort_enable" />
</group> </group>
</menu> </menu>

@ -48,6 +48,12 @@
android:title="@string/import_by_qr_code" android:title="@string/import_by_qr_code"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_import_default"
android:icon="@drawable/ic_import"
android:title="@string/import_default_rule"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_share_source" android:id="@+id/menu_share_source"
android:icon="@drawable/ic_share" android:icon="@drawable/ic_share"

@ -799,5 +799,7 @@
<string name="import_bookshelf">导入书单</string> <string name="import_bookshelf">导入书单</string>
<string name="pre_download">预下载</string> <string name="pre_download">预下载</string>
<string name="pre_download_s">预先下载10章正文</string> <string name="pre_download_s">预先下载10章正文</string>
<string name="sort_enable">启用排序</string>
<string name="background_image">背景图片</string>
</resources> </resources>

@ -13,6 +13,7 @@
<string name="webdav_cache_backup">離線快取書籍備份</string> <string name="webdav_cache_backup">離線快取書籍備份</string>
<string name="webdav_cache_backup_s">匯出本機同時備份到legado資料夾下exports目錄</string> <string name="webdav_cache_backup_s">匯出本機同時備份到legado資料夾下exports目錄</string>
<string name="backup_path">備份路徑</string> <string name="backup_path">備份路徑</string>
<string name="select_backup_path">請選擇備份路徑</string>
<string name="menu_import_old_version">匯入舊版資料</string> <string name="menu_import_old_version">匯入舊版資料</string>
<string name="menu_import_github">匯入Github資料</string> <string name="menu_import_github">匯入Github資料</string>
<string name="menu_replace_rule">淨化取代</string> <string name="menu_replace_rule">淨化取代</string>
@ -29,6 +30,7 @@
<string name="draw">提醒</string> <string name="draw">提醒</string>
<string name="edit">編輯</string> <string name="edit">編輯</string>
<string name="delete">刪除</string> <string name="delete">刪除</string>
<string name="delete_all">刪除所有</string>
<string name="replace">取代</string> <string name="replace">取代</string>
<string name="replace_purify">取代淨化</string> <string name="replace_purify">取代淨化</string>
<string name="replace_purify_desc">配置取代淨化規則</string> <string name="replace_purify_desc">配置取代淨化規則</string>
@ -388,11 +390,14 @@
<string name="source_name">源名稱(sourceName)</string> <string name="source_name">源名稱(sourceName)</string>
<string name="source_url">源URL(sourceUrl)</string> <string name="source_url">源URL(sourceUrl)</string>
<string name="source_group">源分組(sourceGroup)</string> <string name="source_group">源分組(sourceGroup)</string>
<string name="diy_source_group">自訂源分組</string>
<string name="diy_edit_source_group">輸入自訂源分組名稱</string>
<string name="diy_edit_source_group_title">【%s】</string>
<string name="sort_url">分類Url</string> <string name="sort_url">分類Url</string>
<string name="login_url">登入URL(loginUrl)</string> <string name="login_url">登入URL(loginUrl)</string>
<string name="comment">源注釋(sourceComment)</string>
<string name="r_search_url">搜尋地址(url)</string> <string name="r_search_url">搜尋地址(url)</string>
<string name="r_find_url">發現地址規則(url)</string> <string name="r_find_url">發現地址規則(url)</string>
<string name="comment">源注釋(sourceComment)</string>
<string name="r_book_list">書籍列表規則(bookList)</string> <string name="r_book_list">書籍列表規則(bookList)</string>
<string name="r_book_name">書名規則(name)</string> <string name="r_book_name">書名規則(name)</string>
<string name="r_book_url">詳情頁url規則(bookUrl)</string> <string name="r_book_url">詳情頁url規則(bookUrl)</string>
@ -415,6 +420,8 @@
<string name="rule_book_content">正文規則(content)</string> <string name="rule_book_content">正文規則(content)</string>
<string name="rule_next_content">正文下一頁URL規則(nextContentUrl)</string> <string name="rule_next_content">正文下一頁URL規則(nextContentUrl)</string>
<string name="rule_web_js">WebViewJs(webJs)</string> <string name="rule_web_js">WebViewJs(webJs)</string>
<string name="rule_image_style">圖片樣式(imageStyle)</string>
<string name="rule_replace_regex">取代規則(replaceRegex)</string>
<string name="rule_source_regex">資源正則(sourceRegex)</string> <string name="rule_source_regex">資源正則(sourceRegex)</string>
<string name="source_icon">圖示(sourceIcon)</string> <string name="source_icon">圖示(sourceIcon)</string>
@ -449,6 +456,7 @@
<string name="source_http_header">請求頭(header)</string> <string name="source_http_header">請求頭(header)</string>
<string name="debug_source">除錯源</string> <string name="debug_source">除錯源</string>
<string name="import_by_qr_code">二維碼匯入</string> <string name="import_by_qr_code">二維碼匯入</string>
<string name="share_selected_source">分享選取源</string>
<string name="scan_qr_code">掃描二維碼</string> <string name="scan_qr_code">掃描二維碼</string>
<string name="click_on_selected_show_menu">選中時點擊可彈出選單</string> <string name="click_on_selected_show_menu">選中時點擊可彈出選單</string>
<string name="theme">主題</string> <string name="theme">主題</string>
@ -684,6 +692,7 @@
<string name="middle"></string> <string name="middle"></string>
<string name="information">訊息</string> <string name="information">訊息</string>
<string name="switchLayout">切換布局</string> <string name="switchLayout">切換布局</string>
<string name="text_font_weight_converter">文章字重轉換</string>
<!--color--> <!--color-->
<string name="primary">主色調</string> <string name="primary">主色調</string>
@ -702,16 +711,14 @@
<string name="night_navbar_color">夜間,底欄色</string> <string name="night_navbar_color">夜間,底欄色</string>
<string name="auto_change_source">自動換源</string> <string name="auto_change_source">自動換源</string>
<string name="text_full_justify">文字兩端對齊</string> <string name="text_full_justify">文字兩端對齊</string>
<string name="text_bottom_justify">文字底部對齊</string>
<string name="auto_page_speed">自動翻頁速度</string> <string name="auto_page_speed">自動翻頁速度</string>
<string name="sort_by_url">地址排序</string> <string name="sort_by_url">地址排序</string>
<string name="text_font_weight_converter">文章字體轉換</string>
<string name="select_backup_path">請選擇備份路徑</string>
<string name="backup_summary">本機和 WebDav 一起備份</string> <string name="backup_summary">本機和 WebDav 一起備份</string>
<string name="restore_summary">優先從 WebDav 復原,長按從本機復原</string> <string name="restore_summary">優先從 WebDav 復原,長按從本機復原</string>
<string name="import_old_summary">選擇舊版備份資料夾</string> <string name="import_old_summary">選擇舊版備份資料夾</string>
<string name="enabled">已啟用</string> <string name="enabled">已啟用</string>
<string name="disabled">已禁用</string> <string name="disabled">已禁用</string>
<string name="text_bottom_justify">文字底部對齊</string>
<string name="starting_download">正在啟動下載</string> <string name="starting_download">正在啟動下載</string>
<string name="already_in_download">該書已在下載列表</string> <string name="already_in_download">該書已在下載列表</string>
<string name="click_to_open">點擊打開</string> <string name="click_to_open">點擊打開</string>
@ -729,8 +736,6 @@
<string name="restore_ignore">復原忽略列表</string> <string name="restore_ignore">復原忽略列表</string>
<string name="restore_ignore_summary">復原時忽略一些內容不復原,方便不同手機配置不同</string> <string name="restore_ignore_summary">復原時忽略一些內容不復原,方便不同手機配置不同</string>
<string name="read_config">閱讀介面設定</string> <string name="read_config">閱讀介面設定</string>
<string name="rule_image_style">圖片樣式(imageStyle)</string>
<string name="rule_replace_regex">取代規則(replaceRegex)</string>
<string name="group_name">分組名稱</string> <string name="group_name">分組名稱</string>
<string name="note_content">備註內容</string> <string name="note_content">備註內容</string>
<string name="replace_enable_default_t">預設啟用取代淨化</string> <string name="replace_enable_default_t">預設啟用取代淨化</string>
@ -755,7 +760,6 @@
<string name="thread_count">執行緒數</string> <string name="thread_count">執行緒數</string>
<string name="all_read_time">總閱讀時間</string> <string name="all_read_time">總閱讀時間</string>
<string name="un_select_all">全不選</string> <string name="un_select_all">全不選</string>
<string name="delete_all">刪除所有</string>
<string name="import_str">匯入</string> <string name="import_str">匯入</string>
<string name="export_str">匯出</string> <string name="export_str">匯出</string>
<string name="save_theme_config">儲存主題配置</string> <string name="save_theme_config">儲存主題配置</string>
@ -764,7 +768,6 @@
<string name="theme_list">主題列表</string> <string name="theme_list">主題列表</string>
<string name="theme_list_summary">使用儲存主題,匯入,分享主題</string> <string name="theme_list_summary">使用儲存主題,匯入,分享主題</string>
<string name="select_theme">切換預設主題</string> <string name="select_theme">切換預設主題</string>
<string name="share_selected_source">分享選中源</string>
<string name="sort_by_lastUpdateTime">時間排序</string> <string name="sort_by_lastUpdateTime">時間排序</string>
<string name="search_content">全文搜尋</string> <string name="search_content">全文搜尋</string>
<string name="rss_source_empty">關注公眾號[开源阅读]獲取訂閱源!</string> <string name="rss_source_empty">關注公眾號[开源阅读]獲取訂閱源!</string>
@ -800,5 +803,7 @@
<string name="import_bookshelf">导入书单</string> <string name="import_bookshelf">导入书单</string>
<string name="pre_download">预下载</string> <string name="pre_download">预下载</string>
<string name="pre_download_s">预先下载10章正文</string> <string name="pre_download_s">预先下载10章正文</string>
<string name="sort_enable">启用排序</string>
<string name="background_image">背景图片</string>
</resources> </resources>

@ -803,5 +803,7 @@
<string name="import_bookshelf">导入书单</string> <string name="import_bookshelf">导入书单</string>
<string name="pre_download">预下载</string> <string name="pre_download">预下载</string>
<string name="pre_download_s">预先下载10章正文</string> <string name="pre_download_s">预先下载10章正文</string>
<string name="sort_enable">启用排序</string>
<string name="background_image">背景图片</string>
</resources> </resources>

@ -13,4 +13,5 @@
<string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string> <string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string>
<string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string> <string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string>
<string name="tg_url" translatable="false">https://t.me/yueduguanfang</string> <string name="tg_url" translatable="false">https://t.me/yueduguanfang</string>
<string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string>
</resources> </resources>

@ -806,5 +806,7 @@
<string name="import_bookshelf">导入书单</string> <string name="import_bookshelf">导入书单</string>
<string name="pre_download">预下载</string> <string name="pre_download">预下载</string>
<string name="pre_download_s">预先下载10章正文</string> <string name="pre_download_s">预先下载10章正文</string>
<string name="sort_enable">启用排序</string>
<string name="background_image">背景图片</string>
</resources> </resources>

@ -9,13 +9,15 @@
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="check_update" android:key="update_log"
android:title="@string/check_update" android:title="@string/update_log"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="update_log" android:key="check_update"
android:title="@string/update_log" android:title="@string/check_update"
app:allowDividerAbove="false" app:allowDividerAbove="false"
app:allowDividerBelow="false" app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
@ -51,6 +53,12 @@
android:summary="@string/this_github_url" android:summary="@string/this_github_url"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="discord"
android:title="Discord"
android:summary="@string/tg_url"
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="tg" android:key="tg"
android:title="TG" android:title="TG"

@ -42,6 +42,7 @@
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:key="dayThemeCategory"
android:title="@string/day" android:title="@string/day"
app:allowDividerAbove="true" app:allowDividerAbove="true"
app:allowDividerBelow="false" app:allowDividerBelow="false"
@ -72,6 +73,10 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImage"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference
android:defaultValue="@color/md_grey_200" android:defaultValue="@color/md_grey_200"
android:key="colorBottomBackground" android:key="colorBottomBackground"
@ -93,6 +98,7 @@
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory
android:key="nightThemeCategory"
android:title="@string/night" android:title="@string/night"
app:allowDividerAbove="true" app:allowDividerAbove="true"
app:allowDividerBelow="false" app:allowDividerBelow="false"
@ -123,6 +129,10 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImageNight"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference
android:defaultValue="@color/md_grey_800" android:defaultValue="@color/md_grey_800"
android:key="colorBottomBackgroundNight" android:key="colorBottomBackgroundNight"

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<remote-actions
actionset_label="@string/app_name"
version="1.2">
<action
id="next_page"
label="@string/next_page"
priority="1"
repeatable="true"
repeatable_interval="short"
trigger_key="PAGE_DOWN">
<preference name="gesture" value="click" />
</action>
<action
id="prev_page"
label="@string/prev_page"
priority="2"
repeatable="true"
repeatable_interval="short"
trigger_key="PAGE_UP">
<preference name="gesture" value="double_click" />
</action>
</remote-actions>

@ -11,7 +11,7 @@ buildscript {
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:4.1.1" classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.3" classpath "de.timfreiheit.resourceplaceholders:placeholders:0.3"
classpath "com.google.gms:google-services:4.3.4" classpath "com.google.gms:google-services:4.3.4"

Loading…
Cancel
Save