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.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -32,9 +36,8 @@ abstract class BaseViewModel : ViewModel() {
val onError: EventFlow<Throwable> val onError: EventFlow<Throwable>
get() = errorEvent get() = errorEvent
val isLoading: StateFlow<Boolean> val isLoading: StateFlow<Boolean> = loadingCounter.map { it > 0 }
get() = loadingCounter.map { it > 0 } .stateIn(viewModelScope, SharingStarted.Lazily, loadingCounter.value > 0)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), loadingCounter.value > 0)
protected fun launchJob( protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext, 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 -> private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug() throwable.printStackTraceDebug()
if (throwable !is CancellationException) { if (throwable !is CancellationException) {
errorEvent.call(throwable) 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) super.onCreate(savedInstanceState)
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater)) setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar) window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
title = viewModel.query
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view -> 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 sizeResolver = DynamicItemSizeResolver(resources, settings)
val selectionDecoration = MangaSelectionDecoration(this) val selectionDecoration = MangaSelectionDecoration(this)
@@ -88,7 +89,6 @@ class MultiSearchActivity :
setSubtitle(R.string.search_results) setSubtitle(R.string.search_results)
} }
viewModel.query.observe(this) { title = it }
viewModel.list.observe(this) { adapter.items = it } viewModel.list.observe(this) { adapter.items = it }
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView)) viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView))
@@ -130,7 +130,7 @@ class MultiSearchActivity :
} }
override fun onRetryClick(error: Throwable) { override fun onRetryClick(error: Throwable) {
viewModel.doSearch(viewModel.query.value) viewModel.retry()
} }
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit override fun onUpdateFilter(tags: Set<MangaTag>) = Unit

View File

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