diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt index 03c906266..d2793a8e6 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import io.legado.app.data.entities.BookChapter +import io.legado.app.data.model.BookChapterCacheInfo import kotlinx.coroutines.flow.Flow @Dao @@ -20,6 +21,9 @@ interface BookChapterDao { @Query("select * from chapters where bookUrl = :bookUrl order by `index`") fun getChapterList(bookUrl: String): List + @Query("select url, title, isVolume, `index` from chapters where bookUrl = :bookUrl order by `index`") + fun getChapterCacheInfoList(bookUrl: String): List + @Query("select * from chapters where bookUrl = :bookUrl order by `index`") fun getChapterListFlow(bookUrl: String): Flow> diff --git a/app/src/main/java/io/legado/app/data/model/BookChapterCacheInfo.kt b/app/src/main/java/io/legado/app/data/model/BookChapterCacheInfo.kt new file mode 100644 index 000000000..26e684a2b --- /dev/null +++ b/app/src/main/java/io/legado/app/data/model/BookChapterCacheInfo.kt @@ -0,0 +1,8 @@ +package io.legado.app.data.model + +data class BookChapterCacheInfo( + val url: String, + val title: String, + val isVolume: Boolean, + val index: Int, +) diff --git a/app/src/main/java/io/legado/app/model/CacheBook.kt b/app/src/main/java/io/legado/app/model/CacheBook.kt index c3c0647dd..8f8751cd2 100644 --- a/app/src/main/java/io/legado/app/model/CacheBook.kt +++ b/app/src/main/java/io/legado/app/model/CacheBook.kt @@ -184,6 +184,19 @@ object CacheBook { } } + fun removeBook(bookUrl: String): Boolean { + val model = cacheBookMap.remove(bookUrl) ?: return false + model.stop() + updateSummary() + _queueChangedFlow.tryEmit(bookUrl) + postEvent(EventBus.UP_DOWNLOAD, bookUrl) + return true + } + + fun removeChapter(bookUrl: String, chapterIndex: Int): Boolean { + return cacheBookMap[bookUrl]?.removeDownload(chapterIndex) == true + } + fun stop(context: Context) { if (CacheBookService.isRun) { context.startService { @@ -246,6 +259,8 @@ object CacheBook { private val waitDownloadSet = linkedSetOf() private val onDownloadSet = linkedSetOf() + private val pausedDownloadSet = hashSetOf() + private val chapterTasks = hashMapOf>() private val tasks = CompositeCoroutine() private var isStopped = false private var waitingRetry = false @@ -295,6 +310,8 @@ object CacheBook { @Synchronized fun stop() { waitDownloadSet.clear() + pausedDownloadSet.clear() + chapterTasks.clear() tasks.clear() isStopped = true isLoading = false @@ -312,6 +329,7 @@ object CacheBook { fun addDownloads(indices: Iterable) { isStopped = false for (i in indices) { + pausedDownloadSet.remove(i) if (!onDownloadSet.contains(i)) { waitDownloadSet.add(i) } @@ -329,6 +347,7 @@ object CacheBook { @Synchronized private fun onSuccess(chapter: BookChapter) { onDownloadSet.remove(chapter.index) + chapterTasks.remove(chapter.index) successDownloadSet.add(chapter.primaryStr()) errorDownloadMap.remove(chapter.primaryStr()) errorIndexMap[book.bookUrl]?.remove(chapter.index) @@ -346,6 +365,7 @@ object CacheBook { .add(chapter.index) } onDownloadSet.remove(chapter.index) + chapterTasks.remove(chapter.index) } @Synchronized @@ -370,7 +390,8 @@ object CacheBook { @Synchronized private fun onCancel(index: Int) { onDownloadSet.remove(index) - if (!isStopped) waitDownloadSet.add(index) + chapterTasks.remove(index) + if (!isStopped && !pausedDownloadSet.remove(index)) waitDownloadSet.add(index) notifyDownloadSetChanged() } @@ -385,6 +406,28 @@ object CacheBook { notifyDownloadSetChanged() } + @Synchronized + fun removeDownload(index: Int): Boolean { + val removedWaiting = waitDownloadSet.remove(index) + val task = chapterTasks.remove(index) + val removedRunning = onDownloadSet.contains(index) || task != null + if (removedRunning) { + pausedDownloadSet.add(index) + task?.let { + tasks.delete(it) + it.cancel() + } + } + if (!removedWaiting && !removedRunning) return false + notifyDownloadSetChanged() + if (waitDownloadSet.isEmpty() && onDownloadSet.isEmpty()) { + CacheBook.onTaskRemoved(book.bookUrl) + } else { + CacheBook.onTaskQueuesChanged(book.bookUrl) + } + return true + } + /** * 从待下载列表内取第一条下载 */ @@ -421,7 +464,7 @@ object CacheBook { notifyDownloadSetChanged() if (BookHelp.hasContent(book, chapter)) { - Coroutine.async(scope, context, executeContext = context) { + val task = Coroutine.async(scope, context, executeContext = context) { BookHelp.getContent(book, chapter)?.let { BookHelp.saveImages(bookSource, book, chapter, it, 1) } @@ -434,12 +477,15 @@ object CacheBook { }.onCancel { onCancel(chapterIndex) }.onFinally { + chapterTasks.remove(chapterIndex)?.let { tasks.delete(it) } onFinally() } + chapterTasks[chapterIndex] = task + tasks.add(task) return } - WebBook.getContent( + val task = WebBook.getContent( scope, bookSource, book, @@ -458,10 +504,12 @@ object CacheBook { }.onCancel { onCancel(chapterIndex) }.onFinally { + chapterTasks.remove(chapterIndex)?.let { tasks.delete(it) } onFinally() - }.apply { - tasks.add(this) - }.start() + } + chapterTasks[chapterIndex] = task + tasks.add(task) + task.start() } suspend fun downloadAwait(chapter: BookChapter): String { diff --git a/app/src/main/java/io/legado/app/service/CacheBookService.kt b/app/src/main/java/io/legado/app/service/CacheBookService.kt index 6f125c57c..4dde1513e 100644 --- a/app/src/main/java/io/legado/app/service/CacheBookService.kt +++ b/app/src/main/java/io/legado/app/service/CacheBookService.kt @@ -98,8 +98,7 @@ class CacheBookService : BaseService() { } IntentAction.remove -> { val bookUrl = intent.getStringExtra("bookUrl") - CacheBook.cacheBookMap[bookUrl]?.stop() - CacheBook.cacheBookMap.remove(bookUrl) + bookUrl?.let { CacheBook.removeBook(it) } } IntentAction.stop -> stopSelf() } diff --git a/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt b/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt index 3744f3af4..c8412b84c 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Checkbox @@ -44,6 +45,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.adaptiveContentPadding +import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.widget.components.AppLinearProgressIndicator import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.alert.AppAlertDialog @@ -187,7 +189,7 @@ private fun BookCacheManageScreen( DeleteBookCacheDialog( item = pendingDeleteBook, onConfirm = { item -> - onIntent(BookCacheManageIntent.DeleteBookCache(item.book.bookUrl)) + onIntent(BookCacheManageIntent.DeleteBookCache(item.bookUrl)) pendingDeleteBook = null }, onDismiss = { pendingDeleteBook = null } @@ -197,8 +199,10 @@ private fun BookCacheManageScreen( onConfirm = { book, chapter -> onIntent( BookCacheManageIntent.DeleteChapterCache( - book.book.bookUrl, - chapter.chapter.url + book.bookUrl, + chapter.chapterUrl, + chapter.title, + chapter.index, ) ) pendingDeleteChapter = null @@ -221,18 +225,23 @@ private fun LazyListScope.cacheSection( item(key = "$title-header") { AppText( text = title, - modifier = Modifier.padding(start = 4.dp, top = 12.dp, bottom = 4.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), style = LegadoTheme.typography.titleSmallEmphasized, color = LegadoTheme.colorScheme.primary ) } if (books.isEmpty()) { item(key = "$title-empty") { - TextCard(text = emptyText, modifier = Modifier.fillMaxWidth()) + TextCard( + text = emptyText, + modifier = Modifier.fillMaxWidth(), + verticalPadding = 12.dp, + horizontalPadding = 12.dp + ) } } else { books.forEach { item -> - val bookUrl = item.book.bookUrl + val bookUrl = item.bookUrl val expanded = expandedBookUrls.contains(bookUrl) item(key = "$title-book-$bookUrl") { BookCacheBookCard( @@ -247,7 +256,7 @@ private fun LazyListScope.cacheSection( if (expanded) { items( items = chaptersByBookUrl[bookUrl].orEmpty(), - key = { chapter -> "$title-chapter-$bookUrl-${chapter.chapter.url}" } + key = { chapter -> "$title-chapter-$bookUrl-${chapter.chapterUrl}" } ) { chapter -> BookCacheChapterRow( item = chapter, @@ -256,7 +265,15 @@ private fun LazyListScope.cacheSection( onIntent( BookCacheManageIntent.DownloadChapter( bookUrl, - chapter.chapter.index + chapter.index + ) + ) + }, + onStop = { + onIntent( + BookCacheManageIntent.StopChapterDownload( + bookUrl, + chapter.index ) ) }, @@ -284,7 +301,7 @@ private fun BookCacheBookCard( NormalCard( modifier = modifier.fillMaxWidth(), onClick = onToggleExpanded, - containerColor = LegadoTheme.colorScheme.surfaceContainerLow + containerColor = LegadoTheme.colorScheme.surfaceContainer ) { Column( modifier = Modifier @@ -306,20 +323,23 @@ private fun BookCacheBookCard( ) Column(modifier = Modifier.weight(1f)) { AppText( - text = item.book.name, + text = item.name, style = LegadoTheme.typography.titleSmallEmphasized, maxLines = 1, overflow = TextOverflow.Ellipsis ) AppText( - text = item.book.getRealAuthor(), - style = LegadoTheme.typography.bodySmall, + text = item.author, + style = LegadoTheme.typography.labelSmallEmphasized, color = LegadoTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis ) } - TextCard(text = "${item.cachedCount}/${item.totalCount}") + TextCard( + text = "${item.cachedCount}/${item.totalCount}", + backgroundColor = LegadoTheme.colorScheme.cardContainer, + ) } AppLinearProgressIndicator( progress = item.progress, @@ -333,9 +353,22 @@ private fun BookCacheBookCard( AppText( text = "下载中 ${item.downloadingCount} · 等待 ${item.waitingCount} · 失败 ${item.errorCount}", modifier = Modifier.weight(1f), - style = LegadoTheme.typography.bodySmall, + style = LegadoTheme.typography.labelMediumEmphasized, color = LegadoTheme.colorScheme.onSurfaceVariant ) + if (item.isDownloading || item.cachedCount < item.totalCount) { + SmallTonalIconButton( + onClick = { + if (item.isDownloading) { + onIntent(BookCacheManageIntent.StopBookDownload(item.bookUrl)) + } else { + onIntent(BookCacheManageIntent.StartBookDownload(item.bookUrl)) + } + }, + imageVector = if (item.isDownloading) Icons.Default.Stop else Icons.Default.PlayArrow, + contentDescription = if (item.isDownloading) "暂停本书下载" else "开始本书下载" + ) + } SmallTonalIconButton( onClick = { onDeleteBook(item) }, imageVector = Icons.Default.Delete, @@ -351,21 +384,25 @@ private fun BookCacheChapterRow( item: BookCacheChapterItem, modifier: Modifier = Modifier, onDownload: () -> Unit, + onStop: () -> Unit, onDelete: () -> Unit, ) { Row( modifier = modifier .fillMaxWidth() - .padding(horizontal = 8.dp), + .padding( + horizontal = 12.dp, + vertical = 4.dp + ), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { AppText( - text = item.chapter.title, + text = item.title, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = LegadoTheme.typography.bodyMedium + style = LegadoTheme.typography.titleSmallEmphasized ) AppText( text = chapterStatusText(item), @@ -380,11 +417,19 @@ private fun BookCacheChapterRow( } ) } - SmallTonalIconButton( - onClick = onDownload, - imageVector = Icons.Default.Download, - contentDescription = null - ) + if (item.isWaiting || item.isDownloading) { + SmallTonalIconButton( + onClick = onStop, + imageVector = Icons.Default.Stop, + contentDescription = "暂停章节下载" + ) + } else if (!item.isCached) { + SmallTonalIconButton( + onClick = onDownload, + imageVector = Icons.Default.Download, + contentDescription = "下载章节" + ) + } SmallTonalIconButton( onClick = onDelete, imageVector = Icons.Default.Delete, @@ -413,7 +458,7 @@ private fun DeleteBookCacheDialog( show = item != null, onDismissRequest = onDismiss, title = stringResource(R.string.delete), - text = "删除《${item?.book?.name.orEmpty()}》的全部缓存,并从下载队列移除?", + text = "删除《${item?.name.orEmpty()}》的全部缓存,并从下载队列移除?", confirmText = stringResource(android.R.string.ok), onConfirm = { item?.let(onConfirm) }, dismissText = stringResource(android.R.string.cancel), @@ -431,7 +476,7 @@ private fun DeleteChapterCacheDialog( show = item != null, onDismissRequest = onDismiss, title = stringResource(R.string.delete), - text = "删除章节缓存:${item?.second?.chapter?.title.orEmpty()}?", + text = "删除章节缓存:${item?.second?.title.orEmpty()}?", confirmText = stringResource(android.R.string.ok), onConfirm = { item?.let { onConfirm(it.first, it.second) } }, dismissText = stringResource(android.R.string.cancel), diff --git a/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageViewModel.kt index ad894d66a..22580da41 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageViewModel.kt @@ -7,6 +7,7 @@ import io.legado.app.data.dao.BookChapterDao import io.legado.app.data.dao.BookDao import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.data.model.BookChapterCacheInfo import io.legado.app.domain.usecase.CacheBookChaptersUseCase import io.legado.app.domain.usecase.ClearBookCacheUseCase import io.legado.app.help.book.BookHelp @@ -33,12 +34,14 @@ data class BookCacheManageUiState( val notShelfBooks: List = emptyList(), val expandedBookUrls: Set = emptySet(), val chaptersByBookUrl: Map> = emptyMap(), - val downloadSummary: String = CacheBook.downloadSummary, + val downloadSummary: String = "", val version: Long = 0, ) data class BookCacheBookItem( - val book: Book, + val bookUrl: String, + val name: String, + val author: String, val totalCount: Int, val cachedCount: Int, val waitingCount: Int, @@ -51,7 +54,9 @@ data class BookCacheBookItem( } data class BookCacheChapterItem( - val chapter: BookChapter, + val chapterUrl: String, + val title: String, + val index: Int, val isCached: Boolean, val isWaiting: Boolean, val isDownloading: Boolean, @@ -63,10 +68,18 @@ sealed interface BookCacheManageIntent { data object Refresh : BookCacheManageIntent data object StartAllDownloads : BookCacheManageIntent data object StopAllDownloads : BookCacheManageIntent + data class StartBookDownload(val bookUrl: String) : BookCacheManageIntent + data class StopBookDownload(val bookUrl: String) : BookCacheManageIntent data class ToggleBookExpanded(val bookUrl: String) : BookCacheManageIntent data class DeleteBookCache(val bookUrl: String) : BookCacheManageIntent data class DownloadChapter(val bookUrl: String, val chapterIndex: Int) : BookCacheManageIntent - data class DeleteChapterCache(val bookUrl: String, val chapterUrl: String) : BookCacheManageIntent + data class StopChapterDownload(val bookUrl: String, val chapterIndex: Int) : BookCacheManageIntent + data class DeleteChapterCache( + val bookUrl: String, + val chapterUrl: String, + val chapterTitle: String, + val chapterIndex: Int, + ) : BookCacheManageIntent } sealed interface BookCacheManageEffect { @@ -97,15 +110,23 @@ class BookCacheManageViewModel( BookCacheManageIntent.Refresh -> reloadAll(forceDatabase = true) BookCacheManageIntent.StartAllDownloads -> startAllDownloads() BookCacheManageIntent.StopAllDownloads -> stopAllDownloads() + is BookCacheManageIntent.StartBookDownload -> startBookDownload(intent.bookUrl) + is BookCacheManageIntent.StopBookDownload -> stopBookDownload(intent.bookUrl) is BookCacheManageIntent.ToggleBookExpanded -> toggleBookExpanded(intent.bookUrl) is BookCacheManageIntent.DeleteBookCache -> deleteBookCache(intent.bookUrl) is BookCacheManageIntent.DownloadChapter -> downloadChapter( intent.bookUrl, intent.chapterIndex ) + is BookCacheManageIntent.StopChapterDownload -> stopChapterDownload( + intent.bookUrl, + intent.chapterIndex, + ) is BookCacheManageIntent.DeleteChapterCache -> deleteChapterCache( intent.bookUrl, - intent.chapterUrl + intent.chapterUrl, + intent.chapterTitle, + intent.chapterIndex, ) } } @@ -138,9 +159,12 @@ class BookCacheManageViewModel( } } viewModelScope.launch { - CacheBook.downloadSummaryFlow.collect { summary -> + CacheBook.downloadSummaryFlow.collect { _uiState.update { - it.copy(downloadSummary = summary, version = it.version + 1) + it.copy( + downloadSummary = buildDownloadSummary(it.shelfBooks + it.notShelfBooks), + version = it.version + 1, + ) } } } @@ -165,12 +189,12 @@ class BookCacheManageViewModel( item.cachedCount > 0 || item.isDownloading || item.errorCount > 0 } ) - val booksByUrl = items.associateBy { it.book.bookUrl } + val booksByUrl = items.associateBy { it.bookUrl } val retainedExpandedBookUrls = expandedBookUrls.filterTo(linkedSetOf()) { booksByUrl.containsKey(it) } val chaptersByBookUrl = retainedExpandedBookUrls.associateWith { bookUrl -> - buildChapterItems(booksByUrl.getValue(bookUrl).book) + buildChapterItems(bookUrl) } LoadedCacheState( items = items, @@ -185,7 +209,7 @@ class BookCacheManageViewModel( notShelfBooks = result.items.filter { item -> item.isNotShelf }, expandedBookUrls = result.expandedBookUrls, chaptersByBookUrl = result.chaptersByBookUrl, - downloadSummary = CacheBook.downloadSummary, + downloadSummary = buildDownloadSummary(result.items), version = state.version + 1, ) } @@ -217,7 +241,7 @@ class BookCacheManageViewModel( it.cachedCount > 0 || it.isDownloading || it.errorCount > 0 } val chapters = if (expanded && item != null) { - buildChapterItems(item.book) + buildChapterItems(item.bookUrl) } else { null } @@ -225,7 +249,7 @@ class BookCacheManageViewModel( } _uiState.update { state -> val combinedBooks = (state.shelfBooks + state.notShelfBooks) - .filterNot { it.book.bookUrl == bookUrl } + .filterNot { it.bookUrl == bookUrl } .let { items -> result.item?.let { items + it } ?: items } @@ -245,7 +269,7 @@ class BookCacheManageViewModel( notShelfBooks = sortedBooks.filter { item -> item.isNotShelf }, expandedBookUrls = expandedBookUrls, chaptersByBookUrl = chaptersByBookUrl, - downloadSummary = CacheBook.downloadSummary, + downloadSummary = buildDownloadSummary(sortedBooks), version = state.version + 1, ) } @@ -264,7 +288,9 @@ class BookCacheManageViewModel( return null } return BookCacheBookItem( - book = book, + bookUrl = book.bookUrl, + name = book.name, + author = book.getRealAuthor(), totalCount = totalCount, cachedCount = cachedCount, waitingCount = waitingIndices.size, @@ -274,16 +300,19 @@ class BookCacheManageViewModel( ) } - private fun buildChapterItems(book: Book): List { - val chapters = bookChapterDao.getChapterList(book.bookUrl) + private fun buildChapterItems(bookUrl: String): List { + val book = bookDao.getBook(bookUrl) ?: return emptyList() + val chapters = bookChapterDao.getChapterCacheInfoList(bookUrl) val cacheFiles = BookHelp.getChapterFiles(book) - val model = CacheBook.cacheBookMap[book.bookUrl] + val model = CacheBook.cacheBookMap[bookUrl] val waitingIndices = model?.waitingIndices().orEmpty() val downloadingIndices = model?.downloadingIndices().orEmpty() - val errorIndices = errorIndices(book.bookUrl) + val errorIndices = errorIndices(bookUrl) return chapters.map { chapter -> BookCacheChapterItem( - chapter = chapter, + chapterUrl = chapter.url, + title = chapter.title, + index = chapter.index, isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume, isWaiting = waitingIndices.contains(chapter.index), isDownloading = downloadingIndices.contains(chapter.index), @@ -320,9 +349,8 @@ class BookCacheManageViewModel( private fun loadBookChapters(bookUrl: String) { viewModelScope.launch { val chapters = withContext(Dispatchers.IO) { - val book = findBook(bookUrl) ?: bookDao.getBook(bookUrl) ?: return@withContext null - buildChapterItems(book) - } ?: return@launch + buildChapterItems(bookUrl) + } _uiState.update { state -> if (!state.expandedBookUrls.contains(bookUrl)) { state @@ -336,23 +364,22 @@ class BookCacheManageViewModel( } } - private fun findBook(bookUrl: String): Book? { - return (uiState.value.shelfBooks + uiState.value.notShelfBooks) - .firstOrNull { it.book.bookUrl == bookUrl } - ?.book - } - private fun stopAllDownloads() { CacheBook.stop(context) reloadAll(forceDatabase = true) } + private fun stopBookDownload(bookUrl: String) { + CacheBook.removeBook(bookUrl) + scheduleBookReload(bookUrl, debounceMillis = 0) + } + private fun startAllDownloads() { val items = uiState.value.shelfBooks + uiState.value.notShelfBooks execute { items.sumOf { item -> - val chapterIndices = downloadableChapterIndices(item.book) - cacheBookChaptersUseCase.execute(item.book.bookUrl, chapterIndices) + val chapterIndices = downloadableChapterIndices(item.bookUrl) + cacheBookChaptersUseCase.execute(item.bookUrl, chapterIndices) } }.onSuccess { count -> if (count > 0) { @@ -367,12 +394,29 @@ class BookCacheManageViewModel( } } - private fun downloadableChapterIndices(book: Book): List { + private fun startBookDownload(bookUrl: String) { + execute { + cacheBookChaptersUseCase.execute(bookUrl, downloadableChapterIndices(bookUrl)) + }.onSuccess { count -> + if (count > 0) { + _effects.tryEmit(BookCacheManageEffect.ShowMessage("已加入缓存队列: $count 章")) + } else { + _effects.tryEmit(BookCacheManageEffect.ShowMessage("没有可缓存的章节")) + } + }.onError { + _effects.tryEmit(BookCacheManageEffect.ShowMessage("加入缓存队列失败\n${it.localizedMessage}")) + }.onFinally { + scheduleBookReload(bookUrl, debounceMillis = 0) + } + } + + private fun downloadableChapterIndices(bookUrl: String): List { + val book = bookDao.getBook(bookUrl) ?: return emptyList() val cacheFiles = BookHelp.getChapterFiles(book) - val model = CacheBook.cacheBookMap[book.bookUrl] + val model = CacheBook.cacheBookMap[bookUrl] val waitingIndices = model?.waitingIndices().orEmpty() val downloadingIndices = model?.downloadingIndices().orEmpty() - return bookChapterDao.getChapterList(book.bookUrl) + return bookChapterDao.getChapterCacheInfoList(bookUrl) .asSequence() .filterNot { chapter -> chapter.isVolume || @@ -409,13 +453,25 @@ class BookCacheManageViewModel( } } - private fun deleteChapterCache(bookUrl: String, chapterUrl: String) { - val book = findBook(bookUrl) ?: bookDao.getBook(bookUrl) ?: return - val chapter = uiState.value.chaptersByBookUrl[bookUrl] - ?.firstOrNull { it.chapter.url == chapterUrl } - ?.chapter - ?: bookChapterDao.getChapterList(bookUrl).firstOrNull { it.url == chapterUrl } - ?: return + private fun stopChapterDownload(bookUrl: String, chapterIndex: Int) { + if (CacheBook.removeChapter(bookUrl, chapterIndex)) { + scheduleBookReload(bookUrl, debounceMillis = 0) + } + } + + private fun deleteChapterCache( + bookUrl: String, + chapterUrl: String, + chapterTitle: String, + chapterIndex: Int, + ) { + val book = bookDao.getBook(bookUrl) ?: return + val chapter = BookChapter( + url = chapterUrl, + title = chapterTitle, + bookUrl = bookUrl, + index = chapterIndex, + ) execute { BookHelp.delContent(book, chapter) }.onSuccess { @@ -430,7 +486,16 @@ class BookCacheManageViewModel( private fun sortItems(items: List): List { return items.sortedWith(compareByDescending { it.isDownloading } .thenByDescending { it.cachedCount } - .thenBy { it.book.name }) + .thenBy { it.name }) + } + + private fun buildDownloadSummary(items: List): String { + if (items.isEmpty()) return "" + val downloadingCount = items.sumOf { it.downloadingCount } + val waitingCount = items.sumOf { it.waitingCount } + val errorCount = items.sumOf { it.errorCount } + val cachedCount = items.sumOf { it.cachedCount } + return "正在下载:$downloadingCount | 等待中:$waitingCount | 失败:$errorCount | 成功:$cachedCount" } private data class LoadedCacheState( @@ -444,3 +509,12 @@ class BookCacheManageViewModel( val chapters: List?, ) } + +private fun BookChapterCacheInfo.getFileName(): String { + return BookChapter( + url = url, + title = title, + isVolume = isVolume, + index = index, + ).getFileName() +}