Show search results once received
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user