|
@@ -16,6 +16,8 @@ import io.legado.app.help.book.isNotShelf
|
|
|
import io.legado.app.model.CacheBook
|
|
import io.legado.app.model.CacheBook
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
+import kotlinx.coroutines.currentCoroutineContext
|
|
|
|
|
+import kotlinx.coroutines.delay
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
import kotlinx.coroutines.flow.asSharedFlow
|
|
import kotlinx.coroutines.flow.asSharedFlow
|
|
@@ -23,25 +25,27 @@ import kotlinx.coroutines.flow.asStateFlow
|
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.flow.update
|
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.launch
|
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
+import kotlin.math.min
|
|
|
|
|
|
|
|
data class BookCacheManageUiState(
|
|
data class BookCacheManageUiState(
|
|
|
val isLoading: Boolean = true,
|
|
val isLoading: Boolean = true,
|
|
|
val shelfBooks: List<BookCacheBookItem> = emptyList(),
|
|
val shelfBooks: List<BookCacheBookItem> = emptyList(),
|
|
|
val notShelfBooks: List<BookCacheBookItem> = emptyList(),
|
|
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 = CacheBook.downloadSummary,
|
|
|
val version: Long = 0,
|
|
val version: Long = 0,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
data class BookCacheBookItem(
|
|
data class BookCacheBookItem(
|
|
|
val book: Book,
|
|
val book: Book,
|
|
|
- val chapters: List<BookCacheChapterItem>,
|
|
|
|
|
|
|
+ val totalCount: Int,
|
|
|
val cachedCount: Int,
|
|
val cachedCount: Int,
|
|
|
val waitingCount: Int,
|
|
val waitingCount: Int,
|
|
|
val downloadingCount: Int,
|
|
val downloadingCount: Int,
|
|
|
val errorCount: Int,
|
|
val errorCount: Int,
|
|
|
val isNotShelf: Boolean,
|
|
val isNotShelf: Boolean,
|
|
|
) {
|
|
) {
|
|
|
- val totalCount: Int get() = chapters.size
|
|
|
|
|
val progress: Float get() = if (totalCount == 0) 0f else cachedCount.toFloat() / totalCount
|
|
val progress: Float get() = if (totalCount == 0) 0f else cachedCount.toFloat() / totalCount
|
|
|
val isDownloading: Boolean get() = waitingCount > 0 || downloadingCount > 0
|
|
val isDownloading: Boolean get() = waitingCount > 0 || downloadingCount > 0
|
|
|
}
|
|
}
|
|
@@ -59,6 +63,7 @@ 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 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 DeleteChapterCache(val bookUrl: String, val chapterUrl: String) : BookCacheManageIntent
|
|
@@ -83,14 +88,16 @@ class BookCacheManageViewModel(
|
|
|
val effects = _effects.asSharedFlow()
|
|
val effects = _effects.asSharedFlow()
|
|
|
|
|
|
|
|
private var observeJob: Job? = null
|
|
private var observeJob: Job? = null
|
|
|
- private val chapterCache = hashMapOf<String, List<BookChapter>>()
|
|
|
|
|
|
|
+ private var fullReloadJob: Job? = null
|
|
|
|
|
+ private val bookReloadJobs = hashMapOf<String, Job>()
|
|
|
|
|
|
|
|
fun onIntent(intent: BookCacheManageIntent) {
|
|
fun onIntent(intent: BookCacheManageIntent) {
|
|
|
when (intent) {
|
|
when (intent) {
|
|
|
BookCacheManageIntent.Initialize -> initialize()
|
|
BookCacheManageIntent.Initialize -> initialize()
|
|
|
- BookCacheManageIntent.Refresh -> reload()
|
|
|
|
|
|
|
+ BookCacheManageIntent.Refresh -> reloadAll(forceDatabase = true)
|
|
|
BookCacheManageIntent.StartAllDownloads -> startAllDownloads()
|
|
BookCacheManageIntent.StartAllDownloads -> startAllDownloads()
|
|
|
BookCacheManageIntent.StopAllDownloads -> stopAllDownloads()
|
|
BookCacheManageIntent.StopAllDownloads -> stopAllDownloads()
|
|
|
|
|
+ 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,
|
|
@@ -106,23 +113,28 @@ class BookCacheManageViewModel(
|
|
|
private fun initialize() {
|
|
private fun initialize() {
|
|
|
if (observeJob != null) return
|
|
if (observeJob != null) return
|
|
|
observeJob = viewModelScope.launch {
|
|
observeJob = viewModelScope.launch {
|
|
|
- bookDao.flowAll().collect {
|
|
|
|
|
- reload(it)
|
|
|
|
|
|
|
+ bookDao.flowAll().collect { books ->
|
|
|
|
|
+ reloadAll(books = books, forceDatabase = false)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
viewModelScope.launch {
|
|
viewModelScope.launch {
|
|
|
- CacheBook.cacheSuccessFlow.collect {
|
|
|
|
|
- reload()
|
|
|
|
|
|
|
+ CacheBook.cacheSuccessFlow.collect { chapter ->
|
|
|
|
|
+ scheduleBookReload(chapter.bookUrl)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
viewModelScope.launch {
|
|
viewModelScope.launch {
|
|
|
- CacheBook.downloadingIndicesFlow.collect {
|
|
|
|
|
- reload()
|
|
|
|
|
|
|
+ CacheBook.downloadingIndicesFlow.collect { state ->
|
|
|
|
|
+ scheduleBookReload(state.first)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
viewModelScope.launch {
|
|
viewModelScope.launch {
|
|
|
- CacheBook.downloadErrorFlow.collect {
|
|
|
|
|
- reload()
|
|
|
|
|
|
|
+ CacheBook.queueChangedFlow.collect { bookUrl ->
|
|
|
|
|
+ scheduleBookReload(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ viewModelScope.launch {
|
|
|
|
|
+ CacheBook.downloadErrorFlow.collect { state ->
|
|
|
|
|
+ scheduleBookReload(state.first)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
viewModelScope.launch {
|
|
viewModelScope.launch {
|
|
@@ -130,53 +142,146 @@ class BookCacheManageViewModel(
|
|
|
_uiState.update {
|
|
_uiState.update {
|
|
|
it.copy(downloadSummary = summary, version = it.version + 1)
|
|
it.copy(downloadSummary = summary, version = it.version + 1)
|
|
|
}
|
|
}
|
|
|
- reload()
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun reload(books: List<Book>? = null) {
|
|
|
|
|
- viewModelScope.launch(Dispatchers.IO) {
|
|
|
|
|
- val sourceBooks = books ?: bookDao.all
|
|
|
|
|
- val items = sourceBooks
|
|
|
|
|
- .filterNot { it.isLocal || it.isAudio }
|
|
|
|
|
- .mapNotNull { book -> buildBookItem(book) }
|
|
|
|
|
- .filter { item ->
|
|
|
|
|
- item.cachedCount > 0 || item.isDownloading || item.errorCount > 0
|
|
|
|
|
|
|
+ private fun reloadAll(
|
|
|
|
|
+ books: List<Book>? = null,
|
|
|
|
|
+ forceDatabase: Boolean = false,
|
|
|
|
|
+ ) {
|
|
|
|
|
+ fullReloadJob?.cancel()
|
|
|
|
|
+ bookReloadJobs.values.forEach { it.cancel() }
|
|
|
|
|
+ bookReloadJobs.clear()
|
|
|
|
|
+ fullReloadJob = viewModelScope.launch {
|
|
|
|
|
+ val expandedBookUrls = uiState.value.expandedBookUrls
|
|
|
|
|
+ val result = withContext(Dispatchers.IO) {
|
|
|
|
|
+ val sourceBooks = if (forceDatabase) bookDao.all else books ?: bookDao.all
|
|
|
|
|
+ val items = sortItems(
|
|
|
|
|
+ sourceBooks
|
|
|
|
|
+ .filterNot { it.isLocal || it.isAudio }
|
|
|
|
|
+ .mapNotNull { book -> buildBookItem(book) }
|
|
|
|
|
+ .filter { item ->
|
|
|
|
|
+ item.cachedCount > 0 || item.isDownloading || item.errorCount > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ val booksByUrl = items.associateBy { it.book.bookUrl }
|
|
|
|
|
+ val retainedExpandedBookUrls = expandedBookUrls.filterTo(linkedSetOf()) {
|
|
|
|
|
+ booksByUrl.containsKey(it)
|
|
|
}
|
|
}
|
|
|
- .sortedWith(compareByDescending<BookCacheBookItem> { it.isDownloading }
|
|
|
|
|
- .thenByDescending { it.cachedCount }
|
|
|
|
|
- .thenBy { it.book.name })
|
|
|
|
|
- withContext(Dispatchers.Main) {
|
|
|
|
|
- _uiState.update {
|
|
|
|
|
- it.copy(
|
|
|
|
|
- isLoading = false,
|
|
|
|
|
- shelfBooks = items.filterNot { item -> item.isNotShelf },
|
|
|
|
|
- notShelfBooks = items.filter { item -> item.isNotShelf },
|
|
|
|
|
- downloadSummary = CacheBook.downloadSummary,
|
|
|
|
|
- version = it.version + 1,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ val chaptersByBookUrl = retainedExpandedBookUrls.associateWith { bookUrl ->
|
|
|
|
|
+ buildChapterItems(booksByUrl.getValue(bookUrl).book)
|
|
|
}
|
|
}
|
|
|
|
|
+ LoadedCacheState(
|
|
|
|
|
+ items = items,
|
|
|
|
|
+ expandedBookUrls = retainedExpandedBookUrls,
|
|
|
|
|
+ chaptersByBookUrl = chaptersByBookUrl,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ _uiState.update { state ->
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ isLoading = false,
|
|
|
|
|
+ shelfBooks = result.items.filterNot { item -> item.isNotShelf },
|
|
|
|
|
+ notShelfBooks = result.items.filter { item -> item.isNotShelf },
|
|
|
|
|
+ expandedBookUrls = result.expandedBookUrls,
|
|
|
|
|
+ chaptersByBookUrl = result.chaptersByBookUrl,
|
|
|
|
|
+ downloadSummary = CacheBook.downloadSummary,
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun buildBookItem(book: Book): BookCacheBookItem? {
|
|
|
|
|
- val chapters = chapterCache.getOrPut(book.bookUrl) {
|
|
|
|
|
- bookChapterDao.getChapterList(book.bookUrl)
|
|
|
|
|
|
|
+ private fun scheduleBookReload(bookUrl: String, debounceMillis: Long = 80) {
|
|
|
|
|
+ if (bookUrl.isBlank()) return
|
|
|
|
|
+ bookReloadJobs.remove(bookUrl)?.cancel()
|
|
|
|
|
+ bookReloadJobs[bookUrl] = viewModelScope.launch {
|
|
|
|
|
+ if (debounceMillis > 0) {
|
|
|
|
|
+ delay(debounceMillis)
|
|
|
|
|
+ }
|
|
|
|
|
+ reloadBook(bookUrl)
|
|
|
|
|
+ if (bookReloadJobs[bookUrl] == currentCoroutineContext()[Job]) {
|
|
|
|
|
+ bookReloadJobs.remove(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun reloadBook(bookUrl: String) {
|
|
|
|
|
+ val expanded = uiState.value.expandedBookUrls.contains(bookUrl)
|
|
|
|
|
+ val result = withContext(Dispatchers.IO) {
|
|
|
|
|
+ val book = bookDao.getBook(bookUrl)
|
|
|
|
|
+ val item = book
|
|
|
|
|
+ ?.takeUnless { it.isLocal || it.isAudio }
|
|
|
|
|
+ ?.let { buildBookItem(it) }
|
|
|
|
|
+ ?.takeIf {
|
|
|
|
|
+ it.cachedCount > 0 || it.isDownloading || it.errorCount > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ val chapters = if (expanded && item != null) {
|
|
|
|
|
+ buildChapterItems(item.book)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ null
|
|
|
|
|
+ }
|
|
|
|
|
+ LoadedBookState(item, chapters)
|
|
|
|
|
+ }
|
|
|
|
|
+ _uiState.update { state ->
|
|
|
|
|
+ val combinedBooks = (state.shelfBooks + state.notShelfBooks)
|
|
|
|
|
+ .filterNot { it.book.bookUrl == bookUrl }
|
|
|
|
|
+ .let { items ->
|
|
|
|
|
+ result.item?.let { items + it } ?: items
|
|
|
|
|
+ }
|
|
|
|
|
+ val sortedBooks = sortItems(combinedBooks)
|
|
|
|
|
+ val expandedBookUrls = if (result.item == null) {
|
|
|
|
|
+ state.expandedBookUrls - bookUrl
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.expandedBookUrls
|
|
|
|
|
+ }
|
|
|
|
|
+ val chaptersByBookUrl = when {
|
|
|
|
|
+ result.item == null -> state.chaptersByBookUrl - bookUrl
|
|
|
|
|
+ result.chapters != null -> state.chaptersByBookUrl + (bookUrl to result.chapters)
|
|
|
|
|
+ else -> state.chaptersByBookUrl
|
|
|
|
|
+ }
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ shelfBooks = sortedBooks.filterNot { item -> item.isNotShelf },
|
|
|
|
|
+ notShelfBooks = sortedBooks.filter { item -> item.isNotShelf },
|
|
|
|
|
+ expandedBookUrls = expandedBookUrls,
|
|
|
|
|
+ chaptersByBookUrl = chaptersByBookUrl,
|
|
|
|
|
+ downloadSummary = CacheBook.downloadSummary,
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun buildBookItem(book: Book): BookCacheBookItem? {
|
|
|
val cacheFiles = BookHelp.getChapterFiles(book)
|
|
val cacheFiles = BookHelp.getChapterFiles(book)
|
|
|
val model = CacheBook.cacheBookMap[book.bookUrl]
|
|
val model = CacheBook.cacheBookMap[book.bookUrl]
|
|
|
val waitingIndices = model?.waitingIndices().orEmpty()
|
|
val waitingIndices = model?.waitingIndices().orEmpty()
|
|
|
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
|
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
|
|
- val errorIndices = CacheBook.downloadErrorFlow.value
|
|
|
|
|
- .takeIf { it.first == book.bookUrl }
|
|
|
|
|
- ?.second
|
|
|
|
|
- .orEmpty()
|
|
|
|
|
- if (chapters.isEmpty() && cacheFiles.isEmpty() && model == null && !book.isNotShelf) {
|
|
|
|
|
|
|
+ val errorIndices = errorIndices(book.bookUrl)
|
|
|
|
|
+ val totalCount = bookChapterDao.getChapterCount(book.bookUrl)
|
|
|
|
|
+ val cachedFileCount = cacheFiles.count { it.endsWith(".nb") }
|
|
|
|
|
+ val cachedCount = min(cachedFileCount + bookChapterDao.getVolumeCount(book.bookUrl), totalCount)
|
|
|
|
|
+ if (totalCount == 0 && cacheFiles.isEmpty() && model == null && !book.isNotShelf) {
|
|
|
return null
|
|
return null
|
|
|
}
|
|
}
|
|
|
- val chapterItems = chapters.map { chapter ->
|
|
|
|
|
|
|
+ return BookCacheBookItem(
|
|
|
|
|
+ book = book,
|
|
|
|
|
+ totalCount = totalCount,
|
|
|
|
|
+ cachedCount = cachedCount,
|
|
|
|
|
+ waitingCount = waitingIndices.size,
|
|
|
|
|
+ downloadingCount = downloadingIndices.size,
|
|
|
|
|
+ errorCount = errorIndices.size,
|
|
|
|
|
+ isNotShelf = book.isNotShelf,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun buildChapterItems(book: Book): List<BookCacheChapterItem> {
|
|
|
|
|
+ val chapters = bookChapterDao.getChapterList(book.bookUrl)
|
|
|
|
|
+ val cacheFiles = BookHelp.getChapterFiles(book)
|
|
|
|
|
+ val model = CacheBook.cacheBookMap[book.bookUrl]
|
|
|
|
|
+ val waitingIndices = model?.waitingIndices().orEmpty()
|
|
|
|
|
+ val downloadingIndices = model?.downloadingIndices().orEmpty()
|
|
|
|
|
+ val errorIndices = errorIndices(book.bookUrl)
|
|
|
|
|
+ return chapters.map { chapter ->
|
|
|
BookCacheChapterItem(
|
|
BookCacheChapterItem(
|
|
|
chapter = chapter,
|
|
chapter = chapter,
|
|
|
isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
|
|
isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
|
|
@@ -185,29 +290,68 @@ class BookCacheManageViewModel(
|
|
|
isError = errorIndices.contains(chapter.index),
|
|
isError = errorIndices.contains(chapter.index),
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
- return BookCacheBookItem(
|
|
|
|
|
- book = book,
|
|
|
|
|
- chapters = chapterItems,
|
|
|
|
|
- cachedCount = chapterItems.count { it.isCached },
|
|
|
|
|
- waitingCount = waitingIndices.size,
|
|
|
|
|
- downloadingCount = downloadingIndices.size,
|
|
|
|
|
- errorCount = errorIndices.size,
|
|
|
|
|
- isNotShelf = book.isNotShelf,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun errorIndices(bookUrl: String): Set<Int> {
|
|
|
|
|
+ return CacheBook.errorIndices(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun toggleBookExpanded(bookUrl: String) {
|
|
|
|
|
+ val shouldExpand = !_uiState.value.expandedBookUrls.contains(bookUrl)
|
|
|
|
|
+ _uiState.update { state ->
|
|
|
|
|
+ if (shouldExpand) {
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ expandedBookUrls = state.expandedBookUrls + bookUrl,
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ expandedBookUrls = state.expandedBookUrls - bookUrl,
|
|
|
|
|
+ chaptersByBookUrl = state.chaptersByBookUrl - bookUrl,
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (shouldExpand) {
|
|
|
|
|
+ loadBookChapters(bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ _uiState.update { state ->
|
|
|
|
|
+ if (!state.expandedBookUrls.contains(bookUrl)) {
|
|
|
|
|
+ state
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.copy(
|
|
|
|
|
+ chaptersByBookUrl = state.chaptersByBookUrl + (bookUrl to chapters),
|
|
|
|
|
+ version = state.version + 1,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
- reload()
|
|
|
|
|
|
|
+ reloadAll(forceDatabase = true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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 = item.chapters
|
|
|
|
|
- .filterNot { it.isCached || it.isWaiting || it.isDownloading }
|
|
|
|
|
- .map { it.chapter.index }
|
|
|
|
|
|
|
+ val chapterIndices = downloadableChapterIndices(item.book)
|
|
|
cacheBookChaptersUseCase.execute(item.book.bookUrl, chapterIndices)
|
|
cacheBookChaptersUseCase.execute(item.book.bookUrl, chapterIndices)
|
|
|
}
|
|
}
|
|
|
}.onSuccess { count ->
|
|
}.onSuccess { count ->
|
|
@@ -219,10 +363,27 @@ class BookCacheManageViewModel(
|
|
|
}.onError {
|
|
}.onError {
|
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("加入缓存队列失败\n${it.localizedMessage}"))
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("加入缓存队列失败\n${it.localizedMessage}"))
|
|
|
}.onFinally {
|
|
}.onFinally {
|
|
|
- reload()
|
|
|
|
|
|
|
+ reloadAll(forceDatabase = true)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun downloadableChapterIndices(book: Book): List<Int> {
|
|
|
|
|
+ val cacheFiles = BookHelp.getChapterFiles(book)
|
|
|
|
|
+ val model = CacheBook.cacheBookMap[book.bookUrl]
|
|
|
|
|
+ val waitingIndices = model?.waitingIndices().orEmpty()
|
|
|
|
|
+ val downloadingIndices = model?.downloadingIndices().orEmpty()
|
|
|
|
|
+ return bookChapterDao.getChapterList(book.bookUrl)
|
|
|
|
|
+ .asSequence()
|
|
|
|
|
+ .filterNot { chapter ->
|
|
|
|
|
+ chapter.isVolume ||
|
|
|
|
|
+ cacheFiles.contains(chapter.getFileName()) ||
|
|
|
|
|
+ waitingIndices.contains(chapter.index) ||
|
|
|
|
|
+ downloadingIndices.contains(chapter.index)
|
|
|
|
|
+ }
|
|
|
|
|
+ .map { it.index }
|
|
|
|
|
+ .toList()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun deleteBookCache(bookUrl: String) {
|
|
private fun deleteBookCache(bookUrl: String) {
|
|
|
CacheBook.remove(context, bookUrl)
|
|
CacheBook.remove(context, bookUrl)
|
|
|
execute {
|
|
execute {
|
|
@@ -232,7 +393,7 @@ class BookCacheManageViewModel(
|
|
|
}.onError {
|
|
}.onError {
|
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除缓存失败\n${it.localizedMessage}"))
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除缓存失败\n${it.localizedMessage}"))
|
|
|
}.onFinally {
|
|
}.onFinally {
|
|
|
- reload()
|
|
|
|
|
|
|
+ scheduleBookReload(bookUrl, debounceMillis = 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -244,13 +405,17 @@ class BookCacheManageViewModel(
|
|
|
}.onError {
|
|
}.onError {
|
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节缓存失败\n${it.localizedMessage}"))
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节缓存失败\n${it.localizedMessage}"))
|
|
|
}.onFinally {
|
|
}.onFinally {
|
|
|
- reload()
|
|
|
|
|
|
|
+ scheduleBookReload(bookUrl, debounceMillis = 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun deleteChapterCache(bookUrl: String, chapterUrl: String) {
|
|
private fun deleteChapterCache(bookUrl: String, chapterUrl: String) {
|
|
|
- val book = bookDao.getBook(bookUrl) ?: return
|
|
|
|
|
- val chapter = chapterCache[bookUrl]?.firstOrNull { it.url == chapterUrl } ?: return
|
|
|
|
|
|
|
+ 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
|
|
|
execute {
|
|
execute {
|
|
|
BookHelp.delContent(book, chapter)
|
|
BookHelp.delContent(book, chapter)
|
|
|
}.onSuccess {
|
|
}.onSuccess {
|
|
@@ -258,7 +423,24 @@ class BookCacheManageViewModel(
|
|
|
}.onError {
|
|
}.onError {
|
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除章节缓存失败\n${it.localizedMessage}"))
|
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除章节缓存失败\n${it.localizedMessage}"))
|
|
|
}.onFinally {
|
|
}.onFinally {
|
|
|
- reload()
|
|
|
|
|
|
|
+ scheduleBookReload(bookUrl, debounceMillis = 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private fun sortItems(items: List<BookCacheBookItem>): List<BookCacheBookItem> {
|
|
|
|
|
+ return items.sortedWith(compareByDescending<BookCacheBookItem> { it.isDownloading }
|
|
|
|
|
+ .thenByDescending { it.cachedCount }
|
|
|
|
|
+ .thenBy { it.book.name })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private data class LoadedCacheState(
|
|
|
|
|
+ val items: List<BookCacheBookItem>,
|
|
|
|
|
+ val expandedBookUrls: Set<String>,
|
|
|
|
|
+ val chaptersByBookUrl: Map<String, List<BookCacheChapterItem>>,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ private data class LoadedBookState(
|
|
|
|
|
+ val item: BookCacheBookItem?,
|
|
|
|
|
+ val chapters: List<BookCacheChapterItem>?,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|