pull/737/head
Robot 4 years ago
commit f4751e6ba9
  1. 6
      app/build.gradle
  2. 4
      app/src/main/assets/updateLog.md
  3. 2
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  4. 12
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  5. 12
      app/src/main/java/io/legado/app/help/http/AjaxWebView.kt
  6. 20
      app/src/main/java/io/legado/app/help/http/ByteConverter.kt
  7. 2
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  8. 40
      app/src/main/java/io/legado/app/help/http/EncodeConverter.kt
  9. 31
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  10. 6
      app/src/main/java/io/legado/app/help/http/Res.kt
  11. 124
      app/src/main/java/io/legado/app/help/http/StrResponse.kt
  12. 67
      app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt
  13. 60
      app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt
  14. 2
      app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt
  15. 2
      app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt
  16. 39
      app/src/main/java/io/legado/app/help/http/parser/StrResponseParser.kt
  17. 2
      app/src/main/java/io/legado/app/help/http/parser/TextParser.kt
  18. 15
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  19. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  20. 137
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  21. 4
      app/src/main/java/io/legado/app/model/rss/Rss.kt
  22. 2
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  23. 4
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  24. 4
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  25. 10
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  26. 5
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  27. 2
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt
  28. 5
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  29. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  30. 7
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  31. 7
      app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt
  32. 13
      app/src/main/java/io/legado/app/utils/NetworkUtils.kt
  33. 5
      app/src/main/res/layout/activity_explore_show.xml
  34. 1
      app/src/main/res/layout/view_load_more.xml

