|
@@ -12,9 +12,14 @@ import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
|
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
|
|
import io.legado.app.help.book.BookHelp
|
|
import io.legado.app.help.book.BookHelp
|
|
|
import io.legado.app.help.book.isAudio
|
|
import io.legado.app.help.book.isAudio
|
|
|
|
|
+import io.legado.app.help.book.isImage
|
|
|
import io.legado.app.help.book.isLocal
|
|
import io.legado.app.help.book.isLocal
|
|
|
import io.legado.app.help.book.isNotShelf
|
|
import io.legado.app.help.book.isNotShelf
|
|
|
import io.legado.app.model.CacheBook
|
|
import io.legado.app.model.CacheBook
|
|
|
|
|
+import io.legado.app.model.CacheBookModel
|
|
|
|
|
+import io.legado.app.model.cache.CacheBookDownloadState
|
|
|
|
|
+import io.legado.app.model.cache.CacheChapterProgress
|
|
|
|
|
+import io.legado.app.model.cache.CacheChapterProgressPhase
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.Job
|
|
|
import kotlinx.coroutines.currentCoroutineContext
|
|
import kotlinx.coroutines.currentCoroutineContext
|
|
@@ -71,6 +76,8 @@ data class BookCacheChapterItem(
|
|
|
val isDownloading: Boolean,
|
|
val isDownloading: Boolean,
|
|
|
val isPaused: Boolean,
|
|
val isPaused: Boolean,
|
|
|
val isError: Boolean,
|
|
val isError: Boolean,
|
|
|
|
|
+ val downloadProgress: Float? = null,
|
|
|
|
|
+ val progressLabel: String? = null,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
sealed interface BookCacheManageIntent {
|
|
sealed interface BookCacheManageIntent {
|
|
@@ -163,7 +170,13 @@ class BookCacheManageViewModel(
|
|
|
}
|
|
}
|
|
|
viewModelScope.launch {
|
|
viewModelScope.launch {
|
|
|
CacheBook.downloadStateFlow.collect { state ->
|
|
CacheBook.downloadStateFlow.collect { state ->
|
|
|
- state.books.keys.forEach { scheduleDownloadStatusRefresh(it) }
|
|
|
|
|
|
|
+ state.books.forEach { (bookUrl, bookState) ->
|
|
|
|
|
+ if (bookState.runningIndices.isNotEmpty() || bookState.chapterProgress.isNotEmpty()) {
|
|
|
|
|
+ scheduleProgressUpdate(bookUrl, debounceMillis = 100)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ scheduleDownloadStatusRefresh(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
pendingDownloadSummaryRefresh = true
|
|
pendingDownloadSummaryRefresh = true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -257,6 +270,20 @@ class BookCacheManageViewModel(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun scheduleProgressUpdate(bookUrl: String, debounceMillis: Long = 100) {
|
|
|
|
|
+ if (bookUrl.isBlank()) return
|
|
|
|
|
+ bookReloadJobs.remove(bookUrl)?.cancel()
|
|
|
|
|
+ bookReloadJobs[bookUrl] = viewModelScope.launch {
|
|
|
|
|
+ if (debounceMillis > 0) {
|
|
|
|
|
+ delay(debounceMillis)
|
|
|
|
|
+ }
|
|
|
|
|
+ updateDownloadProgressOnly(bookUrl)
|
|
|
|
|
+ if (bookReloadJobs[bookUrl] == currentCoroutineContext()[Job]) {
|
|
|
|
|
+ bookReloadJobs.remove(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun scheduleBookReload(bookUrl: String, debounceMillis: Long = 80) {
|
|
private fun scheduleBookReload(bookUrl: String, debounceMillis: Long = 80) {
|
|
|
if (bookUrl.isBlank()) return
|
|
if (bookUrl.isBlank()) return
|
|
|
bookReloadJobs.remove(bookUrl)?.cancel()
|
|
bookReloadJobs.remove(bookUrl)?.cancel()
|
|
@@ -334,7 +361,11 @@ class BookCacheManageViewModel(
|
|
|
val errorIndices = errorIndices(book.bookUrl)
|
|
val errorIndices = errorIndices(book.bookUrl)
|
|
|
val totalCount = bookChapterDao.getChapterCount(book.bookUrl)
|
|
val totalCount = bookChapterDao.getChapterCount(book.bookUrl)
|
|
|
val cachedFileCount = cacheFiles.count { it.endsWith(".nb") }
|
|
val cachedFileCount = cacheFiles.count { it.endsWith(".nb") }
|
|
|
- val cachedCount = min(cachedFileCount + bookChapterDao.getVolumeCount(book.bookUrl), totalCount)
|
|
|
|
|
|
|
+ val cachedCount = if (book.isImage) {
|
|
|
|
|
+ min(BookHelp.countImageCachedChapters(book), totalCount)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ min(cachedFileCount + bookChapterDao.getVolumeCount(book.bookUrl), totalCount)
|
|
|
|
|
+ }
|
|
|
if (totalCount == 0 && cacheFiles.isEmpty() && waitingCount == 0 && downloadingCount == 0 && pausedCount == 0 && !book.isNotShelf) {
|
|
if (totalCount == 0 && cacheFiles.isEmpty() && waitingCount == 0 && downloadingCount == 0 && pausedCount == 0 && !book.isNotShelf) {
|
|
|
return null
|
|
return null
|
|
|
}
|
|
}
|
|
@@ -362,22 +393,71 @@ class BookCacheManageViewModel(
|
|
|
val chapters = bookChapterDao.getChapterCacheInfoList(bookUrl)
|
|
val chapters = bookChapterDao.getChapterCacheInfoList(bookUrl)
|
|
|
val cacheFiles = BookHelp.getChapterFiles(book)
|
|
val cacheFiles = BookHelp.getChapterFiles(book)
|
|
|
val model = CacheBook.cacheBookMap[bookUrl]
|
|
val model = CacheBook.cacheBookMap[bookUrl]
|
|
|
|
|
+ val bookState = CacheBook.downloadStateFlow.value.books[bookUrl]
|
|
|
val errorIndices = errorIndices(bookUrl)
|
|
val errorIndices = errorIndices(bookUrl)
|
|
|
return chapters.map { chapter ->
|
|
return chapters.map { chapter ->
|
|
|
val isPaused = model?.isPaused(chapter.index) == true
|
|
val isPaused = model?.isPaused(chapter.index) == true
|
|
|
|
|
+ val isWaiting = !isPaused && model?.isWaiting(chapter.index) == true
|
|
|
|
|
+ val isDownloading = !isPaused && model?.isDownloading(chapter.index) == true
|
|
|
|
|
+ val bookChapter = BookChapter(
|
|
|
|
|
+ bookUrl = book.bookUrl,
|
|
|
|
|
+ url = chapter.url,
|
|
|
|
|
+ title = chapter.title,
|
|
|
|
|
+ isVolume = chapter.isVolume,
|
|
|
|
|
+ index = chapter.index,
|
|
|
|
|
+ )
|
|
|
|
|
+ val isCached = if (book.isImage) {
|
|
|
|
|
+ when {
|
|
|
|
|
+ chapter.isVolume -> true
|
|
|
|
|
+ isDownloading -> false
|
|
|
|
|
+ else -> BookHelp.hasImageFilesCached(book, bookChapter)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cacheFiles.contains(chapter.getFileName()) || chapter.isVolume
|
|
|
|
|
+ }
|
|
|
|
|
+ val chapterProgress = bookState?.chapterProgress?.get(chapter.index)
|
|
|
|
|
+ val (downloadProgress, progressLabel) = chapterProgressUi(
|
|
|
|
|
+ isDownloading = isDownloading,
|
|
|
|
|
+ progress = chapterProgress,
|
|
|
|
|
+ )
|
|
|
BookCacheChapterItem(
|
|
BookCacheChapterItem(
|
|
|
chapterUrl = chapter.url,
|
|
chapterUrl = chapter.url,
|
|
|
title = chapter.title,
|
|
title = chapter.title,
|
|
|
index = chapter.index,
|
|
index = chapter.index,
|
|
|
- isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
|
|
|
|
|
- isWaiting = !isPaused && model?.isWaiting(chapter.index) == true,
|
|
|
|
|
- isDownloading = !isPaused && model?.isDownloading(chapter.index) == true,
|
|
|
|
|
|
|
+ isCached = isCached,
|
|
|
|
|
+ isWaiting = isWaiting,
|
|
|
|
|
+ isDownloading = isDownloading,
|
|
|
isPaused = isPaused,
|
|
isPaused = isPaused,
|
|
|
isError = errorIndices.contains(chapter.index),
|
|
isError = errorIndices.contains(chapter.index),
|
|
|
|
|
+ downloadProgress = downloadProgress,
|
|
|
|
|
+ progressLabel = progressLabel,
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun chapterProgressUi(
|
|
|
|
|
+ isDownloading: Boolean,
|
|
|
|
|
+ progress: CacheChapterProgress?,
|
|
|
|
|
+ ): Pair<Float?, String?> {
|
|
|
|
|
+ if (!isDownloading || progress == null) return null to null
|
|
|
|
|
+ return when (progress.phase) {
|
|
|
|
|
+ CacheChapterProgressPhase.CONTENT -> {
|
|
|
|
|
+ if (progress.completed >= progress.total && progress.total > 0) {
|
|
|
|
|
+ progress.fraction to "正文已下载"
|
|
|
|
|
+ } else {
|
|
|
|
|
+ null to "下载正文"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ CacheChapterProgressPhase.IMAGES -> {
|
|
|
|
|
+ if (progress.total <= 0) {
|
|
|
|
|
+ null to "下载图片"
|
|
|
|
|
+ } else {
|
|
|
|
|
+ progress.fraction to "图片 ${progress.completed}/${progress.total}"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun errorIndices(bookUrl: String): Set<Int> {
|
|
private fun errorIndices(bookUrl: String): Set<Int> {
|
|
|
return CacheBook.errorIndices(bookUrl)
|
|
return CacheBook.errorIndices(bookUrl)
|
|
|
}
|
|
}
|
|
@@ -502,7 +582,7 @@ class BookCacheManageViewModel(
|
|
|
for (chapter in bookChapterDao.getChapterCacheInfoList(bookUrl)) {
|
|
for (chapter in bookChapterDao.getChapterCacheInfoList(bookUrl)) {
|
|
|
if (
|
|
if (
|
|
|
chapter.isVolume ||
|
|
chapter.isVolume ||
|
|
|
- cacheFiles.contains(chapter.getFileName()) ||
|
|
|
|
|
|
|
+ isChapterFullyCached(book, chapter, cacheFiles) ||
|
|
|
model?.isPaused(chapter.index) == true ||
|
|
model?.isPaused(chapter.index) == true ||
|
|
|
model?.isWaiting(chapter.index) == true ||
|
|
model?.isWaiting(chapter.index) == true ||
|
|
|
model?.isDownloading(chapter.index) == true
|
|
model?.isDownloading(chapter.index) == true
|
|
@@ -520,6 +600,93 @@ class BookCacheManageViewModel(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun isChapterFullyCached(
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ chapter: BookChapterCacheInfo,
|
|
|
|
|
+ cacheFiles: Set<String>,
|
|
|
|
|
+ ): Boolean {
|
|
|
|
|
+ if (chapter.isVolume) return true
|
|
|
|
|
+ val bookChapter = BookChapter(
|
|
|
|
|
+ bookUrl = book.bookUrl,
|
|
|
|
|
+ url = chapter.url,
|
|
|
|
|
+ title = chapter.title,
|
|
|
|
|
+ isVolume = chapter.isVolume,
|
|
|
|
|
+ index = chapter.index,
|
|
|
|
|
+ )
|
|
|
|
|
+ return if (book.isImage) {
|
|
|
|
|
+ BookHelp.hasImageFilesCached(book, bookChapter)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cacheFiles.contains(chapter.getFileName())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun updateDownloadProgressOnly(bookUrl: String) {
|
|
|
|
|
+ val bookState = CacheBook.downloadStateFlow.value.books[bookUrl] ?: return
|
|
|
|
|
+ val model = CacheBook.cacheBookMap[bookUrl]
|
|
|
|
|
+ _uiState.update { state ->
|
|
|
|
|
+ fun updateBookItem(item: BookCacheBookItem): BookCacheBookItem {
|
|
|
|
|
+ if (item.bookUrl != bookUrl) return item
|
|
|
|
|
+ return applyDownloadStateToBookItem(item, bookState, model)
|
|
|
|
|
+ }
|
|
|
|
|
+ val updatedChapters = state.chaptersByBookUrl[bookUrl]?.map { item ->
|
|
|
|
|
+ val isPaused = model?.isPaused(item.index) == true
|
|
|
|
|
+ val isWaiting = !isPaused && model?.isWaiting(item.index) == true
|
|
|
|
|
+ val isDownloading = !isPaused && model?.isDownloading(item.index) == true
|
|
|
|
|
+ val chapterProgress = bookState.chapterProgress[item.index]
|
|
|
|
|
+ val (downloadProgress, progressLabel) = chapterProgressUi(
|
|
|
|
|
+ isDownloading = isDownloading,
|
|
|
|
|
+ progress = chapterProgress,
|
|
|
|
|
+ )
|
|
|
|
|
+ item.copy(
|
|
|
|
|
+ isWaiting = isWaiting,
|
|
|
|
|
+ isDownloading = isDownloading,
|
|
|
|
|
+ isPaused = isPaused,
|
|
|
|
|
+ downloadProgress = downloadProgress,
|
|
|
|
|
+ progressLabel = progressLabel,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ val shelfBooks = state.shelfBooks.map(::updateBookItem)
|
|
|
|
|
+ val notShelfBooks = state.notShelfBooks.map(::updateBookItem)
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ shelfBooks = shelfBooks,
|
|
|
|
|
+ notShelfBooks = notShelfBooks,
|
|
|
|
|
+ chaptersByBookUrl = if (updatedChapters != null) {
|
|
|
|
|
+ state.chaptersByBookUrl + (bookUrl to updatedChapters)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.chaptersByBookUrl
|
|
|
|
|
+ },
|
|
|
|
|
+ downloadSummary = buildDownloadSummary(shelfBooks + notShelfBooks),
|
|
|
|
|
+ hasPausedDownloads = CacheBook.hasPausedDownloads,
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun applyDownloadStateToBookItem(
|
|
|
|
|
+ item: BookCacheBookItem,
|
|
|
|
|
+ bookState: CacheBookDownloadState,
|
|
|
|
|
+ model: CacheBookModel?,
|
|
|
|
|
+ ): BookCacheBookItem {
|
|
|
|
|
+ val rawWaitingCount = bookState.waitingCount +
|
|
|
|
|
+ CacheBook.pendingAdmissionFlow.value[item.bookUrl].orZero()
|
|
|
|
|
+ val rawDownloadingCount = bookState.runningIndices.size
|
|
|
|
|
+ val isBookPaused = model?.isPaused() == true ||
|
|
|
|
|
+ (CacheBook.hasPausedDownloads && CacheBook.pendingAdmissionFlow.value.containsKey(item.bookUrl))
|
|
|
|
|
+ val pausedCount = if (isBookPaused) {
|
|
|
|
|
+ rawWaitingCount + rawDownloadingCount + bookState.pausedIndices.size
|
|
|
|
|
+ } else {
|
|
|
|
|
+ bookState.pausedIndices.size
|
|
|
|
|
+ }
|
|
|
|
|
+ val waitingCount = if (isBookPaused) 0 else rawWaitingCount
|
|
|
|
|
+ val downloadingCount = if (isBookPaused) 0 else rawDownloadingCount
|
|
|
|
|
+ return item.copy(
|
|
|
|
|
+ waitingCount = waitingCount,
|
|
|
|
|
+ downloadingCount = downloadingCount,
|
|
|
|
|
+ pausedCount = pausedCount,
|
|
|
|
|
+ errorCount = errorIndices(item.bookUrl).size,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun deleteBookCache(bookUrl: String) {
|
|
private fun deleteBookCache(bookUrl: String) {
|
|
|
execute {
|
|
execute {
|
|
|
CacheBook.removeAwait(context, bookUrl)
|
|
CacheBook.removeAwait(context, bookUrl)
|