Merge pull request #9 from gedoor/master

sys code
pull/977/head
ag2s20150909 4 years ago committed by GitHub
commit 6bd2a41e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/build.gradle
  2. 158
      app/src/main/assets/defaultData/httpTTS.json
  3. 55
      app/src/main/assets/defaultData/readConfig.json
  4. 14
      app/src/main/assets/updateLog.md
  5. 7
      app/src/main/java/io/legado/app/App.kt
  6. 29
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  7. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  8. 8
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  9. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  10. 8
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  11. 174
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  12. 122
      app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt
  13. 12
      app/src/main/java/io/legado/app/help/http/RetryInterceptor.kt
  14. 13
      app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt
  15. 14
      app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt
  16. 40
      app/src/main/java/io/legado/app/help/http/parser/StrResponseParser.kt
  17. 33
      app/src/main/java/io/legado/app/help/http/parser/TextParser.kt
  18. 5
      app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt
  19. 19
      app/src/main/java/io/legado/app/lib/permission/Request.kt
  20. 143
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  21. 81
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  22. 44
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  23. 2
      app/src/main/java/io/legado/app/ui/README.md
  24. 14
      app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt
  25. 9
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt
  26. 9
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt
  27. 9
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  28. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  29. 53
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  30. 9
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt
  31. 9
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt
  32. 12
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  33. 2
      app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt
  34. 9
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  35. 34
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt
  36. 2
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt
  37. 1
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  38. 8
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  39. 92
      app/src/main/java/io/legado/app/ui/widget/recycler/RefreshRecyclerView.kt
  40. 27
      app/src/main/java/io/legado/app/utils/OkHttpExtensions.kt
  41. 15
      app/src/main/res/layout/activity_rss_favorites.xml
  42. 13
      app/src/main/res/layout/dialog_read_bg_text.xml
  43. 15
      app/src/main/res/layout/fragment_rss_articles.xml
  44. 2
      app/src/main/res/values-zh-rHK/strings.xml
  45. 4
      app/src/main/res/values-zh-rTW/strings.xml
  46. 2
      app/src/main/res/values-zh/strings.xml
  47. 26
      app/src/main/res/values/strings.xml
  48. 6
      app/src/main/res/xml/pref_config_other.xml
  49. 6
      build.gradle
  50. 2
      gradle/wrapper/gradle-wrapper.properties