@ -172,10 +172,10 @@ dependencies {
implementation 'com.github.gedoor:rhino-android:1.4' implementation 'com.github.gedoor:rhino-android:1.4'
// //
implementation 'com.ljx.rxhttp:rxhttp:2.4.4' implementation 'com.ljx.rxhttp:rxhttp:2.4.4-beta3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
kapt 'com.ljx.rxhttp:rxhttp-compiler:2.4.4' kapt 'com.ljx.rxhttp:rxhttp-compiler:2.4.4-beta3'
//Glide //Glide
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'

@ -3,6 +3,10 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/12/11**
* 修复bug
* 网络访问框架修改为RxHttp, 有bug及时反馈
**2020/12/11** **2020/12/11**
* 修复因修改进度同步导致的bug * 修复因修改进度同步导致的bug

@ -63,7 +63,7 @@ data class BookChapter(
fun getAbsoluteURL(): String { fun getAbsoluteURL(): String {
val urlArray = url.split(AnalyzeUrl.splitUrlRegex) val urlArray = url.split(AnalyzeUrl.splitUrlRegex)
var absoluteUrl = NetworkUtils.getAbsoluteURL(baseUrl, urlArray[0])!! var absoluteUrl = NetworkUtils.getAbsoluteURL(baseUrl, urlArray[0])
if (urlArray.size > 1) { if (urlArray.size > 1) {
absoluteUrl = "$absoluteUrl,${urlArray[1]}" absoluteUrl = "$absoluteUrl,${urlArray[1]}"
} }

@ -31,9 +31,9 @@ interface JsExtensions {
fun ajax(urlStr: String): String? { fun ajax(urlStr: String): String? {
return try { return try {
val analyzeUrl = AnalyzeUrl(urlStr) val analyzeUrl = AnalyzeUrl(urlStr)
val call = analyzeUrl.getResponse(urlStr) runBlocking {
val response = call.execute() analyzeUrl.getStrResponse(urlStr).body
response.body() }
} catch (e: Exception) { } catch (e: Exception) {
e.msg e.msg
} }
@ -45,9 +45,9 @@ interface JsExtensions {
fun connect(urlStr: String): Any { fun connect(urlStr: String): Any {
return try { return try {
val analyzeUrl = AnalyzeUrl(urlStr) val analyzeUrl = AnalyzeUrl(urlStr)
val call = analyzeUrl.getResponse(urlStr) runBlocking {
val response = call.execute() analyzeUrl.getStrResponse(urlStr)
response }
} catch (e: Exception) { } catch (e: Exception) {
e.msg e.msg
} }

@ -39,7 +39,7 @@ class AjaxWebView {
mWebView = createAjaxWebView(params, this) mWebView = createAjaxWebView(params, this)
} }
MSG_SUCCESS -> { MSG_SUCCESS -> {
ajaxWebView.callback?.onResult(msg.obj as Res) ajaxWebView.callback?.onResult(msg.obj as StrResponse)
destroyWebView() destroyWebView()
} }
MSG_ERROR -> { MSG_ERROR -> {
@ -160,7 +160,7 @@ class AjaxWebView {
if (it.isNotEmpty() && it != "null") { if (it.isNotEmpty() && it != "null") {
val content = StringEscapeUtils.unescapeJson(it) val content = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "") .replace("^\"|\"$".toRegex(), "")
handler.obtainMessage(MSG_SUCCESS, Res(url, content)) handler.obtainMessage(MSG_SUCCESS, StrResponse.success(content, url))
.sendToTarget() .sendToTarget()
handler.removeCallbacks(this) handler.removeCallbacks(this)
return@evaluateJavascript return@evaluateJavascript
@ -186,8 +186,10 @@ class AjaxWebView {
override fun onLoadResource(view: WebView, url: String) { override fun onLoadResource(view: WebView, url: String) {
params.sourceRegex?.let { params.sourceRegex?.let {
if (url.matches(it.toRegex())) { if (url.matches(it.toRegex())) {
handler.obtainMessage(MSG_SUCCESS, Res(view.url ?: params.url, url)) handler.obtainMessage(
.sendToTarget() MSG_SUCCESS,
StrResponse.success(url, view.url ?: params.url)
).sendToTarget()
} }
} }
} }
@ -226,7 +228,7 @@ class AjaxWebView {
} }
abstract class Callback { abstract class Callback {
abstract fun onResult(response: Res) abstract fun onResult(response: StrResponse)
abstract fun onError(error: Throwable) abstract fun onError(error: Throwable)
} }
} }

@ -1,20 +0,0 @@
package io.legado.app.help.http
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
class ByteConverter : Converter.Factory() {
override fun responseBodyConverter(
type: Type?,
annotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<ResponseBody, ByteArray>? {
return Converter { value ->
value.bytes()
}
}
}

@ -108,6 +108,6 @@ object CookieStore : CookiePersistor, CookieManager {
} }
private fun createCookieKey(cookie: okhttp3.Cookie): String { private fun createCookieKey(cookie: okhttp3.Cookie): String {
return (if (cookie.secure()) "https" else "http") + "://" + cookie.domain() + cookie.path() + "|" + cookie.name() return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
} }
} }

@ -1,40 +0,0 @@
package io.legado.app.help.http
import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
import java.nio.charset.Charset
class EncodeConverter(private val encode: String? = null) : Converter.Factory() {
override fun responseBodyConverter(
type: Type?,
annotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<ResponseBody, String>? {
return Converter { value ->
val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes())
var charsetName: String? = encode
charsetName?.let {
try {
return@Converter String(responseBytes, Charset.forName(charsetName))
} catch (e: Exception) {
}
}
//根据http头判断
value.contentType()?.charset()?.let {
return@Converter String(responseBytes, it)
}
//根据内容判断
charsetName = EncodingDetect.getHtmlEncode(responseBytes)
String(responseBytes, Charset.forName(charsetName))
}
}
}

@ -1,8 +1,8 @@
package io.legado.app.help.http package io.legado.app.help.http
import io.legado.app.utils.msg
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.*
import retrofit2.Retrofit
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
@ -55,26 +55,6 @@ object HttpHelper {
}) })
} }
inline fun <reified T> getApiService(
baseUrl: String,
encode: String? = null,
proxy: String? = null
): T {
return getRetrofit(baseUrl, encode, proxy).create(T::class.java)
}
fun getRetrofit(
baseUrl: String,
encode: String? = null,
proxy: String? = null
): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
//增加返回值为字符串的支持(以实体类返回)
.addConverterFactory(EncodeConverter(encode))
.client(getProxyClient(proxy))
.build()
}
fun getProxyClient(proxy: String? = null): OkHttpClient { fun getProxyClient(proxy: String? = null): OkHttpClient {
if (proxy.isNullOrBlank()) { if (proxy.isNullOrBlank()) {
return client return client
@ -108,7 +88,7 @@ object HttpHelper {
if (username != "" && password != "") { if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password) val credential: String = Credentials.basic(username, password)
response.request().newBuilder() response.request.newBuilder()
.header("Proxy-Authorization", credential) .header("Proxy-Authorization", credential)
.build() .build()
} }
@ -130,21 +110,22 @@ object HttpHelper {
} }
} }
suspend fun ajax(params: AjaxWebView.AjaxParams): Res = suspend fun ajax(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: Res) { 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.resume(Res(params.url, error.localizedMessage)) block.resume(StrResponse.success(error.msg, params.url))
} }
} }
webView.load(params) webView.load(params)

