Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ object BookHelp {
|
||||
private val downloadDir: File = appCtx.externalFiles
|
||||
private const val cacheFolderName = "book_cache"
|
||||
private const val cacheImageFolderName = "images"
|
||||
private const val cacheEpubFolderName = "epub"
|
||||
private val downloadImages = CopyOnWriteArraySet<String>()
|
||||
|
||||
fun clearCache() {
|
||||
@@ -55,15 +56,24 @@ object BookHelp {
|
||||
*/
|
||||
suspend fun clearInvalidCache() {
|
||||
withContext(IO) {
|
||||
val bookFolderNames = appDb.bookDao.all.map {
|
||||
it.getFolderName()
|
||||
val bookFolderNames = ArrayList<String>()
|
||||
val originNames = ArrayList<String>()
|
||||
appDb.bookDao.all.forEach {
|
||||
bookFolderNames.add(it.getFolderName())
|
||||
if (it.isEpub()) originNames.add(it.originName)
|
||||
}
|
||||
val file = downloadDir.getFile(cacheFolderName)
|
||||
file.listFiles()?.forEach { bookFile ->
|
||||
if (!bookFolderNames.contains(bookFile.name)) {
|
||||
FileUtils.delete(bookFile.absolutePath)
|
||||
downloadDir.getFile(cacheFolderName)
|
||||
.listFiles()?.forEach { bookFile ->
|
||||
if (!bookFolderNames.contains(bookFile.name)) {
|
||||
FileUtils.delete(bookFile.absolutePath)
|
||||
}
|
||||
}
|
||||
downloadDir.getFile(cacheEpubFolderName)
|
||||
.listFiles()?.forEach { epubFile ->
|
||||
if (!originNames.contains(epubFile.name)) {
|
||||
FileUtils.delete(epubFile.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +175,8 @@ object BookHelp {
|
||||
fun getEpubFile(book: Book): ZipFile {
|
||||
val uri = Uri.parse(book.bookUrl)
|
||||
if (uri.isContentScheme()) {
|
||||
val path = FileUtils.getPath(downloadDir, cacheFolderName, book.getFolderName(), book.originName)
|
||||
FileUtils.createFolderIfNotExist(downloadDir, cacheEpubFolderName)
|
||||
val path = FileUtils.getPath(downloadDir, cacheEpubFolderName, book.originName)
|
||||
val file = File(path)
|
||||
val doc = DocumentFile.fromSingleUri(appCtx, uri)
|
||||
?: throw IOException("文件不存在")
|
||||
|
||||
@@ -55,7 +55,7 @@ interface JsExtensions {
|
||||
val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())
|
||||
analyzeUrl.getStrResponseAwait().body
|
||||
}.onFailure {
|
||||
log("ajax(${urlStr}) error\n${it.stackTraceToString()}")
|
||||
AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it)
|
||||
it.printOnDebug()
|
||||
}.getOrElse {
|
||||
it.msg
|
||||
@@ -1008,6 +1008,7 @@ interface JsExtensions {
|
||||
* @param key 密钥
|
||||
* @return 16进制字符串
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
fun HMacHex(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
@@ -1024,6 +1025,7 @@ interface JsExtensions {
|
||||
* @param key 密钥
|
||||
* @return Base64字符串
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
fun HMacBase64(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
@@ -85,17 +87,16 @@ class EpubFile(var book: Book) {
|
||||
if (!File(book.coverUrl!!).exists()) {
|
||||
/*部分书籍DRM处理后,封面获取异常,待优化*/
|
||||
it.coverImage?.inputStream?.use { input ->
|
||||
BitmapUtils.decodeBitmap(input)?.let { cover ->
|
||||
val out =
|
||||
FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
|
||||
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
}
|
||||
val cover = BitmapFactory.decodeStream(input)
|
||||
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
|
||||
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e)
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
@@ -109,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()
|
||||
}
|
||||
@@ -262,8 +264,6 @@ class EpubFile(var book: Book) {
|
||||
}
|
||||
}
|
||||
}
|
||||
book.latestChapterTitle = chapterList.lastOrNull()?.title
|
||||
book.totalChapterNum = chapterList.size
|
||||
return chapterList
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,12 @@ object LocalBook {
|
||||
if (chapters.isEmpty()) {
|
||||
throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
|
||||
}
|
||||
val lh = LinkedHashSet(chapters)
|
||||
lh.forEachIndexed { index, bookChapter -> bookChapter.index = index }
|
||||
return ArrayList(lh)
|
||||
val list = ArrayList(LinkedHashSet(chapters))
|
||||
list.forEachIndexed { index, bookChapter -> bookChapter.index = index }
|
||||
book.latestChapterTitle = list.last().title
|
||||
book.totalChapterNum = list.size
|
||||
book.save()
|
||||
return list
|
||||
}
|
||||
|
||||
fun getContent(book: Book, chapter: BookChapter): String? {
|
||||
@@ -232,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)
|
||||
@@ -254,7 +257,7 @@ object LocalBook {
|
||||
}
|
||||
|
||||
fun saveBookFile(
|
||||
bytes: ByteArray,
|
||||
inputStream: InputStream,
|
||||
fileName: String
|
||||
): Uri {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
@@ -268,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)
|
||||
}
|
||||
|
||||
@@ -77,9 +77,6 @@ class TextFile(private val book: Book) {
|
||||
bookChapter.bookUrl = book.bookUrl
|
||||
bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title)
|
||||
}
|
||||
book.latestChapterTitle = toc.last().title
|
||||
book.totalChapterNum = toc.size
|
||||
book.save()
|
||||
return toc
|
||||
}
|
||||
|
||||
|
||||
@@ -112,8 +112,6 @@ class UmdFile(var book: Book) {
|
||||
DebugLog.d(javaClass.name, chapter.url)
|
||||
chapterList.add(chapter)
|
||||
}
|
||||
book.latestChapterTitle = chapterList.lastOrNull()?.title
|
||||
book.totalChapterNum = chapterList.size
|
||||
return chapterList
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,13 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
|
||||
}
|
||||
|
||||
override fun del(source: TxtTocRule) {
|
||||
viewModel.del(source)
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + source.name)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.del(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun edit(source: TxtTocRule) {
|
||||
@@ -151,7 +157,7 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
|
||||
|
||||
private fun delSourceDialog() {
|
||||
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
|
||||
okButton { viewModel.del(*adapter.selection.toTypedArray()) }
|
||||
yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,7 +976,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
Coroutine.async {
|
||||
val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
|
||||
val payAction = source.getContentRule().payAction
|
||||
if (payAction.isNullOrEmpty()) {
|
||||
if (payAction.isNullOrBlank()) {
|
||||
throw NoStackTraceException("no pay action")
|
||||
}
|
||||
val analyzeRule = AnalyzeRule(book, source)
|
||||
@@ -984,9 +984,9 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
analyzeRule.chapter = chapter
|
||||
analyzeRule.evalJS(payAction).toString()
|
||||
}.onSuccess {
|
||||
if (it.isNotBlank()) {
|
||||
if (it.isAbsUrl()) {
|
||||
startActivity<WebViewActivity> {
|
||||
putExtra("title", R.string.chapter_pay)
|
||||
putExtra("title", getString(R.string.chapter_pay))
|
||||
putExtra("url", it)
|
||||
IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
|
||||
}
|
||||
|
||||
@@ -191,6 +191,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
} else {
|
||||
throw NoStackTraceException("进度同步未启用")
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("拉取阅读进度失败", it)
|
||||
}.onSuccess { progress ->
|
||||
if (progress.durChapterIndex < book.durChapterIndex ||
|
||||
(progress.durChapterIndex == book.durChapterIndex
|
||||
@@ -199,6 +201,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
alertSync?.invoke(progress)
|
||||
} else {
|
||||
ReadBook.setProgress(progress)
|
||||
AppLog.put("自动同步阅读进度成功")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -119,7 +119,7 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
|
||||
abortAnim()
|
||||
if (!hasNext()) return
|
||||
setDirection(PageDirection.NEXT)
|
||||
readView.setStartPoint(viewWidth.toFloat(), 0f, false)
|
||||
readView.setStartPoint(viewWidth.toFloat(), 1f, false)
|
||||
onAnimStart(animationSpeed)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
|
||||
abortAnim()
|
||||
if (!hasPrev()) return
|
||||
setDirection(PageDirection.PREV)
|
||||
readView.setStartPoint(0f, 0f, false)
|
||||
readView.setStartPoint(0f, viewHeight.toFloat(), false)
|
||||
onAnimStart(animationSpeed)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
|
||||
dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) {
|
||||
-(viewWidth + touchX)
|
||||
} else {
|
||||
(viewWidth - touchX + viewWidth)
|
||||
viewWidth - touchX
|
||||
}
|
||||
dy = if (mCornerY > 0) {
|
||||
(viewHeight - touchY)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.model.CheckSource
|
||||
import io.legado.app.model.Debug
|
||||
import io.legado.app.service.CheckSourceService
|
||||
import io.legado.app.ui.association.ImportBookSourceDialog
|
||||
import io.legado.app.ui.book.local.rule.TxtTocRuleActivity
|
||||
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
|
||||
@@ -336,7 +335,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
|
||||
|
||||
override fun onClickSelectBarMainAction() {
|
||||
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
|
||||
okButton { viewModel.del(*adapter.selection.toTypedArray()) }
|
||||
yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
@@ -589,7 +588,13 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
|
||||
}
|
||||
|
||||
override fun del(bookSource: BookSource) {
|
||||
viewModel.del(bookSource)
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.del(bookSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(vararg bookSource: BookSource) {
|
||||
|
||||
@@ -162,7 +162,10 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
|
||||
}
|
||||
|
||||
override fun onClickSelectBarMainAction() {
|
||||
delSourceDialog()
|
||||
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
|
||||
yesButton { viewModel.delSelection(adapter.selection) }
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSelectActionView() {
|
||||
@@ -172,13 +175,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
|
||||
binding.selectActionBar.setCallBack(this)
|
||||
}
|
||||
|
||||
private fun delSourceDialog() {
|
||||
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
|
||||
okButton { viewModel.delSelection(adapter.selection) }
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeReplaceRuleData(searchKey: String? = null) {
|
||||
dataInit = false
|
||||
replaceRuleFlowJob?.cancel()
|
||||
@@ -329,8 +325,14 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
|
||||
}
|
||||
|
||||
override fun delete(rule: ReplaceRule) {
|
||||
setResult(RESULT_OK)
|
||||
viewModel.delete(rule)
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + rule.name)
|
||||
noButton()
|
||||
yesButton {
|
||||
setResult(RESULT_OK)
|
||||
viewModel.delete(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun edit(rule: ReplaceRule) {
|
||||
|
||||
@@ -329,7 +329,13 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
|
||||
}
|
||||
|
||||
override fun del(source: RssSource) {
|
||||
viewModel.del(source)
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + source.sourceName)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.del(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun edit(source: RssSource) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.graphics.Color
|
||||
import com.google.android.renderscript.Toolkit
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.math.*
|
||||
|
||||
|
||||
@@ -82,23 +81,6 @@ object BitmapUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从path中获取Bitmap图片
|
||||
* @param path 图片路径
|
||||
* @return
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun decodeBitmap(inputStream: InputStream): Bitmap? {
|
||||
return inputStream.use {
|
||||
val opts = BitmapFactory.Options()
|
||||
opts.inJustDecodeBounds = true
|
||||
|
||||
BitmapFactory.decodeStream(inputStream, null, opts)
|
||||
opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128)
|
||||
opts.inJustDecodeBounds = false
|
||||
BitmapFactory.decodeStream(inputStream, null, opts)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以最省内存的方式读取本地资源的图片
|
||||
* @param context 设备上下文
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user