@ -152,7 +152,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.6' def room_version = '2.3.0'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:$room_version" testImplementation "androidx.room:room-testing:$room_version"
@ -178,10 +178,7 @@ dependencies {
implementation 'com.github.gedoor:rhino-android:1.5' implementation 'com.github.gedoor:rhino-android:1.5'
// //
//noinspection GradleDependency
implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.ljx.rxhttp:rxhttp:2.5.7'
kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.7'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
@ -191,7 +188,7 @@ dependencies {
implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
// //
implementation 'com.king.zxing:zxing-lite:2.0.2' implementation 'com.king.zxing:zxing-lite:2.0.3'
// //
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'

@ -1,143 +1,233 @@
[ [
{
"id": -100,
"name": "0",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}"
},
{
"id": -99,
"name": "zaixianai.cn",
"url": "<js>\nlet url='https://www.zaixianai.cn/voiceCompose';\n\nlet ua=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\";\n\nlet doc=java.get(url,{\"User-Agent\":ua});\nlet cookie=String(doc.header(\"set-cookie\")).match(/laravel_session=[^\\n]+/)[0];\nlet token=String(doc.body()).match(/token=\"([^\"]+)/)[1];\n\nurl='https://www.zaixianai.cn/Api_getVoice,'+JSON.stringify({\n\"method\": \"POST\",\n\"body\": \"content=\" + java.encodeURI(speakText) + \"&volume=50&speech_rate=0&voice=Aixia&_token=\"+token,\n\"headers\": {\n\"User-Agent\": ua,\n\"cookie\": cookie\n}\n});\n\nlet res=java.ajax(url);\n\n'https://www.zaixianai.cn/voice/'+JSON.parse(res).data.file_name+','+JSON.stringify({\n\"headers\": {\n\"User-Agent\": ua,\n\"accept\": \"*/*\",\n\"referer\": \"https://www.zaixianai.cn/voiceCompose\",\n\"cookie\": cookie,\n\"accept-encoding\": \"identity;q=1, *;q=0\"\n}\n})\n</js>"
},
{
"id": -98,
"name": "台湾女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4007&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{ {
"id": -1, "id": -1,
"name": "度丫丫", "name": "度丫丫",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -2, "id": -2,
"name": "度博文①", "name": "度博文①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -3, "id": -3,
"name": "度博文②", "name": "度博文②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -4, "id": -4,
"name": "度博文③", "name": "度博文③",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -97,
"name": "度小乔",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}"
}, },
{ {
"id": -5, "id": -5,
"name": "度小娇", "name": "度小娇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -6, "id": -6,
"name": "度小宇", "name": "度小宇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=2&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -7, "id": -7,
"name": "度小童", "name": "度小童",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}"
}, },
{
"id": -96,
"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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -95,
"name": "度小粤",
"url": "http://tts.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=cte&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{ {
"id": -8, "id": -8,
"name": "度小美", "name": "度小美",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -94,
"name": "度小芳",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4125&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -9, "id": -9,
"name": "度小萌", "name": "度小萌",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -93,
"name": "度小贤",
"url": "http://tts.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=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"}"
},
{
"id": -92,
"name": "度小雯",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -10, "id": -10,
"name": "度小鹿①", "name": "度小鹿①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -11, "id": -12,
"name": "度小鹿②", "name": "度小鹿②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -12, "id": -11,
"name": "度小鹿③", "name": "度小鹿③",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -91,
"name": "度灵儿",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -13, "id": -13,
"name": "度米朵①", "name": "度米朵①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -14, "id": -14,
"name": "度米朵②", "name": "度米朵②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -15, "id": -15,
"name": "度逍遥-基础", "name": "度逍遥-基础",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -16, "id": -16,
"name": "度逍遥-精品①", "name": "度逍遥-精品①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -17, "id": -17,
"name": "度逍遥-精品②", "name": "度逍遥-精品②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -18, "id": -18,
"name": "情感女声", "name": "情感女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -19, "id": -19,
"name": "情感男声", "name": "情感男声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -20, "id": -20,
"name": "标准女声", "name": "标准女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -90,
"name": "标准女声-基础",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -21, "id": -21,
"name": "标准男声", "name": "标准男声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -22, "id": -89,
"name": "治愈女声", "name": "温柔女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5120&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4126&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -23, "id": -88,
"name": "治愈男声", "name": "甜美女声①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=1200&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=&rate=32=5&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -24, "id": -24,
"name": "甜美女声", "name": "甜美女声",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -25, "id": -25,
"name": "甜美女声②", "name": "甜美女声③",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=160&vol=5&aue=6&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=160&vol=&rate=32=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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -26, "id": -26,
"name": "百度主持", "name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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": -87,
"name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -86,
"name": "百度解说①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4123&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -85,
"name": "百度解说②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4128&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -84,
"name": "百度解说③",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4129&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{ {
"id": -27, "id": -27,
"name": "百度评书①", "name": "百度评书①",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -28, "id": -28,
"name": "百度评书②", "name": "百度评书②",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&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=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -83,
"name": "萝莉少女音",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5201&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
}, },
{ {
"id": -29, "id": -29,

@ -1,5 +1,56 @@
[ [
{ {
"bgStr": "#ffc0edc6",
"bgStrEInk": "#FFFFFF",
"bgStrNight": "#000000",
"bgType": 0,
"bgTypeEInk": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,
"darkStatusIconEInk": true,
"darkStatusIconNight": false,
"footerMode": 0,
"footerPaddingBottom": 10,
"footerPaddingLeft": 13,
"footerPaddingRight": 17,
"footerPaddingTop": 0,
"headerMode": 0,
"headerPaddingBottom": 0,
"headerPaddingLeft": 19,
"headerPaddingRight": 16,
"headerPaddingTop": 10,
"letterSpacing": 0,
"lineSpacingExtra": 10,
"name": "微信读书",
"paddingBottom": 4,
"paddingLeft": 22,
"paddingRight": 22,
"paddingTop": 5,
"pageAnim": 3,
"pageAnimEInk": 3,
"paragraphIndent": "  ",
"paragraphSpacing": 6,
"showFooterLine": true,
"showHeaderLine": true,
"textBold": 0,
"textColor": "#ff0b0b0b",
"textColorEInk": "#000000",
"textColorNight": "#ADADAD",
"textSize": 24,
"tipColor": -10461088,
"tipFooterLeft": 7,
"tipFooterMiddle": 0,
"tipFooterRight": 6,
"tipHeaderLeft": 1,
"tipHeaderMiddle": 0,
"tipHeaderRight": 2,
"titleBottomSpacing": 0,
"titleMode": 0,
"titleSize": 4,
"titleTopSpacing": 0
},
{
"name": "预设1",
"bgStr": "#FFFFFF", "bgStr": "#FFFFFF",
"bgStrNight": "#000000", "bgStrNight": "#000000",
"textColor": "#000000", "textColor": "#000000",
@ -10,6 +61,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设2",
"bgStr": "#DDC090", "bgStr": "#DDC090",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#3E3422", "textColor": "#3E3422",
@ -20,6 +72,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设3",
"bgStr": "#C2D8AA", "bgStr": "#C2D8AA",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#596C44", "textColor": "#596C44",
@ -30,6 +83,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设4",
"bgStr": "#DBB8E2", "bgStr": "#DBB8E2",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#68516C", "textColor": "#68516C",
@ -40,6 +94,7 @@
"darkStatusIconNight": false "darkStatusIconNight": false
}, },
{ {
"name": "预设5",
"bgStr": "#ABCEE0", "bgStr": "#ABCEE0",
"bgStrNight": "#3C3F43", "bgStrNight": "#3C3F43",
"textColor": "#3D4C54", "textColor": "#3D4C54",

@ -3,12 +3,22 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2021/04/18** **2021/05/04**
* epub插图,epublib优化 by ag2s20150909 * 修复bug
* url参数添加重置次数,retry
* 修改默认tts, 手动导入
* 升级android studio
**2021/04/30**
* epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909
* 添加高刷设置
* 其它一些优化
* pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本
**2021/04/16** **2021/04/16**
* 去掉google统计,解决华为手机使用崩溃的bug * 去掉google统计,解决华为手机使用崩溃的bug
* 添加规则订阅时判断重复提醒 * 添加规则订阅时判断重复提醒
* 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局
**2021/04/13** **2021/04/13**
* 修复导入布局bug * 修复导入布局bug

@ -7,7 +7,6 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.channelIdDownload import io.legado.app.constant.AppConst.channelIdDownload
import io.legado.app.constant.AppConst.channelIdReadAloud import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb import io.legado.app.constant.AppConst.channelIdWeb
@ -15,10 +14,8 @@ import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.ThemeConfig.applyDayNight import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.HttpHelper
import io.legado.app.utils.LanguageUtils import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
import rxhttp.wrapper.param.RxHttp
class App : MultiDexApplication() { class App : MultiDexApplication() {
@ -26,10 +23,6 @@ class App : MultiDexApplication() {
super.onCreate() super.onCreate()
CrashHandler(this) CrashHandler(this)
LanguageUtils.setConfiguration(this) LanguageUtils.setConfiguration(this)
RxHttp.init(HttpHelper.client, BuildConfig.DEBUG)
RxHttp.setOnParamAssembly {
it.addHeader(AppConst.UA_NAME, AppConfig.userAgent)
}
createNotificationChannels() createNotificationChannels()
applyDayNight(this) applyDayNight(this)
LiveEventBus.config() LiveEventBus.config()

@ -14,6 +14,7 @@ import androidx.viewbinding.ViewBinding
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
@ -66,6 +67,26 @@ abstract class BaseActivity<VB : ViewBinding>(
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
getPrefBoolean(PreferKey.highBrush)
) {
/**
* 添加高刷新率支持
*/
// 获取系统window支持的模式
@Suppress("DEPRECATION")
val modes = window.windowManager.defaultDisplay.supportedModes
// 对获取的模式,基于刷新率的大小进行排序,从小到大排序
modes.sortBy {
it.refreshRate
}
window.let {
val lp = it.attributes
// 取出最大的那一个刷新率,直接设置给window
lp.preferredDisplayModeId = modes.last().modeId
it.attributes = lp
}
}
window.decorView.disableAutoFill() window.decorView.disableAutoFill()
initTheme() initTheme()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -152,11 +173,9 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView) ATH.applyBackgroundTint(window.decorView)
} }
} }
if (AppConfig.isGooglePlay) { ThemeConfig.getBgImage(this)?.let {
ThemeConfig.getBgImage(this)?.let { kotlin.runCatching {
kotlin.runCatching { window.decorView.background = it
window.decorView.background = it
}
} }
} }
} }

@ -73,6 +73,7 @@ object PreferKey {
const val exportUseReplace = "exportUseReplace" const val exportUseReplace = "exportUseReplace"
const val useZhLayout = "useZhLayout" const val useZhLayout = "useZhLayout"
const val fullScreenGesturesSupport = "fullScreenGesturesSupport" const val fullScreenGesturesSupport = "fullScreenGesturesSupport"
const val highBrush = "highBrush"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -10,6 +10,7 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.androidId import io.legado.app.constant.AppConst.androidId
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx import splitties.init.appCtx
import java.util.* import java.util.*
@ -88,6 +89,13 @@ abstract class AppDatabase : RoomDatabase() {
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})"""
) )
if (AppConfig.isGooglePlay) {
db.execSQL(
"""
delete from rssSources where sourceUrl = 'https://github.com/gedoor/legado/releases'
"""
)
}
} }
} }

@ -107,6 +107,8 @@ data class Book(
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro

@ -4,9 +4,7 @@ import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.help.http.CookieStore import io.legado.app.help.http.*
import io.legado.app.help.http.SSLHelper
import io.legado.app.help.http.StrResponse
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
@ -16,8 +14,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.net.URLEncoder import java.net.URLEncoder
@ -292,7 +288,7 @@ interface JsExtensions {
str.isAbsUrl() -> runBlocking { str.isAbsUrl() -> runBlocking {
var x = CacheManager.getByteArray(key) var x = CacheManager.getByteArray(key)
if (x == null) { if (x == null) {
x = RxHttp.get(str).toByteArray().await() x = okHttpClient.newCall { url(str) }.bytes()
x.let { x.let {
CacheManager.put(key, it) CacheManager.put(key, it)
} }

@ -11,82 +11,28 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
@Suppress("unused") private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
object HttpHelper { ConcurrentHashMap()
}
private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
ConcurrentHashMap()
}
val client: OkHttpClient by lazy {
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
val builder = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
.retryOnConnectionFailure(true)
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
.connectionSpecs(specs)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(getHeaderInterceptor())
builder.build()
}
/** val okHttpClient: OkHttpClient by lazy {
* 缓存代理okHttp val specs = arrayListOf(
*/ ConnectionSpec.MODERN_TLS,
fun getProxyClient(proxy: String? = null): OkHttpClient { ConnectionSpec.COMPATIBLE_TLS,
if (proxy.isNullOrBlank()) { ConnectionSpec.CLEARTEXT
return client )
}
proxyClientCache[proxy]?.let {
return it
}
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = r.findAll(proxy)
val group = ms.first()
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
val type = if (group.groupValues[1] == "http") "http" else "socks"
val host = group.groupValues[2]
val port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
if (type != "direct" && host != "") {
val builder = client.newBuilder()
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
}
val proxyClient = builder.build()
proxyClientCache[proxy] = proxyClient
return proxyClient
}
return client
}
private fun getHeaderInterceptor(): Interceptor { val builder = OkHttpClient.Builder()
return Interceptor { chain -> .connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
.retryOnConnectionFailure(true)
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
.connectionSpecs(specs)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(Interceptor { chain ->
val request = chain.request() val request = chain.request()
.newBuilder() .newBuilder()
.addHeader("Keep-Alive", "300") .addHeader("Keep-Alive", "300")
@ -94,28 +40,72 @@ object HttpHelper {
.addHeader("Cache-Control", "no-cache") .addHeader("Cache-Control", "no-cache")
.build() .build()
chain.proceed(request) chain.proceed(request)
})
builder.build()
}
/**
* 缓存代理okHttp
*/
fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) {
return okHttpClient
}
proxyClientCache[proxy]?.let {
return it
}
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = r.findAll(proxy)
val group = ms.first()
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
val type = if (group.groupValues[1] == "http") "http" else "socks"
val host = group.groupValues[2]
val port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
if (type != "direct" && host != "") {
val builder = okHttpClient.newBuilder()
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
} }
val proxyClient = builder.build()
proxyClientCache[proxy] = proxyClient
return proxyClient
} }
return okHttpClient
}
suspend fun ajax(params: AjaxWebView.AjaxParams): StrResponse = suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse =
suspendCancellableCoroutine { block -> suspendCancellableCoroutine { block ->
val webView = AjaxWebView() val webView = AjaxWebView()
block.invokeOnCancellation { block.invokeOnCancellation {
webView.destroyWebView() webView.destroyWebView()
} }
webView.callback = object : AjaxWebView.Callback() { webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) { override fun onResult(response: StrResponse) {
if (!block.isCompleted) if (!block.isCompleted)
block.resume(response) block.resume(response)
} }
override fun onError(error: Throwable) { override fun onError(error: Throwable) {
if (!block.isCompleted) if (!block.isCompleted)
block.cancel(error) block.cancel(error)
}
} }
webView.load(params)
} }
webView.load(params)
} }

@ -0,0 +1,122 @@
package io.legado.app.help.http
import io.legado.app.constant.AppConst
import io.legado.app.help.AppConfig
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.nio.charset.Charset
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun OkHttpClient.newCall(
retry: Int = 0,
builder: Request.Builder.() -> Unit
): ResponseBody {
val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent)
requestBuilder.apply(builder)
var response: Response? = null
for (i in 0..retry) {
response = this.newCall(requestBuilder.build()).await()
if (response.isSuccessful) {
return response.body!!
}
}
throw IOException(response!!.message)
}
suspend fun OkHttpClient.newCallStrResponse(
retry: Int = 0,
builder: Request.Builder.() -> Unit
): StrResponse {
val requestBuilder = Request.Builder()
requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent)
requestBuilder.apply(builder)
var response: Response? = null
for (i in 0..retry) {
response = this.newCall(requestBuilder.build()).await()
if (response.isSuccessful) {
return StrResponse(response, response.body!!.text())
}
}
throw IOException(response!!.message)
}
suspend fun Call.await(): Response = suspendCancellableCoroutine { block ->
block.invokeOnCancellation {
cancel()
}
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
block.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
block.resume(response)
}
})
}
fun ResponseBody.text(encode: String? = null): String {
val responseBytes = UTF8BOMFighter.removeUTF8BOM(bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
fun Request.Builder.addHeaders(headers: Map<String, String>) {
headers.forEach {
addHeader(it.key, it.value)
}
}
fun Request.Builder.get(url: String, queryMap: Map<String, String>, encoded: Boolean = false) {
val httpBuilder = url.toHttpUrl().newBuilder()
queryMap.forEach {
if (encoded) {
httpBuilder.addEncodedQueryParameter(it.key, it.value)
} else {
httpBuilder.addQueryParameter(it.key, it.value)
}
}
url(httpBuilder.build())
}
fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
val formBody = FormBody.Builder()
form.forEach {
if (encoded) {
formBody.addEncoded(it.key, it.value)
} else {
formBody.add(it.key, it.value)
}
}
post(formBody.build())
}
fun Request.Builder.postJson(json: String?) {
json?.let {
val requestBody = json.toRequestBody("application/json; charset=UTF-8".toMediaType())
post(requestBody)
}
}

@ -0,0 +1,12 @@
package io.legado.app.help.http
import okhttp3.Interceptor
import okhttp3.Response
class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
TODO("Not yet implemented")
}
}

@ -1,13 +0,0 @@
package io.legado.app.help.http.parser
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
@Parser(name = "ByteArray")
class ByteParser : rxhttp.wrapper.parse.Parser<ByteArray> {
override fun onParse(response: Response): ByteArray {
return response.body!!.bytes()
}
}

@ -1,14 +0,0 @@
package io.legado.app.help.http.parser
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import java.io.InputStream
@Parser(name = "InputStream")
class InputStreamParser : rxhttp.wrapper.parse.Parser<InputStream> {
override fun onParse(response: Response): InputStream {
return response.body!!.byteStream()
}
}

@ -1,40 +0,0 @@
package io.legado.app.help.http.parser
import io.legado.app.help.http.StrResponse
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset
@Parser(name = "StrResponse")
class StrResponseParser(private val encode: String? = null) :
rxhttp.wrapper.parse.Parser<StrResponse> {
override fun onParse(response: Response): StrResponse {
val body = getString(response)
return StrResponse(response, body)
}
private fun getString(response: Response): String {
val responseBody = response.body ?: throw HttpStatusCodeException(response, "内容为空")
val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
responseBody.contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
}

@ -1,33 +0,0 @@
package io.legado.app.help.http.parser
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import rxhttp.wrapper.exception.HttpStatusCodeException
import java.nio.charset.Charset
@Parser(name = "Text")
class TextParser(private val encode: String? = null) : rxhttp.wrapper.parse.Parser<String> {
override fun onParse(response: Response): String {
val responseBody = response.body ?: throw HttpStatusCodeException(response, "内容为空")
val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes())
var charsetName: String? = encode
charsetName?.let {
return String(responseBytes, Charset.forName(charsetName))
}
//根据http头判断
responseBody.contentType()?.charset()?.let {
return String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
return String(responseBytes, Charset.forName(charsetName))
}
}

@ -29,11 +29,6 @@ class PermissionsCompat private constructor() {
return this return this
} }
fun requestCode(requestCode: Int): Builder {
request.setRequestCode(requestCode)
return this
}
fun onGranted(callback: () -> Unit): Builder { fun onGranted(callback: () -> Unit): Builder {
request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {
override fun onPermissionsGranted() { override fun onPermissionsGranted() {

@ -46,10 +46,6 @@ internal class Request : OnRequestPermissionsResultCallback {
this.permissions?.addAll(listOf(*permissions)) this.permissions?.addAll(listOf(*permissions))
} }
fun setRequestCode(requestCode: Int) {
this.requestCode = requestCode
}
fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) {
grantedCallback = callback grantedCallback = callback
} }
@ -81,13 +77,10 @@ internal class Request : OnRequestPermissionsResultCallback {
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { showSettingDialog(rationale) {
onPermissionsDenied( onPermissionsDenied(deniedPermissions)
requestCode,
deniedPermissions
)
} }
} else { } else {
onPermissionsDenied(requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} }
} else { } else {
@ -160,7 +153,7 @@ internal class Request : OnRequestPermissionsResultCallback {
RequestPlugins.sResultCallback?.onPermissionsGranted() RequestPlugins.sResultCallback?.onPermissionsGranted()
} }
private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) { private fun onPermissionsDenied(deniedPermissions: Array<String>) {
try { try {
deniedCallback?.onPermissionsDenied(deniedPermissions) deniedCallback?.onPermissionsDenied(deniedPermissions)
} catch (ignore: Exception) { } catch (ignore: Exception) {
@ -178,9 +171,9 @@ internal class Request : OnRequestPermissionsResultCallback {
val rationale = val rationale =
if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale
if (rationale != null) { if (rationale != null) {
showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } showSettingDialog(rationale) { onPermissionsDenied(deniedPermissions) }
} else { } else {
onPermissionsDenied(requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} else { } else {
onPermissionsGranted() onPermissionsGranted()
@ -192,7 +185,7 @@ internal class Request : OnRequestPermissionsResultCallback {
if (deniedPermissions == null) { if (deniedPermissions == null) {
onPermissionsGranted() onPermissionsGranted()
} else { } else {
onPermissionsDenied(this.requestCode, deniedPermissions) onPermissionsDenied(deniedPermissions)
} }
} }

@ -1,14 +1,13 @@
package io.legado.app.lib.webdav package io.legado.app.lib.webdav
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.newCall
import io.legado.app.utils.await import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toInputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -23,16 +22,16 @@ class WebDav(urlStr: String) {
// 指定返回哪些属性 // 指定返回哪些属性
private const val DIR = private const val DIR =
"""<?xml version="1.0"?> """<?xml version="1.0"?>
<a:propfind xmlns:a="DAV:"> <a:propfind xmlns:a="DAV:">
<a:prop> <a:prop>
<a:displayname/> <a:displayname/>
<a:resourcetype/> <a:resourcetype/>
<a:getcontentlength/> <a:getcontentlength/>
<a:creationdate/> <a:creationdate/>
<a:getlastmodified/> <a:getlastmodified/>
%s %s
</a:prop> </a:prop>
</a:propfind>""" </a:propfind>"""
} }
private val url: URL = URL(urlStr) private val url: URL = URL(urlStr)
@ -59,16 +58,9 @@ class WebDav(urlStr: String) {
* @return 远程文件是否存在 * @return 远程文件是否存在
*/ */
suspend fun indexFileInfo(): Boolean { suspend fun indexFileInfo(): Boolean {
propFindResponse(ArrayList())?.let { response -> propFindResponse(ArrayList())?.let {
if (!response.isSuccessful) { if (it.text().isNotEmpty()) {
this.exists = false return true
return false
}
response.body?.let {
@Suppress("BlockingMethodInNonBlockingContext")
if (it.string().isNotEmpty()) {
return true
}
} }
} }
return false return false
@ -81,19 +73,13 @@ class WebDav(urlStr: String) {
* @return 文件列表 * @return 文件列表
*/ */
suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response -> propFindResponse(propsList)?.text()?.let { body ->
if (response.isSuccessful) { return parseDir(body)
response.body?.let { body ->
@Suppress("BlockingMethodInNonBlockingContext")
return parseDir(body.string())
}
}
} }
return ArrayList() return ArrayList()
} }
@Throws(IOException::class) private suspend fun propFindResponse(propsList: ArrayList<String>): ResponseBody? {
private suspend fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? {
val requestProps = StringBuilder() val requestProps = StringBuilder()
for (p in propsList) { for (p in propsList) {
requestProps.append("<a:").append(p).append("/>\n") requestProps.append("<a:").append(p).append("/>\n")
@ -103,19 +89,21 @@ class WebDav(urlStr: String) {
} else { } else {
String.format(DIR, requestProps.toString() + "\n") String.format(DIR, requestProps.toString() + "\n")
} }
httpUrl?.let { url -> val url = httpUrl
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 val auth = HttpAuth.auth
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 if (url != null && auth != null) {
val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType()) return kotlin.runCatching {
val request = Request.Builder() okHttpClient.newCall {
.url(url) url(url)
.method("PROPFIND", requestBody) addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
HttpAuth.auth?.let { // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
request.header("Authorization", Credentials.basic(it.user, it.pass)) val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType())
} method("PROPFIND", requestBody)
request.header("Depth", if (depth < 0) "infinity" else depth.toString()) }
return HttpHelper.client.newCall(request.build()).await() }.onFailure {
it.printStackTrace()
}.getOrNull()
} }
return null return null
} }
@ -160,11 +148,15 @@ class WebDav(urlStr: String) {
* @return 是否创建成功 * @return 是否创建成功
*/ */
suspend fun makeAsDir(): Boolean { suspend fun makeAsDir(): Boolean {
httpUrl?.let { url -> val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(url) if (url != null && auth != null) {
.method("MKCOL", null) okHttpClient.newCall {
return execRequest(request) url(url)
method("MKCOL", null)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}
return true
} }
return false return false
} }
@ -198,11 +190,15 @@ class WebDav(urlStr: String) {
if (!file.exists()) return false if (!file.exists()) return false
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = file.asRequestBody(contentType?.toMediaType()) val fileBody = file.asRequestBody(contentType?.toMediaType())
httpUrl?.let { val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(it) if (url != null && auth != null) {
.put(fileBody) okHttpClient.newCall {
return execRequest(request) url(url)
put(fileBody)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}
return true
} }
return false return false
} }
@ -210,37 +206,28 @@ class WebDav(urlStr: String) {
suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean {
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = byteArray.toRequestBody(contentType?.toMediaType()) val fileBody = byteArray.toRequestBody(contentType?.toMediaType())
httpUrl?.let { val url = httpUrl
val request = Request.Builder() val auth = HttpAuth.auth
.url(it) if (url != null && auth != null) {
.put(fileBody) okHttpClient.newCall {
return execRequest(request) url(url)
put(fileBody)
addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}
return true
} }
return false return false
} }
/**
* 执行请求获取响应结果
* @param requestBuilder 因为还需要追加验证信息所以此处传递Request.Builder的对象而不是Request的对象
* @return 请求执行的结果
*/
@Throws(IOException::class)
private suspend fun execRequest(requestBuilder: Request.Builder): Boolean {
HttpAuth.auth?.let {
requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass))
}
val response = HttpHelper.client.newCall(requestBuilder.build()).await()
return response.isSuccessful
}
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun getInputStream(): InputStream? { private suspend fun getInputStream(): InputStream? {
val url = httpUrl val url = httpUrl
val auth = HttpAuth.auth val auth = HttpAuth.auth
if (url != null && auth != null) { if (url != null && auth != null) {
return RxHttp.get(url) return okHttpClient.newCall {
.addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) url(url)
.toInputStream().await() addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
}.byteStream()
} }
return null return null
} }

@ -16,9 +16,6 @@ import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions import io.legado.app.help.JsExtensions
import io.legado.app.help.http.* import io.legado.app.help.http.*
import io.legado.app.utils.* import io.legado.app.utils.*
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import rxhttp.wrapper.param.toStrResponse
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -58,6 +55,7 @@ class AnalyzeUrl(
private var charset: String? = null private var charset: String? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
private var proxy: String? = null private var proxy: String? = null
private var retry: Int = 0
init { init {
baseUrl = baseUrl.split(splitUrlRegex, 1)[0] baseUrl = baseUrl.split(splitUrlRegex, 1)[0]
@ -200,6 +198,7 @@ class AnalyzeUrl(
option.js?.let { option.js?.let {
evalJS(it) evalJS(it)
} }
retry = option.retry
} }
} }
headerMap[UA_NAME] ?: let { headerMap[UA_NAME] ?: let {
@ -305,62 +304,43 @@ class AnalyzeUrl(
params.sourceRegex = sourceRegex params.sourceRegex = sourceRegex
params.postData = body?.toByteArray() params.postData = body?.toByteArray()
params.tag = tag params.tag = tag
return HttpHelper.ajax(params) return getWebViewSrc(params)
} }
return when (method) { return getProxyClient(proxy).newCallStrResponse(retry) {
RequestMethod.POST -> { removeHeader(UA_NAME)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { addHeaders(headerMap)
RxHttp.postForm(url) when (method) {
.setAssemblyEnabled(false) RequestMethod.POST -> {
.setOkClient(HttpHelper.getProxyClient(proxy)) url(url)
.addAllEncoded(fieldMap) if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
.addAllHeader(headerMap) postForm(fieldMap, true)
.toStrResponse().await() } else {
} else { postJson(body)
RxHttp.postJson(url) }
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toStrResponse().await()
} }
else -> get(url, fieldMap, true)
} }
else -> RxHttp.get(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} }
} }
suspend fun getByteArray(tag: String? = null): ByteArray { suspend fun getByteArray(tag: String? = null): ByteArray {
setCookie(tag) setCookie(tag)
return when (method) { @Suppress("BlockingMethodInNonBlockingContext")
RequestMethod.POST -> { return getProxyClient(proxy).newCall(retry) {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { removeHeader(UA_NAME)
RxHttp.postForm(url) addHeaders(headerMap)
.setAssemblyEnabled(false) when (method) {
.setOkClient(HttpHelper.getProxyClient(proxy)) RequestMethod.POST -> {
.addAllEncoded(fieldMap) url(url)
.addAllHeader(headerMap) if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
.toByteArray().await() postForm(fieldMap, true)
} else { } else {
RxHttp.postJson(url) postJson(body)
.setAssemblyEnabled(false) }
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toByteArray().await()
} }
else -> get(url, fieldMap, true)
} }
else -> RxHttp.get(url) }.bytes()
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
}
} }
private fun setCookie(tag: String?) { private fun setCookie(tag: String?) {
@ -393,7 +373,8 @@ class AnalyzeUrl(
val headers: Any?, val headers: Any?,
val body: Any?, val body: Any?,
val type: String?, val type: String?,
val js: String? val js: String?,
val retry: Int = 0
) )
} }

@ -13,6 +13,9 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
@ -45,11 +48,11 @@ object BookContent {
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
analyzeRule.setRedirectUrl(baseUrl) analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = mNextChapterUrl analyzeRule.nextChapterUrl = mNextChapterUrl
scope.ensureActive()
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
) )
content.append(contentData.content).append("\n") content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
@ -58,6 +61,7 @@ object BookContent {
== NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl) == NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl)
) break ) break
nextUrlList.add(nextUrl) nextUrlList.add(nextUrl)
scope.ensureActive()
val res = AnalyzeUrl( val res = AnalyzeUrl(
ruleUrl = nextUrl, ruleUrl = nextUrl,
book = book, book = book,
@ -75,29 +79,27 @@ object BookContent {
} }
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) { } else if (contentData.nextUrl.size > 1) {
val contentDataList = arrayListOf<ContentData<String>>() Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${contentData.nextUrl.size}")
for (item in contentData.nextUrl) { withContext(IO) {
if (!nextUrlList.contains(item)) val asyncArray = Array(contentData.nextUrl.size) {
contentDataList.add(ContentData(nextUrl = item)) async(IO) {
} val urlStr = contentData.nextUrl[it]
for (item in contentDataList) { val analyzeUrl = AnalyzeUrl(
withContext(scope.coroutineContext) { ruleUrl = urlStr,
val res = AnalyzeUrl( book = book,
ruleUrl = item.nextUrl, headerMapF = bookSource.getHeaderMap()
book = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponse(bookSource.bookSourceUrl)
res.body?.let { nextBody ->
contentData = analyzeContent(
book, item.nextUrl, res.url, nextBody, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
) )
item.content = contentData.content val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
analyzeContent(
book, urlStr, res.url, res.body!!, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
).content
} }
} }
} asyncArray.forEach { coroutine ->
for (item in contentDataList) { scope.ensureActive()
content.append(item.content).append("\n") content.append(coroutine.await()).append("\n")
}
} }
} }
content.deleteCharAt(content.length - 1) content.deleteCharAt(content.length - 1)

@ -10,7 +10,7 @@
* book\source 书源界面 * book\source 书源界面
* book\changeCover 封面换源界面 * book\changeCover 封面换源界面
* book\changeSource 换源界面 * book\changeSource 换源界面
* book\chapterList 目录界面 * book\toc 目录界面
* book\download 下载界面 * book\download 下载界面
* book\explore 发现界面 * book\explore 发现界面
* book\local 书籍导入界面 * book\local 书籍导入界面

@ -1,14 +1,10 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.databinding.ActivityTranslucenceBinding import io.legado.app.databinding.ActivityTranslucenceBinding
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
class FileAssociationActivity : class FileAssociationActivity :
@ -39,14 +35,4 @@ class FileAssociationActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
finish()
//返回后直接跳转到主页面
gotoMainActivity()
}
private fun gotoMainActivity() {
startActivity<MainActivity>()
}
} }

@ -9,13 +9,14 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.SourceHelp import io.legado.app.help.SourceHelp
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.help.storage.OldRule import io.legado.app.help.storage.OldRule
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
var groupName: String? = null var groupName: String? = null
@ -111,7 +112,9 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
} }
private suspend fun importSourceUrl(url: String) { private suspend fun importSourceUrl(url: String) {
RxHttp.get(url).toText("utf-8").await().let { body -> okHttpClient.newCall {
url(url)
}.text("utf-8").let { body ->
val items: List<Map<String, Any>> = Restore.jsonPath.parse(body).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(body).read("$")
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)

@ -4,10 +4,11 @@ import android.app.Application
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.help.storage.OldReplace import io.legado.app.help.storage.OldReplace
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
@ -18,7 +19,9 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
fun import(text: String) { fun import(text: String) {
execute { execute {
if (text.isAbsUrl()) { if (text.isAbsUrl()) {
RxHttp.get(text).toText("utf-8").await().let { okHttpClient.newCall {
url(text)
}.text("utf-8").let {
val rules = OldReplace.jsonToReplaceRules(it) val rules = OldReplace.jsonToReplaceRules(it)
allRules.addAll(rules) allRules.addAll(rules)
} }

@ -9,10 +9,11 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.SourceHelp import io.legado.app.help.SourceHelp
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
import io.legado.app.utils.* import io.legado.app.utils.*
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var groupName: String? = null var groupName: String? = null
@ -106,7 +107,9 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
} }
private suspend fun importSourceUrl(url: String) { private suspend fun importSourceUrl(url: String) {
RxHttp.get(url).toText("utf-8").await().let { body -> okHttpClient.newCall {
url(url)
}.text("utf-8").let { body ->
val items: List<Map<String, Any>> = Restore.jsonPath.parse(body).read("$") val items: List<Map<String, Any>> = Restore.jsonPath.parse(body).read("$")
for (item in items) { for (item in items) {
val jsonItem = Restore.jsonPath.parse(item) val jsonItem = Restore.jsonPath.parse(item)

@ -80,6 +80,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
private val sourceEditActivity = private val sourceEditActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
viewModel.upBookSource { viewModel.upBookSource {
upView() upView()
@ -88,12 +89,14 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
private val replaceActivity = private val replaceActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
viewModel.replaceRuleChanged() viewModel.replaceRuleChanged()
} }
} }
private val searchContentActivity = private val searchContentActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
it.data?.let { data -> it.data?.let { data ->
data.getIntExtra("index", ReadBook.durChapterIndex).let { index -> data.getIntExtra("index", ReadBook.durChapterIndex).let { index ->
viewModel.searchContentQuery = data.getStringExtra("query") ?: "" viewModel.searchContentQuery = data.getStringExtra("query") ?: ""

@ -15,8 +15,12 @@ import io.legado.app.constant.EventBus
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogReadBgTextBinding import io.legado.app.databinding.DialogReadBgTextBinding
import io.legado.app.databinding.ItemBgImageBinding import io.legado.app.databinding.ItemBgImageBinding
import io.legado.app.help.DefaultData
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
@ -27,8 +31,6 @@ import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import java.io.File import java.io.File
class BgTextConfigDialog : BaseDialogFragment() { class BgTextConfigDialog : BaseDialogFragment() {
@ -96,25 +98,23 @@ class BgTextConfigDialog : BaseDialogFragment() {
(activity as ReadBookActivity).bottomDialog-- (activity as ReadBookActivity).bottomDialog--
} }
private fun initView() { private fun initView() = with(binding) {
val bg = requireContext().bottomBackground val bg = requireContext().bottomBackground
val isLight = ColorUtils.isColorLight(bg) val isLight = ColorUtils.isColorLight(bg)
primaryTextColor = requireContext().getPrimaryTextColor(isLight) primaryTextColor = requireContext().getPrimaryTextColor(isLight)
secondaryTextColor = requireContext().getSecondaryTextColor(isLight) secondaryTextColor = requireContext().getSecondaryTextColor(isLight)
binding.rootView.setBackgroundColor(bg) rootView.setBackgroundColor(bg)
binding.swDarkStatusIcon.setTextColor(primaryTextColor) tvNameTitle.setTextColor(primaryTextColor)
binding.ivImport.setColorFilter(primaryTextColor) tvName.setTextColor(secondaryTextColor)
binding.ivExport.setColorFilter(primaryTextColor) ivEdit.setColorFilter(secondaryTextColor)
binding.ivDelete.setColorFilter(primaryTextColor) tvRestore.setTextColor(primaryTextColor)
binding.tvBgImage.setTextColor(primaryTextColor) swDarkStatusIcon.setTextColor(primaryTextColor)
} ivImport.setColorFilter(primaryTextColor)
ivExport.setColorFilter(primaryTextColor)
@SuppressLint("InflateParams") ivDelete.setColorFilter(primaryTextColor)
private fun initData() = with(ReadBookConfig.durConfig) { tvBgImage.setTextColor(primaryTextColor)
binding.tvName.text = name.ifBlank { "文字" }
binding.swDarkStatusIcon.isChecked = curStatusIconDark()
adapter = BgAdapter(requireContext(), secondaryTextColor) adapter = BgAdapter(requireContext(), secondaryTextColor)
binding.recyclerView.adapter = adapter recyclerView.adapter = adapter
adapter.addHeaderView { adapter.addHeaderView {
ItemBgImageBinding.inflate(layoutInflater, it, false).apply { ItemBgImageBinding.inflate(layoutInflater, it, false).apply {
tvName.setTextColor(secondaryTextColor) tvName.setTextColor(secondaryTextColor)
@ -131,6 +131,12 @@ class BgTextConfigDialog : BaseDialogFragment() {
} }
} }
@SuppressLint("InflateParams")
private fun initData() = with(ReadBookConfig.durConfig) {
binding.tvName.text = name.ifBlank { "文字" }
binding.swDarkStatusIcon.isChecked = curStatusIconDark()
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun initEvent() = with(ReadBookConfig.durConfig) { private fun initEvent() = with(ReadBookConfig.durConfig) {
binding.ivEdit.setOnClickListener { binding.ivEdit.setOnClickListener {
@ -148,6 +154,17 @@ class BgTextConfigDialog : BaseDialogFragment() {
cancelButton() cancelButton()
}.show() }.show()
} }
binding.tvRestore.setOnClickListener {
val defaultConfigs = DefaultData.readConfigs
val layoutNames = defaultConfigs.map { it.name }
selector("选择预设布局", layoutNames) { _, i ->
if (i >= 0) {
ReadBookConfig.durConfig = defaultConfigs[i]
initData()
postEvent(EventBus.UP_CONFIG, true)
}
}
}
binding.swDarkStatusIcon.setOnCheckedChangeListener { _, isChecked -> binding.swDarkStatusIcon.setOnCheckedChangeListener { _, isChecked ->
setCurStatusIconDark(isChecked) setCurStatusIconDark(isChecked)
(activity as? ReadBookActivity)?.upSystemUiVisibility() (activity as? ReadBookActivity)?.upSystemUiVisibility()
@ -291,7 +308,9 @@ class BgTextConfigDialog : BaseDialogFragment() {
private fun importNetConfig(url: String) { private fun importNetConfig(url: String) {
execute { execute {
RxHttp.get(url).toByteArray().await().let { okHttpClient.newCall {
url(url)
}.bytes().let {
importConfig(it) importConfig(it)
} }
}.onError { }.onError {

@ -6,9 +6,10 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.utils.* import io.legado.app.utils.*
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class SpeakEngineViewModel(application: Application) : BaseViewModel(application) { class SpeakEngineViewModel(application: Application) : BaseViewModel(application) {
@ -20,7 +21,9 @@ class SpeakEngineViewModel(application: Application) : BaseViewModel(application
fun importOnLine(url: String) { fun importOnLine(url: String) {
execute { execute {
RxHttp.get(url).toText("utf-8").await().let { json -> okHttpClient.newCall {
url(url)
}.text("utf-8").let { json ->
import(json) import(json)
} }
}.onSuccess { }.onSuccess {

@ -5,10 +5,11 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.TxtTocRule import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonArray
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class TocRegexViewModel(application: Application) : BaseViewModel(application) { class TocRegexViewModel(application: Application) : BaseViewModel(application) {
@ -29,7 +30,9 @@ class TocRegexViewModel(application: Application) : BaseViewModel(application) {
fun importOnLine(url: String, finally: (msg: String) -> Unit) { fun importOnLine(url: String, finally: (msg: String) -> Unit) {
execute { execute {
RxHttp.get(url).toText("utf-8").await().let { json -> okHttpClient.newCall {
url(url)
}.text("utf-8").let { json ->
GSON.fromJsonArray<TxtTocRule>(json)?.let { GSON.fromJsonArray<TxtTocRule>(json)?.let {
appDb.txtTocRuleDao.insert(*it.toTypedArray()) appDb.txtTocRuleDao.insert(*it.toTypedArray())
} }

@ -28,7 +28,6 @@ import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.prefs.ColorPreference import io.legado.app.ui.widget.prefs.ColorPreference
import io.legado.app.ui.widget.prefs.PreferenceCategory
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File import java.io.File
@ -55,15 +54,8 @@ class ThemeConfigFragment : BasePreferenceFragment(),
if (Build.VERSION.SDK_INT < 26) { if (Build.VERSION.SDK_INT < 26) {
preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon) preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon)
} }
if (AppConfig.isGooglePlay) { upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage))
upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage)) upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
} else {
findPreference<PreferenceCategory>("dayThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImage)
findPreference<PreferenceCategory>("nightThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImageN)
}
upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString()) upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString())
findPreference<ColorPreference>(PreferKey.cBackground)?.let { findPreference<ColorPreference>(PreferKey.cBackground)?.let {
it.onSaveColor = { color -> it.onSaveColor = { color ->

@ -23,6 +23,7 @@ class FilePickerActivity :
private val selectDocTree = private val selectDocTree =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
it ?: return@registerForActivityResult
if (it.isContentScheme()) { if (it.isContentScheme()) {
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
it, it,
@ -33,6 +34,7 @@ class FilePickerActivity :
} }
private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) { private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
it ?: return@registerForActivityResult
onResult(Intent().setData(it)) onResult(Intent().setData(it))
} }

@ -7,13 +7,14 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text
import io.legado.app.model.webBook.PreciseSearch import io.legado.app.model.webBook.PreciseSearch
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -87,7 +88,9 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val text = str.trim() val text = str.trim()
when { when {
text.isAbsUrl() -> { text.isAbsUrl() -> {
RxHttp.get(text).toText().await().let { okHttpClient.newCall {
url(text)
}.text().let {
importBookshelf(it, groupId) importBookshelf(it, groupId)
} }
} }

@ -16,6 +16,7 @@ import io.legado.app.data.entities.RssArticle
import io.legado.app.databinding.FragmentRssArticlesBinding import io.legado.app.databinding.FragmentRssArticlesBinding
import io.legado.app.databinding.ViewLoadMoreBinding import io.legado.app.databinding.ViewLoadMoreBinding
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.ui.widget.recycler.LoadMoreView
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
@ -50,18 +51,18 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.init(arguments) viewModel.init(arguments)
initView() initView()
binding.refreshRecyclerView.startLoading()
initView() initView()
initData() initData()
} }
private fun initView() = with(binding) { private fun initView() = with(binding) {
ATH.applyEdgeEffectColor(refreshRecyclerView.recyclerView) refreshLayout.setColorSchemeColors(accentColor)
refreshRecyclerView.recyclerView.layoutManager = if (activityViewModel.isGridLayout) { ATH.applyEdgeEffectColor(recyclerView)
refreshRecyclerView.recyclerView.setPadding(8, 0, 8, 0) recyclerView.layoutManager = if (activityViewModel.isGridLayout) {
recyclerView.setPadding(8, 0, 8, 0)
GridLayoutManager(requireContext(), 2) GridLayoutManager(requireContext(), 2)
} else { } else {
refreshRecyclerView.recyclerView.addItemDecoration(VerticalDivider(requireContext())) recyclerView.addItemDecoration(VerticalDivider(requireContext()))
LinearLayoutManager(requireContext()) LinearLayoutManager(requireContext())
} }
@ -70,18 +71,15 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment) 2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment)
else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment) else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment)
} }
refreshRecyclerView.recyclerView.adapter = adapter recyclerView.adapter = adapter
loadMoreView = LoadMoreView(requireContext()) loadMoreView = LoadMoreView(requireContext())
adapter.addFooterView { adapter.addFooterView {
ViewLoadMoreBinding.bind(loadMoreView) ViewLoadMoreBinding.bind(loadMoreView)
} }
refreshRecyclerView.onRefreshStart = { refreshLayout.setOnRefreshListener {
activityViewModel.rssSource?.let { loadArticles()
viewModel.loadContent(it)
}
} }
refreshRecyclerView.recyclerView.addOnScrollListener(object : recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollVertically(1)) { if (!recyclerView.canScrollVertically(1)) {
@ -89,6 +87,10 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
} }
} }
}) })
refreshLayout.post {
refreshLayout.isRefreshing = true
loadArticles()
}
} }
private fun initData() { private fun initData() {
@ -101,6 +103,12 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
} }
} }
private fun loadArticles() {
activityViewModel.rssSource?.let {
viewModel.loadContent(it)
}
}
private fun scrollToBottom() { private fun scrollToBottom() {
if (viewModel.isLoading) return if (viewModel.isLoading) return
if (loadMoreView.hasMore && adapter.getActualItemCount() > 0) { if (loadMoreView.hasMore && adapter.getActualItemCount() > 0) {
@ -113,7 +121,7 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
override fun observeLiveBus() { override fun observeLiveBus() {
viewModel.loadFinally.observe(viewLifecycleOwner) { viewModel.loadFinally.observe(viewLifecycleOwner) {
binding.refreshRecyclerView.stopLoading() binding.refreshLayout.isRefreshing = false
if (it) { if (it) {
loadMoreView.startLoad() loadMoreView.startLoad()
} else { } else {

@ -28,7 +28,7 @@ class RssFavoritesActivity : BaseActivity<ActivityRssFavoritesBinding>(),
} }
private fun initView() { private fun initView() {
binding.refreshRecyclerView.recyclerView.let { binding.recyclerView.let {
it.layoutManager = LinearLayoutManager(this) it.layoutManager = LinearLayoutManager(this)
it.addItemDecoration(VerticalDivider(this)) it.addItemDecoration(VerticalDivider(this))
adapter = RssFavoritesAdapter(this, this) adapter = RssFavoritesAdapter(this, this)

@ -37,7 +37,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
override val viewModel: ReadRssViewModel override val viewModel: ReadRssViewModel
by viewModels() by viewModels()
private val savePathRequestCode = 132
private val imagePathKey = "" private val imagePathKey = ""
private var starMenuItem: MenuItem? = null private var starMenuItem: MenuItem? = null
private var ttsMenuItem: MenuItem? = null private var ttsMenuItem: MenuItem? = null

@ -16,6 +16,8 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.data.entities.RssStar import io.legado.app.data.entities.RssStar
import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.rss.Rss import io.legado.app.model.rss.Rss
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.DocumentUtils
@ -24,8 +26,6 @@ import io.legado.app.utils.isContentScheme
import io.legado.app.utils.writeBytes import io.legado.app.utils.writeBytes
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -147,7 +147,9 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
private suspend fun webData2bitmap(data: String): ByteArray? { private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) { return if (URLUtil.isValidUrl(data)) {
RxHttp.get(data).toByteArray().await() okHttpClient.newCall {
url(data)
}.bytes()
} else { } else {
Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT)
} }

@ -1,92 +0,0 @@
package io.legado.app.ui.widget.recycler
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.databinding.ViewRefreshRecyclerBinding
import io.legado.app.lib.theme.ATH
@SuppressLint("ClickableViewAccessibility")
class RefreshRecyclerView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
private var binding: ViewRefreshRecyclerBinding
private var durTouchX = -1000000f
private var durTouchY = -1000000f
var onRefreshStart: (() -> Unit)? = null
init {
orientation = VERTICAL
val view = LayoutInflater.from(context).inflate(R.layout.view_refresh_recycler, this, true)
binding = ViewRefreshRecyclerBinding.bind(view)
ATH.applyEdgeEffectColor(binding.recyclerView)
binding.recyclerView.setOnTouchListener(object : OnTouchListener {
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
durTouchX = event.x
durTouchY = event.y
}
MotionEvent.ACTION_MOVE -> {
if (durTouchX == -1000000f) {
durTouchX = event.x
}
if (durTouchY == -1000000f)
durTouchY = event.y
val dY = event.y - durTouchY //>0下拉
durTouchY = event.y
if (!binding.refreshProgressBar.isAutoLoading &&
binding.refreshProgressBar.getSecondDurProgress() == binding.refreshProgressBar.secondFinalProgress
) {
binding.recyclerView.adapter?.let {
if (it.itemCount > 0) {
if (0 == (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()) {
binding.refreshProgressBar.setSecondDurProgress((binding.refreshProgressBar.getSecondDurProgress() + dY / 2).toInt())
}
} else {
binding.refreshProgressBar.setSecondDurProgress((binding.refreshProgressBar.getSecondDurProgress() + dY / 2).toInt())
}
}
return binding.refreshProgressBar.getSecondDurProgress() > 0
}
}
MotionEvent.ACTION_UP -> {
if (!binding.refreshProgressBar.isAutoLoading &&
binding.refreshProgressBar.secondMaxProgress > 0 &&
binding.refreshProgressBar.getSecondDurProgress() > 0
) {
if (binding.refreshProgressBar.getSecondDurProgress() >= binding.refreshProgressBar.secondMaxProgress) {
binding.refreshProgressBar.isAutoLoading = true
onRefreshStart?.invoke()
} else {
binding.refreshProgressBar.setSecondDurProgressWithAnim(0)
}
}
durTouchX = -1000000f
durTouchY = -1000000f
}
}
return false
}
})
}
val recyclerView get() = binding.recyclerView
fun startLoading() {
binding.refreshProgressBar.isAutoLoading = true
onRefreshStart?.invoke()
}
fun stopLoading() {
binding.refreshProgressBar.isAutoLoading = false
}
}

@ -1,27 +0,0 @@
package io.legado.app.utils
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun Call.await(): Response = suspendCancellableCoroutine { block ->
block.invokeOnCancellation {
cancel()
}
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
block.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
block.resume(response)
}
})
}

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@ -13,13 +12,19 @@
app:title="@string/favorites" app:title="@string/favorites"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.recycler.RefreshRecyclerView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_recycler_view" android:id="@+id/refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar" app:layout_constraintTop_toBottomOf="@id/title_bar"
app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_bias="0.0">
tools:layout_editor_absoluteX="0dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -16,6 +16,7 @@
android:gravity="center_vertical"> android:gravity="center_vertical">
<TextView <TextView
android:id="@+id/tv_name_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/style_name" android:text="@string/style_name"
@ -39,6 +40,18 @@
app:tint="@color/secondaryText" app:tint="@color/secondaryText"
android:contentDescription="@string/edit" /> android:contentDescription="@string/edit" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_restore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore"
android:textColor="@color/primaryText" />
</LinearLayout> </LinearLayout>
<io.legado.app.lib.theme.view.ATESwitch <io.legado.app.lib.theme.view.ATESwitch

@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/refresh_layout"
android:id="@+id/content_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar">
<io.legado.app.ui.widget.recycler.RefreshRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/refresh_recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

@ -820,5 +820,7 @@
<string name="content_src">正文源码</string> <string name="content_src">正文源码</string>
<string name="list_src">列表源码</string> <string name="list_src">列表源码</string>
<string name="url_already">此url已订阅</string> <string name="url_already">此url已订阅</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用屏幕最高刷新率</string>
</resources> </resources>

@ -812,6 +812,8 @@
<string name="export_to_web_dav">匯出到WebDav</string> <string name="export_to_web_dav">匯出到WebDav</string>
<string name="reverse_content">反轉內容</string> <string name="reverse_content">反轉內容</string>
<string name="debug">除錯</string> <string name="debug">除錯</string>
<string name="crash_log">當機日誌</string>
<string name="use_zh_layout">使用自訂中文分行</string>
<string name="image_style">圖片樣式</string> <string name="image_style">圖片樣式</string>
<string name="system_tts">系統TTS</string> <string name="system_tts">系統TTS</string>
<string name="export_type">匯出格式</string> <string name="export_type">匯出格式</string>
@ -822,5 +824,7 @@
<string name="content_src">正文原始碼</string> <string name="content_src">正文原始碼</string>
<string name="list_src">列表原始碼</string> <string name="list_src">列表原始碼</string>
<string name="url_already">此url已訂閱</string> <string name="url_already">此url已訂閱</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用螢幕最高刷新率</string>
</resources> </resources>

@ -824,5 +824,7 @@
<string name="content_src">正文源码</string> <string name="content_src">正文源码</string>
<string name="list_src">列表源码</string> <string name="list_src">列表源码</string>
<string name="url_already">此url已订阅</string> <string name="url_already">此url已订阅</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用屏幕最高刷新率</string>
</resources> </resources>

@ -397,7 +397,6 @@
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
<string name="source_http_header">请求头(header)</string>
<string name="r_search_url">搜索地址(url)</string> <string name="r_search_url">搜索地址(url)</string>
<string name="r_find_url">发现地址规则(url)</string> <string name="r_find_url">发现地址规则(url)</string>
<string name="r_book_list">书籍列表规则(bookList)</string> <string name="r_book_list">书籍列表规则(bookList)</string>
@ -455,9 +454,10 @@
<string name="net_error_10003">Data parsing failed</string> <string name="net_error_10003">Data parsing failed</string>
<!--error string end--> <!--error string end-->
<string name="source_http_header">HTTP Header</string>
<string name="debug_source">Debug source</string> <string name="debug_source">Debug source</string>
<string name="import_by_qr_code">Import from QR code</string> <string name="import_by_qr_code">Import from QR code</string>
<string name="share_selected_source">Share selected sources</string>
<string name="scan_qr_code">Scan QR code</string> <string name="scan_qr_code">Scan QR code</string>
<string name="click_on_selected_show_menu">Tap to display Menu when selected </string> <string name="click_on_selected_show_menu">Tap to display Menu when selected </string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
@ -721,7 +721,6 @@
<string name="import_old_summary">Select a legacy backup folder</string> <string name="import_old_summary">Select a legacy backup folder</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="starting_download">Starting download</string> <string name="starting_download">Starting download</string>
<string name="already_in_download">This book is already in Download list</string> <string name="already_in_download">This book is already in Download list</string>
<string name="click_to_open">click to open</string> <string name="click_to_open">click to open</string>
@ -744,7 +743,6 @@
<string name="replace_enable_default_t">Enable replace rule by default</string> <string name="replace_enable_default_t">Enable replace rule by default</string>
<string name="replace_enable_default_s">For new added books</string> <string name="replace_enable_default_s">For new added books</string>
<string name="select_restore_file">Select restore file</string> <string name="select_restore_file">Select restore file</string>
<string name="select_theme">Switch default theme</string>
<string name="day_background_too_dark">Day background can not be too dark!</string> <string name="day_background_too_dark">Day background can not be too dark!</string>
<string name="day_bottom_bar_too_dark">Day bottom can not be too dark!</string> <string name="day_bottom_bar_too_dark">Day bottom can not be too dark!</string>
<string name="night_background_too_light">Night background can not be too bright!</string> <string name="night_background_too_light">Night background can not be too bright!</string>
@ -771,13 +769,13 @@
<string name="save_night_theme_summary">Save night theme config</string> <string name="save_night_theme_summary">Save night theme config</string>
<string name="theme_list">Theme list</string> <string name="theme_list">Theme list</string>
<string name="theme_list_summary">Save, Import, Share theme</string> <string name="theme_list_summary">Save, Import, Share theme</string>
<string name="share_selected_source">Share selected sources</string> <string name="select_theme">Switch default theme</string>
<string name="sort_by_lastUpdateTime">Sort by update time</string> <string name="sort_by_lastUpdateTime">Sort by update time</string>
<string name="search_content">Search content</string> <string name="search_content">Search content</string>
<string name="rss_source_empty">关注公众号[开源阅读]获取订阅源!</string> <string name="rss_source_empty">Follow [开源阅读] at WeChat for more RSS source!</string>
<string name="explore_empty">当前没有发现源,关注公众号[开源阅读]添加带发现的书源!</string> <string name="explore_empty">当前没有发现源,关注公众号[开源阅读]添加带发现的书源!</string>
<string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string> <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>
<string name="theme_name">主题名称</string> <string name="theme_name">Theme name</string>
<string name="auto_clear_expired">自动清除过期搜索数据</string> <string name="auto_clear_expired">自动清除过期搜索数据</string>
<string name="auto_clear_expired_summary">超过一天的搜索数据</string> <string name="auto_clear_expired_summary">超过一天的搜索数据</string>
<string name="re_segment">重新分段</string> <string name="re_segment">重新分段</string>
@ -786,13 +784,13 @@
<string name="scan_folder">智能扫描</string> <string name="scan_folder">智能扫描</string>
<string name="copy_book_url">拷贝书籍URL</string> <string name="copy_book_url">拷贝书籍URL</string>
<string name="copy_toc_url">拷贝目录URL</string> <string name="copy_toc_url">拷贝目录URL</string>
<string name="no_book">没有书籍</string> <string name="no_book">NO BOOK</string>
<string name="keep_original_name">保留原名</string> <string name="keep_original_name">Keep original name</string>
<string name="click_regional_config">点击区域设置</string> <string name="click_regional_config">点击区域设置</string>
<string name="colse">关闭</string> <string name="colse">Close</string>
<string name="next_page">下一页</string> <string name="next_page">Next Page</string>
<string name="prev_page">上一页</string> <string name="prev_page">Prev Page</string>
<string name="non_action">无操作</string> <string name="non_action">No Action</string>
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">footer <![CDATA[&]]> header</string> <string name="header_footer">footer <![CDATA[&]]> header</string>
@ -827,5 +825,7 @@
<string name="content_src">正文源码</string> <string name="content_src">正文源码</string>
<string name="list_src">列表源码</string> <string name="list_src">列表源码</string>
<string name="url_already">此url已订阅</string> <string name="url_already">此url已订阅</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用屏幕最高刷新率</string>
</resources> </resources>

@ -55,6 +55,12 @@
android:key="userAgent" android:key="userAgent"
android:title="UserAgent" /> android:title="UserAgent" />
<io.legado.app.ui.widget.prefs.SwitchPreference
android:key="highBrush"
android:defaultValue="false"
android:title="@string/high_brush_title"
android:summary="@string/high_brush_summary" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="preDownload" android:key="preDownload"

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.4.32' ext.kotlin_version = '1.5.0'
repositories { repositories {
google() google()
maven { url "https://maven.aliyun.com/nexus/content/groups/public/" } maven { url "https://maven.aliyun.com/nexus/content/groups/public/" }
@ -10,8 +10,8 @@ buildscript {
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.android.tools.build:gradle:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0"
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.3" classpath "de.timfreiheit.resourceplaceholders:placeholders:0.3"
} }
} }

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip

Loading…
Cancel
Save