@ -1,6 +0,0 @@
package io.legado.app.help.http
data class Res(
val url: String,
val body: String?,
)

@ -0,0 +1,124 @@
package io.legado.app.help.http
import okhttp3.*
import okhttp3.Response.Builder
import java.util.*
/**
* An HTTP response.
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
class StrResponse private constructor(
val rawResponse: Response,
val body: String?,
val errorBody: ResponseBody?
) {
val raw get() = rawResponse
val url: String
get() {
raw.networkResponse?.let {
return it.request.url.toString()
}
return raw.request.url.toString()
}
fun code(): Int {
return rawResponse.code
}
fun message(): String {
return rawResponse.message
}
fun headers(): Headers {
return rawResponse.headers
}
val isSuccessful: Boolean
get() = rawResponse.isSuccessful
fun errorBody(): ResponseBody? {
return errorBody
}
override fun toString(): String {
return rawResponse.toString()
}
companion object {
fun success(code: Int, body: String): StrResponse {
require(!(code < 200 || code >= 300)) { "code < 200 or >= 300: $code" }
return success(
body,
Builder() //
.code(code)
.message("Response.success()")
.protocol(Protocol.HTTP_1_1)
.request(Request.Builder().url("http://localhost/").build())
.build()
)
}
fun success(body: String, headers: Headers): StrResponse {
return success(
body,
Builder() //
.code(200)
.message("OK")
.protocol(Protocol.HTTP_1_1)
.headers(headers)
.request(Request.Builder().url("http://localhost/").build())
.build()
)
}
fun success(body: String, url: String): StrResponse {
return success(
body,
Builder() //
.code(200)
.message("OK")
.protocol(Protocol.HTTP_1_1)
.request(Request.Builder().url(url).build())
.build()
)
}
@JvmOverloads
fun success(
body: String?, rawResponse: Response =
Builder() //
.code(200)
.message("OK")
.protocol(Protocol.HTTP_1_1)
.request(Request.Builder().url("http://localhost/").build())
.build()
): StrResponse {
Objects.requireNonNull(rawResponse, "rawResponse == null")
require(rawResponse.isSuccessful) { "rawResponse must be successful response" }
return StrResponse(rawResponse, body, null)
}
fun error(code: Int, body: ResponseBody?): StrResponse {
Objects.requireNonNull(body, "body == null")
require(code >= 400) { "code < 400: $code" }
return error(
body,
Builder() //
.code(code)
.message("Response.error()")
.protocol(Protocol.HTTP_1_1)
.request(Request.Builder().url("http://localhost/").build())
.build()
)
}
fun error(body: ResponseBody?, rawResponse: Response): StrResponse {
Objects.requireNonNull(body, "body == null")
Objects.requireNonNull(rawResponse, "rawResponse == null")
require(!rawResponse.isSuccessful) { "rawResponse should not be successful response" }
return StrResponse(rawResponse, null, body)
}
}
}

@ -1,67 +0,0 @@
package io.legado.app.help.http.api
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.QueryMap
import retrofit2.http.Url
/**
* Created by GKF on 2018/1/21.
* get web content
*/
@Suppress("unused")
interface HttpGetApi {
@GET
suspend fun getAsync(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Response<String>
@GET
suspend fun getMapAsync(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<String>
@GET
fun get(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
fun getByte(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET
fun getMap(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
fun getMapByte(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET
suspend fun getByteAsync(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
@GET
suspend fun getMapByteAsync(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
}

@ -1,60 +0,0 @@
package io.legado.app.help.http.api
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.*
/**
* Created by GKF on 2018/1/29.
* post
*/
@Suppress("unused")
interface HttpPostApi {
@FormUrlEncoded
@POST
suspend fun postMapAsync(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<String>
@POST
suspend fun postBodyAsync(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Response<String>
@FormUrlEncoded
@POST
fun postMap(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<String>
@POST
fun postBody(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Call<String>
@FormUrlEncoded
@POST
suspend fun postMapByteAsync(
@Url url: String,
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
@POST
suspend fun postBodyByteAsync(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
}

@ -7,7 +7,7 @@ import rxhttp.wrapper.annotation.Parser
class ByteParser : rxhttp.wrapper.parse.Parser<ByteArray> { class ByteParser : rxhttp.wrapper.parse.Parser<ByteArray> {
override fun onParse(response: Response): ByteArray { override fun onParse(response: Response): ByteArray {
return response.body()!!.bytes() return response.body!!.bytes()
} }
} }

@ -8,7 +8,7 @@ import java.io.InputStream
class InputStreamParser : rxhttp.wrapper.parse.Parser<InputStream> { class InputStreamParser : rxhttp.wrapper.parse.Parser<InputStream> {
override fun onParse(response: Response): InputStream { override fun onParse(response: Response): InputStream {
return response.body()!!.byteStream() return response.body!!.byteStream()
} }
} }

@ -0,0 +1,39 @@
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(val encode: String? = null) : rxhttp.wrapper.parse.Parser<StrResponse> {
override fun onParse(response: Response): StrResponse {
val body = getString(response)
return StrResponse.success(body, response)
}
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))
}
}

@ -12,7 +12,7 @@ class TextParser(val encode: String? = null) : rxhttp.wrapper.parse.Parser<Strin
override fun onParse(response: Response): String { override fun onParse(response: Response): String {
val responseBody = response.body() ?: throw HttpStatusCodeException(response, "内容为空") val responseBody = response.body ?: throw HttpStatusCodeException(response, "内容为空")
val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes()) val responseBytes = UTF8BOMFighter.removeUTF8BOM(responseBody.bytes())
var charsetName: String? = encode var charsetName: String? = encode

@ -2,6 +2,9 @@ package io.legado.app.lib.webdav
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toInputStream import rxhttp.wrapper.param.toInputStream
@ -64,7 +67,7 @@ class WebDav(urlStr: String) {
this.exists = false this.exists = false
return false return false
} }
response.body()?.let { response.body?.let {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
if (it.string().isNotEmpty()) { if (it.string().isNotEmpty()) {
return true return true
@ -83,7 +86,7 @@ class WebDav(urlStr: String) {
suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response -> propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
response.body()?.let { body -> response.body?.let { body ->
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
return parseDir(body.string()) return parseDir(body.string())
} }
@ -107,7 +110,7 @@ class WebDav(urlStr: String) {
httpUrl?.let { url -> httpUrl?.let { url ->
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
val requestBody = RequestBody.create(MediaType.parse("text/plain"), requestPropsStr) val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType())
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.method("PROPFIND", requestBody) .method("PROPFIND", requestBody)
@ -197,9 +200,8 @@ class WebDav(urlStr: String) {
suspend fun upload(localPath: String, contentType: String? = null): Boolean { suspend fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath) val file = File(localPath)
if (!file.exists()) return false if (!file.exists()) return false
val mediaType = contentType?.let { MediaType.parse(it) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = RequestBody.create(mediaType, file) val fileBody = file.asRequestBody(contentType?.toMediaType())
httpUrl?.let { httpUrl?.let {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)
@ -210,9 +212,8 @@ class WebDav(urlStr: String) {
} }
suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean {
val mediaType = contentType?.let { MediaType.parse(it) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = RequestBody.create(mediaType, byteArray) val fileBody = byteArray.toRequestBody(contentType?.toMediaType())
httpUrl?.let { httpUrl?.let {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)

@ -647,7 +647,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
return try { return try {
val analyzeUrl = AnalyzeUrl(urlStr, book = book) val analyzeUrl = AnalyzeUrl(urlStr, book = book)
runBlocking { runBlocking {
analyzeUrl.getRes(urlStr).body analyzeUrl.getStrResponse(urlStr).body
} }
} catch (e: Exception) { } catch (e: Exception) {
e.localizedMessage e.localizedMessage

@ -15,15 +15,10 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.CacheManager 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.help.http.api.HttpGetApi
import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.utils.* import io.legado.app.utils.*
import okhttp3.FormBody
import okhttp3.MediaType
import okhttp3.RequestBody
import retrofit2.Call
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray 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
@ -50,7 +45,6 @@ class AnalyzeUrl(
companion object { companion object {
val splitUrlRegex = Regex(",\\s*(?=\\{)") val splitUrlRegex = Regex(",\\s*(?=\\{)")
private val pagePattern = Pattern.compile("<(.*?)>") private val pagePattern = Pattern.compile("<(.*?)>")
private val jsonType = MediaType.parse("application/json; charset=utf-8")
} }
var url: String = "" var url: String = ""
@ -61,7 +55,6 @@ class AnalyzeUrl(
private var queryStr: String? = null private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>() private val fieldMap = LinkedHashMap<String, String>()
private var charset: String? = null private var charset: String? = null
private var requestBody: RequestBody? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
private var proxy: String? = null private var proxy: String? = null
@ -172,7 +165,7 @@ class AnalyzeUrl(
*/ */
private fun initUrl() { private fun initUrl() {
var urlArray = ruleUrl.split(splitUrlRegex, 2) var urlArray = ruleUrl.split(splitUrlRegex, 2)
url = urlArray[0] url = NetworkUtils.getAbsoluteURL(baseUrl, urlArray[0])
urlHasQuery = urlArray[0] urlHasQuery = urlArray[0]
NetworkUtils.getBaseUrl(url)?.let { NetworkUtils.getBaseUrl(url)?.let {
baseUrl = it baseUrl = it
@ -223,13 +216,9 @@ class AnalyzeUrl(
} }
RequestMethod.POST -> { RequestMethod.POST -> {
body?.let { body?.let {
if (it.isJson()) { if (!it.isJson()) {
requestBody = RequestBody.create(jsonType, it)
} else {
analyzeFields(it) analyzeFields(it)
} }
} ?: let {
requestBody = FormBody.Builder().build()
} }
} }
} }
@ -285,56 +274,15 @@ class AnalyzeUrl(
return book?.variableMap?.get(key) ?: "" return book?.variableMap?.get(key) ?: ""
} }
fun getResponse(tag: String): Call<String> { suspend fun getStrResponse(
val cookie = CookieStore.getCookie(tag)
if (cookie.isNotEmpty()) {
val cookieMap = CookieStore.cookieToMap(cookie)
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
cookieMap.putAll(customCookieMap)
val newCookie = CookieStore.mapToCookie(cookieMap)
newCookie?.let {
headerMap.put("Cookie", it)
}
}
return when {
method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset, proxy)
.postMap(url, fieldMap, headerMap)
} else {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset, proxy)
.postBody(url, requestBody!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset, proxy)
.get(url, headerMap)
else -> HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset, proxy)
.getMap(url, fieldMap, headerMap)
}
}
suspend fun getRes(
tag: String, tag: String,
jsStr: String? = null, jsStr: String? = null,
sourceRegex: String? = null, sourceRegex: String? = null,
): Res { ): StrResponse {
if (type != null) { if (type != null) {
return Res(url, StringUtils.byteToHexString(getByteArray(tag))) return StrResponse.success(StringUtils.byteToHexString(getByteArray(tag)), url)
}
val cookie = CookieStore.getCookie(tag)
if (cookie.isNotEmpty()) {
val cookieMap = CookieStore.cookieToMap(cookie)
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
cookieMap.putAll(customCookieMap)
val newCookie = CookieStore.mapToCookie(cookieMap)
newCookie?.let {
headerMap.put("Cookie", it)
}
} }
setCookie(tag)
if (useWebView) { if (useWebView) {
val params = AjaxWebView.AjaxParams(url) val params = AjaxWebView.AjaxParams(url)
params.headerMap = headerMap params.headerMap = headerMap
@ -345,65 +293,70 @@ class AnalyzeUrl(
params.tag = tag params.tag = tag
return HttpHelper.ajax(params) return HttpHelper.ajax(params)
} }
val res = when { return when (method) {
method == RequestMethod.POST -> { RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty()) {
HttpHelper RxHttp.postForm(url)
.getApiService<HttpPostApi>(baseUrl, charset, proxy) .setOkClient(HttpHelper.getProxyClient(proxy))
.postMapAsync(url, fieldMap, headerMap) .addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} else { } else {
HttpHelper RxHttp.postJson(url)
.getApiService<HttpPostApi>(baseUrl, charset, proxy) .setOkClient(HttpHelper.getProxyClient(proxy))
.postBodyAsync(url, requestBody!!, headerMap) .addAll(body)
} .addAllHeader(headerMap)
} .toStrResponse().await()
fieldMap.isEmpty() -> {
HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset, proxy)
.getAsync(url, headerMap)
} }
else -> {
HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset, proxy)
.getMapAsync(url, fieldMap, headerMap)
} }
else -> RxHttp.get(url)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap)
.toStrResponse().await()
} }
return Res(NetworkUtils.getUrl(res), res.body())
} }
suspend fun getByteArray(tag: String? = null): ByteArray { suspend fun getByteArray(tag: String? = null): ByteArray {
if (tag != null) { setCookie(tag)
val cookie = CookieStore.getCookie(tag)
if (cookie.isNotEmpty()) {
val cookieMap = CookieStore.cookieToMap(cookie)
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
cookieMap.putAll(customCookieMap)
val newCookie = CookieStore.mapToCookie(cookieMap)
newCookie?.let {
headerMap.put("Cookie", it)
}
}
}
return when (method) { return when (method) {
RequestMethod.POST -> { RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) { if (fieldMap.isNotEmpty()) {
RxHttp.postForm(url) RxHttp.postForm(url)
.addAll(fieldMap) .setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.addAllHeader(headerMap) .addAllHeader(headerMap)
.toByteArray().await() .toByteArray().await()
} else { } else {
RxHttp.postJson(url) RxHttp.postJson(url)
.setOkClient(HttpHelper.getProxyClient(proxy))
.addAll(body) .addAll(body)
.addAllHeader(headerMap) .addAllHeader(headerMap)
.toByteArray().await() .toByteArray().await()
} }
} }
else -> RxHttp.get(url) else -> RxHttp.get(url)
.addAll(fieldMap) .setOkClient(HttpHelper.getProxyClient(proxy))
.addAllEncoded(fieldMap)
.toByteArray().await() .toByteArray().await()
} }
} }
private fun setCookie(tag: String?) {
if (tag != null) {
val cookie = CookieStore.getCookie(tag)
if (cookie.isNotEmpty()) {
val cookieMap = CookieStore.cookieToMap(cookie)
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
cookieMap.putAll(customCookieMap)
val newCookie = CookieStore.mapToCookie(cookieMap)
newCookie?.let {
headerMap.put("Cookie", it)
}
}
}
}
fun getGlideUrl(): GlideUrl { fun getGlideUrl(): GlideUrl {
val headers = LazyHeaders.Builder() val headers = LazyHeaders.Builder()
headerMap.forEach { (key, value) -> headerMap.forEach { (key, value) ->

@ -26,7 +26,7 @@ object Rss {
page = page, page = page,
headerMapF = rssSource.getHeaderMap() headerMapF = rssSource.getHeaderMap()
) )
val body = analyzeUrl.getRes(rssSource.sourceUrl).body val body = analyzeUrl.getStrResponse(rssSource.sourceUrl).body
RssParserByRule.parseXML(sortName, sortUrl, body, rssSource) RssParserByRule.parseXML(sortName, sortUrl, body, rssSource)
} }
} }
@ -42,7 +42,7 @@ object Rss {
val body = AnalyzeUrl( val body = AnalyzeUrl(
rssArticle.link, baseUrl = rssArticle.origin, rssArticle.link, baseUrl = rssArticle.origin,
headerMapF = rssSource?.getHeaderMap() headerMapF = rssSource?.getHeaderMap()
).getRes(rssArticle.origin) ).getStrResponse(rssArticle.origin)
.body .body
val analyzeRule = AnalyzeRule() val analyzeRule = AnalyzeRule()
analyzeRule.setContent(body) analyzeRule.setContent(body)

@ -112,7 +112,7 @@ object RssParserByRule {
rssArticle.image = analyzeRule.getString(ruleImage, true) rssArticle.image = analyzeRule.getString(ruleImage, true)
Debug.log(sourceUrl, "${rssArticle.image}", log) Debug.log(sourceUrl, "${rssArticle.image}", log)
Debug.log(sourceUrl, "┌获取文章链接", log) Debug.log(sourceUrl, "┌获取文章链接", log)
rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink))!! rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink))
Debug.log(sourceUrl, "${rssArticle.link}", log) Debug.log(sourceUrl, "${rssArticle.link}", log)
if (rssArticle.title.isBlank()) { if (rssArticle.title.isBlank()) {
return null return null

@ -63,7 +63,7 @@ object BookChapterList {
ruleUrl = nextUrl, ruleUrl = nextUrl,
book = book, book = book,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getRes(bookSource.bookSourceUrl) ).getStrResponse(bookSource.bookSourceUrl)
.body?.let { nextBody -> .body?.let { nextBody ->
chapterData = analyzeChapterList( chapterData = analyzeChapterList(
book, nextUrl, nextBody, tocRule, listRule, bookSource book, nextUrl, nextBody, tocRule, listRule, bookSource
@ -132,7 +132,7 @@ object BookChapterList {
ruleUrl = chapterData.nextUrl, ruleUrl = chapterData.nextUrl,
book = book, book = book,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getRes(bookSource.bookSourceUrl).body ).getStrResponse(bookSource.bookSourceUrl).body
?: throw Exception("${chapterData.nextUrl}, 下载失败") ?: throw Exception("${chapterData.nextUrl}, 下载失败")
val nextChapterData = analyzeChapterList( val nextChapterData = analyzeChapterList(
book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource, book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource,

@ -56,7 +56,7 @@ object BookContent {
ruleUrl = nextUrl, ruleUrl = nextUrl,
book = book, book = book,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getRes(bookSource.bookSourceUrl).body?.let { nextBody -> ).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody ->
contentData = analyzeContent( contentData = analyzeContent(
book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false
) )
@ -78,7 +78,7 @@ object BookContent {
ruleUrl = item.nextUrl, ruleUrl = item.nextUrl,
book = book, book = book,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getRes(bookSource.bookSourceUrl).body?.let { ).getStrResponse(bookSource.bookSourceUrl).body?.let {
contentData = analyzeContent( contentData = analyzeContent(
book, item.nextUrl, it, contentRule, bookChapter, bookSource, false book, item.nextUrl, it, contentRule, bookChapter, bookSource, false
) )

@ -46,7 +46,7 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap(), headerMapF = bookSource.getHeaderMap(),
book = variableBook book = variableBook
) )
val res = analyzeUrl.getRes(bookSource.bookSourceUrl) val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
return BookList.analyzeBookList( return BookList.analyzeBookList(
scope, scope,
res.body, res.body,
@ -77,7 +77,7 @@ class WebBook(val bookSource: BookSource) {
baseUrl = sourceUrl, baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
) )
val res = analyzeUrl.getRes(bookSource.bookSourceUrl) val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl)
BookList.analyzeBookList( BookList.analyzeBookList(
scope, scope,
res.body, res.body,
@ -110,7 +110,7 @@ class WebBook(val bookSource: BookSource) {
baseUrl = sourceUrl, baseUrl = sourceUrl,
headerMapF = bookSource.getHeaderMap(), headerMapF = bookSource.getHeaderMap(),
book = book book = book
).getRes(bookSource.bookSourceUrl) ).getStrResponse(bookSource.bookSourceUrl)
BookInfo.analyzeBookInfo(book, res.body, bookSource, book.bookUrl, canReName) BookInfo.analyzeBookInfo(book, res.body, bookSource, book.bookUrl, canReName)
} }
book book
@ -141,7 +141,7 @@ class WebBook(val bookSource: BookSource) {
ruleUrl = book.tocUrl, ruleUrl = book.tocUrl,
baseUrl = book.bookUrl, baseUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getRes(bookSource.bookSourceUrl) ).getStrResponse(bookSource.bookSourceUrl)
BookChapterList.analyzeChapterList( BookChapterList.analyzeChapterList(
this, this,
book, book,
@ -201,7 +201,7 @@ class WebBook(val bookSource: BookSource) {
headerMapF = bookSource.getHeaderMap(), headerMapF = bookSource.getHeaderMap(),
book = book, book = book,
chapter = bookChapter chapter = bookChapter
).getRes( ).getStrResponse(
bookSource.bookSourceUrl, bookSource.bookSourceUrl,
jsStr = bookSource.getContentRule().webJs, jsStr = bookSource.getContentRule().webJs,
sourceRegex = bookSource.getContentRule().sourceRegex sourceRegex = bookSource.getContentRule().sourceRegex

@ -109,9 +109,8 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
content.split("\n").forEachIndexed { index, text -> content.split("\n").forEachIndexed { index, text ->
val matcher = AppPattern.imgPattern.matcher(text) val matcher = AppPattern.imgPattern.matcher(text)
if (matcher.find()) { if (matcher.find()) {
var src = matcher.group(1) matcher.group(1)?.let {
src = NetworkUtils.getAbsoluteURL(chapter.url, src) val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
src?.let {
srcList.add(Triple(chapter.title, index, src)) srcList.add(Triple(chapter.title, index, src))
} }
} }

@ -1,7 +1,6 @@
package io.legado.app.ui.book.explore package io.legado.app.ui.book.explore
import android.os.Bundle import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
@ -37,7 +36,6 @@ class ExploreShowActivity : VMBaseActivity<ActivityExploreShowBinding, ExploreSh
private fun initRecyclerView() { private fun initRecyclerView() {
adapter = ExploreShowAdapter(this, this) adapter = ExploreShowAdapter(this, this)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.addItemDecoration(VerticalDivider(this)) binding.recyclerView.addItemDecoration(VerticalDivider(this))
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
loadMoreView = LoadMoreView(this) loadMoreView = LoadMoreView(this)

@ -8,11 +8,13 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.msg
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
class ExploreShowViewModel(application: Application) : BaseViewModel(application) { class ExploreShowViewModel(application: Application) : BaseViewModel(application) {
val booksData = MutableLiveData<List<SearchBook>>() val booksData = MutableLiveData<List<SearchBook>>()
val errorLiveData = MutableLiveData<String>()
private var bookSource: BookSource? = null private var bookSource: BookSource? = null
private val variableBook = SearchBook() private val variableBook = SearchBook()
private var exploreUrl: String? = null private var exploreUrl: String? = null
@ -39,6 +41,9 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
booksData.postValue(searchBooks) booksData.postValue(searchBooks)
App.db.searchBookDao.insert(*searchBooks.toTypedArray()) App.db.searchBookDao.insert(*searchBooks.toTypedArray())
page++ page++
}.onError {
it.printStackTrace()
errorLiveData.postValue(it.msg)
} }
} }
} }

@ -97,6 +97,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.loadContent(resetPageOffset = true) ReadBook.loadContent(resetPageOffset = true)
} }
} }
if (!BaseReadAloudService.isRun) {
syncBookProgress(book)
}
} }
} }

@ -95,15 +95,14 @@ object ChapterProvider {
contents.forEachIndexed { index, text -> contents.forEachIndexed { index, text ->
val matcher = AppPattern.imgPattern.matcher(text) val matcher = AppPattern.imgPattern.matcher(text)
if (matcher.find()) { if (matcher.find()) {
var src = matcher.group(1) matcher.group(1)?.let {
if (!book.isEpub()) { if (!book.isEpub()) {
src = NetworkUtils.getAbsoluteURL(bookChapter.url, src) val src = NetworkUtils.getAbsoluteURL(bookChapter.url, it)
}
src?.let {
durY = setTypeImage( durY = setTypeImage(
book, bookChapter, src, durY, textPages, imageStyle book, bookChapter, src, durY, textPages, imageStyle
) )
} }
}
} else { } else {
val isTitle = index == 0 val isTitle = index == 0
val textPaint = if (isTitle) paint.first else paint.second val textPaint = if (isTitle) paint.first else paint.second

@ -47,4 +47,11 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
binding.tvText.visible() binding.tvText.visible()
} }
fun error(msg: String) {
hasMore = false
binding.rotateLoading.hide()
binding.tvText.text = msg
binding.tvText.visible()
}
} }

@ -1,6 +1,5 @@
package io.legado.app.utils package io.legado.app.utils
import retrofit2.Response
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
@ -11,13 +10,6 @@ import java.util.regex.Pattern
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused", "MemberVisibilityCanBePrivate")
object NetworkUtils { object NetworkUtils {
fun getUrl(response: Response<*>): String {
response.raw().networkResponse()?.let {
return it.request().url().toString()
}
return response.raw().request().url().toString()
}
private val notNeedEncoding: BitSet by lazy { private val notNeedEncoding: BitSet by lazy {
val bitSet = BitSet(256) val bitSet = BitSet(256)
for (i in 'a'.toInt()..'z'.toInt()) { for (i in 'a'.toInt()..'z'.toInt()) {
@ -77,9 +69,8 @@ object NetworkUtils {
/** /**
* 获取绝对地址 * 获取绝对地址
*/ */
fun getAbsoluteURL(baseURL: String?, relativePath: String?): String? { fun getAbsoluteURL(baseURL: String?, relativePath: String): String {
if (baseURL.isNullOrEmpty()) return relativePath if (baseURL.isNullOrEmpty()) return relativePath
if (relativePath.isNullOrEmpty()) return baseURL
if (relativePath.isAbsUrl()) return relativePath if (relativePath.isAbsUrl()) return relativePath
var relativeUrl = relativePath var relativeUrl = relativePath
try { try {
@ -96,7 +87,7 @@ object NetworkUtils {
/** /**
* 获取绝对地址 * 获取绝对地址
*/ */
fun getAbsoluteURL(baseURL: URL?, relativePath: String): String? { fun getAbsoluteURL(baseURL: URL?, relativePath: String): String {
if (baseURL == null) return relativePath if (baseURL == null) return relativePath
var relativeUrl = relativePath var relativeUrl = relativePath
try { try {

@ -25,8 +25,9 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_search" />
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout> </io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>

@ -25,6 +25,7 @@
android:textSize="14sp" android:textSize="14sp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:visibility="invisible" android:visibility="invisible"
android:singleLine="true"
tools:text="加载状态" tools:text="加载状态"
tools:visibility="visible" /> tools:visibility="visible" />

Loading…
Cancel
Save