pull/2239/head
Horis 2 years ago
parent 56f3a414ce
commit 90a3ba54d5
  1. 2
      app/src/main/assets/epub/main.css
  2. 2
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  3. 18
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  4. 8
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  5. 48
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  6. 3
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  7. 10
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  8. 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  9. 37
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  10. 51
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt
  11. 4
      app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt
  12. 2
      app/src/main/java/io/legado/app/utils/HtmlFormatter.kt
  13. 27
      app/src/main/java/io/legado/app/web/HttpServer.kt

@ -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%;
}

@ -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)
}

@ -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)
}
}

@ -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
}

@ -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()
}
}
/**
* 上传文件
*/

@ -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()
}

@ -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)
}

@ -199,6 +199,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
alertSync?.invoke(progress)
} else {
ReadBook.setProgress(progress)
context.toastOnUi("自动同步阅读进度成功")
}
}

@ -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 {

@ -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<String, Bitmap>(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
}
}

@ -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)
}
}
}

@ -14,7 +14,7 @@ object HtmlFormatter {
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val formatImagePattern = Pattern.compile(
"<img[^>]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|<img[^>]*data-[^=]*= *\"([^\"]*)\"[^>]*>|<img[^>]*src *= *\"([^\"]*)\"[^>]*>",
"<img[^>]*src *= *\"([^\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})\"[^>]*>|<img[^>]*data-[^=>]*= *\"([^\">]*)\"[^>]*>|<img[^>]*src *= *\"([^\">]*)\"[^>]*>",
Pattern.CASE_INSENSITIVE
)

@ -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"])

Loading…
Cancel
Save