Show search results once received

This commit is contained in:
Koitharu
2023-08-10 09:09:04 +03:00
parent c874d73c04
commit fa0289eb27
3 changed files with 56 additions and 55 deletions

View File

@@ -7,10 +7,14 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -32,9 +36,8 @@ abstract class BaseViewModel : ViewModel() {
val onError: EventFlow<Throwable>
get() = errorEvent
val isLoading: StateFlow<Boolean>
get() = loadingCounter.map { it > 0 }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), loadingCounter.value > 0)
val isLoading: StateFlow<Boolean> = loadingCounter.map { it > 0 }
.stateIn(viewModelScope, SharingStarted.Lazily, loadingCounter.value > 0)
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
@@ -55,14 +58,24 @@ abstract class BaseViewModel : ViewModel() {
}
}
protected fun <T> Flow<T>.withLoading() = onStart {
loadingCounter.increment()
}.onCompletion {
loadingCounter.decrement()
}
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
errorEvent.call(error)
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
errorEvent.call(throwable)
}
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
}

View File

@@ -60,9 +60,10 @@ class MultiSearchActivity :
super.onCreate(savedInstanceState)
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
title = viewModel.query
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view ->
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value))
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query))
}
val sizeResolver = DynamicItemSizeResolver(resources, settings)
val selectionDecoration = MangaSelectionDecoration(this)
@@ -88,7 +89,6 @@ class MultiSearchActivity :
setSubtitle(R.string.search_results)
}
viewModel.query.observe(this) { title = it }
viewModel.list.observe(this) { adapter.items = it }
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView))
@@ -130,7 +130,7 @@ class MultiSearchActivity :
}
override fun onRetryClick(error: Throwable) {
viewModel.doSearch(viewModel.query.value)
viewModel.retry()
}
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit

View File

@@ -1,22 +1,24 @@
package org.koitharu.kotatsu.search.ui.multi
import androidx.annotation.CheckResult
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ListMode
@@ -48,15 +50,17 @@ class MultiSearchViewModel @Inject constructor(
private val sourcesRepository: MangaSourcesRepository,
) : BaseViewModel() {
private var searchJob: Job? = null
private val listData = MutableStateFlow<List<MultiSearchListModel>>(emptyList())
private val loadingData = MutableStateFlow(false)
val onDownloadStarted = MutableEventFlow<Unit>()
val query = savedStateHandle.get<String>(MultiSearchActivity.EXTRA_QUERY).orEmpty()
private val retryCounter = MutableStateFlow(0)
private val listData = retryCounter.flatMapLatest {
searchImpl(query).withLoading().withErrorHandling()
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val query = MutableStateFlow(savedStateHandle.get<String>(MultiSearchActivity.EXTRA_QUERY).orEmpty())
val list: StateFlow<List<ListModel>> = combine(
listData,
loadingData,
listData.filterNotNull(),
isLoading,
) { list, loading ->
when {
list.isEmpty() -> listOf(
@@ -76,13 +80,10 @@ class MultiSearchViewModel @Inject constructor(
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
doSearch(query.value)
}
fun getItems(ids: Set<Long>): Set<Manga> {
val snapshot = listData.value ?: return emptySet()
val result = HashSet<Manga>(ids.size)
listData.value.forEach { x ->
snapshot.forEach { x ->
for (item in x.list) {
if (item.id in ids) {
result.add(item.manga)
@@ -92,21 +93,8 @@ class MultiSearchViewModel @Inject constructor(
return result
}
fun doSearch(q: String) {
val prevJob = searchJob
searchJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
try {
listData.value = emptyList()
loadingData.value = true
query.value = q
searchImpl(q)
} catch (e: CancellationException) {
throw e
} finally {
loadingData.value = false
}
}
fun retry() {
retryCounter.value = retryCounter.value + 1
}
fun download(items: Set<Manga>) {
@@ -116,13 +104,14 @@ class MultiSearchViewModel @Inject constructor(
}
}
private suspend fun searchImpl(q: String) = coroutineScope {
@CheckResult
private fun searchImpl(q: String): Flow<List<MultiSearchListModel>> = channelFlow {
val sources = sourcesRepository.getEnabledSources()
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = sources.map { source ->
async(dispatcher) {
runCatchingCancellable {
withTimeout(8_000) {
val semaphore = Semaphore(MAX_PARALLELISM)
for (source in sources) {
launch {
val item = runCatchingCancellable {
semaphore.withPermit {
mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID, extraProvider)
}
@@ -139,12 +128,11 @@ class MultiSearchViewModel @Inject constructor(
MultiSearchListModel(source, true, emptyList(), error)
},
)
if (item != null) {
send(item)
}
}
}
for (deferred in deferredList) {
deferred.await()?.let { item ->
listData.update { x -> x + item }
}
}
}
}.runningFold<MultiSearchListModel, List<MultiSearchListModel>?>(null) { list, item -> list.orEmpty() + item }
.filterNotNull()
}