From 90a3ba54d565c9580e4fd750dfe6eb6876fa0f12 Mon Sep 17 00:00:00 2001 From: Horis <821938089@qq.com> Date: Sun, 4 Sep 2022 19:36:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/epub/main.css | 2 +- .../app/api/controller/BookController.kt | 2 +- .../main/java/io/legado/app/help/AppWebDav.kt | 18 +++++-- .../java/io/legado/app/lib/webdav/WebDav.kt | 8 +-- .../app/model/analyzeRule/AnalyzeUrl.kt | 48 +++++++++++++++++ .../io/legado/app/model/localBook/EpubFile.kt | 3 ++ .../legado/app/model/localBook/LocalBook.kt | 10 ++-- .../app/ui/book/read/ReadBookViewModel.kt | 1 + .../app/ui/book/read/page/ContentTextView.kt | 37 +++++++++++++- .../book/read/page/provider/ImageProvider.kt | 51 +++++++++++++++++-- .../book/remote/manager/RemoteBookWebDav.kt | 4 +- .../java/io/legado/app/utils/HtmlFormatter.kt | 2 +- .../main/java/io/legado/app/web/HttpServer.kt | 27 ++++++++-- 13 files changed, 188 insertions(+), 25 deletions(-) diff --git a/app/src/main/assets/epub/main.css b/app/src/main/assets/epub/main.css index fcb287442..b74f6ac93 100644 --- a/app/src/main/assets/epub/main.css +++ b/app/src/main/assets/epub/main.css @@ -203,7 +203,7 @@ h2.head { text-align: left; font-weight: bold; font-size: 1.1em; - margin: -3em 2em 2em 0; + margin: 1em 2em 2em 0; color: #3f83e8; line-height: 140%; } diff --git a/app/src/main/java/io/legado/app/api/controller/BookController.kt b/app/src/main/java/io/legado/app/api/controller/BookController.kt index b5c6739e1..d37a43aac 100644 --- a/app/src/main/java/io/legado/app/api/controller/BookController.kt +++ b/app/src/main/java/io/legado/app/api/controller/BookController.kt @@ -82,7 +82,7 @@ object BookController { this.bookUrl = bookUrl val bitmap = runBlocking { ImageProvider.cacheImage(book, src, bookSource) - ImageProvider.getImage(book, src, width) + ImageProvider.getImage(book, src, width)!! } return returnData.setData(bitmap) } diff --git a/app/src/main/java/io/legado/app/help/AppWebDav.kt b/app/src/main/java/io/legado/app/help/AppWebDav.kt index b78ca1b05..3c498c315 100644 --- a/app/src/main/java/io/legado/app/help/AppWebDav.kt +++ b/app/src/main/java/io/legado/app/help/AppWebDav.kt @@ -17,6 +17,7 @@ import io.legado.app.lib.webdav.Authorization import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDavException import io.legado.app.lib.webdav.WebDavFile +import io.legado.app.ui.widget.dialog.WaitDialog import io.legado.app.utils.* import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.IO @@ -109,10 +110,19 @@ object AppWebDav { items = names ) { _, index -> if (index in 0 until names.size) { - Coroutine.async { + val waitDialog = WaitDialog(context) + waitDialog.setText("恢复中…") + waitDialog.show() + val task = Coroutine.async { restoreWebDav(names[index]) }.onError { + AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it) appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}") + }.onFinally(Main) { + waitDialog.dismiss() + } + waitDialog.setOnCancelListener { + task.cancel() } } } @@ -186,7 +196,7 @@ object AppWebDav { } } catch (e: Exception) { val msg = "WebDav导出\n${e.localizedMessage}" - AppLog.put(msg) + AppLog.put(msg, e) appCtx.toastOnUi(msg) } } @@ -201,7 +211,7 @@ object AppWebDav { val url = getProgressUrl(book.name, book.author) WebDav(url, authorization).upload(json.toByteArray(), "application/json") }.onError { - AppLog.put("上传进度失败\n${it.localizedMessage}") + AppLog.put("上传进度失败\n${it.localizedMessage}", it) } } @@ -214,7 +224,7 @@ object AppWebDav { val url = getProgressUrl(bookProgress.name, bookProgress.author) WebDav(url, authorization).upload(json.toByteArray(), "application/json") }.onError { - AppLog.put("上传进度失败\n${it.localizedMessage}") + AppLog.put("上传进度失败\n${it.localizedMessage}", it) } } diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index d26934b1f..1753cba60 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -232,7 +232,7 @@ open class WebDav(val path: String, val authorization: Authorization) { } } }.onFailure { - AppLog.put("WebDav创建目录失败\n${it.localizedMessage}") + AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it) }.isSuccess } @@ -286,6 +286,7 @@ open class WebDav(val path: String, val authorization: Authorization) { checkResult(it) } }.onFailure { + AppLog.put("WebDav上传失败\n${it.localizedMessage}", it) throw WebDavException("WebDav上传失败\n${it.localizedMessage}") } } @@ -303,12 +304,13 @@ open class WebDav(val path: String, val authorization: Authorization) { checkResult(it) } }.onFailure { + AppLog.put("WebDav上传失败\n${it.localizedMessage}", it) throw WebDavException("WebDav上传失败\n${it.localizedMessage}") } } @Throws(WebDavException::class) - private suspend fun downloadInputStream(): InputStream { + suspend fun downloadInputStream(): InputStream { val url = httpUrl ?: throw WebDavException("WebDav下载出错\nurl为空") val byteStream = webDavClient.newCallResponse { url(url) @@ -332,7 +334,7 @@ open class WebDav(val path: String, val authorization: Authorization) { checkResult(it) } }.onFailure { - AppLog.put("WebDav删除失败\n${it.localizedMessage}") + AppLog.put("WebDav删除失败\n${it.localizedMessage}", it) }.isSuccess } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 0ee4bb678..dbc2e385a 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.runBlocking import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response +import java.io.ByteArrayInputStream +import java.io.InputStream import java.net.URLEncoder import java.util.regex.Pattern @@ -497,6 +499,52 @@ class AnalyzeUrl( } } + /** + * 访问网站,返回InputStream + */ + suspend fun getInputStreamAwait(): InputStream { + val concurrentRecord = fetchStart() + + @Suppress("RegExpRedundantEscape") + val dataUriFindResult = dataUriRegex.find(urlNoQuery) + @Suppress("BlockingMethodInNonBlockingContext") + if (dataUriFindResult != null) { + val dataUriBase64 = dataUriFindResult.groupValues[1] + val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT) + fetchEnd(concurrentRecord) + return ByteArrayInputStream(byteArray) + } else { + setCookie(source?.getKey()) + val inputStream = getProxyClient(proxy).newCallResponseBody(retry) { + addHeaders(headerMap) + when (method) { + RequestMethod.POST -> { + url(urlNoQuery) + val contentType = headerMap["Content-Type"] + val body = body + if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { + postForm(fieldMap, true) + } else if (!contentType.isNullOrBlank()) { + val requestBody = body.toRequestBody(contentType.toMediaType()) + post(requestBody) + } else { + postJson(body) + } + } + else -> get(urlNoQuery, fieldMap, true) + } + }.byteStream() + fetchEnd(concurrentRecord) + return inputStream + } + } + + fun getInputStream(): InputStream { + return runBlocking { + getInputStreamAwait() + } + } + /** * 上传文件 */ diff --git a/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt index b171680d1..b3bdf41f6 100644 --- a/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt @@ -3,6 +3,7 @@ package io.legado.app.model.localBook import android.graphics.Bitmap import android.graphics.BitmapFactory import android.text.TextUtils +import io.legado.app.constant.AppLog import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp @@ -95,6 +96,7 @@ class EpubFile(var book: Book) { } } } catch (e: Exception) { + AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e) e.printOnDebug() } } @@ -108,6 +110,7 @@ class EpubFile(var book: Book) { val zipFile = BookHelp.getEpubFile(book) EpubReader().readEpubLazy(zipFile, "utf-8") }.onFailure { + AppLog.put("读取Epub文件失败\n${it.localizedMessage}", it) it.printOnDebug() }.getOrNull() } diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 11bb5f85a..866a74c2c 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -235,8 +235,8 @@ object LocalBook { AppConfig.defaultBookTreeUri ?: throw NoStackTraceException("没有设置书籍保存位置!") val bytes = when { - str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray() - str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT) + str.isAbsUrl() -> AnalyzeUrl(str, source = source).getInputStream() + str.isDataUrl() -> ByteArrayInputStream(Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)) else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL") } return saveBookFile(bytes, fileName) @@ -257,7 +257,7 @@ object LocalBook { } fun saveBookFile( - bytes: ByteArray, + inputStream: InputStream, fileName: String ): Uri { val defaultBookTreeUri = AppConfig.defaultBookTreeUri @@ -271,14 +271,14 @@ object LocalBook { ?: throw SecurityException("Permission Denial") } appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream -> - oStream.write(bytes) + inputStream.copyTo(oStream) } doc.uri } else { val treeFile = File(treeUri.path!!) val file = treeFile.getFile(fileName) FileOutputStream(file).use { oStream -> - oStream.write(bytes) + inputStream.copyTo(oStream) } Uri.fromFile(file) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index acac64422..8312972b1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -199,6 +199,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { alertSync?.invoke(progress) } else { ReadBook.setProgress(progress) + context.toastOnUi("自动同步阅读进度成功") } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 3c20387cf..232e90959 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -7,6 +7,7 @@ import android.graphics.RectF import android.util.AttributeSet import android.view.View import io.legado.app.R +import io.legado.app.constant.AppLog import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Bookmark import io.legado.app.help.config.AppConfig @@ -44,6 +45,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at val selectEnd = TextPos(0, 0, 0) var textPage: TextPage = TextPage() private set + private var drawVisibleImageOnly = false + private var cacheIncreased = false + private val increaseSize = 8 * 1024 * 1024 + private val maxCacheSize = 256 * 1024 * 1024 //滚动参数 private val pageFactory: TextPageFactory get() = callBack.pageFactory @@ -86,6 +91,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at super.onDraw(canvas) canvas.clipRect(visibleRect) drawPage(canvas) + drawVisibleImageOnly = false + cacheIncreased = false } /** @@ -173,12 +180,40 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at ) { val book = ReadBook.book ?: return + val isVisible = when { + lineTop > 0 -> lineTop < height + lineTop < 0 -> lineBottom > 0 + else -> true + } + if (drawVisibleImageOnly && !isVisible) { + return + } + if (drawVisibleImageOnly && + isVisible && + !cacheIncreased && + ImageProvider.isTriggerRecycled() && + !ImageProvider.isImageAlive(book, textChar.charData) + ) { + val newSize = ImageProvider.bitmapLruCache.maxSize() + increaseSize + if (newSize < maxCacheSize) { + ImageProvider.bitmapLruCache.resize(newSize) + AppLog.put("图片缓存不够大,自动扩增至${(newSize / 1024 / 1024)}MB。") + cacheIncreased = true + } + return + } val bitmap = ImageProvider.getImage( book, textChar.charData, (textChar.end - textChar.start).toInt(), (lineBottom - lineTop).toInt() - ) + ) { + if (!drawVisibleImageOnly && isVisible) { + drawVisibleImageOnly = true + invalidate() + } + } ?: return + val rectF = if (textLine.isImage) { RectF(textChar.start, lineTop, textChar.end, lineBottom) } else { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt index 19dd436f0..90ec2fa9a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt @@ -6,15 +6,18 @@ import android.util.Size import androidx.collection.LruCache import io.legado.app.R import io.legado.app.constant.AppLog.putDebug +import io.legado.app.constant.PageAnim import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.exception.NoStackTraceException import io.legado.app.help.BookHelp import io.legado.app.help.config.AppConfig import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.ReadBook import io.legado.app.model.localBook.EpubFile import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.withContext import splitties.init.appCtx import java.io.File @@ -32,6 +35,7 @@ object ImageProvider { */ private const val M = 1024 * 1024 val cacheSize get() = AppConfig.bitmapCacheSize * M + var triggerRecycled = false val bitmapLruCache = object : LruCache(cacheSize) { override fun sizeOf(filePath: String, bitmap: Bitmap): Int { @@ -47,6 +51,7 @@ object ImageProvider { //错误图片不能释放,占位用,防止一直重复获取图片 if (oldBitmap != errorBitmap) { oldBitmap.recycle() + triggerRecycled = true putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath") putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes") } @@ -110,12 +115,13 @@ object ImageProvider { book: Book, src: String, width: Int, - height: Int? = null - ): Bitmap { + height: Int? = null, + block: (() -> Unit)? = null + ): Bitmap? { //src为空白时 可能被净化替换掉了 或者规则失效 if (book.getUseReplaceRule() && src.isBlank()) { - book.setUseReplaceRule(false) - appCtx.toastOnUi(R.string.error_image_url_empty) + book.setUseReplaceRule(false) + appCtx.toastOnUi(R.string.error_image_url_empty) } val vFile = BookHelp.getImage(book, src) if (!vFile.exists()) return errorBitmap @@ -123,6 +129,30 @@ object ImageProvider { //bitmapLruCache的key同一改成缓存文件的路径 val cacheBitmap = bitmapLruCache.get(vFile.absolutePath) if (cacheBitmap != null) return cacheBitmap + if (height != null && ReadBook.pageAnim() == PageAnim.scrollPageAnim) { + Coroutine.async { + kotlin.runCatching { + val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) + ?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap)) + withContext(Main) { + bitmapLruCache.put(vFile.absolutePath, bitmap) + } + }.onFailure { + //错误图片占位,防止重复获取 + withContext(Main) { + bitmapLruCache.put(vFile.absolutePath, errorBitmap) + } + putDebug( + "ImageProvider: decode bitmap failed. path: ${vFile.absolutePath}\n$it", + it + ) + } + withContext(Main) { + block?.invoke() + } + } + return null + } @Suppress("BlockingMethodInNonBlockingContext") return kotlin.runCatching { val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) @@ -139,4 +169,17 @@ object ImageProvider { }.getOrDefault(errorBitmap) } + fun isImageAlive(book: Book, src: String): Boolean { + val vFile = BookHelp.getImage(book, src) + if (!vFile.exists()) return true // 使用 errorBitmap + val cacheBitmap = bitmapLruCache.get(vFile.absolutePath) + return cacheBitmap != null + } + + fun isTriggerRecycled(): Boolean { + val tmp = triggerRecycled + triggerRecycled = false + return tmp + } + } diff --git a/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt b/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt index e407258c1..d5659fab7 100644 --- a/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt +++ b/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt @@ -76,8 +76,8 @@ object RemoteBookWebDav : RemoteBookManager() { override suspend fun getRemoteBook(remoteBook: RemoteBook): Uri? { return AppWebDav.authorization?.let { val webdav = WebDav(remoteBook.path, it) - webdav.download().let { bytes -> - LocalBook.saveBookFile(bytes, remoteBook.filename) + webdav.downloadInputStream().let { inputStream -> + LocalBook.saveBookFile(inputStream, remoteBook.filename) } } } diff --git a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt index ad374c857..dfc46ecbe 100644 --- a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt +++ b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt @@ -14,7 +14,7 @@ object HtmlFormatter { private val notImgHtmlRegex = "])[^<>]*>".toRegex() private val otherHtmlRegex = "])[^<>]*>".toRegex() private val formatImagePattern = Pattern.compile( - "]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|]*data-[^=]*= *\"([^\"]*)\"[^>]*>|]*src *= *\"([^\"]*)\"[^>]*>", + "]*src *= *\"([^\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})\"[^>]*>|]*data-[^=>]*= *\"([^\">]*)\"[^>]*>|]*src *= *\"([^\">]*)\"[^>]*>", Pattern.CASE_INSENSITIVE ) diff --git a/app/src/main/java/io/legado/app/web/HttpServer.kt b/app/src/main/java/io/legado/app/web/HttpServer.kt index a141b0c9f..c8f31c6b1 100644 --- a/app/src/main/java/io/legado/app/web/HttpServer.kt +++ b/app/src/main/java/io/legado/app/web/HttpServer.kt @@ -7,9 +7,11 @@ import io.legado.app.api.ReturnData import io.legado.app.api.controller.BookController import io.legado.app.api.controller.BookSourceController import io.legado.app.api.controller.RssSourceController +import io.legado.app.utils.FileUtils +import io.legado.app.utils.externalFiles import io.legado.app.web.utils.AssetsWeb -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import splitties.init.appCtx +import java.io.* class HttpServer(port: Int) : NanoHTTPD(port) { @@ -91,7 +93,26 @@ class HttpServer(port: Int) : NanoHTTPD(port) { byteArray.size.toLong() ) } else { - newFixedLengthResponse(Gson().toJson(returnData)) + try { + newFixedLengthResponse(Gson().toJson(returnData)) + } catch (e: OutOfMemoryError) { + val path = FileUtils.getPath( + appCtx.externalFiles, + "book_cache", + "bookSources.json" + ) + val file = FileUtils.createFileIfNotExist(path) + BufferedWriter(FileWriter(file)).use { + Gson().toJson(returnData, it) + } + val fis = FileInputStream(file) + newFixedLengthResponse( + Response.Status.OK, + "application/json", + fis, + fis.available().toLong() + ) + } } response.addHeader("Access-Control-Allow-Methods", "GET, POST") response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])