988 lines
33 KiB
Kotlin
988 lines
33 KiB
Kotlin
package io.legado.app.model
|
|
|
|
import io.legado.app.constant.AppLog
|
|
import io.legado.app.constant.EventBus
|
|
import io.legado.app.constant.PageAnim.scrollPageAnim
|
|
import io.legado.app.data.appDb
|
|
import io.legado.app.data.entities.Book
|
|
import io.legado.app.data.entities.BookChapter
|
|
import io.legado.app.data.entities.BookProgress
|
|
import io.legado.app.data.entities.BookSource
|
|
import io.legado.app.data.entities.ReadRecord
|
|
import io.legado.app.help.AppWebDav
|
|
import io.legado.app.help.book.BookHelp
|
|
import io.legado.app.help.book.ContentProcessor
|
|
import io.legado.app.help.book.isImage
|
|
import io.legado.app.help.book.isLocal
|
|
import io.legado.app.help.book.isPdf
|
|
import io.legado.app.help.book.isSameNameAuthor
|
|
import io.legado.app.help.book.readSimulating
|
|
import io.legado.app.help.book.simulatedTotalChapterNum
|
|
import io.legado.app.help.book.update
|
|
import io.legado.app.help.config.AppConfig
|
|
import io.legado.app.help.config.ReadBookConfig
|
|
import io.legado.app.help.coroutine.Coroutine
|
|
import io.legado.app.help.globalExecutor
|
|
import io.legado.app.model.localBook.TextFile
|
|
import io.legado.app.model.webBook.WebBook
|
|
import io.legado.app.service.BaseReadAloudService
|
|
import io.legado.app.service.CacheBookService
|
|
import io.legado.app.ui.book.read.page.entities.TextChapter
|
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
|
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
|
import io.legado.app.utils.postEvent
|
|
import io.legado.app.utils.stackTraceStr
|
|
import io.legado.app.utils.toastOnUi
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.MainScope
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.cancelChildren
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.ensureActive
|
|
import kotlinx.coroutines.flow.collect
|
|
import kotlinx.coroutines.flow.receiveAsFlow
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
import kotlinx.coroutines.withContext
|
|
import splitties.init.appCtx
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
|
|
|
|
@Suppress("MemberVisibilityCanBePrivate")
|
|
object ReadBook : CoroutineScope by MainScope() {
|
|
var book: Book? = null
|
|
var callBack: CallBack? = null
|
|
var inBookshelf = false
|
|
var tocChanged = false
|
|
var chapterSize = 0
|
|
var simulatedChapterSize = 0
|
|
var durChapterIndex = 0
|
|
var durChapterPos = 0
|
|
var isLocalBook = true
|
|
var chapterChanged = false
|
|
var prevTextChapter: TextChapter? = null
|
|
set(value) {
|
|
field?.cancelLayout()
|
|
field = value
|
|
}
|
|
var curTextChapter: TextChapter? = null
|
|
set(value) {
|
|
field?.cancelLayout()
|
|
field = value
|
|
}
|
|
var nextTextChapter: TextChapter? = null
|
|
set(value) {
|
|
field?.cancelLayout()
|
|
field = value
|
|
}
|
|
var bookSource: BookSource? = null
|
|
var msg: String? = null
|
|
private val loadingChapters = arrayListOf<Int>()
|
|
private val readRecord = ReadRecord()
|
|
var readStartTime: Long = System.currentTimeMillis()
|
|
|
|
/* 跳转进度前进度记录 */
|
|
var lastBookProgress: BookProgress? = null
|
|
|
|
/* web端阅读进度记录 */
|
|
var webBookProgress: BookProgress? = null
|
|
|
|
var preDownloadTask: Job? = null
|
|
val downloadedChapters = hashSetOf<Int>()
|
|
val downloadFailChapters = hashMapOf<Int, Int>()
|
|
var contentProcessor: ContentProcessor? = null
|
|
val downloadScope = CoroutineScope(SupervisorJob() + IO)
|
|
val preDownloadSemaphore = Semaphore(2)
|
|
val executor = globalExecutor
|
|
|
|
fun resetData(book: Book) {
|
|
ReadBook.book = book
|
|
readRecord.bookName = book.name
|
|
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
|
|
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
|
simulatedChapterSize = if (book.readSimulating()) {
|
|
book.simulatedTotalChapterNum()
|
|
} else {
|
|
chapterSize
|
|
}
|
|
contentProcessor = ContentProcessor.get(book)
|
|
durChapterIndex = book.durChapterIndex
|
|
durChapterPos = book.durChapterPos
|
|
isLocalBook = book.isLocal
|
|
clearTextChapter()
|
|
callBack?.upContent()
|
|
callBack?.upMenuView()
|
|
callBack?.upPageAnim()
|
|
upWebBook(book)
|
|
lastBookProgress = null
|
|
webBookProgress = null
|
|
TextFile.clear()
|
|
synchronized(this) {
|
|
loadingChapters.clear()
|
|
downloadedChapters.clear()
|
|
downloadFailChapters.clear()
|
|
}
|
|
}
|
|
|
|
fun upData(book: Book) {
|
|
ReadBook.book = book
|
|
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
|
simulatedChapterSize = if (book.readSimulating()) {
|
|
book.simulatedTotalChapterNum()
|
|
} else {
|
|
chapterSize
|
|
}
|
|
if (durChapterIndex != book.durChapterIndex || tocChanged) {
|
|
durChapterIndex = book.durChapterIndex
|
|
durChapterPos = book.durChapterPos
|
|
clearTextChapter()
|
|
}
|
|
if (curTextChapter?.isCompleted == false) {
|
|
curTextChapter = null
|
|
}
|
|
if (nextTextChapter?.isCompleted == false) {
|
|
nextTextChapter = null
|
|
}
|
|
if (prevTextChapter?.isCompleted == false) {
|
|
prevTextChapter = null
|
|
}
|
|
callBack?.upMenuView()
|
|
upWebBook(book)
|
|
synchronized(this) {
|
|
loadingChapters.clear()
|
|
downloadedChapters.clear()
|
|
downloadFailChapters.clear()
|
|
}
|
|
}
|
|
|
|
fun upWebBook(book: Book) {
|
|
if (book.isLocal) {
|
|
bookSource = null
|
|
if (book.getImageStyle().isNullOrBlank() && (book.isImage || book.isPdf)) {
|
|
book.setImageStyle(Book.imgStyleFull)
|
|
}
|
|
} else {
|
|
appDb.bookSourceDao.getBookSource(book.origin)?.let {
|
|
bookSource = it
|
|
if (book.getImageStyle().isNullOrBlank()) {
|
|
var imageStyle = it.getContentRule().imageStyle
|
|
if (imageStyle.isNullOrBlank() && (book.isImage || book.isPdf)) {
|
|
imageStyle = Book.imgStyleFull
|
|
}
|
|
book.setImageStyle(imageStyle)
|
|
if (imageStyle.equals(Book.imgStyleSingle, true)) {
|
|
book.setPageAnim(0)
|
|
}
|
|
}
|
|
} ?: let {
|
|
bookSource = null
|
|
}
|
|
}
|
|
}
|
|
|
|
fun upReadBookConfig(book: Book) {
|
|
val oldIndex = ReadBookConfig.styleSelect
|
|
ReadBookConfig.isComic = book.isImage
|
|
if (oldIndex != ReadBookConfig.styleSelect) {
|
|
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
if (AppConfig.readBarStyleFollowPage) {
|
|
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun setProgress(progress: BookProgress) {
|
|
if (progress.durChapterIndex < chapterSize &&
|
|
(durChapterIndex != progress.durChapterIndex
|
|
|| durChapterPos != progress.durChapterPos)
|
|
) {
|
|
durChapterIndex = progress.durChapterIndex
|
|
durChapterPos = progress.durChapterPos
|
|
clearTextChapter()
|
|
callBack?.upContent()
|
|
loadContent(resetPageOffset = true)
|
|
}
|
|
}
|
|
|
|
//暂时保存跳转前进度
|
|
fun saveCurrentBookProgress() {
|
|
if (lastBookProgress != null) return //避免进度条连续跳转不能覆盖最初的进度记录
|
|
lastBookProgress = book?.let { BookProgress(it) }
|
|
}
|
|
|
|
//恢复跳转前进度
|
|
fun restoreLastBookProgress() {
|
|
lastBookProgress?.let {
|
|
setProgress(it)
|
|
lastBookProgress = null
|
|
}
|
|
}
|
|
|
|
fun clearTextChapter() {
|
|
prevTextChapter = null
|
|
curTextChapter = null
|
|
nextTextChapter = null
|
|
}
|
|
|
|
fun clearSearchResult() {
|
|
curTextChapter?.clearSearchResult()
|
|
prevTextChapter?.clearSearchResult()
|
|
nextTextChapter?.clearSearchResult()
|
|
}
|
|
|
|
fun uploadProgress(successAction: (() -> Unit)? = null) {
|
|
book?.let {
|
|
launch(IO) {
|
|
AppWebDav.uploadBookProgress(it)
|
|
ensureActive()
|
|
it.update()
|
|
successAction?.invoke()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 同步阅读进度
|
|
* 如果当前进度快于服务器进度或者没有进度进行上传,如果慢与服务器进度则执行传入动作
|
|
*/
|
|
fun syncProgress(
|
|
newProgressAction: ((progress: BookProgress) -> Unit)? = null,
|
|
uploadSuccessAction: (() -> Unit)? = null,
|
|
syncSuccessAction: (() -> Unit)? = null
|
|
) {
|
|
if (!AppConfig.syncBookProgress) return
|
|
book?.let {
|
|
Coroutine.async {
|
|
AppWebDav.getBookProgress(it)
|
|
}.onError {
|
|
AppLog.put("拉取阅读进度失败", it)
|
|
}.onSuccess { progress ->
|
|
if (progress == null || progress.durChapterIndex < it.durChapterIndex ||
|
|
(progress.durChapterIndex == it.durChapterIndex
|
|
&& progress.durChapterPos < it.durChapterPos)
|
|
) {
|
|
// 服务器没有进度或者进度比服务器快,上传现有进度
|
|
Coroutine.async {
|
|
AppWebDav.uploadBookProgress(BookProgress(it), uploadSuccessAction)
|
|
it.update()
|
|
}
|
|
} else if (progress.durChapterIndex > it.durChapterIndex ||
|
|
progress.durChapterPos > it.durChapterPos
|
|
) {
|
|
// 进度比服务器慢,执行传入动作
|
|
newProgressAction?.invoke(progress)
|
|
} else {
|
|
syncSuccessAction?.invoke()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun upReadTime() {
|
|
executor.execute {
|
|
if (!AppConfig.enableReadRecord) {
|
|
return@execute
|
|
}
|
|
readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime
|
|
readStartTime = System.currentTimeMillis()
|
|
readRecord.lastRead = System.currentTimeMillis()
|
|
appDb.readRecordDao.insert(readRecord)
|
|
}
|
|
}
|
|
|
|
fun upMsg(msg: String?) {
|
|
if (ReadBook.msg != msg) {
|
|
ReadBook.msg = msg
|
|
callBack?.upContent()
|
|
}
|
|
}
|
|
|
|
fun moveToNextPage(): Boolean {
|
|
var hasNextPage = false
|
|
curTextChapter?.let {
|
|
val nextPagePos = it.getNextPageLength(durChapterPos)
|
|
if (nextPagePos >= 0) {
|
|
hasNextPage = true
|
|
it.getPage(durPageIndex)?.removePageAloudSpan()
|
|
durChapterPos = nextPagePos
|
|
callBack?.cancelSelect()
|
|
callBack?.upContent()
|
|
saveRead(true)
|
|
}
|
|
}
|
|
return hasNextPage
|
|
}
|
|
|
|
fun moveToPrevPage(): Boolean {
|
|
var hasPrevPage = false
|
|
curTextChapter?.let {
|
|
val prevPagePos = it.getPrevPageLength(durChapterPos)
|
|
if (prevPagePos >= 0) {
|
|
hasPrevPage = true
|
|
durChapterPos = prevPagePos
|
|
callBack?.upContent()
|
|
saveRead(true)
|
|
}
|
|
}
|
|
return hasPrevPage
|
|
}
|
|
|
|
fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
|
|
if (durChapterIndex < simulatedChapterSize - 1) {
|
|
durChapterPos = 0
|
|
durChapterIndex++
|
|
prevTextChapter = curTextChapter
|
|
curTextChapter = nextTextChapter
|
|
nextTextChapter = null
|
|
if (curTextChapter == null) {
|
|
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
|
|
if (upContentInPlace) callBack?.upContent()
|
|
loadContent(durChapterIndex, upContent, resetPageOffset = false)
|
|
} else if (upContent && upContentInPlace) {
|
|
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
|
|
callBack?.upContent()
|
|
}
|
|
loadContent(durChapterIndex.plus(1), upContent, false)
|
|
saveRead()
|
|
callBack?.upMenuView()
|
|
AppLog.putDebug("moveToNextChapter-curPageChanged()")
|
|
curPageChanged()
|
|
return true
|
|
} else {
|
|
AppLog.putDebug("跳转下一章失败,没有下一章")
|
|
return false
|
|
}
|
|
}
|
|
|
|
suspend fun moveToNextChapterAwait(
|
|
upContent: Boolean,
|
|
upContentInPlace: Boolean = true
|
|
): Boolean {
|
|
if (durChapterIndex < simulatedChapterSize - 1) {
|
|
durChapterPos = 0
|
|
durChapterIndex++
|
|
prevTextChapter = curTextChapter
|
|
curTextChapter = nextTextChapter
|
|
nextTextChapter = null
|
|
if (curTextChapter == null) {
|
|
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
|
|
if (upContentInPlace) callBack?.upContentAwait()
|
|
loadContentAwait(durChapterIndex, upContent, resetPageOffset = false)
|
|
} else if (upContent && upContentInPlace) {
|
|
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
|
|
callBack?.upContentAwait()
|
|
}
|
|
loadContent(durChapterIndex.plus(1), upContent, false)
|
|
saveRead()
|
|
callBack?.upMenuView()
|
|
AppLog.putDebug("moveToNextChapter-curPageChanged()")
|
|
curPageChanged()
|
|
return true
|
|
} else {
|
|
AppLog.putDebug("跳转下一章失败,没有下一章")
|
|
return false
|
|
}
|
|
}
|
|
|
|
fun moveToPrevChapter(
|
|
upContent: Boolean,
|
|
toLast: Boolean = true,
|
|
upContentInPlace: Boolean = true
|
|
): Boolean {
|
|
if (durChapterIndex > 0) {
|
|
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
|
|
durChapterIndex--
|
|
nextTextChapter = curTextChapter
|
|
curTextChapter = prevTextChapter
|
|
prevTextChapter = null
|
|
if (curTextChapter == null) {
|
|
if (upContentInPlace) callBack?.upContent()
|
|
loadContent(durChapterIndex, upContent, resetPageOffset = false)
|
|
} else if (upContent && upContentInPlace) {
|
|
callBack?.upContent()
|
|
}
|
|
loadContent(durChapterIndex.minus(1), upContent, false)
|
|
saveRead()
|
|
callBack?.upMenuView()
|
|
curPageChanged()
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
fun skipToPage(index: Int, success: (() -> Unit)? = null) {
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
callBack?.upContent {
|
|
success?.invoke()
|
|
}
|
|
curPageChanged()
|
|
saveRead(true)
|
|
}
|
|
|
|
fun setPageIndex(index: Int) {
|
|
recycleRecorders(durPageIndex, index)
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
saveRead(true)
|
|
curPageChanged(true)
|
|
}
|
|
|
|
fun recycleRecorders(beforeIndex: Int, afterIndex: Int) {
|
|
if (!AppConfig.optimizeRender) {
|
|
return
|
|
}
|
|
executor.execute {
|
|
val textChapter = curTextChapter ?: return@execute
|
|
if (afterIndex > beforeIndex) {
|
|
textChapter.getPage(afterIndex - 2)?.recycleRecorders()
|
|
}
|
|
if (afterIndex < beforeIndex) {
|
|
textChapter.getPage(afterIndex + 3)?.recycleRecorders()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
|
|
if (index < chapterSize) {
|
|
clearTextChapter()
|
|
callBack?.upContent()
|
|
durChapterIndex = index
|
|
ReadBook.durChapterPos = durChapterPos
|
|
saveRead()
|
|
loadContent(resetPageOffset = true) {
|
|
success?.invoke()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 当前页面变化
|
|
*/
|
|
private fun curPageChanged(pageChanged: Boolean = false) {
|
|
callBack?.pageChanged()
|
|
curTextChapter?.let {
|
|
if (BaseReadAloudService.isRun && it.isCompleted) {
|
|
val scrollPageAnim = pageAnim() == 3
|
|
if (scrollPageAnim && pageChanged) {
|
|
ReadAloud.pause(appCtx)
|
|
} else {
|
|
readAloud(!BaseReadAloudService.pause)
|
|
}
|
|
}
|
|
}
|
|
upReadTime()
|
|
preDownload()
|
|
}
|
|
|
|
/**
|
|
* 朗读
|
|
*/
|
|
fun readAloud(play: Boolean = true, startPos: Int = 0) {
|
|
book ?: return
|
|
val textChapter = curTextChapter ?: return
|
|
if (textChapter.isCompleted) {
|
|
ReadAloud.play(appCtx, play, startPos = startPos)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 当前页数
|
|
*/
|
|
val durPageIndex: Int
|
|
get() {
|
|
return curTextChapter?.getPageIndexByCharIndex(durChapterPos) ?: durChapterPos
|
|
}
|
|
|
|
/**
|
|
* 是否排版到了当前阅读位置
|
|
*/
|
|
val isLayoutAvailable inline get() = durPageIndex >= 0
|
|
|
|
val isScroll inline get() = pageAnim() == scrollPageAnim
|
|
|
|
val contentLoadFinish get() = curTextChapter != null || msg != null
|
|
|
|
/**
|
|
* chapterOnDur: 0为当前页,1为下一页,-1为上一页
|
|
*/
|
|
fun textChapter(chapterOnDur: Int = 0): TextChapter? {
|
|
return when (chapterOnDur) {
|
|
0 -> curTextChapter
|
|
1 -> nextTextChapter
|
|
-1 -> prevTextChapter
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加载当前章节和前后一章内容
|
|
* @param resetPageOffset 滚动阅读是否重置滚动位置
|
|
* @param success 当前章节加载完成回调
|
|
*/
|
|
fun loadContent(
|
|
resetPageOffset: Boolean,
|
|
success: (() -> Unit)? = null
|
|
) {
|
|
loadContent(durChapterIndex, resetPageOffset = resetPageOffset) {
|
|
success?.invoke()
|
|
}
|
|
loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
|
|
loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
|
|
}
|
|
|
|
fun loadOrUpContent() {
|
|
if (curTextChapter == null) {
|
|
loadContent(durChapterIndex)
|
|
} else {
|
|
callBack?.upContent()
|
|
}
|
|
if (nextTextChapter == null) {
|
|
loadContent(durChapterIndex + 1)
|
|
}
|
|
if (prevTextChapter == null) {
|
|
loadContent(durChapterIndex - 1)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加载章节内容
|
|
* @param index 章节序号
|
|
* @param upContent 是否更新视图
|
|
* @param resetPageOffset 滚动阅读是否重置滚动位置
|
|
* @param success 加载完成回调
|
|
*/
|
|
fun loadContent(
|
|
index: Int,
|
|
upContent: Boolean = true,
|
|
resetPageOffset: Boolean = false,
|
|
success: (() -> Unit)? = null
|
|
) {
|
|
Coroutine.async {
|
|
val book = book!!
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return@async
|
|
if (addLoading(index)) {
|
|
BookHelp.getContent(book, chapter)?.let {
|
|
contentLoadFinish(
|
|
book,
|
|
chapter,
|
|
it,
|
|
upContent,
|
|
resetPageOffset,
|
|
success = success
|
|
)
|
|
} ?: download(
|
|
downloadScope,
|
|
chapter,
|
|
resetPageOffset
|
|
)
|
|
}
|
|
}.onError {
|
|
AppLog.put("加载正文出错\n${it.localizedMessage}")
|
|
}
|
|
}
|
|
|
|
suspend fun loadContentAwait(
|
|
index: Int,
|
|
upContent: Boolean = true,
|
|
resetPageOffset: Boolean = false,
|
|
success: (() -> Unit)? = null
|
|
) = withContext(IO) {
|
|
if (addLoading(index)) {
|
|
try {
|
|
val book = book!!
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index)!!
|
|
val content = BookHelp.getContent(book, chapter) ?: downloadAwait(chapter)
|
|
contentLoadFinishAwait(book, chapter, content, upContent, resetPageOffset)
|
|
success?.invoke()
|
|
} catch (e: Exception) {
|
|
AppLog.put("加载正文出错\n${e.localizedMessage}")
|
|
} finally {
|
|
removeLoading(index)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 下载正文
|
|
*/
|
|
private suspend fun downloadIndex(index: Int) {
|
|
if (index < 0) return
|
|
if (index > chapterSize - 1) {
|
|
upToc()
|
|
return
|
|
}
|
|
val book = book ?: return
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return
|
|
if (BookHelp.hasContent(book, chapter)) {
|
|
downloadedChapters.add(chapter.index)
|
|
} else {
|
|
delay(1000)
|
|
if (addLoading(index)) {
|
|
download(downloadScope, chapter, false, preDownloadSemaphore)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 下载正文
|
|
*/
|
|
private fun download(
|
|
scope: CoroutineScope,
|
|
chapter: BookChapter,
|
|
resetPageOffset: Boolean,
|
|
semaphore: Semaphore? = null,
|
|
success: (() -> Unit)? = null
|
|
) {
|
|
val book = book ?: return removeLoading(chapter.index)
|
|
val bookSource = bookSource
|
|
if (bookSource != null) {
|
|
CacheBook.getOrCreate(bookSource, book).download(scope, chapter, semaphore)
|
|
} else {
|
|
val msg = if (book.isLocal) "无内容" else "没有书源"
|
|
contentLoadFinish(
|
|
book,
|
|
chapter,
|
|
"加载正文失败\n$msg",
|
|
resetPageOffset = resetPageOffset,
|
|
success = success
|
|
)
|
|
}
|
|
}
|
|
|
|
private suspend fun downloadAwait(chapter: BookChapter): String {
|
|
val book = book!!
|
|
val bookSource = bookSource
|
|
if (bookSource != null) {
|
|
return CacheBook.getOrCreate(bookSource, book).downloadAwait(chapter)
|
|
} else {
|
|
val msg = if (book.isLocal) "无内容" else "没有书源"
|
|
return "加载正文失败\n$msg"
|
|
}
|
|
}
|
|
|
|
@Synchronized
|
|
private fun addLoading(index: Int): Boolean {
|
|
if (loadingChapters.contains(index)) return false
|
|
loadingChapters.add(index)
|
|
return true
|
|
}
|
|
|
|
@Synchronized
|
|
fun removeLoading(index: Int) {
|
|
loadingChapters.remove(index)
|
|
}
|
|
|
|
/**
|
|
* 内容加载完成
|
|
*/
|
|
fun contentLoadFinish(
|
|
book: Book,
|
|
chapter: BookChapter,
|
|
content: String,
|
|
upContent: Boolean = true,
|
|
resetPageOffset: Boolean,
|
|
canceled: Boolean = false,
|
|
success: (() -> Unit)? = null
|
|
) {
|
|
removeLoading(chapter.index)
|
|
if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
|
|
return
|
|
}
|
|
Coroutine.async {
|
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
val displayTitle = chapter.getDisplayTitle(
|
|
contentProcessor.getTitleReplaceRules(),
|
|
book.getUseReplaceRule()
|
|
)
|
|
val contents = contentProcessor
|
|
.getContent(book, chapter, content, includeTitle = false)
|
|
val textChapter = ChapterProvider.getTextChapterAsync(
|
|
this@ReadBook, book, chapter, displayTitle, contents, simulatedChapterSize
|
|
)
|
|
when (val offset = chapter.index - durChapterIndex) {
|
|
0 -> {
|
|
curTextChapter = textChapter
|
|
callBack?.upMenuView()
|
|
var available = false
|
|
for (page in textChapter.layoutChannel) {
|
|
val index = page.index
|
|
if (!available && page.containPos(durChapterPos)) {
|
|
if (upContent) {
|
|
callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
available = true
|
|
}
|
|
if (upContent && isScroll) {
|
|
if (max(index - 3, 0) < durPageIndex) {
|
|
callBack?.upContent(offset, false)
|
|
}
|
|
}
|
|
callBack?.onLayoutPageCompleted(index, page)
|
|
}
|
|
if (upContent) callBack?.upContent(offset, !available && resetPageOffset)
|
|
curPageChanged()
|
|
callBack?.contentLoadFinish()
|
|
}
|
|
|
|
-1 -> {
|
|
prevTextChapter = textChapter
|
|
textChapter.layoutChannel.receiveAsFlow().collect()
|
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
|
|
1 -> {
|
|
nextTextChapter = textChapter
|
|
for (page in textChapter.layoutChannel) {
|
|
if (page.index > 1) {
|
|
continue
|
|
}
|
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
}
|
|
}
|
|
|
|
return@async
|
|
}.onError {
|
|
AppLog.put("ChapterProvider ERROR", it)
|
|
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
|
|
}.onSuccess {
|
|
success?.invoke()
|
|
}
|
|
}
|
|
|
|
suspend fun contentLoadFinishAwait(
|
|
book: Book,
|
|
chapter: BookChapter,
|
|
content: String,
|
|
upContent: Boolean = true,
|
|
resetPageOffset: Boolean
|
|
) {
|
|
removeLoading(chapter.index)
|
|
if (chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
|
|
return
|
|
}
|
|
kotlin.runCatching {
|
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
val displayTitle = chapter.getDisplayTitle(
|
|
contentProcessor.getTitleReplaceRules(),
|
|
book.getUseReplaceRule()
|
|
)
|
|
val contents = contentProcessor
|
|
.getContent(book, chapter, content, includeTitle = false)
|
|
val textChapter = ChapterProvider.getTextChapterAsync(
|
|
this@ReadBook, book, chapter, displayTitle, contents, simulatedChapterSize
|
|
)
|
|
when (val offset = chapter.index - durChapterIndex) {
|
|
0 -> {
|
|
curTextChapter = textChapter
|
|
callBack?.upMenuView()
|
|
var available = false
|
|
for (page in textChapter.layoutChannel) {
|
|
val index = page.index
|
|
if (!available && page.containPos(durChapterPos)) {
|
|
if (upContent) {
|
|
callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
available = true
|
|
}
|
|
if (upContent && isScroll) {
|
|
if (max(index - 3, 0) < durPageIndex) {
|
|
callBack?.upContent(offset, false)
|
|
}
|
|
}
|
|
callBack?.onLayoutPageCompleted(index, page)
|
|
}
|
|
if (upContent) callBack?.upContent(offset, !available && resetPageOffset)
|
|
curPageChanged()
|
|
callBack?.contentLoadFinish()
|
|
}
|
|
|
|
-1 -> {
|
|
prevTextChapter = textChapter
|
|
textChapter.layoutChannel.receiveAsFlow().collect()
|
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
|
|
1 -> {
|
|
nextTextChapter = textChapter
|
|
for (page in textChapter.layoutChannel) {
|
|
if (page.index > 1) {
|
|
continue
|
|
}
|
|
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}.onFailure {
|
|
AppLog.put("ChapterProvider ERROR", it)
|
|
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
|
|
}
|
|
}
|
|
|
|
@Synchronized
|
|
fun upToc() {
|
|
val bookSource = bookSource ?: return
|
|
val book = book ?: return
|
|
if (!book.canUpdate) return
|
|
if (System.currentTimeMillis() - book.lastCheckTime < 600000) return
|
|
book.lastCheckTime = System.currentTimeMillis()
|
|
WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList ->
|
|
if (book.bookUrl == ReadBook.book?.bookUrl
|
|
&& cList.size > chapterSize
|
|
) {
|
|
appDb.bookChapterDao.delByBook(book.bookUrl)
|
|
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
|
saveRead()
|
|
chapterSize = cList.size
|
|
simulatedChapterSize = book.simulatedTotalChapterNum()
|
|
nextTextChapter ?: loadContent(durChapterIndex + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun pageAnim(): Int {
|
|
return book?.getPageAnim() ?: ReadBookConfig.pageAnim
|
|
}
|
|
|
|
fun setCharset(charset: String) {
|
|
book?.let {
|
|
it.charset = charset
|
|
callBack?.loadChapterList(it)
|
|
}
|
|
saveRead()
|
|
}
|
|
|
|
fun saveRead(pageChanged: Boolean = false) {
|
|
executor.execute {
|
|
val book = book ?: return@execute
|
|
book.lastCheckCount = 0
|
|
book.durChapterTime = System.currentTimeMillis()
|
|
val chapterChanged = book.durChapterIndex != durChapterIndex
|
|
book.durChapterIndex = durChapterIndex
|
|
book.durChapterPos = durChapterPos
|
|
if (!pageChanged || chapterChanged) {
|
|
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
|
|
book.durChapterTitle = it.getDisplayTitle(
|
|
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
|
|
book.getUseReplaceRule()
|
|
)
|
|
}
|
|
}
|
|
appDb.bookDao.update(book)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 预下载
|
|
*/
|
|
private fun preDownload() {
|
|
if (book?.isLocal == true) return
|
|
executor.execute {
|
|
if (AppConfig.preDownloadNum < 2) {
|
|
return@execute
|
|
}
|
|
preDownloadTask?.cancel()
|
|
preDownloadTask = launch(IO) {
|
|
//预下载
|
|
launch {
|
|
val maxChapterIndex =
|
|
min(durChapterIndex + AppConfig.preDownloadNum, chapterSize)
|
|
for (i in durChapterIndex.plus(2)..maxChapterIndex) {
|
|
if (downloadedChapters.contains(i)) continue
|
|
if ((downloadFailChapters[i] ?: 0) >= 3) continue
|
|
downloadIndex(i)
|
|
}
|
|
}
|
|
launch {
|
|
val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum)
|
|
for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
|
|
if (downloadedChapters.contains(i)) continue
|
|
if ((downloadFailChapters[i] ?: 0) >= 3) continue
|
|
downloadIndex(i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun cancelPreDownloadTask() {
|
|
if (contentLoadFinish) {
|
|
preDownloadTask?.cancel()
|
|
downloadScope.coroutineContext.cancelChildren()
|
|
}
|
|
}
|
|
|
|
fun onChapterListUpdated(newBook: Book) {
|
|
if (newBook.isSameNameAuthor(book)) {
|
|
book = newBook
|
|
chapterSize = newBook.totalChapterNum
|
|
simulatedChapterSize = newBook.simulatedTotalChapterNum()
|
|
if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) {
|
|
durChapterIndex = simulatedChapterSize - 1
|
|
}
|
|
callBack ?: clearTextChapter()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 注册回调
|
|
*/
|
|
fun register(cb: CallBack) {
|
|
callBack?.notifyBookChanged()
|
|
callBack = cb
|
|
}
|
|
|
|
/**
|
|
* 取消注册回调
|
|
*/
|
|
fun unregister(cb: CallBack) {
|
|
if (callBack === cb) {
|
|
callBack = null
|
|
}
|
|
msg = null
|
|
preDownloadTask?.cancel()
|
|
downloadScope.coroutineContext.cancelChildren()
|
|
coroutineContext.cancelChildren()
|
|
ImageProvider.clear()
|
|
if (!CacheBookService.isRun) {
|
|
CacheBook.close()
|
|
}
|
|
}
|
|
|
|
interface CallBack : LayoutProgressListener {
|
|
fun upMenuView()
|
|
|
|
fun loadChapterList(book: Book)
|
|
|
|
fun upContent(
|
|
relativePosition: Int = 0,
|
|
resetPageOffset: Boolean = true,
|
|
success: (() -> Unit)? = null
|
|
)
|
|
|
|
suspend fun upContentAwait(
|
|
relativePosition: Int = 0,
|
|
resetPageOffset: Boolean = true,
|
|
success: (() -> Unit)? = null
|
|
)
|
|
|
|
fun pageChanged()
|
|
|
|
fun contentLoadFinish()
|
|
|
|
fun upPageAnim(upRecorder: Boolean = false)
|
|
|
|
fun notifyBookChanged()
|
|
|
|
fun sureNewProgress(progress: BookProgress)
|
|
|
|
fun cancelSelect()
|
|
}
|
|
|
|
}
|