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

This commit is contained in:
HapeLee
2026-04-28 00:47:53 +08:00
parent 827df0dec7
commit c2e1cc9971
13 changed files with 529 additions and 245 deletions
@@ -35,6 +35,9 @@ interface BookChapterDao {
@Query("select count(url) from chapters where bookUrl = :bookUrl")
fun getChapterCount(bookUrl: String): Int
@Query("select count(url) from chapters where bookUrl = :bookUrl and isVolume = 1")
fun getVolumeCount(bookUrl: String): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookChapter: BookChapter)
@@ -59,4 +62,4 @@ interface BookChapterDao {
AND `index` = :chapterIndex
""")
suspend fun getChapterTitleByUrlAndIndex(bookUrl: String, chapterIndex: Int): String?
}
}
@@ -59,6 +59,9 @@ interface SearchBookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(searchBooks: List<SearchBook>): List<Long>
@Query("delete from searchBooks where name = :name and author = :author")
fun clear(name: String, author: String)
@@ -70,4 +73,4 @@ interface SearchBookDao {
@Delete
fun delete(vararg searchBook: SearchBook)
}
}
@@ -39,7 +39,9 @@ sealed interface SearchSessionEvent {
data object Started : SearchSessionEvent
data class Progress(
val books: List<SearchBook>,
val upsertBooks: List<SearchBook>,
val removedBookUrls: List<String>,
val resultCount: Int,
val processedSources: Int,
val totalSources: Int,
) : SearchSessionEvent
@@ -120,32 +122,36 @@ class SearchRepositoryImpl(
) : SearchSession {
private val sessionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val _events = MutableSharedFlow<SearchSessionEvent>(extraBufferCapacity = 64)
private val _events = MutableSharedFlow<SearchSessionEvent>(extraBufferCapacity = 8)
override val events: Flow<SearchSessionEvent> = _events.asSharedFlow()
private val searchModel = SearchModel(sessionScope, object : SearchModel.CallBack {
override fun getSearchScope(): SearchScope = scopeProvider()
override fun onSearchStart() {
_events.tryEmit(SearchSessionEvent.Started)
override suspend fun onSearchStart() {
_events.emit(SearchSessionEvent.Started)
}
override fun onSearchSuccess(
searchBooks: List<SearchBook>,
override suspend fun onSearchSuccess(
upsertBooks: List<SearchBook>,
removedBookUrls: List<String>,
resultCount: Int,
processedSources: Int,
totalSources: Int,
) {
_events.tryEmit(
_events.emit(
SearchSessionEvent.Progress(
books = searchBooks.toList(),
upsertBooks = upsertBooks,
removedBookUrls = removedBookUrls,
resultCount = resultCount,
processedSources = processedSources,
totalSources = totalSources,
)
)
}
override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) {
_events.tryEmit(SearchSessionEvent.Finished(isEmpty, hasMore))
override suspend fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) {
_events.emit(SearchSessionEvent.Finished(isEmpty, hasMore))
}
override fun onSearchCancel(exception: Throwable?) {
@@ -44,7 +44,7 @@ data class BatchChangeBookSourceResult(
data class BatchChangeSourceCandidate(
val source: BookSource,
val book: Book,
val chapters: List<BookChapter>,
val chapterCount: Int,
)
data class BatchChangeSourcePreviewItem(
@@ -172,14 +172,8 @@ class ChangeBookSourceUseCase(
} else {
val candidates = arrayListOf<BatchChangeSourceCandidate>()
sources.filterNot { it.bookSourceUrl == book.origin }.forEach { source ->
findBookInSource(book, source)?.let { (newBook, chapters) ->
candidates.add(
BatchChangeSourceCandidate(
source = source,
book = newBook,
chapters = chapters,
)
)
findBookInSource(book, source)?.let { candidate ->
candidates.add(candidate)
}
}
if (candidates.isEmpty()) {
@@ -199,50 +193,40 @@ class ChangeBookSourceUseCase(
.map { it.second }
}
fun changePreviewItems(
items: List<BatchChangeSourcePreviewItem>,
options: ChangeSourceMigrationOptions,
): BatchChangeBookSourceResult {
var changedCount = 0
var failedCount = 0
var skippedCount = 0
items.forEach { item ->
if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
skippedCount++
return@forEach
}
val candidate = item.selectedCandidate
if (candidate == null) {
failedCount++
return@forEach
}
changeTo(item.oldBook, candidate.book, candidate.chapters, options)
changedCount++
}
return BatchChangeBookSourceResult(changedCount, failedCount, skippedCount)
}
private suspend fun findBookInSource(
oldBook: Book,
source: BookSource,
): Pair<Book, List<BookChapter>>? {
): BatchChangeSourceCandidate? {
val newBook = WebBook.preciseSearchAwait(source, oldBook.name, oldBook.author)
.onFailure {
AppLog.put("搜索书籍出错\n${it.localizedMessage}", it, true)
}.getOrNull() ?: return null
val chapters = loadCandidateChapters(source, newBook) ?: return null
return BatchChangeSourceCandidate(
source = source,
book = newBook,
chapterCount = chapters.size,
)
}
suspend fun loadCandidateChapters(
source: BookSource,
book: Book,
): List<BookChapter>? {
val infoLoaded = kotlin.runCatching {
if (newBook.tocUrl.isEmpty()) {
WebBook.getBookInfoAwait(source, newBook)
if (book.tocUrl.isEmpty()) {
WebBook.getBookInfoAwait(source, book)
}
}.onFailure {
AppLog.put("获取书籍详情出错\n${it.localizedMessage}", it, true)
}.isSuccess
if (!infoLoaded) return null
val chapters = WebBook.getChapterListAwait(source, newBook)
val chapters = WebBook.getChapterListAwait(source, book)
.onFailure {
AppLog.put("获取目录出错\n${it.localizedMessage}", it, true)
}.getOrNull() ?: return null
return newBook to chapters
book.totalChapterNum = chapters.size
return chapters
}
private fun Book.applyMigrationTo(
@@ -100,6 +100,9 @@ object CacheBook {
MutableStateFlow<Pair<String, Set<Int>>>("" to emptySet())
val downloadingIndicesFlow = _downloadingIndicesFlow.asStateFlow()
private val _queueChangedFlow = MutableSharedFlow<String>(extraBufferCapacity = 64)
val queueChangedFlow = _queueChangedFlow.asSharedFlow()
private val _downloadErrorFlow =
MutableStateFlow<Pair<String, Set<Int>>>("" to emptySet())
val downloadErrorFlow = _downloadErrorFlow.asStateFlow()
@@ -113,6 +116,10 @@ object CacheBook {
val cacheBookMap: ConcurrentHashMap<String, CacheBookModel>
get() = coordinator.taskMap
fun errorIndices(bookUrl: String): Set<Int> {
return errorIndexMap[bookUrl]?.toSet().orEmpty()
}
private fun collectQueueStats(): QueueStats {
var waiting = 0
var downloading = 0
@@ -222,6 +229,7 @@ object CacheBook {
private fun onTaskQueuesChanged(bookUrl: String) {
updateSummary()
_queueChangedFlow.tryEmit(bookUrl)
postEvent(EventBus.UP_DOWNLOAD, bookUrl)
}
@@ -297,8 +305,13 @@ object CacheBook {
@Synchronized
fun addDownload(start: Int, end: Int) {
addDownloads(start..end)
}
@Synchronized
fun addDownloads(indices: Iterable<Int>) {
isStopped = false
for (i in start..end) {
for (i in indices) {
if (!onDownloadSet.contains(i)) {
waitDownloadSet.add(i)
}
@@ -33,15 +33,22 @@ import kotlin.coroutines.coroutineContext
import kotlin.math.min
class SearchModel(private val scope: CoroutineScope, private val callBack: CallBack) {
private companion object {
const val MAX_RETAINED_SEARCH_RESULTS = 1000
}
val threadCount = OtherConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
private var mSearchId = 0L
private var searchPage = 1
private var searchKey: String = ""
private var bookSourceParts = emptyList<BookSourcePart>()
private var searchBooks = arrayListOf<SearchBook>()
private val equalBooks = LinkedHashMap<SearchBookKey, SearchBook>()
private val containsBooks = LinkedHashMap<SearchBookKey, SearchBook>()
private val otherBooks = LinkedHashMap<SearchBookKey, SearchBook>()
private var searchJob: Job? = null
private var workingState = MutableStateFlow(true)
private var resultLimitReached = false
private fun initSearchPool() {
searchPool?.close()
@@ -58,7 +65,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
if (mSearchId != 0L) {
close()
}
searchBooks.clear()
clearSearchBooks()
bookSourceParts = callBack.getSearchScope().getBookSourceParts()
if (bookSourceParts.isEmpty()) {
callBack.onSearchCancel(NoStackTraceException("启用书源为空"))
@@ -102,84 +109,100 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
book.releaseHtmlData()
}
hasMore = hasMore || items.isNotEmpty()
appDb.searchBookDao.insert(*items.toTypedArray())
mergeItems(items, precision)
if (items.isNotEmpty()) {
appDb.searchBookDao.insert(items)
}
val change = mergeItems(items, precision)
currentCoroutineContext().ensureActive()
processedParts++
callBack.onSearchSuccess(searchBooks, processedParts, totalParts)
callBack.onSearchSuccess(
upsertBooks = change.upsertBooks,
removedBookUrls = change.removedBookUrls,
resultCount = searchBookCount(),
processedSources = processedParts,
totalSources = totalParts,
)
}.onCompletion {
if (it == null) callBack.onSearchFinish(searchBooks.isEmpty(), hasMore)
if (it == null) {
callBack.onSearchFinish(
isEmpty = searchBookCount() == 0,
hasMore = hasMore && !resultLimitReached,
)
}
}.catch {
AppLog.put("书源搜索出错\n${it.localizedMessage}", it)
}.collect()
}
}
private suspend fun mergeItems(newDataS: List<SearchBook>, precision: Boolean) {
if (newDataS.isNotEmpty()) {
val copyData = ArrayList(searchBooks)
val equalData = arrayListOf<SearchBook>()
val containsData = arrayListOf<SearchBook>()
val otherData = arrayListOf<SearchBook>()
copyData.forEach {
coroutineContext.ensureActive()
if (it.name == searchKey || it.author == searchKey) {
equalData.add(it)
} else if (it.name.contains(searchKey) || it.author.contains(searchKey)) {
containsData.add(it)
} else {
otherData.add(it)
}
}
newDataS.forEach { nBook ->
coroutineContext.ensureActive()
if (nBook.name == searchKey || nBook.author == searchKey) {
var hasSame = false
equalData.forEach { pBook ->
coroutineContext.ensureActive()
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
hasSame = true
}
}
if (!hasSame) {
equalData.add(nBook)
}
} else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) {
var hasSame = false
containsData.forEach { pBook ->
coroutineContext.ensureActive()
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
hasSame = true
}
}
if (!hasSame) {
containsData.add(nBook)
}
} else if (!precision) {
var hasSame = false
otherData.forEach { pBook ->
coroutineContext.ensureActive()
if (pBook.name == nBook.name && pBook.author == nBook.author) {
pBook.addOrigin(nBook.origin)
hasSame = true
}
}
if (!hasSame) {
otherData.add(nBook)
}
}
}
coroutineContext.ensureActive()
equalData.sortByDescending { it.origins.size }
equalData.addAll(containsData.sortedByDescending { it.origins.size })
if (!precision) {
equalData.addAll(otherData)
}
coroutineContext.ensureActive()
searchBooks = equalData
private suspend fun mergeItems(newDataS: List<SearchBook>, precision: Boolean): SearchBookChange {
if (newDataS.isEmpty()) {
return SearchBookChange()
}
val upsertBooks = arrayListOf<SearchBook>()
val removedBookUrls = linkedSetOf<String>()
newDataS.forEach { nBook ->
coroutineContext.ensureActive()
val bucket = classifyBucket(nBook, precision) ?: return@forEach
val key = SearchBookKey(nBook.name, nBook.author)
val currentBook = bucket[key]
if (currentBook == null) {
bucket[key] = nBook
upsertBooks.add(nBook)
} else {
currentBook.addOrigin(nBook.origin)
upsertBooks.add(currentBook)
}
trimSearchBooks()?.let { removed ->
removedBookUrls.add(removed.bookUrl)
upsertBooks.removeAll { it.bookUrl == removed.bookUrl }
}
}
return SearchBookChange(upsertBooks, removedBookUrls.toList())
}
private fun classifyBucket(
book: SearchBook,
precision: Boolean,
): LinkedHashMap<SearchBookKey, SearchBook>? {
return when {
book.name == searchKey || book.author == searchKey -> equalBooks
book.name.contains(searchKey) || book.author.contains(searchKey) -> containsBooks
!precision -> otherBooks
else -> null
}
}
private fun trimSearchBooks(): SearchBook? {
if (searchBookCount() <= MAX_RETAINED_SEARCH_RESULTS) {
return null
}
resultLimitReached = true
return removeLast(otherBooks)
?: removeLowestOrigin(containsBooks)
?: removeLowestOrigin(equalBooks)
}
private fun removeLast(bucket: LinkedHashMap<SearchBookKey, SearchBook>): SearchBook? {
val key = bucket.keys.lastOrNull() ?: return null
return bucket.remove(key)
}
private fun removeLowestOrigin(bucket: LinkedHashMap<SearchBookKey, SearchBook>): SearchBook? {
val key = bucket.entries.minByOrNull { it.value.origins.size }?.key ?: return null
return bucket.remove(key)
}
private fun searchBookCount(): Int {
return equalBooks.size + containsBooks.size + otherBooks.size
}
private fun clearSearchBooks() {
equalBooks.clear()
containsBooks.clear()
otherBooks.clear()
resultLimitReached = false
}
fun cancelSearch() {
@@ -204,10 +227,26 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
interface CallBack {
fun getSearchScope(): SearchScope
fun onSearchStart()
fun onSearchSuccess(searchBooks: List<SearchBook>, processedSources: Int, totalSources: Int)
fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean)
suspend fun onSearchStart()
suspend fun onSearchSuccess(
upsertBooks: List<SearchBook>,
removedBookUrls: List<String>,
resultCount: Int,
processedSources: Int,
totalSources: Int,
)
suspend fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean)
fun onSearchCancel(exception: Throwable? = null)
}
private data class SearchBookKey(
val name: String,
val author: String,
)
private data class SearchBookChange(
val upsertBooks: List<SearchBook> = emptyList(),
val removedBookUrls: List<String> = emptyList(),
)
}
@@ -163,10 +163,8 @@ class CacheBookService : BaseService() {
}
}
//添加每一章到下载队列
indices.forEach { index ->
cacheBook.addDownload(index)
}
//添加章到下载队列
cacheBook.addDownloads(indices)
notificationContent = CacheBook.downloadSummary
upCacheBookNotification()
@@ -31,7 +31,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -94,10 +93,9 @@ private fun BookCacheManageScreen(
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
var pendingDeleteBook by remember { mutableStateOf<BookCacheBookItem?>(null) }
var pendingDeleteChapter by remember { mutableStateOf<Pair<BookCacheBookItem, BookCacheChapterItem>?>(null) }
var expandedBookUrls by rememberSaveable { mutableStateOf(emptySet<String>()) }
val allBooks = state.shelfBooks + state.notShelfBooks
val hasRunningDownload = allBooks.any { it.isDownloading }
val hasDownloadTarget = allBooks.any { it.chapters.any { chapter -> !chapter.isCached } }
val hasDownloadTarget = allBooks.any { it.cachedCount < it.totalCount }
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -160,9 +158,10 @@ private fun BookCacheManageScreen(
title = "书架书籍",
emptyText = "没有书架内书籍缓存或下载任务",
books = state.shelfBooks,
expandedBookUrls = expandedBookUrls,
expandedBookUrls = state.expandedBookUrls,
chaptersByBookUrl = state.chaptersByBookUrl,
onToggleExpanded = { bookUrl ->
expandedBookUrls = expandedBookUrls.toggle(bookUrl)
onIntent(BookCacheManageIntent.ToggleBookExpanded(bookUrl))
},
onIntent = onIntent,
onDeleteBook = { pendingDeleteBook = it },
@@ -172,9 +171,10 @@ private fun BookCacheManageScreen(
title = "未在书架",
emptyText = "没有未在书架的书籍下载状态",
books = state.notShelfBooks,
expandedBookUrls = expandedBookUrls,
expandedBookUrls = state.expandedBookUrls,
chaptersByBookUrl = state.chaptersByBookUrl,
onToggleExpanded = { bookUrl ->
expandedBookUrls = expandedBookUrls.toggle(bookUrl)
onIntent(BookCacheManageIntent.ToggleBookExpanded(bookUrl))
},
onIntent = onIntent,
onDeleteBook = { pendingDeleteBook = it },
@@ -212,6 +212,7 @@ private fun LazyListScope.cacheSection(
emptyText: String,
books: List<BookCacheBookItem>,
expandedBookUrls: Set<String>,
chaptersByBookUrl: Map<String, List<BookCacheChapterItem>>,
onToggleExpanded: (String) -> Unit,
onIntent: (BookCacheManageIntent) -> Unit,
onDeleteBook: (BookCacheBookItem) -> Unit,
@@ -245,7 +246,7 @@ private fun LazyListScope.cacheSection(
}
if (expanded) {
items(
items = item.chapters,
items = chaptersByBookUrl[bookUrl].orEmpty(),
key = { chapter -> "$title-chapter-$bookUrl-${chapter.chapter.url}" }
) { chapter ->
BookCacheChapterRow(
@@ -392,10 +393,6 @@ private fun BookCacheChapterRow(
}
}
private fun Set<String>.toggle(value: String): Set<String> {
return if (contains(value)) this - value else this + value
}
private fun chapterStatusText(item: BookCacheChapterItem): String {
return when {
item.isDownloading -> "下载中"
@@ -16,6 +16,8 @@ import io.legado.app.help.book.isNotShelf
import io.legado.app.model.CacheBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
@@ -23,25 +25,27 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.min
data class BookCacheManageUiState(
val isLoading: Boolean = true,
val shelfBooks: 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 version: Long = 0,
)
data class BookCacheBookItem(
val book: Book,
val chapters: List<BookCacheChapterItem>,
val totalCount: Int,
val cachedCount: Int,
val waitingCount: Int,
val downloadingCount: Int,
val errorCount: Int,
val isNotShelf: Boolean,
) {
val totalCount: Int get() = chapters.size
val progress: Float get() = if (totalCount == 0) 0f else cachedCount.toFloat() / totalCount
val isDownloading: Boolean get() = waitingCount > 0 || downloadingCount > 0
}
@@ -59,6 +63,7 @@ sealed interface BookCacheManageIntent {
data object Refresh : BookCacheManageIntent
data object StartAllDownloads : BookCacheManageIntent
data object StopAllDownloads : 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
@@ -83,14 +88,16 @@ class BookCacheManageViewModel(
val effects = _effects.asSharedFlow()
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) {
when (intent) {
BookCacheManageIntent.Initialize -> initialize()
BookCacheManageIntent.Refresh -> reload()
BookCacheManageIntent.Refresh -> reloadAll(forceDatabase = true)
BookCacheManageIntent.StartAllDownloads -> startAllDownloads()
BookCacheManageIntent.StopAllDownloads -> stopAllDownloads()
is BookCacheManageIntent.ToggleBookExpanded -> toggleBookExpanded(intent.bookUrl)
is BookCacheManageIntent.DeleteBookCache -> deleteBookCache(intent.bookUrl)
is BookCacheManageIntent.DownloadChapter -> downloadChapter(
intent.bookUrl,
@@ -106,23 +113,28 @@ class BookCacheManageViewModel(
private fun initialize() {
if (observeJob != null) return
observeJob = viewModelScope.launch {
bookDao.flowAll().collect {
reload(it)
bookDao.flowAll().collect { books ->
reloadAll(books = books, forceDatabase = false)
}
}
viewModelScope.launch {
CacheBook.cacheSuccessFlow.collect {
reload()
CacheBook.cacheSuccessFlow.collect { chapter ->
scheduleBookReload(chapter.bookUrl)
}
}
viewModelScope.launch {
CacheBook.downloadingIndicesFlow.collect {
reload()
CacheBook.downloadingIndicesFlow.collect { state ->
scheduleBookReload(state.first)
}
}
viewModelScope.launch {
CacheBook.downloadErrorFlow.collect {
reload()
CacheBook.queueChangedFlow.collect { bookUrl ->
scheduleBookReload(bookUrl)
}
}
viewModelScope.launch {
CacheBook.downloadErrorFlow.collect { state ->
scheduleBookReload(state.first)
}
}
viewModelScope.launch {
@@ -130,53 +142,146 @@ class BookCacheManageViewModel(
_uiState.update {
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 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 chapters = chapterCache.getOrPut(book.bookUrl) {
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 = 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
}
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(
chapter = chapter,
isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
@@ -185,29 +290,68 @@ class BookCacheManageViewModel(
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() {
CacheBook.stop(context)
reload()
reloadAll(forceDatabase = true)
}
private fun startAllDownloads() {
val items = uiState.value.shelfBooks + uiState.value.notShelfBooks
execute {
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)
}
}.onSuccess { count ->
@@ -219,10 +363,27 @@ class BookCacheManageViewModel(
}.onError {
_effects.tryEmit(BookCacheManageEffect.ShowMessage("加入缓存队列失败\n${it.localizedMessage}"))
}.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) {
CacheBook.remove(context, bookUrl)
execute {
@@ -232,7 +393,7 @@ class BookCacheManageViewModel(
}.onError {
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除缓存失败\n${it.localizedMessage}"))
}.onFinally {
reload()
scheduleBookReload(bookUrl, debounceMillis = 0)
}
}
@@ -244,13 +405,17 @@ class BookCacheManageViewModel(
}.onError {
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节缓存失败\n${it.localizedMessage}"))
}.onFinally {
reload()
scheduleBookReload(bookUrl, debounceMillis = 0)
}
}
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 {
BookHelp.delContent(book, chapter)
}.onSuccess {
@@ -258,7 +423,24 @@ class BookCacheManageViewModel(
}.onError {
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除章节缓存失败\n${it.localizedMessage}"))
}.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>?,
)
}
@@ -684,7 +684,7 @@ private fun BookshelfManageScreen(
) {
items(filteredBooks, key = { it.bookUrl }) { book ->
val cacheCount = remember(renderVersion, book.bookUrl) {
viewModel.getCacheChapters(book.bookUrl)?.size ?: 0
viewModel.getCacheCount(book.bookUrl) ?: 0
}
val isDownloading = remember(renderVersion, book.bookUrl) {
viewModel.isBookDownloading(book.bookUrl)
@@ -1512,7 +1512,7 @@ private fun BatchChangePreviewRow(
chapterCount = if (item.status == BatchChangeSourcePreviewStatus.Skipped) {
null
} else {
candidate?.chapters?.size
candidate?.chapterCount
},
onClick = {
candidate?.book?.let { onOpenBook(it, false) }
@@ -1624,7 +1624,7 @@ private fun OtherSourceOptionsSheet(
val candidate = currentItem.candidates[index]
SelectionItemCard(
title = candidate.source.bookSourceName,
subtitle = "${candidate.book.name} · ${candidate.chapters.size}",
subtitle = "${candidate.book.name} · ${candidate.chapterCount}",
supportingContent = {
AppText(
text = candidate.book.getRealAuthor(),
@@ -35,6 +35,7 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.ui.main.bookshelf.toLightBook
import io.legado.app.utils.cnCompare
import io.legado.app.utils.move
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -45,6 +46,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
import java.util.concurrent.ConcurrentHashMap
data class BookshelfManageScreenExportConfig(
val exportUseReplace: Boolean = true,
@@ -158,9 +161,11 @@ class BookshelfManageScreenViewModel(
private val _effects = MutableSharedFlow<BookshelfManageScreenEffect>(extraBufferCapacity = 32)
val effects = _effects.asSharedFlow()
private val cacheChapters = hashMapOf<String, HashSet<String>>()
private val cacheCounts = ConcurrentHashMap<String, Int>()
private var booksJob: Job? = null
private var groupsJob: Job? = null
private var cacheLoadJob: Job? = null
private val cacheCountJobs = ConcurrentHashMap<String, Job>()
private var observersStarted = false
fun dispatch(intent: BookshelfManageScreenIntent) {
@@ -285,9 +290,7 @@ class BookshelfManageScreenViewModel(
}
}
fun getCacheChapters(bookUrl: String): Set<String>? = cacheChapters[bookUrl]
fun getAllCacheChapters(): Map<String, Set<String>> = cacheChapters
fun getCacheCount(bookUrl: String): Int? = cacheCounts[bookUrl]
fun isBookDownloading(bookUrl: String): Boolean {
return CacheBook.cacheBookMap[bookUrl]?.isStop() == false
@@ -354,7 +357,7 @@ class BookshelfManageScreenViewModel(
bookSortOrder = bookshelfManageScreenConfig.bookshelfSortOrder
)
}
loadCacheFiles(books)
loadCacheCounts(books)
}
}
}
@@ -427,22 +430,20 @@ class BookshelfManageScreenViewModel(
}
}
private fun loadCacheFiles(books: List<Book>) {
execute {
private fun loadCacheCounts(books: List<Book>) {
cacheLoadJob?.cancel()
cacheCountJobs.values.forEach { it.cancel() }
cacheCountJobs.clear()
cacheLoadJob = viewModelScope.launch(Dispatchers.IO) {
val visibleBookUrls = books.mapTo(hashSetOf()) { it.bookUrl }
cacheCounts.keys.toList().forEach { bookUrl ->
if (!visibleBookUrls.contains(bookUrl)) {
cacheCounts.remove(bookUrl)
}
}
books.forEach { book ->
if (!book.isLocal && !cacheChapters.contains(book.bookUrl)) {
val chapterCaches = hashSetOf<String>()
val cacheNames = BookHelp.getChapterFiles(book)
if (cacheNames.isNotEmpty()) {
bookChapterDao.getChapterList(book.bookUrl).also {
book.totalChapterNum = it.size
}.forEach { chapter ->
if (cacheNames.contains(chapter.getFileName()) || chapter.isVolume) {
chapterCaches.add(chapter.url)
}
}
}
cacheChapters[book.bookUrl] = chapterCaches
if (!book.isLocal && !cacheCounts.containsKey(book.bookUrl)) {
cacheCounts[book.bookUrl] = calculateCacheCount(book)
emitBookChanged(book.bookUrl)
}
ensureActive()
@@ -452,9 +453,28 @@ class BookshelfManageScreenViewModel(
private fun onChapterCached(chapter: BookChapter) {
val bookUrl = chapter.bookUrl
val chapterSet = cacheChapters.getOrPut(bookUrl) { hashSetOf() }
chapterSet.add(chapter.url)
emitBookChanged(bookUrl)
scheduleCacheCountRefresh(bookUrl)
}
private fun scheduleCacheCountRefresh(bookUrl: String) {
cacheCountJobs.remove(bookUrl)?.cancel()
cacheCountJobs[bookUrl] = viewModelScope.launch(Dispatchers.IO) {
val book = bookDao.getBook(bookUrl) ?: return@launch
if (!uiState.value.books.any { it.bookUrl == bookUrl }) {
return@launch
}
cacheCounts[bookUrl] = calculateCacheCount(book)
emitBookChanged(bookUrl)
cacheCountJobs.remove(bookUrl)
}
}
private fun calculateCacheCount(book: Book): Int {
val cacheNames = BookHelp.getChapterFiles(book)
if (cacheNames.isEmpty()) return 0
val totalCount = bookChapterDao.getChapterCount(book.bookUrl)
val cachedFileCount = cacheNames.count { it.endsWith(".nb") }
return min(cachedFileCount + bookChapterDao.getVolumeCount(book.bookUrl), totalCount)
}
private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
@@ -500,7 +520,7 @@ class BookshelfManageScreenViewModel(
deleteBooksUseCase.execute(bookUrls, deleteOriginal)
}.onSuccess { deletedBookUrls ->
_uiState.update { it.copy(deleteBookOriginal = deleteOriginal) }
deletedBookUrls.forEach { cacheChapters.remove(it) }
deletedBookUrls.forEach { cacheCounts.remove(it) }
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除成功"))
}.onError {
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("删除失败\n${it.localizedMessage}"))
@@ -513,7 +533,7 @@ class BookshelfManageScreenViewModel(
clearBookCacheUseCase.execute(bookUrls)
}.onSuccess { clearedBookUrls ->
clearedBookUrls.forEach { bookUrl ->
cacheChapters[bookUrl] = hashSetOf()
cacheCounts[bookUrl] = 0
emitBookChanged(bookUrl)
}
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("缓存已清理"))
@@ -578,8 +598,8 @@ class BookshelfManageScreenViewModel(
changeBookSourceUseCase.changeTo(oldBook, book, chapters, options)
}.onSuccess { result ->
result ?: return@onSuccess
cacheChapters.remove(result.oldBookUrl)
cacheChapters[result.book.bookUrl] = hashSetOf()
cacheCounts.remove(result.oldBookUrl)
cacheCounts[result.book.bookUrl] = 0
emitBookChanged(result.book.bookUrl)
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("换源完成"))
}.onError {
@@ -665,15 +685,19 @@ class BookshelfManageScreenViewModel(
val candidate = item.selectedCandidate ?: return
execute {
val oldBook = bookDao.getBook(oldBookUrl) ?: item.oldBook
val chapters = changeBookSourceUseCase.loadCandidateChapters(
candidate.source,
candidate.book
) ?: error("获取目录失败")
changeBookSourceUseCase.changeTo(
oldBook = oldBook,
newBook = candidate.book,
chapters = candidate.chapters,
chapters = chapters,
options = uiState.value.batchChangeOptions,
)
}.onSuccess { result ->
cacheChapters.remove(result.oldBookUrl)
cacheChapters[result.book.bookUrl] = hashSetOf()
cacheCounts.remove(result.oldBookUrl)
cacheCounts[result.book.bookUrl] = 0
removePreviewItem(oldBookUrl)
emitBookChanged(result.book.bookUrl)
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("迁移完成"))
@@ -726,8 +750,15 @@ class BookshelfManageScreenViewModel(
state.copy(
batchChangePreviewItems = state.batchChangePreviewItems.map { item ->
if (item.oldBook.bookUrl == oldBookUrl) {
book.totalChapterNum = chapters.size
item.copy(
candidates = listOf(BatchChangeSourceCandidate(source, book, chapters)) +
candidates = listOf(
BatchChangeSourceCandidate(
source = source,
book = book,
chapterCount = chapters.size
)
) +
item.candidates,
selectedCandidateIndex = 0,
status = BatchChangeSourcePreviewStatus.Matched
@@ -746,12 +777,16 @@ class BookshelfManageScreenViewModel(
} ?: return
val candidate = item.selectedCandidate ?: return
execute {
val chapters = changeBookSourceUseCase.loadCandidateChapters(
candidate.source,
candidate.book
) ?: error("获取目录失败")
candidate.book.removeType(BookType.notShelf)
if (candidate.book.order == 0) {
candidate.book.order = bookDao.minOrder - 1
}
bookDao.insert(candidate.book)
bookChapterDao.insert(*candidate.chapters.toTypedArray())
bookChapterDao.insert(*chapters.toTypedArray())
candidate.book
}.onSuccess {
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("已添加到书架"))
@@ -795,15 +830,19 @@ class BookshelfManageScreenViewModel(
}
val candidate = item.selectedCandidate ?: return@forEachIndexed
val oldBook = bookDao.getBook(item.oldBook.bookUrl) ?: item.oldBook
val chapters = changeBookSourceUseCase.loadCandidateChapters(
candidate.source,
candidate.book
) ?: return@forEachIndexed
changeBookSourceUseCase.changeTo(
oldBook = oldBook,
newBook = candidate.book,
chapters = candidate.chapters,
chapters = chapters,
options = uiState.value.batchChangeOptions,
)
}
}.onSuccess {
cacheChapters.clear()
cacheCounts.clear()
_uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量迁移完成"))
}.onError {
@@ -824,7 +863,7 @@ class BookshelfManageScreenViewModel(
clearBookCacheUseCase.execute(book.bookUrl)
}.onSuccess { bookUrl ->
bookUrl ?: return@onSuccess
cacheChapters[bookUrl] = hashSetOf()
cacheCounts[bookUrl] = 0
emitBookChanged(bookUrl)
}
}
@@ -50,6 +50,7 @@ class SearchViewModel(
private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
private val searchScope = SearchScope(AppConfig.searchScope)
private val searchSession = repository.createSearchSession { searchScope }
private val searchResultBooks = LinkedHashMap<String, SearchBook>()
private var currentSearchId = 0L
@@ -231,10 +232,15 @@ class SearchViewModel(
}
is SearchSessionEvent.Progress -> {
val shelf = bookshelfKeys.value
event.removedBookUrls.forEach { searchResultBooks.remove(it) }
event.upsertBooks.forEach { book ->
searchResultBooks[book.bookUrl] = book
}
_uiState.update {
it.copy(
results = event.books.toSearchResultItems(shelf),
results = buildSearchResultItems(
shelf = bookshelfKeys.value,
),
processedSources = event.processedSources,
totalSources = event.totalSources,
)
@@ -292,6 +298,7 @@ class SearchViewModel(
updateQuery(keyword, showSuggestions = false)
currentSearchId = System.currentTimeMillis()
searchResultBooks.clear()
_uiState.update {
it.copy(
committedQuery = keyword,
@@ -418,6 +425,12 @@ class SearchViewModel(
}
}
private fun buildSearchResultItems(
shelf: Set<BookShelfKey>,
): List<SearchResultItemUi> {
return searchResultBooks.values.toList().toSearchResultItems(shelf)
}
private fun List<SearchResultItemUi>.withShelfState(
shelf: Set<BookShelfKey>
): List<SearchResultItemUi> {
@@ -26,6 +26,7 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
private val normalClosure = NanoWSD.WebSocketFrame.CloseCode.NormalClosure
private val searchModel = SearchModel(this, this)
private val sentBookUrls = linkedSetOf<String>()
private val SEARCH_FINISH = "Search finish"
@@ -66,6 +67,7 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
close(normalClosure, SEARCH_FINISH, false)
return@launch
}
sentBookUrls.clear()
searchModel.search(System.currentTimeMillis(), key)
}
}
@@ -82,19 +84,24 @@ class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) :
override fun getSearchScope(): SearchScope = SearchScope(AppConfig.searchScope)
override fun onSearchStart() {
override suspend fun onSearchStart() {
}
override fun onSearchSuccess(
searchBooks: List<SearchBook>,
override suspend fun onSearchSuccess(
upsertBooks: List<SearchBook>,
removedBookUrls: List<String>,
resultCount: Int,
processedSources: Int,
totalSources: Int
) {
send(GSON.toJson(searchBooks))
val newBooks = upsertBooks.filter { sentBookUrls.add(it.bookUrl) }
if (newBooks.isNotEmpty()) {
send(GSON.toJson(newBooks))
}
}
override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) = close(normalClosure, SEARCH_FINISH, false)
override suspend fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) = close(normalClosure, SEARCH_FINISH, false)
override fun onSearchCancel(exception: Throwable?) = close(normalClosure, exception?.toString() ?: SEARCH_FINISH, false)