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: |
echo "清空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
run: |
echo "给apk增加签名"

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

@ -150,7 +150,15 @@
<activity
android:name=".ui.book.read.ReadBookActivity"
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
android:name=".ui.book.info.BookInfoActivity"

@ -2,141 +2,146 @@
{
"id": -1,
"name": "度丫丫",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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对象的可用属性
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符.
> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符.
```
url // 章节地址
title // 章节标题
@ -93,3 +92,17 @@ 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)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**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
* 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc)
* 换源刷新之前删除原搜索记录
* 优化web调试
**2021/01/05**
* 修复崩溃bug
**2021/01/03**
* 导出书单只保留书名与作者,导入时自动查找可用源
@ -68,18 +85,7 @@
* 阅读进度从页数改为字数,排版变化时定位更准确
* 修改viewBinding
* 修复中文排序
* 去掉FontJs规则,可以写在替换规则里,例: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
}
```
* 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看
**2020/11/30**
* 优化阅读界面设置

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

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

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

@ -12,10 +12,10 @@ interface BookSourceDao {
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")
fun liveDataSearch(searchKey: String = ""): LiveData<List<BookSource>>
fun liveDataSearch(searchKey: String): LiveData<List<BookSource>>
@Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
fun liveDataGroupSearch(searchKey: String = ""): LiveData<List<BookSource>>
fun liveDataGroupSearch(searchKey: String): LiveData<List<BookSource>>
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
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")
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>>
@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>>
@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>>
@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")
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")
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")
fun getBookSource(key: String): BookSource?

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

@ -63,6 +63,10 @@ interface SearchBookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
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")
fun clearExpired(time: Long)
}

@ -2,6 +2,7 @@ package io.legado.app.help
import io.legado.app.App
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.utils.GSON
import io.legado.app.utils.fromJsonArray
@ -45,17 +46,25 @@ object DefaultData {
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() {
App.db.httpTTSDao.deleteDefault()
httpTTS.let {
App.db.httpTTSDao.insert(*it.toTypedArray())
}
App.db.httpTTSDao.insert(*httpTTS.toTypedArray())
}
fun importDefaultTocRules() {
App.db.txtTocRule.deleteDefault()
txtTocRules.let {
App.db.txtTocRule.insert(*it.toTypedArray())
}
App.db.txtTocRule.insert(*txtTocRules.toTypedArray())
}
fun importDefaultRssSources() {
App.db.rssSourceDao.insert(*rssSources.toTypedArray())
}
}

@ -65,8 +65,11 @@ object LocalConfig {
get() = isLastVersion(1, "ruleHelpVersion")
val hasUpHttpTTS: Boolean
get() = isLastVersion(1, "httpTtsVersion")
get() = !isLastVersion(2, "httpTtsVersion")
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 accentColor: String,
var backgroundColor: String,
var backgroundImage: String? = null,
var bottomBackground: String
)

@ -29,7 +29,7 @@ object Backup {
"bookmark.json",
"bookGroup.json",
"bookSource.json",
"rssSource.json",
"rssSources.json",
"rssStar.json",
"replaceRule.json",
"readRecord.json",
@ -61,7 +61,7 @@ object Backup {
writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(App.db.bookGroupDao.all, "bookGroup.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.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath)

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

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

@ -646,7 +646,9 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
bindings["chapter"] = chapter
bindings["title"] = chapter?.title
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) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
@ -338,7 +338,7 @@ class AnalyzeUrl(
setCookie(tag)
return when (method) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))

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

@ -93,15 +93,18 @@ object BookChapterList {
for (item in chapterDataList) {
downloadToc(
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 {
block.resumeWithException(it)
block.cancel(it)
}
}
@ -114,7 +117,8 @@ object BookChapterList {
listRule: String,
chapterList: ArrayList<BookChapter>,
chapterDataList: ArrayList<ChapterData<String>>,
onFinish: () -> Unit
onFinish: () -> Unit,
onError: (error: Throwable) -> Unit
) {
Coroutine.async(scope = scope) {
val nextBody = AnalyzeUrl(
@ -142,7 +146,7 @@ object BookChapterList {
}
}
}.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.EventBus
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
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.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent
@ -21,7 +23,7 @@ import kotlin.math.min
class CheckSourceService : BaseService() {
private var threadCount = AppConfig.threadCount
private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var searchCoroutine = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
private var tasks = CompositeCoroutine()
private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>()
@ -62,7 +64,7 @@ class CheckSourceService : BaseService() {
override fun onDestroy() {
super.onDestroy()
tasks.clear()
searchPool.close()
searchCoroutine.close()
postEvent(EventBus.CHECK_SOURCE_DONE, 0)
}
@ -96,18 +98,36 @@ class CheckSourceService : BaseService() {
if (index < allIds.size) {
val sourceUrl = allIds[index]
App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl, source.bookSourceName)
} else {
CheckSource(source).check(this, searchPool) {
onNext(it, source.bookSourceName)
}
}
check(source)
} ?: 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) {
synchronized(this) {
check()

@ -12,11 +12,17 @@ import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.postEvent
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
import org.jetbrains.anko.collections.forEachWithIndex
import java.io.File
import java.io.FileDescriptor
import java.io.FileInputStream
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.util.*
class HttpReadAloudService : BaseReadAloudService(),
MediaPlayer.OnPreparedListener,
@ -55,12 +61,17 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun play() {
if (contentList.isEmpty()) return
if (nowSpeak == 0) {
downloadAudio()
} else {
val file = getSpeakFile(nowSpeak)
if (file.exists()) {
playAudio(FileInputStream(file).fd)
ReadAloud.httpTTS?.let {
val fileName = md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), contentList[nowSpeak])
if (nowSpeak == 0) {
downloadAudio()
} else {
val file = getSpeakFileAsMd5(fileName)
if (file.exists()) {
playAudio(FileInputStream(file).fd)
} else {
downloadAudio()
}
}
}
}
@ -68,28 +79,53 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun downloadAudio() {
task?.cancel()
task = execute {
FileUtils.deleteFile(ttsFolder)
for (index in 0 until contentList.size) {
if (isActive) {
ReadAloud.httpTTS?.let {
AnalyzeUrl(
it.url,
speakText = contentList[index],
speakSpeed = AppConfig.ttsSpeechRate
).getByteArray().let { bytes ->
if (isActive) {
val file = getSpeakFile(index)
file.writeBytes(bytes)
if (index == nowSpeak) {
@Suppress("BlockingMethodInNonBlockingContext")
val fis = FileInputStream(file)
playAudio(fis.fd)
removeCacheFile()
ReadAloud.httpTTS?.let {
contentList.forEachWithIndex { index, item ->
if (isActive) {
val fileName =
md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item)
if (hasSpeakFile(fileName)) { //已经下载好的语音缓存
if (index == nowSpeak) {
val file = getSpeakFileAsMd5(fileName)
@Suppress("BlockingMethodInNonBlockingContext")
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 {
return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3")
private fun speakFilePath() = ttsFolder + File.separator
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) {
super.pauseReadAloud(pause)
mediaPlayer.pause()

@ -2,64 +2,35 @@ package io.legado.app.service.help
import android.content.Context
import android.content.Intent
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.IntentAction
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
class CheckSource(val source: BookSource) {
object CheckSource {
var keyword = "我的"
companion object {
var keyword = "我的"
fun start(context: Context, sources: List<BookSource>) {
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)
}
fun start(context: Context, sources: List<BookSource>) {
if (sources.isEmpty()) {
context.toast(R.string.non_select)
return
}
fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.stop
context.startService(it)
}
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)
}
}
fun check(
scope: CoroutineScope,
context: CoroutineContext,
onNext: (sourceUrl: String) -> Unit
): 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)
}
fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.stop
context.startService(it)
}
}
}

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

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

@ -10,10 +10,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookSource
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.help.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import org.jetbrains.anko.sdk27.listeners.onClick
/**
@ -85,7 +85,14 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
when (item.itemId) {
R.id.menu_new_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
okButton {
alertBinding.editView.text?.toString()?.let { group ->

@ -10,10 +10,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.RssSource
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.help.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import org.jetbrains.anko.sdk27.listeners.onClick
/**
@ -85,7 +85,14 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
when (item.itemId) {
R.id.menu_new_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
okButton {
alertBinding.editView.text?.toString()?.let { group ->

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

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

@ -51,10 +51,8 @@ import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
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_local -> item.isVisible = !onLine
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) {
R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule()
R.id.menu_re_segment -> item.isChecked = book.getReSegment()
@ -245,12 +241,6 @@ class ReadBookActivity : ReadBookBaseActivity(),
supportFragmentManager,
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_get_progress -> ReadBook.book?.let {
viewModel.syncBookProgress(it) { progress ->
@ -310,6 +300,14 @@ class ReadBookActivity : ReadBookBaseActivity(),
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 -> {
binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT)
return true
@ -531,13 +529,18 @@ class ReadBookActivity : ReadBookBaseActivity(),
/**
* 更新内容
*/
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
autoPageProgress = 0
override fun upContent(
relativePosition: Int,
resetPageOffset: Boolean,
success: (() -> Unit)?
) {
launch {
autoPageProgress = 0
binding.readView.upContent(relativePosition, resetPageOffset)
binding.readMenu.setSeekPage(ReadBook.durPageIndex())
loadStates = false
success?.invoke()
}
loadStates = false
}
/**
@ -715,6 +718,15 @@ class ReadBookActivity : ReadBookBaseActivity(),
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)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
requestCodeEditSource -> viewModel.upBookSource()
requestCodeEditSource -> viewModel.upBookSource {
upView()
}
requestCodeChapterList ->
data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index ->
if (index != ReadBook.durChapterIndex) {
@ -795,43 +809,35 @@ class ReadBookActivity : ReadBookBaseActivity(),
}
private fun skipToSearch(index: Int, indexWithinChapter: Int) {
launch(IO) {
viewModel.openChapter(index)
// block until load correct chapter and pages
var pages = ReadBook.curTextChapter?.pages
while (ReadBook.durChapterIndex != index || pages == null) {
delay(100L)
pages = ReadBook.curTextChapter?.pages
}
viewModel.openChapter(index) {
val pages = ReadBook.curTextChapter?.pages ?: return@openChapter
val positions = ReadBook.searchResultPositions(
pages,
indexWithinChapter,
viewModel.searchContentQuery
)
while (ReadBook.durPageIndex() != positions[0]) {
delay(100L)
ReadBook.skipToPage(positions[0])
}
withContext(Main) {
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
delay(20L)
when (positions[3]) {
0 -> binding.readView.curPage.selectEndMoveIndex(
0,
positions[1],
positions[2] + viewModel.searchContentQuery.length - 1
)
1 -> binding.readView.curPage.selectEndMoveIndex(
0,
positions[1] + 1,
positions[4]
)
//consider change page, jump to scroll position
-1 -> binding.readView.curPage
.selectEndMoveIndex(1, 0, positions[4])
ReadBook.skipToPage(positions[0]) {
launch {
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
delay(20L)
when (positions[3]) {
0 -> binding.readView.curPage.selectEndMoveIndex(
0,
positions[1],
positions[2] + viewModel.searchContentQuery.length - 1
)
1 -> binding.readView.curPage.selectEndMoveIndex(
0,
positions[1] + 1,
positions[4]
)
//consider change page, jump to scroll position
-1 -> binding.readView.curPage
.selectEndMoveIndex(1, 0, positions[4])
}
binding.readView.isTextSelected = true
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.curTextChapter = null
ReadBook.nextTextChapter = null
@ -260,7 +260,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.durChapterPos = durChapterPos
}
ReadBook.saveRead()
ReadBook.loadContent(resetPageOffset = true)
ReadBook.loadContent(resetPageOffset = true) {
success?.invoke()
}
}
fun removeFromBookshelf(success: (() -> Unit)?) {
@ -271,13 +273,15 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
}
}
fun upBookSource() {
fun upBookSource(success: (() -> Unit)?) {
execute {
ReadBook.book?.let { book ->
App.db.bookSourceDao.getBookSource(book.origin)?.let {
ReadBook.webBook = WebBook(it)
}
}
}.onSuccess {
success?.invoke()
}
}

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

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

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

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

@ -25,6 +25,12 @@ data class TextPage(
val lineSize get() = textLines.size
val charSize get() = text.length
fun getLine(index: Int): TextLine {
return textLines.getOrElse(index) {
textLines.last()
}
}
fun upLinesPosition() = ChapterProvider.apply {
if (!ReadBookConfig.textBottomJustify) 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 searchView: SearchView
private var bookSourceLiveDate: LiveData<List<BookSource>>? = null
private var groups = linkedSetOf<String>()
private val groups = linkedSetOf<String>()
private var groupMenu: SubMenu? = null
private var sort = Sort.Default
private var sortAscending = true
@ -130,6 +130,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
sortCheck(Sort.Update)
initLiveDataBookSource(searchView.query?.toString())
}
R.id.menu_sort_enable -> {
item.isChecked = true
sortCheck(Sort.Enable)
initLiveDataBookSource(searchView.query?.toString())
}
R.id.menu_enabled_group -> {
searchView.setQuery(getString(R.string.enabled), true)
}
@ -187,29 +192,44 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
else -> {
App.db.bookSourceDao.liveDataSearch("%$searchKey%")
}
}
bookSourceLiveDate?.observe(this, { data ->
val sourceList =
if (sortAscending) when (sort) {
Sort.Weight -> data.sortedBy { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
o1.bookSourceName.cnCompare(o2.bookSourceName)
}.apply {
observe(this@BookSourceActivity, { data ->
val sourceList =
if (sortAscending) when (sort) {
Sort.Weight -> data.sortedBy { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
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 }
Sort.Update -> data.sortedByDescending { it.lastUpdateTime }
else -> data
}
else when (sort) {
Sort.Weight -> data.sortedByDescending { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
o2.bookSourceName.cnCompare(o1.bookSourceName)
else when (sort) {
Sort.Weight -> data.sortedByDescending { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
o2.bookSourceName.cnCompare(o1.bookSourceName)
}
Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
Sort.Update -> data.sortedBy { 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.reversed()
}
Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
Sort.Update -> data.sortedBy { it.lastUpdateTime }
else -> data.reversed()
}
adapter.setItems(sourceList, adapter.diffItemCallback)
})
adapter.setItems(sourceList, adapter.diffItemCallback)
})
}
}
private fun showHelp() {
@ -229,7 +249,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun initLiveDataGroup() {
App.db.bookSourceDao.liveGroup().observe(this, {
groups.clear()
it.map { group ->
it.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
upGroupMenu()
@ -483,6 +503,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
}
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.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectDefaultCover()
selectImage(requestCodeCover)
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.defaultCover)
} else {
selectDefaultCover()
selectImage(requestCodeCover)
}
}
}
@ -183,11 +183,12 @@ class OtherConfigFragment : BasePreferenceFragment(),
}.show()
}
private fun selectDefaultCover() {
@Suppress("SameParameterValue")
private fun selectImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, requestCodeCover)
startActivityForResult(intent, requestCode)
}
private fun isProcessTextEnabled(): Boolean {

@ -1,13 +1,17 @@
package io.legado.app.ui.config
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import io.legado.app.App
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.LauncherIconHelp
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.selector
import io.legado.app.lib.theme.ATH
import io.legado.app.ui.widget.number.NumberPickerDialog
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 java.io.File
@Suppress("SameParameterValue")
class ThemeConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeBgImage = 234
private val requestCodeBgImageN = 342
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme)
if (Build.VERSION.SDK_INT < 26) {
findPreference<IconListPreference>(PreferKey.launcherIcon)?.let {
preferenceScreen.removePreference(it)
}
preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon)
}
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())
findPreference<ColorPreference>(PreferKey.cBackground)?.let {
@ -168,10 +186,39 @@ class ThemeConfigFragment : BasePreferenceFragment(),
}
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"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)
}
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")
private fun saveThemeAlert(key: String) {
alert(R.string.theme_name) {
@ -211,6 +258,61 @@ class ThemeConfigFragment : BasePreferenceFragment(),
when (preferenceKey) {
PreferKey.barElevation -> preference.summary =
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) {
DefaultData.importDefaultTocRules()
}
if (LocalConfig.hasUpRssSources) {
DefaultData.importDefaultRssSources()
}
}
}
}

@ -96,7 +96,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FilePickerDialog.C
}
if (AppConfig.isGooglePlay) {
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 {
initData(newText ?: "")
initData(newText)
return false
}
})
@ -105,7 +105,7 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
adapter.addHeaderView {
ItemRssBinding.inflate(layoutInflater, it, false).apply {
tvName.setText(R.string.rule_subscription)
ivIcon.setImageResource(R.mipmap.ic_launcher)
ivIcon.setImageResource(R.drawable.image_legado)
root.onClick {
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 = 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, {
adapter.setItems(it)
})

@ -154,14 +154,15 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
else -> {
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() {
@ -239,7 +240,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
}
override fun onQueryTextChange(newText: String?): Boolean {
observeReplaceRuleData("%$newText%")
observeReplaceRuleData(newText)
return false
}

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

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

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

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

@ -9,6 +9,7 @@ import io.legado.app.App
import io.legado.app.BuildConfig
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.RssSource
import io.legado.app.help.DefaultData
import io.legado.app.utils.*
import org.jetbrains.anko.toast
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 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()

@ -1,6 +1,7 @@
package io.legado.app.web
import android.os.Looper
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoWSD
import io.legado.app.App
@ -22,12 +23,12 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
override fun onOpen() {
launch(IO) {
do {
delay(30000)
runCatching {
kotlin.runCatching {
while (isOpen) {
ping("ping".toByteArray())
delay(30000)
}
} while (isOpen)
}
}
}
@ -42,21 +43,21 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
override fun onMessage(message: NanoWSD.WebSocketFrame) {
if (!message.textPayload.isJson()) return
kotlin.runCatching {
val debugBean = GSON.fromJsonObject<Map<String, String>>(message.textPayload)
if (debugBean != null) {
val tag = debugBean["tag"]
val key = debugBean["key"]
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
kotlin.runCatching {
launch(IO) {
kotlin.runCatching {
val debugBean = GSON.fromJsonObject<Map<String, String>>(message.textPayload)
if (debugBean != null) {
val tag = debugBean["tag"]
val key = debugBean["key"]
if (tag.isNullOrBlank() || key.isNullOrBlank()) {
send(App.INSTANCE.getString(R.string.cannot_empty))
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) {
kotlin.runCatching {
send(msg)
if (state == -1 || state == 1000) {
Debug.cancelDebug(true)
if (Looper.getMainLooper() == Looper.myLooper()) {
launch(IO) {
runCatching {
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="6"
android:hint="@string/content" />
</io.legado.app.ui.widget.text.TextInputLayout>
@ -37,6 +38,7 @@
android:id="@+id/edit_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="6"
android:hint="@string/note_content" />
</io.legado.app.ui.widget.text.TextInputLayout>

@ -11,8 +11,8 @@
<io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover"
android:layout_width="60dp"
android:layout_height="80dp"
android:layout_width="66dp"
android:layout_height="90dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
@ -97,7 +97,7 @@
android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@+id/tv_read"
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"
tools:ignore="RtlSymmetry" />
@ -126,7 +126,7 @@
android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@id/tv_last"
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" />
<androidx.appcompat.widget.AppCompatImageView
@ -154,7 +154,7 @@
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
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" />
<View

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

@ -49,14 +49,6 @@
android:title="@string/bookmark_add"
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">
<item

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

@ -48,6 +48,12 @@
android:title="@string/import_by_qr_code"
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
android:id="@+id/menu_share_source"
android:icon="@drawable/ic_share"

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

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

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

@ -13,4 +13,5 @@
<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="tg_url" translatable="false">https://t.me/yueduguanfang</string>
<string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string>
</resources>

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

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

@ -42,6 +42,7 @@
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.PreferenceCategory
android:key="dayThemeCategory"
android:title="@string/day"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
@ -72,6 +73,10 @@
app:cpv_dialogType="preset"
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
android:defaultValue="@color/md_grey_200"
android:key="colorBottomBackground"
@ -93,6 +98,7 @@
</io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory
android:key="nightThemeCategory"
android:title="@string/night"
app:allowDividerAbove="true"
app:allowDividerBelow="false"
@ -123,6 +129,10 @@
app:cpv_dialogType="preset"
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
android:defaultValue="@color/md_grey_800"
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/" }
}
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 "de.timfreiheit.resourceplaceholders:placeholders:0.3"
classpath "com.google.gms:google-services:4.3.4"

Loading…
Cancel
Save