[优化] 依旧优化某些界面的OOM问题
This commit is contained in:
@@ -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>>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user