625 lines
21 KiB
Kotlin
625 lines
21 KiB
Kotlin
package io.legato.kazusa.model
|
|
|
|
import io.legato.kazusa.constant.AppLog
|
|
import io.legato.kazusa.data.appDb
|
|
import io.legato.kazusa.data.entities.Book
|
|
import io.legato.kazusa.data.entities.BookChapter
|
|
import io.legato.kazusa.data.entities.BookProgress
|
|
import io.legato.kazusa.data.entities.BookSource
|
|
import io.legato.kazusa.data.entities.ReadRecord
|
|
import io.legato.kazusa.help.AppWebDav
|
|
import io.legato.kazusa.help.ConcurrentRateLimiter
|
|
import io.legato.kazusa.help.book.BookHelp
|
|
import io.legato.kazusa.help.book.ContentProcessor
|
|
import io.legato.kazusa.help.book.isLocal
|
|
import io.legato.kazusa.help.book.isSameNameAuthor
|
|
import io.legato.kazusa.help.book.readSimulating
|
|
import io.legato.kazusa.help.book.simulatedTotalChapterNum
|
|
import io.legato.kazusa.help.book.update
|
|
import io.legato.kazusa.help.config.AppConfig
|
|
import io.legato.kazusa.help.coroutine.Coroutine
|
|
import io.legato.kazusa.help.globalExecutor
|
|
import io.legato.kazusa.model.webBook.WebBook
|
|
import io.legato.kazusa.ui.book.manga.entities.BaseMangaPage
|
|
import io.legato.kazusa.ui.book.manga.entities.MangaChapter
|
|
import io.legato.kazusa.ui.book.manga.entities.MangaContent
|
|
import io.legato.kazusa.ui.book.manga.entities.MangaPage
|
|
import io.legato.kazusa.ui.book.manga.entities.ReaderLoading
|
|
import io.legato.kazusa.utils.mapIndexed
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.CoroutineStart
|
|
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.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.toList
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
import kotlin.math.min
|
|
|
|
@Suppress("MemberVisibilityCanBePrivate")
|
|
object ReadManga : CoroutineScope by MainScope() {
|
|
var inBookshelf = false
|
|
var book: Book? = null
|
|
val executor = globalExecutor
|
|
var durChapterIndex = 0 //章节位置
|
|
var chapterSize = 0//总章节
|
|
var durChapterPos = 0
|
|
var chapterChanged = false
|
|
var prevMangaChapter: MangaChapter? = null
|
|
var curMangaChapter: MangaChapter? = null
|
|
var nextMangaChapter: MangaChapter? = null
|
|
var bookSource: BookSource? = null
|
|
var readStartTime: Long = System.currentTimeMillis()
|
|
private val readRecord = ReadRecord()
|
|
private val loadingChapters = arrayListOf<Int>()
|
|
var simulatedChapterSize = 0
|
|
var mCallback: Callback? = null
|
|
var preDownloadTask: Job? = null
|
|
val downloadedChapters = hashSetOf<Int>()
|
|
val downloadFailChapters = hashMapOf<Int, Int>()
|
|
val downloadScope = CoroutineScope(SupervisorJob() + IO)
|
|
val preDownloadSemaphore = Semaphore(2)
|
|
var rateLimiter = ConcurrentRateLimiter(null)
|
|
val mangaContents get() = buildMangaContent()
|
|
val hasNextChapter get() = durChapterIndex < simulatedChapterSize - 1
|
|
|
|
fun resetData(book: Book) {
|
|
ReadManga.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
|
|
}
|
|
durChapterIndex = book.durChapterIndex
|
|
durChapterPos = book.durChapterPos
|
|
clearMangaChapter()
|
|
upWebBook(book)
|
|
synchronized(this) {
|
|
loadingChapters.clear()
|
|
downloadedChapters.clear()
|
|
downloadFailChapters.clear()
|
|
}
|
|
}
|
|
|
|
fun upData(book: Book) {
|
|
ReadManga.book = book
|
|
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
|
simulatedChapterSize = if (book.readSimulating()) {
|
|
book.simulatedTotalChapterNum()
|
|
} else {
|
|
chapterSize
|
|
}
|
|
|
|
if (durChapterIndex != book.durChapterIndex) {
|
|
durChapterIndex = book.durChapterIndex
|
|
durChapterPos = book.durChapterPos
|
|
clearMangaChapter()
|
|
}
|
|
upWebBook(book)
|
|
synchronized(this) {
|
|
loadingChapters.clear()
|
|
downloadedChapters.clear()
|
|
downloadFailChapters.clear()
|
|
}
|
|
}
|
|
|
|
fun upWebBook(book: Book) {
|
|
appDb.bookSourceDao.getBookSource(book.origin)?.let {
|
|
bookSource = it
|
|
rateLimiter = ConcurrentRateLimiter(it)
|
|
} ?: let {
|
|
bookSource = null
|
|
}
|
|
}
|
|
|
|
fun clearMangaChapter() {
|
|
prevMangaChapter = null
|
|
curMangaChapter = null
|
|
nextMangaChapter = null
|
|
}
|
|
|
|
//每次切换章节更新阅读记录
|
|
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)
|
|
}
|
|
}
|
|
|
|
@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 loadContent() {
|
|
clearMangaChapter()
|
|
loadContent(durChapterIndex)
|
|
loadContent(durChapterIndex + 1)
|
|
loadContent(durChapterIndex - 1)
|
|
}
|
|
|
|
fun loadOrUpContent() {
|
|
if (curMangaChapter == null) {
|
|
loadContent(durChapterIndex)
|
|
} else {
|
|
mCallback?.upContent()
|
|
}
|
|
if (nextMangaChapter == null) {
|
|
loadContent(durChapterIndex + 1)
|
|
}
|
|
if (prevMangaChapter == null) {
|
|
loadContent(durChapterIndex - 1)
|
|
}
|
|
}
|
|
|
|
private fun loadContent(index: Int) {
|
|
Coroutine.async {
|
|
val book = book!!
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return@async
|
|
if (addLoading(index)) {
|
|
BookHelp.getContent(book, chapter)?.let {
|
|
contentLoadFinish(chapter, it)
|
|
} ?: run {
|
|
download(downloadScope, chapter)
|
|
}
|
|
}
|
|
}.onError {
|
|
AppLog.put("加载正文出错\n${it.localizedMessage}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 内容加载完成
|
|
*/
|
|
suspend fun contentLoadFinish(
|
|
chapter: BookChapter,
|
|
content: String?,
|
|
errorMsg: String = "加载内容失败",
|
|
canceled: Boolean = false
|
|
) {
|
|
removeLoading(chapter.index)
|
|
if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
|
|
return
|
|
}
|
|
when (val offset = chapter.index - durChapterIndex) {
|
|
0 -> {
|
|
if (content == null) {
|
|
mCallback?.loadFail(errorMsg)
|
|
return
|
|
}
|
|
if (content.isEmpty() && !chapter.isVolume) {
|
|
mCallback?.loadFail("正文内容为空")
|
|
return
|
|
}
|
|
val mangaChapter = getManageChapter(chapter, content)
|
|
if (mangaChapter.imageCount == 0 && !chapter.isVolume) {
|
|
mCallback?.loadFail("正文没有图片")
|
|
return
|
|
}
|
|
curMangaChapter = mangaChapter
|
|
mCallback?.upContent()
|
|
}
|
|
|
|
-1, 1 -> {
|
|
if (content == null || (!chapter.isVolume && content.isEmpty())) {
|
|
return
|
|
}
|
|
val mangaChapter = getManageChapter(chapter, content)
|
|
if (mangaChapter.imageCount == 0 && !chapter.isVolume) {
|
|
return
|
|
}
|
|
|
|
when (offset) {
|
|
-1 -> prevMangaChapter = mangaChapter
|
|
1 -> nextMangaChapter = mangaChapter
|
|
}
|
|
|
|
mCallback?.upContent()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun buildMangaContent(): MangaContent {
|
|
val items = arrayListOf<BaseMangaPage>()
|
|
var pos = 0
|
|
var curFinish = false
|
|
var nextFinish = false
|
|
prevMangaChapter?.let {
|
|
pos += it.pages.size
|
|
items.addAll(it.pages)
|
|
}
|
|
curMangaChapter?.let {
|
|
curFinish = true
|
|
items.addAll(it.pages)
|
|
durChapterPos = if (it.imageCount > 0) {
|
|
durChapterPos.coerceIn(0, it.imageCount - 1)
|
|
} else {
|
|
0
|
|
}
|
|
pos += durChapterPos
|
|
if (!AppConfig.hideMangaTitle && it.imageCount > 0) {
|
|
pos++
|
|
}
|
|
}
|
|
nextMangaChapter?.let {
|
|
nextFinish = true
|
|
items.addAll(it.pages)
|
|
}
|
|
return MangaContent(pos, items, curFinish, nextFinish)
|
|
}
|
|
|
|
/**
|
|
* 加载下一章
|
|
*/
|
|
fun moveToNextChapter(toFirst: Boolean = false): Boolean {
|
|
if (durChapterIndex < simulatedChapterSize - 1) {
|
|
if (toFirst) {
|
|
mCallback?.showLoading()
|
|
durChapterPos = 0
|
|
}
|
|
durChapterIndex++
|
|
prevMangaChapter = curMangaChapter
|
|
curMangaChapter = nextMangaChapter
|
|
nextMangaChapter = null
|
|
if (curMangaChapter == null) {
|
|
mCallback?.startLoad()
|
|
loadContent(durChapterIndex)
|
|
} else {
|
|
mCallback?.upContent()
|
|
}
|
|
loadContent(durChapterIndex + 1)
|
|
saveRead()
|
|
AppLog.putDebug("moveToNextChapter-curPageChanged()")
|
|
curPageChanged()
|
|
return true
|
|
} else {
|
|
AppLog.putDebug("跳转下一章失败,没有下一章")
|
|
return false
|
|
}
|
|
}
|
|
|
|
fun moveToPrevChapter(toFirst: Boolean = false): Boolean {
|
|
if (durChapterIndex > 0) {
|
|
if (toFirst) {
|
|
mCallback?.showLoading()
|
|
durChapterPos = 0
|
|
}
|
|
durChapterIndex--
|
|
nextMangaChapter = curMangaChapter
|
|
curMangaChapter = prevMangaChapter
|
|
prevMangaChapter = null
|
|
if (curMangaChapter == null) {
|
|
loadContent(durChapterIndex)
|
|
} else {
|
|
mCallback?.upContent()
|
|
}
|
|
loadContent(durChapterIndex - 1)
|
|
saveRead()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun curPageChanged() {
|
|
upReadTime()
|
|
preDownload()
|
|
}
|
|
|
|
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 downloadNetworkContent(
|
|
bookSource: BookSource,
|
|
scope: CoroutineScope,
|
|
chapter: BookChapter,
|
|
book: Book,
|
|
semaphore: Semaphore?,
|
|
success: suspend (String) -> Unit = {},
|
|
error: suspend () -> Unit = {},
|
|
cancel: suspend () -> Unit = {},
|
|
) {
|
|
WebBook.getContent(
|
|
scope,
|
|
bookSource,
|
|
book,
|
|
chapter,
|
|
start = CoroutineStart.LAZY,
|
|
executeContext = IO,
|
|
semaphore = semaphore
|
|
).onSuccess { content ->
|
|
success.invoke(content)
|
|
}.onError {
|
|
error.invoke()
|
|
}.onCancel {
|
|
cancel.invoke()
|
|
}.start()
|
|
}
|
|
|
|
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 (curMangaChapter != null && nextMangaChapter != null) {
|
|
preDownloadTask?.cancel()
|
|
downloadScope.coroutineContext.cancelChildren()
|
|
}
|
|
}
|
|
|
|
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, preDownloadSemaphore)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取正文
|
|
*/
|
|
private suspend fun download(
|
|
scope: CoroutineScope,
|
|
chapter: BookChapter,
|
|
semaphore: Semaphore? = null,
|
|
) {
|
|
val book = book ?: return removeLoading(chapter.index)
|
|
val bookSource = bookSource
|
|
if (bookSource != null) {
|
|
downloadNetworkContent(bookSource, scope, chapter, book, semaphore, success = {
|
|
downloadedChapters.add(chapter.index)
|
|
downloadFailChapters.remove(chapter.index)
|
|
contentLoadFinish(chapter, it)
|
|
}, error = {
|
|
downloadFailChapters[chapter.index] =
|
|
(downloadFailChapters[chapter.index] ?: 0) + 1
|
|
contentLoadFinish(chapter, null)
|
|
}, cancel = {
|
|
contentLoadFinish(chapter, null, canceled = true)
|
|
})
|
|
} else {
|
|
contentLoadFinish(chapter, null, "加载内容失败 没有书源")
|
|
}
|
|
}
|
|
|
|
@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 == ReadManga.book?.bookUrl
|
|
&& cList.size > chapterSize
|
|
) {
|
|
appDb.bookChapterDao.delByBook(book.bookUrl)
|
|
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
|
saveRead()
|
|
chapterSize = cList.size
|
|
simulatedChapterSize = book.simulatedTotalChapterNum()
|
|
nextMangaChapter ?: loadContent(durChapterIndex + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
val book = book ?: return
|
|
Coroutine.async {
|
|
AppWebDav.getBookProgress(book)
|
|
}.onError {
|
|
AppLog.put("拉取阅读进度失败", it)
|
|
}.onSuccess { progress ->
|
|
if (progress == null || progress.durChapterIndex < book.durChapterIndex ||
|
|
(progress.durChapterIndex == book.durChapterIndex
|
|
&& progress.durChapterPos < book.durChapterPos)
|
|
) {
|
|
// 服务器没有进度或者进度比服务器快,上传现有进度
|
|
Coroutine.async {
|
|
AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction)
|
|
book.update()
|
|
}
|
|
} else if (progress.durChapterIndex > book.durChapterIndex ||
|
|
progress.durChapterPos > book.durChapterPos
|
|
) {
|
|
// 进度比服务器慢,执行传入动作
|
|
newProgressAction?.invoke(progress)
|
|
} else {
|
|
syncSuccessAction?.invoke()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun setProgress(progress: BookProgress) {
|
|
if (progress.durChapterIndex < chapterSize &&
|
|
(durChapterIndex != progress.durChapterIndex
|
|
|| durChapterPos != progress.durChapterPos)
|
|
) {
|
|
mCallback?.showLoading()
|
|
if (progress.durChapterIndex == durChapterIndex) {
|
|
durChapterPos = progress.durChapterPos
|
|
mCallback?.upContent()
|
|
} else {
|
|
durChapterIndex = progress.durChapterIndex
|
|
durChapterPos = progress.durChapterPos
|
|
loadContent()
|
|
}
|
|
saveRead()
|
|
}
|
|
}
|
|
|
|
fun showLoading() {
|
|
mCallback?.showLoading()
|
|
}
|
|
|
|
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
|
|
}
|
|
if (mCallback == null) {
|
|
clearMangaChapter()
|
|
} else {
|
|
loadContent()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 注册回调
|
|
*/
|
|
fun register(cb: Callback) {
|
|
mCallback = cb
|
|
}
|
|
|
|
/**
|
|
* 取消注册回调
|
|
*/
|
|
fun unregister(cb: Callback) {
|
|
if (mCallback === cb) {
|
|
mCallback = null
|
|
}
|
|
preDownloadTask?.cancel()
|
|
preDownloadTask = null
|
|
downloadScope.coroutineContext.cancelChildren()
|
|
coroutineContext.cancelChildren()
|
|
}
|
|
|
|
private suspend fun getManageChapter(chapter: BookChapter, content: String): MangaChapter {
|
|
val list = BookHelp.flowImages(chapter, content)
|
|
.distinctUntilChanged().mapIndexed { index, src ->
|
|
MangaPage(
|
|
chapterIndex = chapter.index,
|
|
chapterSize = chapterSize,
|
|
mImageUrl = src,
|
|
index = index,
|
|
mChapterName = chapter.title
|
|
)
|
|
}.toList()
|
|
|
|
val imageCount = list.size
|
|
|
|
list.forEach {
|
|
it.imageCount = imageCount
|
|
}
|
|
|
|
if (AppConfig.hideMangaTitle && imageCount > 0) {
|
|
return MangaChapter(chapter, list, imageCount)
|
|
}
|
|
|
|
val pages = mutableListOf<BaseMangaPage>()
|
|
|
|
if (imageCount == 0 && chapter.isVolume) {
|
|
pages.add(ReaderLoading(chapter.index, -1, chapter.title, true))
|
|
} else {
|
|
pages.add(ReaderLoading(chapter.index, -1, "阅读 ${chapter.title}"))
|
|
pages.addAll(list)
|
|
pages.add(ReaderLoading(chapter.index, imageCount, "已读完 ${chapter.title}"))
|
|
}
|
|
|
|
return MangaChapter(chapter, pages, imageCount)
|
|
}
|
|
|
|
interface Callback {
|
|
fun upContent()
|
|
fun loadFail(msg: String)
|
|
fun sureNewProgress(progress: BookProgress)
|
|
fun showLoading()
|
|
fun startLoad()
|
|
}
|
|
} |