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

This commit is contained in:
HapeLee committed 2026-04-28 01:52:21 +08:00
1 parent c2e1cc9971
commit e9461d4a52
6 files changed
+251 -73

No files matched your search

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