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. 160
      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"
//room
def room_version = '2.2.6'
def room_version = '2.3.0'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
@ -178,10 +178,7 @@ dependencies {
implementation 'com.github.gedoor:rhino-android:1.5'
//
//noinspection GradleDependency
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
implementation 'com.github.bumptech.glide:glide:4.11.0'
@ -191,7 +188,7 @@ dependencies {
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'

@ -1,147 +1,237 @@
[
{
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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}"
},
{
"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,
"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,
"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,
"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": "度小鹿②",
"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": "度小鹿③",
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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}"
"id": -89,
"name": "温柔女声",
"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,
"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}"
"id": -88,
"name": "甜美女声①",
"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,
"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}"
"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=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}"
},
{
"id": -25,
"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}"
"name": "甜美女声③",
"url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=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,
"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}"
},
{
"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,
"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,
"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,
"name": "阿里云语音",
"url": "<js>/*播音人Aiting可改其他https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/speaker.json,详见https://ai.aliyun.com/nls/tts*/;eval(''+java.ajax('https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/ttsDemo.js'));ttsDemo(speakText,speakSpeed,'Aiting')</js>"
}
]
]

@ -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",
"bgStrNight": "#000000",
"textColor": "#000000",
@ -10,6 +61,7 @@
"darkStatusIconNight": false
},
{
"name": "预设2",
"bgStr": "#DDC090",
"bgStrNight": "#3C3F43",
"textColor": "#3E3422",
@ -20,6 +72,7 @@
"darkStatusIconNight": false
},
{
"name": "预设3",
"bgStr": "#C2D8AA",
"bgStrNight": "#3C3F43",
"textColor": "#596C44",
@ -30,6 +83,7 @@
"darkStatusIconNight": false
},
{
"name": "预设4",
"bgStr": "#DBB8E2",
"bgStrNight": "#3C3F43",
"textColor": "#68516C",
@ -40,6 +94,7 @@
"darkStatusIconNight": false
},
{
"name": "预设5",
"bgStr": "#ABCEE0",
"bgStrNight": "#3C3F43",
"textColor": "#3D4C54",

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

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

@ -14,6 +14,7 @@ import androidx.viewbinding.ViewBinding
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.help.AppConfig
import io.legado.app.help.ThemeConfig
@ -66,6 +67,26 @@ abstract class BaseActivity<VB : ViewBinding>(
}
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()
initTheme()
super.onCreate(savedInstanceState)
@ -152,11 +173,9 @@ abstract class BaseActivity<VB : ViewBinding>(
ATH.applyBackgroundTint(window.decorView)
}
}
if (AppConfig.isGooglePlay) {
ThemeConfig.getBgImage(this)?.let {
kotlin.runCatching {
window.decorView.background = it
}
ThemeConfig.getBgImage(this)?.let {
kotlin.runCatching {
window.decorView.background = it
}
}
}

@ -73,6 +73,7 @@ object PreferKey {
const val exportUseReplace = "exportUseReplace"
const val useZhLayout = "useZhLayout"
const val fullScreenGesturesSupport = "fullScreenGesturesSupport"
const val highBrush = "highBrush"
const val cPrimary = "colorPrimary"
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.data.dao.*
import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx
import java.util.*
@ -88,6 +89,13 @@ abstract class AppDatabase : RoomDatabase() {
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
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 getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro

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

@ -11,82 +11,28 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@Suppress("unused")
object HttpHelper {
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()
}
private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
ConcurrentHashMap()
}
/**
* 缓存代理okHttp
*/
fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) {
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
}
val okHttpClient: OkHttpClient by lazy {
val specs = arrayListOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS,
ConnectionSpec.CLEARTEXT
)
private fun getHeaderInterceptor(): Interceptor {
return Interceptor { chain ->
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(Interceptor { chain ->
val request = chain.request()
.newBuilder()
.addHeader("Keep-Alive", "300")
@ -94,28 +40,72 @@ object HttpHelper {
.addHeader("Cache-Control", "no-cache")
.build()
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 =
suspendCancellableCoroutine { block ->
val webView = AjaxWebView()
block.invokeOnCancellation {
webView.destroyWebView()
}
webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) {
suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse =
suspendCancellableCoroutine { block ->
val webView = AjaxWebView()
block.invokeOnCancellation {
webView.destroyWebView()
}
webView.callback = object : AjaxWebView.Callback() {
override fun onResult(response: StrResponse) {
if (!block.isCompleted)
block.resume(response)
}
if (!block.isCompleted)
block.resume(response)
}
override fun onError(error: Throwable) {
if (!block.isCompleted)
block.cancel(error)
}
override fun onError(error: Throwable) {
if (!block.isCompleted)
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
}
fun requestCode(requestCode: Int): Builder {
request.setRequestCode(requestCode)
return this
}
fun onGranted(callback: () -> Unit): Builder {
request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {
override fun onPermissionsGranted() {

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

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

@ -16,9 +16,6 @@ import io.legado.app.help.CacheManager
import io.legado.app.help.JsExtensions
import io.legado.app.help.http.*
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.util.*
import java.util.regex.Pattern
@ -58,6 +55,7 @@ class AnalyzeUrl(
private var charset: String? = null
private var method = RequestMethod.GET
private var proxy: String? = null
private var retry: Int = 0
init {
baseUrl = baseUrl.split(splitUrlRegex, 1)[0]
@ -200,6 +198,7 @@ class AnalyzeUrl(
option.js?.let {
evalJS(it)
}
retry = option.retry
}
}
headerMap[UA_NAME] ?: let {
@ -305,62 +304,43 @@ class AnalyzeUrl(
params.sourceRegex = sourceRegex
params.postData = body?.toByteArray()
params.tag = tag
return HttpHelper.ajax(params)
return getWebViewSrc(params)
}
return when (method) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} else {
RxHttp.postJson(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toStrResponse().await()
return getProxyClient(proxy).newCallStrResponse(retry) {
removeHeader(UA_NAME)
addHeaders(headerMap)
when (method) {
RequestMethod.POST -> {
url(url)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
} else {
postJson(body)
}
}
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 {
setCookie(tag)
return when (method) {
RequestMethod.POST -> {
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
RxHttp.postForm(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
} else {
RxHttp.postJson(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body)
.addAllHeader(headerMap)
.toByteArray().await()
@Suppress("BlockingMethodInNonBlockingContext")
return getProxyClient(proxy).newCall(retry) {
removeHeader(UA_NAME)
addHeaders(headerMap)
when (method) {
RequestMethod.POST -> {
url(url)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
} else {
postJson(body)
}
}
else -> get(url, fieldMap, true)
}
else -> RxHttp.get(url)
.setAssemblyEnabled(false)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toByteArray().await()
}
}.bytes()
}
private fun setCookie(tag: String?) {
@ -393,7 +373,8 @@ class AnalyzeUrl(
val headers: Any?,
val body: Any?,
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.NetworkUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import splitties.init.appCtx
@ -45,11 +48,11 @@ object BookContent {
val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl)
analyzeRule.setRedirectUrl(baseUrl)
analyzeRule.nextChapterUrl = mNextChapterUrl
scope.ensureActive()
var contentData = analyzeContent(
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
)
content.append(contentData.content).append("\n")
if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0]
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
@ -58,6 +61,7 @@ object BookContent {
== NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl)
) break
nextUrlList.add(nextUrl)
scope.ensureActive()
val res = AnalyzeUrl(
ruleUrl = nextUrl,
book = book,
@ -75,29 +79,27 @@ object BookContent {
}
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) {
val contentDataList = arrayListOf<ContentData<String>>()
for (item in contentData.nextUrl) {
if (!nextUrlList.contains(item))
contentDataList.add(ContentData(nextUrl = item))
}
for (item in contentDataList) {
withContext(scope.coroutineContext) {
val res = AnalyzeUrl(
ruleUrl = item.nextUrl,
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
Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${contentData.nextUrl.size}")
withContext(IO) {
val asyncArray = Array(contentData.nextUrl.size) {
async(IO) {
val urlStr = contentData.nextUrl[it]
val analyzeUrl = AnalyzeUrl(
ruleUrl = urlStr,
book = book,
headerMapF = bookSource.getHeaderMap()
)
item.content = contentData.content
val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
analyzeContent(
book, urlStr, res.url, res.body!!, contentRule,
bookChapter, bookSource, mNextChapterUrl, false
).content
}
}
}
for (item in contentDataList) {
content.append(item.content).append("\n")
asyncArray.forEach { coroutine ->
scope.ensureActive()
content.append(coroutine.await()).append("\n")
}
}
}
content.deleteCharAt(content.length - 1)

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

@ -1,14 +1,10 @@
package io.legado.app.ui.association
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Theme
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
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.help.AppConfig
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.Restore
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
var groupName: String? = null
@ -111,7 +112,9 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
}
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("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)

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

@ -9,10 +9,11 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource
import io.legado.app.help.AppConfig
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.utils.*
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var groupName: String? = null
@ -106,7 +107,9 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
}
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("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)

@ -80,6 +80,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
}
private val sourceEditActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
if (it.resultCode == RESULT_OK) {
viewModel.upBookSource {
upView()
@ -88,12 +89,14 @@ class ReadBookActivity : ReadBookBaseActivity(),
}
private val replaceActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
if (it.resultCode == RESULT_OK) {
viewModel.replaceRuleChanged()
}
}
private val searchContentActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult
it.data?.let { data ->
data.getIntExtra("index", ReadBook.durChapterIndex).let { index ->
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.DialogReadBgTextBinding
import io.legado.app.databinding.ItemBgImageBinding
import io.legado.app.help.DefaultData
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.selector
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
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.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import java.io.File
class BgTextConfigDialog : BaseDialogFragment() {
@ -96,25 +98,23 @@ class BgTextConfigDialog : BaseDialogFragment() {
(activity as ReadBookActivity).bottomDialog--
}
private fun initView() {
private fun initView() = with(binding) {
val bg = requireContext().bottomBackground
val isLight = ColorUtils.isColorLight(bg)
primaryTextColor = requireContext().getPrimaryTextColor(isLight)
secondaryTextColor = requireContext().getSecondaryTextColor(isLight)
binding.rootView.setBackgroundColor(bg)
binding.swDarkStatusIcon.setTextColor(primaryTextColor)
binding.ivImport.setColorFilter(primaryTextColor)
binding.ivExport.setColorFilter(primaryTextColor)
binding.ivDelete.setColorFilter(primaryTextColor)
binding.tvBgImage.setTextColor(primaryTextColor)
}
@SuppressLint("InflateParams")
private fun initData() = with(ReadBookConfig.durConfig) {
binding.tvName.text = name.ifBlank { "文字" }
binding.swDarkStatusIcon.isChecked = curStatusIconDark()
rootView.setBackgroundColor(bg)
tvNameTitle.setTextColor(primaryTextColor)
tvName.setTextColor(secondaryTextColor)
ivEdit.setColorFilter(secondaryTextColor)
tvRestore.setTextColor(primaryTextColor)
swDarkStatusIcon.setTextColor(primaryTextColor)
ivImport.setColorFilter(primaryTextColor)
ivExport.setColorFilter(primaryTextColor)
ivDelete.setColorFilter(primaryTextColor)
tvBgImage.setTextColor(primaryTextColor)
adapter = BgAdapter(requireContext(), secondaryTextColor)
binding.recyclerView.adapter = adapter
recyclerView.adapter = adapter
adapter.addHeaderView {
ItemBgImageBinding.inflate(layoutInflater, it, false).apply {
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")
private fun initEvent() = with(ReadBookConfig.durConfig) {
binding.ivEdit.setOnClickListener {
@ -148,6 +154,17 @@ class BgTextConfigDialog : BaseDialogFragment() {
cancelButton()
}.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 ->
setCurStatusIconDark(isChecked)
(activity as? ReadBookActivity)?.upSystemUiVisibility()
@ -291,7 +308,9 @@ class BgTextConfigDialog : BaseDialogFragment() {
private fun importNetConfig(url: String) {
execute {
RxHttp.get(url).toByteArray().await().let {
okHttpClient.newCall {
url(url)
}.bytes().let {
importConfig(it)
}
}.onError {

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

@ -5,10 +5,11 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.TxtTocRule
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.fromJsonArray
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class TocRegexViewModel(application: Application) : BaseViewModel(application) {
@ -29,7 +30,9 @@ class TocRegexViewModel(application: Application) : BaseViewModel(application) {
fun importOnLine(url: String, finally: (msg: String) -> Unit) {
execute {
RxHttp.get(url).toText("utf-8").await().let { json ->
okHttpClient.newCall {
url(url)
}.text("utf-8").let { json ->
GSON.fromJsonArray<TxtTocRule>(json)?.let {
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.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.prefs.ColorPreference
import io.legado.app.ui.widget.prefs.PreferenceCategory
import io.legado.app.utils.*
import java.io.File
@ -55,15 +54,8 @@ class ThemeConfigFragment : BasePreferenceFragment(),
if (Build.VERSION.SDK_INT < 26) {
preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon)
}
if (AppConfig.isGooglePlay) {
upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage))
upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
} else {
findPreference<PreferenceCategory>("dayThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImage)
findPreference<PreferenceCategory>("nightThemeCategory")
?.removePreferenceRecursively(PreferKey.bgImageN)
}
upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage))
upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString())
findPreference<ColorPreference>(PreferKey.cBackground)?.let {
it.onSaveColor = { color ->

@ -23,6 +23,7 @@ class FilePickerActivity :
private val selectDocTree =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
it ?: return@registerForActivityResult
if (it.isContentScheme()) {
contentResolver.takePersistableUriPermission(
it,
@ -33,6 +34,7 @@ class FilePickerActivity :
}
private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
it ?: return@registerForActivityResult
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.BookGroup
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.WebBook
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.isActive
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -87,7 +88,9 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val text = str.trim()
when {
text.isAbsUrl() -> {
RxHttp.get(text).toText().await().let {
okHttpClient.newCall {
url(text)
}.text().let {
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.ViewLoadMoreBinding
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.widget.recycler.LoadMoreView
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?) {
viewModel.init(arguments)
initView()
binding.refreshRecyclerView.startLoading()
initView()
initData()
}
private fun initView() = with(binding) {
ATH.applyEdgeEffectColor(refreshRecyclerView.recyclerView)
refreshRecyclerView.recyclerView.layoutManager = if (activityViewModel.isGridLayout) {
refreshRecyclerView.recyclerView.setPadding(8, 0, 8, 0)
refreshLayout.setColorSchemeColors(accentColor)
ATH.applyEdgeEffectColor(recyclerView)
recyclerView.layoutManager = if (activityViewModel.isGridLayout) {
recyclerView.setPadding(8, 0, 8, 0)
GridLayoutManager(requireContext(), 2)
} else {
refreshRecyclerView.recyclerView.addItemDecoration(VerticalDivider(requireContext()))
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
LinearLayoutManager(requireContext())
}
@ -70,18 +71,15 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment)
else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment)
}
refreshRecyclerView.recyclerView.adapter = adapter
recyclerView.adapter = adapter
loadMoreView = LoadMoreView(requireContext())
adapter.addFooterView {
ViewLoadMoreBinding.bind(loadMoreView)
}
refreshRecyclerView.onRefreshStart = {
activityViewModel.rssSource?.let {
viewModel.loadContent(it)
}
refreshLayout.setOnRefreshListener {
loadArticles()
}
refreshRecyclerView.recyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollVertically(1)) {
@ -89,6 +87,10 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
}
}
})
refreshLayout.post {
refreshLayout.isRefreshing = true
loadArticles()
}
}
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() {
if (viewModel.isLoading) return
if (loadMoreView.hasMore && adapter.getActualItemCount() > 0) {
@ -113,7 +121,7 @@ class RssArticlesFragment : VMBaseFragment<RssArticlesViewModel>(R.layout.fragme
override fun observeLiveBus() {
viewModel.loadFinally.observe(viewLifecycleOwner) {
binding.refreshRecyclerView.stopLoading()
binding.refreshLayout.isRefreshing = false
if (it) {
loadMoreView.startLoad()
} else {

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

@ -37,7 +37,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
override val viewModel: ReadRssViewModel
by viewModels()
private val savePathRequestCode = 132
private val imagePathKey = ""
private var starMenuItem: 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.RssSource
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.rss.Rss
import io.legado.app.utils.DocumentUtils
@ -24,8 +26,6 @@ import io.legado.app.utils.isContentScheme
import io.legado.app.utils.writeBytes
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import java.io.File
import java.util.*
@ -147,7 +147,9 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) {
RxHttp.get(data).toByteArray().await()
okHttpClient.newCall {
url(data)
}.bytes()
} else {
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"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -13,13 +12,19 @@
app:title="@string/favorites"
app:layout_constraintTop_toTopOf="parent" />
<io.legado.app.ui.widget.recycler.RefreshRecyclerView
android:id="@+id/refresh_recycler_view"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar"
app:layout_constraintVertical_bias="0.0"
tools:layout_editor_absoluteX="0dp" />
app:layout_constraintVertical_bias="0.0">
<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>

@ -16,6 +16,7 @@
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_name_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/style_name"
@ -39,6 +40,18 @@
app:tint="@color/secondaryText"
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>
<io.legado.app.lib.theme.view.ATESwitch

@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content_view"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar">
android:layout_height="match_parent">
<io.legado.app.ui.widget.recycler.RefreshRecyclerView
android:id="@+id/refresh_recycler_view"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="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="list_src">列表源码</string>
<string name="url_already">此url已订阅</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用屏幕最高刷新率</string>
</resources>

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

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

@ -397,7 +397,6 @@
<string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
<string name="source_http_header">请求头(header)</string>
<string name="r_search_url">搜索地址(url)</string>
<string name="r_find_url">发现地址规则(url)</string>
<string name="r_book_list">书籍列表规则(bookList)</string>
@ -455,9 +454,10 @@
<string name="net_error_10003">Data parsing failed</string>
<!--error string end-->
<string name="source_http_header">HTTP Header</string>
<string name="debug_source">Debug source</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="click_on_selected_show_menu">Tap to display Menu when selected </string>
<string name="theme">Theme</string>
@ -721,7 +721,6 @@
<string name="import_old_summary">Select a legacy backup folder</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="starting_download">Starting download</string>
<string name="already_in_download">This book is already in Download list</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_s">For new added books</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_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>
@ -771,13 +769,13 @@
<string name="save_night_theme_summary">Save night theme config</string>
<string name="theme_list">Theme list</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="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="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_summary">超过一天的搜索数据</string>
<string name="re_segment">重新分段</string>
@ -786,13 +784,13 @@
<string name="scan_folder">智能扫描</string>
<string name="copy_book_url">拷贝书籍URL</string>
<string name="copy_toc_url">拷贝目录URL</string>
<string name="no_book">没有书籍</string>
<string name="keep_original_name">保留原名</string>
<string name="no_book">NO BOOK</string>
<string name="keep_original_name">Keep original name</string>
<string name="click_regional_config">点击区域设置</string>
<string name="colse">关闭</string>
<string name="next_page">下一页</string>
<string name="prev_page">上一页</string>
<string name="non_action">无操作</string>
<string name="colse">Close</string>
<string name="next_page">Next Page</string>
<string name="prev_page">Prev Page</string>
<string name="non_action">No Action</string>
<string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string>
<string name="header_footer">footer <![CDATA[&]]> header</string>
@ -827,5 +825,7 @@
<string name="content_src">正文源码</string>
<string name="list_src">列表源码</string>
<string name="url_already">此url已订阅</string>
<string name="high_brush_title">高刷</string>
<string name="high_brush_summary">使用屏幕最高刷新率</string>
</resources>

@ -55,6 +55,12 @@
android:key="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
android:defaultValue="true"
android:key="preDownload"

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

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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