[优化] 重写下载核心
This commit is contained in:
@@ -2,15 +2,36 @@ package io.legado.app.data.repository
|
||||
|
||||
import io.legado.app.data.dao.BookDao
|
||||
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.model.CacheBook
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
import io.legado.app.model.cache.ChapterSelection
|
||||
import splitties.init.appCtx
|
||||
|
||||
class CacheBookDownloadRepository(
|
||||
private val bookDao: BookDao
|
||||
) : BookCacheDownloadGateway {
|
||||
|
||||
override suspend fun start(request: CacheDownloadRequest) {
|
||||
val book = bookDao.getBook(request.bookUrl) ?: return
|
||||
CacheBook.start(appCtx, request, isLocal = book.isLocal)
|
||||
}
|
||||
|
||||
override suspend fun start(bookUrl: String, chapterIndices: List<Int>) {
|
||||
val book = bookDao.getBook(bookUrl) ?: return
|
||||
CacheBook.start(appCtx, book, chapterIndices)
|
||||
start(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Indices(chapterIndices.toSet()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun start(bookUrl: String, startIndex: Int, endIndex: Int) {
|
||||
start(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Range(startIndex, endIndex),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.legado.app.data.repository.WebDavReadingProgressRepository
|
||||
import io.legado.app.domain.gateway.BookCacheCleanupGateway
|
||||
import io.legado.app.domain.gateway.AppStartupGateway
|
||||
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
||||
import io.legado.app.domain.gateway.BookSearchGateway
|
||||
import io.legado.app.domain.gateway.BookSourceCallbackGateway
|
||||
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
|
||||
import io.legado.app.domain.gateway.LocalBookGateway
|
||||
@@ -46,6 +47,7 @@ import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
||||
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
|
||||
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
|
||||
import io.legado.app.domain.usecase.SearchBooksUseCase
|
||||
import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
|
||||
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
|
||||
import io.legado.app.domain.usecase.UploadReadingProgressUseCase
|
||||
@@ -140,7 +142,12 @@ val appModule = module {
|
||||
single<ReadingProgressGateway> { WebDavReadingProgressRepository() }
|
||||
single<BookDomainRepository> { BookDomainRepositoryImpl(get(), get()) }
|
||||
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
||||
single<SearchRepository> { SearchRepositoryImpl(get()) }
|
||||
single {
|
||||
SearchRepositoryImpl(get())
|
||||
}
|
||||
single<SearchRepository> { get<SearchRepositoryImpl>() }
|
||||
single<BookSearchGateway> { get<SearchRepositoryImpl>() }
|
||||
singleOf(::SearchBooksUseCase)
|
||||
|
||||
single<ImageLoader> {
|
||||
ImageLoader.Builder(get())
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package io.legado.app.domain.gateway
|
||||
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
|
||||
interface BookCacheDownloadGateway {
|
||||
suspend fun start(request: CacheDownloadRequest)
|
||||
suspend fun start(bookUrl: String, chapterIndices: List<Int>)
|
||||
suspend fun start(bookUrl: String, startIndex: Int, endIndex: Int)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package io.legado.app.domain.usecase
|
||||
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
||||
import io.legado.app.domain.model.CacheableBook
|
||||
import io.legado.app.domain.repository.BookDomainRepository
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
import io.legado.app.model.cache.CacheDownloadSource
|
||||
import io.legado.app.model.cache.ChapterSelection
|
||||
|
||||
class BatchCacheDownloadUseCase(
|
||||
private val bookRepository: BookDomainRepository,
|
||||
@@ -31,13 +34,16 @@ class BatchCacheDownloadUseCase(
|
||||
): Boolean {
|
||||
if (book.isLocal) return false
|
||||
if (skipAudioBooks && book.isAudio) return false
|
||||
val indices = if (downloadAllChapters) {
|
||||
(0..book.lastChapterIndex).toList()
|
||||
} else {
|
||||
(book.durChapterIndex..book.lastChapterIndex).toList()
|
||||
}
|
||||
if (indices.isEmpty()) return false
|
||||
bookCacheDownloadGateway.start(book.bookUrl, indices)
|
||||
val startIndex = if (downloadAllChapters) 0 else book.durChapterIndex
|
||||
val endIndex = book.lastChapterIndex
|
||||
if (endIndex < startIndex) return false
|
||||
bookCacheDownloadGateway.start(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = book.bookUrl,
|
||||
selection = ChapterSelection.Range(startIndex, endIndex),
|
||||
source = CacheDownloadSource.Batch,
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.legado.app.domain.usecase
|
||||
|
||||
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
import io.legado.app.model.cache.CacheDownloadSource
|
||||
import io.legado.app.model.cache.ChapterSelection
|
||||
|
||||
class CacheBookChaptersUseCase(
|
||||
private val bookCacheDownloadGateway: BookCacheDownloadGateway
|
||||
@@ -9,7 +12,25 @@ class CacheBookChaptersUseCase(
|
||||
suspend fun execute(bookUrl: String, chapterIndices: Iterable<Int>): Int {
|
||||
val indices = chapterIndices.distinct()
|
||||
if (indices.isEmpty()) return 0
|
||||
bookCacheDownloadGateway.start(bookUrl, indices)
|
||||
bookCacheDownloadGateway.start(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Indices(indices.toSet()),
|
||||
source = CacheDownloadSource.Manual,
|
||||
)
|
||||
)
|
||||
return indices.size
|
||||
}
|
||||
|
||||
suspend fun executeRange(bookUrl: String, startIndex: Int, endIndex: Int): Int {
|
||||
if (endIndex < startIndex) return 0
|
||||
bookCacheDownloadGateway.start(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Range(startIndex, endIndex),
|
||||
source = CacheDownloadSource.Manual,
|
||||
)
|
||||
)
|
||||
return endIndex - startIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,26 @@ package io.legado.app.model
|
||||
|
||||
import android.content.Context
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.IntentAction
|
||||
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.BookSource
|
||||
import io.legado.app.exception.ConcurrentException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.model.cache.CacheDownloadQueue
|
||||
import io.legado.app.model.cache.CacheDownloadRepository
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
import io.legado.app.model.cache.CacheDownloadSource
|
||||
import io.legado.app.model.cache.CacheDownloadStateStore
|
||||
import io.legado.app.model.cache.ChapterSelection
|
||||
import io.legado.app.model.cache.ReadingCacheEvent
|
||||
import io.legado.app.model.cache.ReadingCacheEvents
|
||||
import io.legado.app.service.CacheBookService
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.utils.onEachParallel
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.startService
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -75,20 +79,20 @@ object CacheBook {
|
||||
if (!emitted) delay(800)
|
||||
}
|
||||
}.onStart {
|
||||
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
|
||||
updateSummary()
|
||||
}.onEachParallel(OtherConfig.cacheBookThreadCount.coerceAtLeast(1)) {
|
||||
coroutineScope {
|
||||
it.download(this, context)
|
||||
}
|
||||
}.onCompletion {
|
||||
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
|
||||
updateSummary()
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private val coordinator = CacheBookCoordinator()
|
||||
private val stateStore = CacheDownloadStateStore()
|
||||
val downloadStateFlow = stateStore.stateFlow
|
||||
|
||||
private val _cacheSuccessFlow = MutableSharedFlow<BookChapter>(extraBufferCapacity = 64)
|
||||
val cacheSuccessFlow = _cacheSuccessFlow.asSharedFlow()
|
||||
@@ -117,7 +121,7 @@ object CacheBook {
|
||||
get() = coordinator.taskMap
|
||||
|
||||
fun errorIndices(bookUrl: String): Set<Int> {
|
||||
return errorIndexMap[bookUrl]?.toSet().orEmpty()
|
||||
return stateStore.bookState(bookUrl)?.failedIndices.orEmpty()
|
||||
}
|
||||
|
||||
private fun collectQueueStats(): QueueStats {
|
||||
@@ -168,11 +172,54 @@ object CacheBook {
|
||||
}
|
||||
|
||||
fun start(context: Context, book: Book, selectedIndices: List<Int>) {
|
||||
if (!book.isLocal && selectedIndices.isNotEmpty()) {
|
||||
context.startService<CacheBookService> {
|
||||
action = IntentAction.start
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
putIntegerArrayListExtra("indices", ArrayList(selectedIndices))
|
||||
start(
|
||||
context = context,
|
||||
request = CacheDownloadRequest(
|
||||
bookUrl = book.bookUrl,
|
||||
selection = ChapterSelection.Indices(selectedIndices.toSet()),
|
||||
),
|
||||
isLocal = book.isLocal,
|
||||
)
|
||||
}
|
||||
|
||||
fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
|
||||
start(
|
||||
context = context,
|
||||
request = CacheDownloadRequest(
|
||||
bookUrl = book.bookUrl,
|
||||
selection = ChapterSelection.Range(startIndex, endIndex),
|
||||
),
|
||||
isLocal = book.isLocal,
|
||||
)
|
||||
}
|
||||
|
||||
fun start(context: Context, request: CacheDownloadRequest, isLocal: Boolean = false) {
|
||||
if (isLocal) return
|
||||
when (val selection = request.selection) {
|
||||
is ChapterSelection.Range -> {
|
||||
if (selection.end < selection.start) return
|
||||
}
|
||||
is ChapterSelection.Indices -> {
|
||||
if (selection.values.isEmpty()) return
|
||||
}
|
||||
is ChapterSelection.Single -> Unit
|
||||
}
|
||||
context.startService<CacheBookService> {
|
||||
action = IntentAction.start
|
||||
putExtra("bookUrl", request.bookUrl)
|
||||
putExtra("source", request.source.name)
|
||||
when (val selection = request.selection) {
|
||||
is ChapterSelection.Range -> {
|
||||
putExtra("start", selection.start)
|
||||
putExtra("end", selection.end)
|
||||
}
|
||||
is ChapterSelection.Indices -> {
|
||||
putIntegerArrayListExtra("indices", ArrayList(selection.values))
|
||||
}
|
||||
is ChapterSelection.Single -> {
|
||||
putExtra("start", selection.index)
|
||||
putExtra("end", selection.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,9 +234,9 @@ object CacheBook {
|
||||
fun removeBook(bookUrl: String): Boolean {
|
||||
val model = cacheBookMap.remove(bookUrl) ?: return false
|
||||
model.stop()
|
||||
stateStore.removeBook(bookUrl)
|
||||
updateSummary()
|
||||
_queueChangedFlow.tryEmit(bookUrl)
|
||||
postEvent(EventBus.UP_DOWNLOAD, bookUrl)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -211,6 +258,7 @@ object CacheBook {
|
||||
successDownloadSet.clear()
|
||||
errorDownloadMap.clear()
|
||||
errorIndexMap.clear()
|
||||
stateStore.clear()
|
||||
updateSummary()
|
||||
}
|
||||
|
||||
@@ -241,15 +289,26 @@ object CacheBook {
|
||||
get() = lastQueueStats.waitingCount > 0 || lastQueueStats.downloadingCount > 0
|
||||
|
||||
private fun onTaskQueuesChanged(bookUrl: String) {
|
||||
cacheBookMap[bookUrl]?.let { model ->
|
||||
stateStore.updateBookQueue(
|
||||
bookUrl = bookUrl,
|
||||
waitingCount = model.queueCounts().first,
|
||||
runningIndices = model.downloadingIndices(),
|
||||
)
|
||||
_downloadingIndicesFlow.tryEmit(bookUrl to model.downloadingIndices())
|
||||
_downloadErrorFlow.tryEmit(bookUrl to errorIndices(bookUrl))
|
||||
}
|
||||
updateSummary()
|
||||
_queueChangedFlow.tryEmit(bookUrl)
|
||||
postEvent(EventBus.UP_DOWNLOAD, bookUrl)
|
||||
}
|
||||
|
||||
private fun onTaskRemoved(bookUrl: String) {
|
||||
private fun onTaskRemoved(bookUrl: String, clearState: Boolean = false) {
|
||||
cacheBookMap.remove(bookUrl)
|
||||
if (clearState) {
|
||||
stateStore.removeBook(bookUrl)
|
||||
}
|
||||
updateSummary()
|
||||
postEvent(EventBus.UP_DOWNLOAD, bookUrl)
|
||||
_queueChangedFlow.tryEmit(bookUrl)
|
||||
}
|
||||
|
||||
class CacheBookModel(
|
||||
@@ -257,40 +316,50 @@ object CacheBook {
|
||||
@Volatile var book: Book
|
||||
) {
|
||||
|
||||
private val waitDownloadSet = linkedSetOf<Int>()
|
||||
private val queue = CacheDownloadQueue()
|
||||
private val onDownloadSet = linkedSetOf<Int>()
|
||||
private val pausedDownloadSet = hashSetOf<Int>()
|
||||
private val chapterTasks = hashMapOf<Int, Coroutine<*>>()
|
||||
private val tasks = CompositeCoroutine()
|
||||
private val repository = CacheDownloadRepository()
|
||||
private var isStopped = false
|
||||
private var waitingRetry = false
|
||||
private var isLoading = false
|
||||
|
||||
init {
|
||||
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
|
||||
}
|
||||
|
||||
private fun notifyDownloadSetChanged() {
|
||||
_downloadingIndicesFlow.tryEmit(book.bookUrl to onDownloadSet.toSet())
|
||||
if (cacheBookMap[book.bookUrl] === this) {
|
||||
stateStore.updateBookQueue(
|
||||
bookUrl = book.bookUrl,
|
||||
waitingCount = queue.waitingCount(),
|
||||
runningIndices = onDownloadSet.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyErrorChanged() {
|
||||
val errors = errorIndexMap[book.bookUrl]?.toSet() ?: emptySet()
|
||||
val errors = errorIndices(book.bookUrl)
|
||||
_downloadErrorFlow.tryEmit(book.bookUrl to errors)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun queueCounts(): Pair<Int, Int> = waitDownloadSet.size to onDownloadSet.size
|
||||
fun queueCounts(): Pair<Int, Int> = queue.waitingCount() to onDownloadSet.size
|
||||
|
||||
@Synchronized
|
||||
fun waitingIndices(): Set<Int> = waitDownloadSet.toSet()
|
||||
fun isWaiting(index: Int): Boolean = queue.isWaiting(index)
|
||||
|
||||
@Synchronized
|
||||
fun isDownloading(index: Int): Boolean = onDownloadSet.contains(index)
|
||||
|
||||
@Synchronized
|
||||
fun waitingIndices(): Set<Int> = queue.waitingIndices()
|
||||
|
||||
@Synchronized
|
||||
fun downloadingIndices(): Set<Int> = onDownloadSet.toSet()
|
||||
|
||||
@Synchronized
|
||||
fun isRun(): Boolean {
|
||||
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading
|
||||
return queue.waitingCount() > 0 || onDownloadSet.isNotEmpty() || isLoading
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -309,7 +378,7 @@ object CacheBook {
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
waitDownloadSet.clear()
|
||||
queue.clear()
|
||||
pausedDownloadSet.clear()
|
||||
chapterTasks.clear()
|
||||
tasks.clear()
|
||||
@@ -322,18 +391,39 @@ object CacheBook {
|
||||
|
||||
@Synchronized
|
||||
fun addDownload(start: Int, end: Int) {
|
||||
addDownloads(start..end)
|
||||
addRequest(
|
||||
CacheDownloadRequest(
|
||||
book.bookUrl,
|
||||
ChapterSelection.Range(start, end),
|
||||
CacheDownloadSource.ReadPreload,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addDownloads(indices: Iterable<Int>) {
|
||||
val values = indices.toSet()
|
||||
if (values.isEmpty()) return
|
||||
addRequest(
|
||||
CacheDownloadRequest(
|
||||
book.bookUrl,
|
||||
ChapterSelection.Indices(values),
|
||||
CacheDownloadSource.Manual,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addRequest(request: CacheDownloadRequest) {
|
||||
isStopped = false
|
||||
for (i in indices) {
|
||||
pausedDownloadSet.remove(i)
|
||||
if (!onDownloadSet.contains(i)) {
|
||||
waitDownloadSet.add(i)
|
||||
when (val selection = request.selection) {
|
||||
is ChapterSelection.Range -> {
|
||||
pausedDownloadSet.removeAll { it in selection.start..selection.end }
|
||||
}
|
||||
is ChapterSelection.Indices -> selection.values.forEach { pausedDownloadSet.remove(it) }
|
||||
is ChapterSelection.Single -> pausedDownloadSet.remove(selection.index)
|
||||
}
|
||||
queue.enqueue(request)
|
||||
cacheBookMap[book.bookUrl] = this
|
||||
isLoading = false
|
||||
notifyDownloadSetChanged()
|
||||
@@ -351,6 +441,7 @@ object CacheBook {
|
||||
successDownloadSet.add(chapter.primaryStr())
|
||||
errorDownloadMap.remove(chapter.primaryStr())
|
||||
errorIndexMap[book.bookUrl]?.remove(chapter.index)
|
||||
stateStore.markSuccess(book.bookUrl, chapter.index)
|
||||
notifyDownloadSetChanged()
|
||||
notifyErrorChanged()
|
||||
_cacheSuccessFlow.tryEmit(chapter)
|
||||
@@ -363,6 +454,7 @@ object CacheBook {
|
||||
errorDownloadMap.merge(chapter.primaryStr(), 1) { old, inc -> old + inc }
|
||||
errorIndexMap.getOrPut(book.bookUrl) { ConcurrentHashMap.newKeySet() }
|
||||
.add(chapter.index)
|
||||
stateStore.markFailed(book.bookUrl, chapter.index)
|
||||
}
|
||||
onDownloadSet.remove(chapter.index)
|
||||
chapterTasks.remove(chapter.index)
|
||||
@@ -372,7 +464,7 @@ object CacheBook {
|
||||
private fun onPostError(chapter: BookChapter, error: Throwable) {
|
||||
val retryCount = errorDownloadMap[chapter.primaryStr()] ?: 0
|
||||
if (retryCount < 3 && !isStopped) {
|
||||
waitDownloadSet.add(chapter.index)
|
||||
queue.enqueue(ChapterSelection.Single(chapter.index))
|
||||
} else {
|
||||
AppLog.put("下载${book.name}-${chapter.title}失败\n${error.localizedMessage}", error)
|
||||
}
|
||||
@@ -391,14 +483,16 @@ object CacheBook {
|
||||
private fun onCancel(index: Int) {
|
||||
onDownloadSet.remove(index)
|
||||
chapterTasks.remove(index)
|
||||
if (!isStopped && !pausedDownloadSet.remove(index)) waitDownloadSet.add(index)
|
||||
if (!isStopped && !pausedDownloadSet.remove(index)) {
|
||||
queue.enqueue(ChapterSelection.Single(index))
|
||||
}
|
||||
notifyDownloadSetChanged()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun onFinally() {
|
||||
val bookUrl = book.bookUrl
|
||||
if (waitDownloadSet.isEmpty() && onDownloadSet.isEmpty()) {
|
||||
if (queue.waitingCount() == 0 && onDownloadSet.isEmpty()) {
|
||||
CacheBook.onTaskRemoved(bookUrl)
|
||||
} else {
|
||||
CacheBook.onTaskQueuesChanged(bookUrl)
|
||||
@@ -408,7 +502,7 @@ object CacheBook {
|
||||
|
||||
@Synchronized
|
||||
fun removeDownload(index: Int): Boolean {
|
||||
val removedWaiting = waitDownloadSet.remove(index)
|
||||
val removedWaiting = queue.removeChapter(index)
|
||||
val task = chapterTasks.remove(index)
|
||||
val removedRunning = onDownloadSet.contains(index) || task != null
|
||||
if (removedRunning) {
|
||||
@@ -420,8 +514,8 @@ object CacheBook {
|
||||
}
|
||||
if (!removedWaiting && !removedRunning) return false
|
||||
notifyDownloadSetChanged()
|
||||
if (waitDownloadSet.isEmpty() && onDownloadSet.isEmpty()) {
|
||||
CacheBook.onTaskRemoved(book.bookUrl)
|
||||
if (queue.waitingCount() == 0 && onDownloadSet.isEmpty()) {
|
||||
CacheBook.onTaskRemoved(book.bookUrl, clearState = true)
|
||||
} else {
|
||||
CacheBook.onTaskQueuesChanged(book.bookUrl)
|
||||
}
|
||||
@@ -433,42 +527,39 @@ object CacheBook {
|
||||
*/
|
||||
@Synchronized
|
||||
fun download(scope: CoroutineScope, context: CoroutineContext) {
|
||||
val chapterIndex = waitDownloadSet.firstOrNull()
|
||||
if (chapterIndex == null) {
|
||||
val candidate = queue.next(book.bookUrl, onDownloadSet)
|
||||
if (candidate == null) {
|
||||
if (!isLoading && onDownloadSet.isEmpty()) {
|
||||
CacheBook.onTaskRemoved(book.bookUrl)
|
||||
}
|
||||
return
|
||||
}
|
||||
val chapterIndex = candidate.chapterIndex
|
||||
if (onDownloadSet.contains(chapterIndex)) {
|
||||
waitDownloadSet.remove(chapterIndex)
|
||||
return
|
||||
}
|
||||
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, chapterIndex) ?: run {
|
||||
waitDownloadSet.remove(chapterIndex)
|
||||
val chapter = repository.getChapter(book.bookUrl, chapterIndex) ?: run {
|
||||
return
|
||||
}
|
||||
if (chapter.isVolume) {
|
||||
postEvent(EventBus.SAVE_CONTENT, Pair(book, chapter))
|
||||
_cacheSuccessFlow.tryEmit(chapter)
|
||||
waitDownloadSet.remove(chapterIndex)
|
||||
return
|
||||
}
|
||||
if (BookHelp.hasImageContent(book, chapter)) {
|
||||
waitDownloadSet.remove(chapterIndex)
|
||||
if (repository.hasImageContent(book, chapter)) {
|
||||
return
|
||||
}
|
||||
|
||||
waitDownloadSet.remove(chapterIndex)
|
||||
onDownloadSet.add(chapterIndex)
|
||||
notifyDownloadSetChanged()
|
||||
|
||||
if (BookHelp.hasContent(book, chapter)) {
|
||||
val task = Coroutine.async(scope, context, executeContext = context) {
|
||||
BookHelp.getContent(book, chapter)?.let {
|
||||
BookHelp.saveImages(bookSource, book, chapter, it, 1)
|
||||
}
|
||||
}.onSuccess {
|
||||
if (repository.hasContent(book, chapter)) {
|
||||
val task = repository.saveCachedImagesTask(
|
||||
scope = scope,
|
||||
context = context,
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
chapter = chapter,
|
||||
).onSuccess {
|
||||
onSuccess(chapter)
|
||||
}.onError {
|
||||
onPreError(chapter, it)
|
||||
@@ -485,14 +576,14 @@ object CacheBook {
|
||||
return
|
||||
}
|
||||
|
||||
val task = WebBook.getContent(
|
||||
scope,
|
||||
bookSource,
|
||||
book,
|
||||
chapter,
|
||||
val task = repository.downloadContentTask(
|
||||
scope = scope,
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
chapter = chapter,
|
||||
context = context,
|
||||
start = CoroutineStart.LAZY,
|
||||
executeContext = context
|
||||
executeContext = context,
|
||||
).onSuccess { content ->
|
||||
onSuccess(chapter)
|
||||
downloadFinish(chapter, content)
|
||||
@@ -515,11 +606,11 @@ object CacheBook {
|
||||
suspend fun downloadAwait(chapter: BookChapter): String {
|
||||
synchronized(this) {
|
||||
onDownloadSet.add(chapter.index)
|
||||
waitDownloadSet.remove(chapter.index)
|
||||
queue.removeChapter(chapter.index)
|
||||
notifyDownloadSetChanged()
|
||||
}
|
||||
try {
|
||||
val content = WebBook.getContentAwait(bookSource, book, chapter)
|
||||
val content = repository.downloadContentAwait(bookSource, book, chapter)
|
||||
onSuccess(chapter)
|
||||
ReadBook.downloadedChapters.add(chapter.index)
|
||||
ReadBook.downloadFailChapters.remove(chapter.index)
|
||||
@@ -544,15 +635,16 @@ object CacheBook {
|
||||
) {
|
||||
if (onDownloadSet.contains(chapter.index)) return
|
||||
onDownloadSet.add(chapter.index)
|
||||
waitDownloadSet.remove(chapter.index)
|
||||
queue.removeChapter(chapter.index)
|
||||
notifyDownloadSetChanged()
|
||||
|
||||
WebBook.getContent(
|
||||
scope,
|
||||
bookSource,
|
||||
book,
|
||||
chapter,
|
||||
repository.downloadContentTask(
|
||||
scope = scope,
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
chapter = chapter,
|
||||
start = CoroutineStart.LAZY,
|
||||
context = IO,
|
||||
executeContext = IO,
|
||||
semaphore = semaphore
|
||||
).onSuccess { content ->
|
||||
@@ -579,15 +671,15 @@ object CacheBook {
|
||||
resetPageOffset: Boolean = false,
|
||||
canceled: Boolean = false
|
||||
) {
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.contentLoadFinish(
|
||||
ReadingCacheEvents.emit(
|
||||
ReadingCacheEvent.ContentReady(
|
||||
book = book,
|
||||
chapter = chapter,
|
||||
content = content,
|
||||
resetPageOffset = resetPageOffset,
|
||||
canceled = canceled
|
||||
canceled = canceled,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ 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.cache.ReadingCacheEvent
|
||||
import io.legado.app.model.cache.ReadingCacheEvents
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.service.BaseReadAloudService
|
||||
import io.legado.app.service.CacheBookService
|
||||
@@ -88,6 +90,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
||||
private val nextChapterLoadingLock = Mutex()
|
||||
var readStartTime: Long = System.currentTimeMillis()
|
||||
|
||||
init {
|
||||
launch {
|
||||
ReadingCacheEvents.events.collect { event ->
|
||||
when (event) {
|
||||
is ReadingCacheEvent.ContentReady -> {
|
||||
if (book?.bookUrl == event.book.bookUrl) {
|
||||
contentLoadFinish(
|
||||
book = event.book,
|
||||
chapter = event.chapter,
|
||||
content = event.content,
|
||||
resetPageOffset = event.resetPageOffset,
|
||||
canceled = event.canceled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 跳转进度前进度记录 */
|
||||
var lastBookProgress: BookProgress? = null
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.legado.app.model.cache
|
||||
|
||||
data class CacheDownloadRequest(
|
||||
val bookUrl: String,
|
||||
val selection: ChapterSelection,
|
||||
val source: CacheDownloadSource = CacheDownloadSource.Manual,
|
||||
)
|
||||
|
||||
sealed interface ChapterSelection {
|
||||
data class Range(val start: Int, val end: Int) : ChapterSelection
|
||||
data class Indices(val values: Set<Int>) : ChapterSelection
|
||||
data class Single(val index: Int) : ChapterSelection
|
||||
}
|
||||
|
||||
enum class CacheDownloadSource {
|
||||
Manual,
|
||||
Batch,
|
||||
ReadPreload,
|
||||
}
|
||||
|
||||
data class CacheDownloadCandidate(
|
||||
val bookUrl: String,
|
||||
val chapterIndex: Int,
|
||||
)
|
||||
|
||||
data class CacheDownloadQueueSnapshot(
|
||||
val waitingCount: Int,
|
||||
)
|
||||
|
||||
data class CacheDownloadState(
|
||||
val isRunning: Boolean = false,
|
||||
val totalWaiting: Int = 0,
|
||||
val totalRunning: Int = 0,
|
||||
val totalSuccess: Int = 0,
|
||||
val totalFailure: Int = 0,
|
||||
val books: Map<String, CacheBookDownloadState> = emptyMap(),
|
||||
)
|
||||
|
||||
data class CacheBookDownloadState(
|
||||
val bookUrl: String,
|
||||
val waitingCount: Int = 0,
|
||||
val runningIndices: Set<Int> = emptySet(),
|
||||
val failedIndices: Set<Int> = emptySet(),
|
||||
val successIndices: Set<Int> = emptySet(),
|
||||
val successCount: Int = 0,
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
package io.legado.app.model.cache
|
||||
|
||||
class CacheDownloadQueue {
|
||||
|
||||
private data class RangeCursor(
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
var next: Int = start,
|
||||
) {
|
||||
fun contains(index: Int): Boolean = index in next..end
|
||||
fun remainingCount(
|
||||
emittedIndices: Set<Int>,
|
||||
removedIndices: Set<Int>,
|
||||
): Int {
|
||||
if (next > end) return 0
|
||||
val rawCount = end - next + 1
|
||||
val emittedCount = emittedIndices.count { it in next..end }
|
||||
val removedCount = removedIndices.count { it in next..end && it !in emittedIndices }
|
||||
val excludedCount = emittedCount + removedCount
|
||||
return rawCount - excludedCount
|
||||
}
|
||||
}
|
||||
|
||||
private val ranges = ArrayDeque<RangeCursor>()
|
||||
private val indices = linkedSetOf<Int>()
|
||||
private val emittedIndices = hashSetOf<Int>()
|
||||
private val removedIndices = hashSetOf<Int>()
|
||||
|
||||
fun enqueue(request: CacheDownloadRequest) {
|
||||
enqueue(request.selection)
|
||||
}
|
||||
|
||||
fun enqueue(selection: ChapterSelection) {
|
||||
when (selection) {
|
||||
is ChapterSelection.Range -> addRange(selection.start, selection.end)
|
||||
is ChapterSelection.Indices -> addIndices(selection.values)
|
||||
is ChapterSelection.Single -> addIndex(selection.index)
|
||||
}
|
||||
}
|
||||
|
||||
fun next(bookUrl: String, runningIndices: Set<Int>): CacheDownloadCandidate? {
|
||||
while (indices.isNotEmpty()) {
|
||||
val index = indices.first()
|
||||
indices.remove(index)
|
||||
if (index in runningIndices || index in removedIndices) continue
|
||||
emittedIndices.add(index)
|
||||
return CacheDownloadCandidate(bookUrl, index)
|
||||
}
|
||||
|
||||
while (ranges.isNotEmpty()) {
|
||||
val cursor = ranges.first()
|
||||
while (cursor.next <= cursor.end) {
|
||||
val index = cursor.next++
|
||||
if (index in removedIndices || index in runningIndices) continue
|
||||
if (emittedIndices.add(index)) {
|
||||
return CacheDownloadCandidate(bookUrl, index)
|
||||
}
|
||||
}
|
||||
ranges.removeFirst()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeChapter(index: Int): Boolean {
|
||||
val removed = indices.remove(index) || isWaiting(index)
|
||||
removedIndices.add(index)
|
||||
return removed
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
ranges.clear()
|
||||
indices.clear()
|
||||
emittedIndices.clear()
|
||||
removedIndices.clear()
|
||||
}
|
||||
|
||||
fun snapshot(): CacheDownloadQueueSnapshot {
|
||||
return CacheDownloadQueueSnapshot(waitingCount = waitingCount())
|
||||
}
|
||||
|
||||
fun waitingCount(): Int {
|
||||
val indexCount = indices.count { it !in emittedIndices && it !in removedIndices }
|
||||
val rangeCount = ranges.sumOf { it.remainingCount(emittedIndices, removedIndices) }
|
||||
return indexCount + rangeCount
|
||||
}
|
||||
|
||||
fun isWaiting(index: Int): Boolean {
|
||||
if (index in emittedIndices || index in removedIndices) return false
|
||||
return indices.contains(index) || ranges.any { it.contains(index) }
|
||||
}
|
||||
|
||||
fun waitingIndices(): Set<Int> {
|
||||
return buildSet {
|
||||
indices.filterTo(this) { it !in emittedIndices && it !in removedIndices }
|
||||
ranges.forEach { cursor ->
|
||||
for (index in cursor.next..cursor.end) {
|
||||
if (index !in emittedIndices && index !in removedIndices) add(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addRange(start: Int, end: Int) {
|
||||
if (end < start) return
|
||||
removedIndices.removeAll { it in start..end }
|
||||
ranges.add(RangeCursor(start, end))
|
||||
}
|
||||
|
||||
private fun addIndices(values: Iterable<Int>) {
|
||||
values.forEach { addIndex(it) }
|
||||
}
|
||||
|
||||
private fun addIndex(index: Int) {
|
||||
emittedIndices.remove(index)
|
||||
removedIndices.remove(index)
|
||||
indices.add(index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package io.legado.app.model.cache
|
||||
|
||||
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.BookSource
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class CacheDownloadRepository {
|
||||
|
||||
fun getChapter(bookUrl: String, index: Int): BookChapter? {
|
||||
return appDb.bookChapterDao.getChapter(bookUrl, index)
|
||||
}
|
||||
|
||||
fun hasImageContent(book: Book, chapter: BookChapter): Boolean {
|
||||
return BookHelp.hasImageContent(book, chapter)
|
||||
}
|
||||
|
||||
fun hasContent(book: Book, chapter: BookChapter): Boolean {
|
||||
return BookHelp.hasContent(book, chapter)
|
||||
}
|
||||
|
||||
fun saveCachedImagesTask(
|
||||
scope: CoroutineScope,
|
||||
context: CoroutineContext,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
chapter: BookChapter,
|
||||
): Coroutine<Unit> {
|
||||
return Coroutine.async(scope, context, executeContext = context) {
|
||||
BookHelp.getContent(book, chapter)?.let {
|
||||
BookHelp.saveImages(bookSource, book, chapter, it, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadContentTask(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
chapter: BookChapter,
|
||||
context: CoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.LAZY,
|
||||
executeContext: CoroutineContext = context,
|
||||
semaphore: Semaphore? = null,
|
||||
): Coroutine<String> {
|
||||
return WebBook.getContent(
|
||||
scope = scope,
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
bookChapter = chapter,
|
||||
context = context,
|
||||
start = start,
|
||||
executeContext = executeContext,
|
||||
semaphore = semaphore,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun downloadContentAwait(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
chapter: BookChapter,
|
||||
): String {
|
||||
return WebBook.getContentAwait(bookSource, book, chapter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.legado.app.model.cache
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class CacheDownloadStateStore {
|
||||
|
||||
private val _stateFlow = MutableStateFlow(CacheDownloadState())
|
||||
val stateFlow = _stateFlow.asStateFlow()
|
||||
|
||||
val state: CacheDownloadState
|
||||
get() = _stateFlow.value
|
||||
|
||||
fun updateBookQueue(
|
||||
bookUrl: String,
|
||||
waitingCount: Int,
|
||||
runningIndices: Set<Int>,
|
||||
) {
|
||||
updateBook(bookUrl) { current ->
|
||||
current.copy(
|
||||
waitingCount = waitingCount,
|
||||
runningIndices = runningIndices,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun markSuccess(bookUrl: String, chapterIndex: Int) {
|
||||
updateBook(bookUrl) { current ->
|
||||
val successIndices = current.successIndices + chapterIndex
|
||||
current.copy(
|
||||
runningIndices = current.runningIndices - chapterIndex,
|
||||
failedIndices = current.failedIndices - chapterIndex,
|
||||
successIndices = successIndices,
|
||||
successCount = successIndices.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun markFailed(bookUrl: String, chapterIndex: Int) {
|
||||
updateBook(bookUrl) { current ->
|
||||
current.copy(
|
||||
runningIndices = current.runningIndices - chapterIndex,
|
||||
failedIndices = current.failedIndices + chapterIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearFailure(bookUrl: String, chapterIndex: Int) {
|
||||
updateBook(bookUrl) { current ->
|
||||
current.copy(failedIndices = current.failedIndices - chapterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeBook(bookUrl: String) {
|
||||
_stateFlow.update { state ->
|
||||
state.copy(books = state.books - bookUrl).recalculate()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_stateFlow.value = CacheDownloadState()
|
||||
}
|
||||
|
||||
fun bookState(bookUrl: String): CacheBookDownloadState? {
|
||||
return state.books[bookUrl]
|
||||
}
|
||||
|
||||
private fun updateBook(
|
||||
bookUrl: String,
|
||||
transform: (CacheBookDownloadState) -> CacheBookDownloadState,
|
||||
) {
|
||||
_stateFlow.update { state ->
|
||||
val current = state.books[bookUrl] ?: CacheBookDownloadState(bookUrl)
|
||||
state.copy(books = state.books + (bookUrl to transform(current))).recalculate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CacheDownloadState.recalculate(): CacheDownloadState {
|
||||
val totalWaiting = books.values.sumOf { it.waitingCount }
|
||||
val totalRunning = books.values.sumOf { it.runningIndices.size }
|
||||
val totalFailure = books.values.sumOf { it.failedIndices.size }
|
||||
val totalSuccess = books.values.sumOf { it.successCount }
|
||||
return copy(
|
||||
isRunning = totalWaiting > 0 || totalRunning > 0,
|
||||
totalWaiting = totalWaiting,
|
||||
totalRunning = totalRunning,
|
||||
totalFailure = totalFailure,
|
||||
totalSuccess = totalSuccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.legado.app.model.cache
|
||||
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
sealed interface ReadingCacheEvent {
|
||||
data class ContentReady(
|
||||
val book: Book,
|
||||
val chapter: BookChapter,
|
||||
val content: String,
|
||||
val resetPageOffset: Boolean,
|
||||
val canceled: Boolean,
|
||||
) : ReadingCacheEvent
|
||||
}
|
||||
|
||||
object ReadingCacheEvents {
|
||||
private val _events = MutableSharedFlow<ReadingCacheEvent>(extraBufferCapacity = 32)
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
fun emit(event: ReadingCacheEvent) {
|
||||
_events.tryEmit(event)
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,18 @@ import io.legado.app.R
|
||||
import io.legado.app.base.BaseService
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.IntentAction
|
||||
import io.legado.app.constant.NotificationId
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.book.update
|
||||
import io.legado.app.model.CacheBook
|
||||
import io.legado.app.model.cache.CacheDownloadRequest
|
||||
import io.legado.app.model.cache.CacheDownloadSource
|
||||
import io.legado.app.model.cache.ChapterSelection
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.ui.main.MainActivity
|
||||
import io.legado.app.utils.activityPendingIntent
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.servicePendingIntent
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
@@ -75,7 +76,6 @@ class CacheBookService : BaseService() {
|
||||
delay(1000)
|
||||
notificationContent = CacheBook.downloadSummary
|
||||
upCacheBookNotification()
|
||||
postEvent(EventBus.UP_DOWNLOAD, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,18 +111,39 @@ class CacheBookService : BaseService() {
|
||||
cachePool.close()
|
||||
CacheBook.close()
|
||||
super.onDestroy()
|
||||
postEvent(EventBus.UP_DOWNLOAD, "")
|
||||
}
|
||||
|
||||
private fun addDownloadData(bookUrl: String?, indices: List<Int>) {
|
||||
private fun addDownloadData(bookUrl: String?, indices: Iterable<Int>) {
|
||||
bookUrl ?: return
|
||||
if (indices.isEmpty()) return
|
||||
val values = indices.toSet()
|
||||
if (values.isEmpty()) return
|
||||
addDownloadRequest(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Indices(values),
|
||||
source = CacheDownloadSource.Manual,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun addDownloadData(bookUrl: String?, start: Int, end: Int) {
|
||||
bookUrl ?: return
|
||||
if (end < start) return
|
||||
addDownloadRequest(
|
||||
CacheDownloadRequest(
|
||||
bookUrl = bookUrl,
|
||||
selection = ChapterSelection.Range(start, end),
|
||||
source = CacheDownloadSource.Manual,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun addDownloadRequest(request: CacheDownloadRequest) {
|
||||
execute {
|
||||
val cacheBook = CacheBook.getOrCreate(bookUrl) ?: return@execute
|
||||
val cacheBook = CacheBook.getOrCreate(request.bookUrl) ?: return@execute
|
||||
|
||||
val book = cacheBook.book
|
||||
val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl)
|
||||
val chapterCount = appDb.bookChapterDao.getChapterCount(request.bookUrl)
|
||||
|
||||
if (chapterCount == 0) {
|
||||
cacheBook.setLoading()
|
||||
@@ -132,7 +153,7 @@ class CacheBookService : BaseService() {
|
||||
kotlin.runCatching {
|
||||
WebBook.getBookInfoAwait(cacheBook.bookSource, book)
|
||||
}.onFailure {
|
||||
removeDownload(bookUrl)
|
||||
removeDownload(request.bookUrl)
|
||||
AppLog.put(
|
||||
"《$name》目录为空且加载详情页失败\n${it.localizedMessage}",
|
||||
it,
|
||||
@@ -147,7 +168,7 @@ class CacheBookService : BaseService() {
|
||||
book.totalChapterNum = 0
|
||||
book.update()
|
||||
}
|
||||
removeDownload(bookUrl)
|
||||
removeDownload(request.bookUrl)
|
||||
AppLog.put(
|
||||
"《$name》目录为空且加载目录失败\n${it.localizedMessage}",
|
||||
it,
|
||||
@@ -163,7 +184,7 @@ class CacheBookService : BaseService() {
|
||||
}
|
||||
|
||||
//添加章节到下载队列
|
||||
cacheBook.addDownloads(indices)
|
||||
cacheBook.addRequest(request)
|
||||
|
||||
notificationContent = CacheBook.downloadSummary
|
||||
upCacheBookNotification()
|
||||
@@ -174,14 +195,9 @@ class CacheBookService : BaseService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDownloadData(bookUrl: String?, start: Int, end: Int) {
|
||||
addDownloadData(bookUrl, (start..end).toList())
|
||||
}
|
||||
|
||||
|
||||
private fun removeDownload(bookUrl: String?) {
|
||||
CacheBook.cacheBookMap[bookUrl]?.stop()
|
||||
postEvent(EventBus.UP_DOWNLOAD, "")
|
||||
if (downloadJob == null && CacheBook.isRun) {
|
||||
download()
|
||||
return
|
||||
|
||||
+82
-39
@@ -19,6 +19,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
@@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.min
|
||||
|
||||
data class BookCacheManageUiState(
|
||||
@@ -94,6 +97,11 @@ class BookCacheManageViewModel(
|
||||
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||
) : BaseViewModel(application) {
|
||||
|
||||
private companion object {
|
||||
const val DOWNLOAD_BATCH_SIZE = 512
|
||||
const val DOWNLOAD_STATUS_REFRESH_INTERVAL_MILLIS = 2_000L
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(BookCacheManageUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
@@ -103,6 +111,9 @@ class BookCacheManageViewModel(
|
||||
private var observeJob: Job? = null
|
||||
private var fullReloadJob: Job? = null
|
||||
private val bookReloadJobs = hashMapOf<String, Job>()
|
||||
private val pendingDownloadRefreshBookUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
@Volatile
|
||||
private var pendingDownloadSummaryRefresh = false
|
||||
|
||||
fun onIntent(intent: BookCacheManageIntent) {
|
||||
when (intent) {
|
||||
@@ -140,32 +151,24 @@ class BookCacheManageViewModel(
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.cacheSuccessFlow.collect { chapter ->
|
||||
scheduleBookReload(chapter.bookUrl)
|
||||
scheduleDownloadStatusRefresh(chapter.bookUrl)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadingIndicesFlow.collect { state ->
|
||||
scheduleBookReload(state.first)
|
||||
CacheBook.downloadStateFlow.collect { state ->
|
||||
state.books.keys.forEach { scheduleDownloadStatusRefresh(it) }
|
||||
pendingDownloadSummaryRefresh = true
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.queueChangedFlow.collect { bookUrl ->
|
||||
scheduleBookReload(bookUrl)
|
||||
scheduleDownloadStatusRefresh(bookUrl)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadErrorFlow.collect { state ->
|
||||
scheduleBookReload(state.first)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadSummaryFlow.collect {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
downloadSummary = buildDownloadSummary(it.shelfBooks + it.notShelfBooks),
|
||||
version = it.version + 1,
|
||||
)
|
||||
}
|
||||
while (isActive) {
|
||||
delay(DOWNLOAD_STATUS_REFRESH_INTERVAL_MILLIS)
|
||||
flushDownloadStatusRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +219,31 @@ class BookCacheManageViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleDownloadStatusRefresh(bookUrl: String) {
|
||||
if (bookUrl.isNotBlank()) {
|
||||
pendingDownloadRefreshBookUrls.add(bookUrl)
|
||||
}
|
||||
pendingDownloadSummaryRefresh = true
|
||||
}
|
||||
|
||||
private suspend fun flushDownloadStatusRefresh() {
|
||||
val bookUrls = pendingDownloadRefreshBookUrls.toList()
|
||||
bookUrls.forEach { pendingDownloadRefreshBookUrls.remove(it) }
|
||||
val shouldRefreshSummary = pendingDownloadSummaryRefresh || bookUrls.isNotEmpty()
|
||||
pendingDownloadSummaryRefresh = false
|
||||
bookUrls.forEach { bookUrl ->
|
||||
reloadBook(bookUrl)
|
||||
}
|
||||
if (shouldRefreshSummary) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
downloadSummary = buildDownloadSummary(it.shelfBooks + it.notShelfBooks),
|
||||
version = it.version + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleBookReload(bookUrl: String, debounceMillis: Long = 80) {
|
||||
if (bookUrl.isBlank()) return
|
||||
bookReloadJobs.remove(bookUrl)?.cancel()
|
||||
@@ -278,8 +306,7 @@ class BookCacheManageViewModel(
|
||||
private fun buildBookItem(book: Book): BookCacheBookItem? {
|
||||
val cacheFiles = BookHelp.getChapterFiles(book)
|
||||
val model = CacheBook.cacheBookMap[book.bookUrl]
|
||||
val waitingIndices = model?.waitingIndices().orEmpty()
|
||||
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
||||
val (waitingCount, downloadingCount) = model?.queueCounts() ?: (0 to 0)
|
||||
val errorIndices = errorIndices(book.bookUrl)
|
||||
val totalCount = bookChapterDao.getChapterCount(book.bookUrl)
|
||||
val cachedFileCount = cacheFiles.count { it.endsWith(".nb") }
|
||||
@@ -293,8 +320,8 @@ class BookCacheManageViewModel(
|
||||
author = book.getRealAuthor(),
|
||||
totalCount = totalCount,
|
||||
cachedCount = cachedCount,
|
||||
waitingCount = waitingIndices.size,
|
||||
downloadingCount = downloadingIndices.size,
|
||||
waitingCount = waitingCount,
|
||||
downloadingCount = downloadingCount,
|
||||
errorCount = errorIndices.size,
|
||||
isNotShelf = book.isNotShelf,
|
||||
)
|
||||
@@ -305,8 +332,6 @@ class BookCacheManageViewModel(
|
||||
val chapters = bookChapterDao.getChapterCacheInfoList(bookUrl)
|
||||
val cacheFiles = BookHelp.getChapterFiles(book)
|
||||
val model = CacheBook.cacheBookMap[bookUrl]
|
||||
val waitingIndices = model?.waitingIndices().orEmpty()
|
||||
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
||||
val errorIndices = errorIndices(bookUrl)
|
||||
return chapters.map { chapter ->
|
||||
BookCacheChapterItem(
|
||||
@@ -314,8 +339,8 @@ class BookCacheManageViewModel(
|
||||
title = chapter.title,
|
||||
index = chapter.index,
|
||||
isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
|
||||
isWaiting = waitingIndices.contains(chapter.index),
|
||||
isDownloading = downloadingIndices.contains(chapter.index),
|
||||
isWaiting = model?.isWaiting(chapter.index) == true,
|
||||
isDownloading = model?.isDownloading(chapter.index) == true,
|
||||
isError = errorIndices.contains(chapter.index),
|
||||
)
|
||||
}
|
||||
@@ -377,10 +402,14 @@ class BookCacheManageViewModel(
|
||||
private fun startAllDownloads() {
|
||||
val items = uiState.value.shelfBooks + uiState.value.notShelfBooks
|
||||
execute {
|
||||
items.sumOf { item ->
|
||||
val chapterIndices = downloadableChapterIndices(item.bookUrl)
|
||||
cacheBookChaptersUseCase.execute(item.bookUrl, chapterIndices)
|
||||
var count = 0
|
||||
items.forEach { item ->
|
||||
downloadableChapterIndexBatches(item.bookUrl).forEach { chapterIndices ->
|
||||
count += cacheBookChaptersUseCase.execute(item.bookUrl, chapterIndices)
|
||||
}
|
||||
currentCoroutineContext().ensureActive()
|
||||
}
|
||||
count
|
||||
}.onSuccess { count ->
|
||||
if (count > 0) {
|
||||
_effects.tryEmit(BookCacheManageEffect.ShowMessage("已加入缓存队列: $count 章"))
|
||||
@@ -396,7 +425,11 @@ class BookCacheManageViewModel(
|
||||
|
||||
private fun startBookDownload(bookUrl: String) {
|
||||
execute {
|
||||
cacheBookChaptersUseCase.execute(bookUrl, downloadableChapterIndices(bookUrl))
|
||||
var count = 0
|
||||
downloadableChapterIndexBatches(bookUrl).forEach { chapterIndices ->
|
||||
count += cacheBookChaptersUseCase.execute(bookUrl, chapterIndices)
|
||||
}
|
||||
count
|
||||
}.onSuccess { count ->
|
||||
if (count > 0) {
|
||||
_effects.tryEmit(BookCacheManageEffect.ShowMessage("已加入缓存队列: $count 章"))
|
||||
@@ -410,22 +443,32 @@ class BookCacheManageViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadableChapterIndices(bookUrl: String): List<Int> {
|
||||
val book = bookDao.getBook(bookUrl) ?: return emptyList()
|
||||
private fun downloadableChapterIndexBatches(
|
||||
bookUrl: String,
|
||||
batchSize: Int = DOWNLOAD_BATCH_SIZE,
|
||||
): Sequence<List<Int>> = sequence {
|
||||
val book = bookDao.getBook(bookUrl) ?: return@sequence
|
||||
val cacheFiles = BookHelp.getChapterFiles(book)
|
||||
val model = CacheBook.cacheBookMap[bookUrl]
|
||||
val waitingIndices = model?.waitingIndices().orEmpty()
|
||||
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
||||
return bookChapterDao.getChapterCacheInfoList(bookUrl)
|
||||
.asSequence()
|
||||
.filterNot { chapter ->
|
||||
var batch = ArrayList<Int>(batchSize)
|
||||
for (chapter in bookChapterDao.getChapterCacheInfoList(bookUrl)) {
|
||||
if (
|
||||
chapter.isVolume ||
|
||||
cacheFiles.contains(chapter.getFileName()) ||
|
||||
waitingIndices.contains(chapter.index) ||
|
||||
downloadingIndices.contains(chapter.index)
|
||||
model?.isWaiting(chapter.index) == true ||
|
||||
model?.isDownloading(chapter.index) == true
|
||||
) {
|
||||
continue
|
||||
}
|
||||
.map { it.index }
|
||||
.toList()
|
||||
batch.add(chapter.index)
|
||||
if (batch.size == batchSize) {
|
||||
yield(batch)
|
||||
batch = ArrayList(batchSize)
|
||||
}
|
||||
}
|
||||
if (batch.isNotEmpty()) {
|
||||
yield(batch)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteBookCache(bookUrl: String) {
|
||||
|
||||
@@ -37,7 +37,9 @@ import io.legado.app.utils.cnCompare
|
||||
import io.legado.app.utils.move
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
@@ -155,6 +157,10 @@ class BookshelfManageScreenViewModel(
|
||||
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
|
||||
) : BaseViewModel(application) {
|
||||
|
||||
private companion object {
|
||||
const val DOWNLOAD_STATUS_REFRESH_INTERVAL_MILLIS = 2_000L
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(BookshelfManageScreenUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
@@ -165,8 +171,11 @@ class BookshelfManageScreenViewModel(
|
||||
private var booksJob: Job? = null
|
||||
private var groupsJob: Job? = null
|
||||
private var cacheLoadJob: Job? = null
|
||||
private val cacheCountJobs = ConcurrentHashMap<String, Job>()
|
||||
private var observersStarted = false
|
||||
private val pendingDownloadStatusBookUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
private val pendingCacheCountRefreshBookUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
@Volatile
|
||||
private var pendingDownloadRunningRefresh = false
|
||||
|
||||
fun dispatch(intent: BookshelfManageScreenIntent) {
|
||||
when (intent) {
|
||||
@@ -367,28 +376,26 @@ class BookshelfManageScreenViewModel(
|
||||
observersStarted = true
|
||||
viewModelScope.launch {
|
||||
CacheBook.cacheSuccessFlow.collect { chapter ->
|
||||
onChapterCached(chapter)
|
||||
scheduleCacheCountRefresh(chapter.bookUrl)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadingIndicesFlow.collect { (bookUrl, _) ->
|
||||
syncDownloadRunning()
|
||||
if (bookUrl.isNotEmpty()) {
|
||||
emitBookChanged(bookUrl)
|
||||
CacheBook.downloadStateFlow.collect { state ->
|
||||
state.books.keys.forEach { bookUrl ->
|
||||
scheduleDownloadStatusRefresh(bookUrl)
|
||||
}
|
||||
scheduleDownloadStatusRefresh()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadErrorFlow.collect { (bookUrl, _) ->
|
||||
syncDownloadRunning()
|
||||
if (bookUrl.isNotEmpty()) {
|
||||
emitBookChanged(bookUrl)
|
||||
}
|
||||
CacheBook.queueChangedFlow.collect { bookUrl ->
|
||||
scheduleDownloadStatusRefresh(bookUrl)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
CacheBook.downloadSummaryFlow.collect {
|
||||
syncDownloadRunning()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
delay(DOWNLOAD_STATUS_REFRESH_INTERVAL_MILLIS)
|
||||
flushDownloadStatusRefresh()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
@@ -432,8 +439,6 @@ class BookshelfManageScreenViewModel(
|
||||
|
||||
private fun loadCacheCounts(books: List<Book>) {
|
||||
cacheLoadJob?.cancel()
|
||||
cacheCountJobs.values.forEach { it.cancel() }
|
||||
cacheCountJobs.clear()
|
||||
cacheLoadJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val visibleBookUrls = books.mapTo(hashSetOf()) { it.bookUrl }
|
||||
cacheCounts.keys.toList().forEach { bookUrl ->
|
||||
@@ -451,22 +456,48 @@ class BookshelfManageScreenViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChapterCached(chapter: BookChapter) {
|
||||
val bookUrl = chapter.bookUrl
|
||||
scheduleCacheCountRefresh(bookUrl)
|
||||
private fun scheduleCacheCountRefresh(bookUrl: String) {
|
||||
if (bookUrl.isNotBlank()) {
|
||||
pendingCacheCountRefreshBookUrls.add(bookUrl)
|
||||
}
|
||||
pendingDownloadRunningRefresh = true
|
||||
}
|
||||
|
||||
private fun scheduleCacheCountRefresh(bookUrl: String) {
|
||||
cacheCountJobs.remove(bookUrl)?.cancel()
|
||||
cacheCountJobs[bookUrl] = viewModelScope.launch(Dispatchers.IO) {
|
||||
val book = bookDao.getBook(bookUrl) ?: return@launch
|
||||
if (!uiState.value.books.any { it.bookUrl == bookUrl }) {
|
||||
return@launch
|
||||
}
|
||||
cacheCounts[bookUrl] = calculateCacheCount(book)
|
||||
emitBookChanged(bookUrl)
|
||||
cacheCountJobs.remove(bookUrl)
|
||||
private fun scheduleDownloadStatusRefresh(bookUrl: String = "") {
|
||||
if (bookUrl.isNotBlank()) {
|
||||
pendingDownloadStatusBookUrls.add(bookUrl)
|
||||
}
|
||||
pendingDownloadRunningRefresh = true
|
||||
}
|
||||
|
||||
private suspend fun flushDownloadStatusRefresh() {
|
||||
val cacheRefreshBookUrls = pendingCacheCountRefreshBookUrls.toList()
|
||||
cacheRefreshBookUrls.forEach { pendingCacheCountRefreshBookUrls.remove(it) }
|
||||
val statusBookUrls = pendingDownloadStatusBookUrls.toList()
|
||||
statusBookUrls.forEach { pendingDownloadStatusBookUrls.remove(it) }
|
||||
val shouldSyncDownloadRunning = pendingDownloadRunningRefresh ||
|
||||
cacheRefreshBookUrls.isNotEmpty() ||
|
||||
statusBookUrls.isNotEmpty()
|
||||
pendingDownloadRunningRefresh = false
|
||||
val changedBookUrls = linkedSetOf<String>()
|
||||
val visibleBookUrls = uiState.value.books.mapTo(hashSetOf()) { it.bookUrl }
|
||||
cacheRefreshBookUrls.forEach { bookUrl ->
|
||||
if (visibleBookUrls.contains(bookUrl)) {
|
||||
bookDao.getBook(bookUrl)?.let { book ->
|
||||
cacheCounts[bookUrl] = calculateCacheCount(book)
|
||||
changedBookUrls.add(bookUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
statusBookUrls.forEach { bookUrl ->
|
||||
if (visibleBookUrls.contains(bookUrl)) {
|
||||
changedBookUrls.add(bookUrl)
|
||||
}
|
||||
}
|
||||
if (shouldSyncDownloadRunning) {
|
||||
syncDownloadRunning()
|
||||
}
|
||||
emitBooksChanged(changedBookUrls)
|
||||
}
|
||||
|
||||
private fun calculateCacheCount(book: Book): Int {
|
||||
@@ -496,7 +527,7 @@ class BookshelfManageScreenViewModel(
|
||||
syncDownloadRunning()
|
||||
} else {
|
||||
execute {
|
||||
cacheBookChaptersUseCase.execute(book.bookUrl, 0..book.lastChapterIndex)
|
||||
cacheBookChaptersUseCase.executeRange(book.bookUrl, 0, book.lastChapterIndex)
|
||||
}.onFinally {
|
||||
syncDownloadRunning()
|
||||
}
|
||||
@@ -873,4 +904,12 @@ class BookshelfManageScreenViewModel(
|
||||
_effects.tryEmit(BookshelfManageScreenEffect.NotifyBookChanged(bookUrl))
|
||||
}
|
||||
|
||||
private fun emitBooksChanged(bookUrls: Set<String>) {
|
||||
if (bookUrls.isEmpty()) return
|
||||
_uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) }
|
||||
bookUrls.forEach { bookUrl ->
|
||||
_effects.tryEmit(BookshelfManageScreenEffect.NotifyBookChanged(bookUrl))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -303,8 +303,7 @@ abstract class BaseReadBookActivity :
|
||||
val end = editEnd.text!!.toString().let {
|
||||
if (it.isEmpty()) book.totalChapterNum else it.toInt()
|
||||
}
|
||||
val indices = (start - 1..end - 1).toList()
|
||||
CacheBook.start(this@BaseReadBookActivity, book, indices)
|
||||
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.legado.app.help.bookmark.BookmarkExporter
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.CacheBook
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.model.cache.CacheBookDownloadState
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.config.readConfig.ReadConfig
|
||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||
@@ -89,8 +90,7 @@ data class TocDomainItem(
|
||||
)
|
||||
|
||||
private data class DownloadContext(
|
||||
val downloadingPair: Pair<String, Set<Int>>,
|
||||
val errorPair: Pair<String, Set<Int>>,
|
||||
val downloadState: CacheBookDownloadState?,
|
||||
val cachedFiles: Set<String>
|
||||
)
|
||||
|
||||
@@ -206,11 +206,11 @@ class TocViewModel(
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val downloadContextFlow = combine(
|
||||
CacheBook.downloadingIndicesFlow,
|
||||
CacheBook.downloadErrorFlow,
|
||||
bookState.filterNotNull().map { it.bookUrl }.distinctUntilChanged(),
|
||||
CacheBook.downloadStateFlow,
|
||||
_cacheFileNames
|
||||
) { downloading, errors, cached ->
|
||||
DownloadContext(downloading, errors, cached)
|
||||
) { bookUrl, state, cached ->
|
||||
DownloadContext(state.books[bookUrl], cached)
|
||||
}
|
||||
|
||||
private val uiConfigFlow = combine(
|
||||
@@ -263,15 +263,13 @@ class TocViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val (downloadingPair, errorPair, cachedFiles) = downloadCtx
|
||||
val downloadingIndices =
|
||||
if (downloadingPair.first == book.bookUrl) downloadingPair.second else emptySet()
|
||||
val errorIndices =
|
||||
if (errorPair.first == book.bookUrl) errorPair.second else emptySet()
|
||||
val runningIndices = downloadCtx.downloadState?.runningIndices.orEmpty()
|
||||
val errorIndices = downloadCtx.downloadState?.failedIndices.orEmpty()
|
||||
val cachedFiles = downloadCtx.cachedFiles
|
||||
|
||||
processedChapters.map { chapter ->
|
||||
val downloadState = when {
|
||||
chapter.index in downloadingIndices -> DownloadState.DOWNLOADING
|
||||
chapter.index in runningIndices -> DownloadState.DOWNLOADING
|
||||
chapter.index in errorIndices -> DownloadState.ERROR
|
||||
chapter.getFileName() in cachedFiles -> DownloadState.SUCCESS
|
||||
else -> DownloadState.NONE
|
||||
|
||||
Reference in New Issue
Block a user