Просмотр исходного кода

[优化] 依旧优化某些界面的OOM问题

HapeLee 6 месяцев назад
Родитель
Сommit
e9461d4a52

+ 4 - 0
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<BookChapter>
 
+    @Query("select url, title, isVolume, `index` from chapters where bookUrl = :bookUrl order by `index`")
+    fun getChapterCacheInfoList(bookUrl: String): List<BookChapterCacheInfo>
+
     @Query("select * from chapters where bookUrl = :bookUrl order by `index`")
     fun getChapterListFlow(bookUrl: String): Flow<List<BookChapter>>
 

+ 8 - 0
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,
+)

+ 54 - 6
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<CacheBookService> {
@@ -246,6 +259,8 @@ object CacheBook {
 
         private val waitDownloadSet = linkedSetOf<Int>()
         private val onDownloadSet = linkedSetOf<Int>()
+        private val pausedDownloadSet = hashSetOf<Int>()
+        private val chapterTasks = hashMapOf<Int, Coroutine<*>>()
         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<Int>) {
             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 {

+ 1 - 2
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()
             }

+ 69 - 24
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),

+ 115 - 41
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<BookCacheBookItem> = emptyList(),
     val expandedBookUrls: Set<String> = emptySet(),
     val chaptersByBookUrl: Map<String, List<BookCacheChapterItem>> = 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<BookCacheChapterItem> {
-        val chapters = bookChapterDao.getChapterList(book.bookUrl)
+    private fun buildChapterItems(bookUrl: String): List<BookCacheChapterItem> {
+        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<Int> {
+    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<Int> {
+        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<BookCacheBookItem>): List<BookCacheBookItem> {
         return items.sortedWith(compareByDescending<BookCacheBookItem> { it.isDownloading }
             .thenByDescending { it.cachedCount }
-            .thenBy { it.book.name })
+            .thenBy { it.name })
+    }
+
+    private fun buildDownloadSummary(items: List<BookCacheBookItem>): 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<BookCacheChapterItem>?,
     )
 }
+
+private fun BookChapterCacheInfo.getFileName(): String {
+    return BookChapter(
+        url = url,
+        title = title,
+        isVolume = isVolume,
+        index = index,
+    ).getFileName()
+}