diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt index 424ab726e..244636ded 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt @@ -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 get() = errorEvent - val isLoading: StateFlow - get() = loadingCounter.map { it > 0 } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), loadingCounter.value > 0) + val isLoading: StateFlow = 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 Flow.withLoading() = onStart { + loadingCounter.increment() + }.onCompletion { + loadingCounter.decrement() + } + + protected fun Flow.withErrorHandling() = catch { error -> + errorEvent.call(error) + } + + protected fun MutableStateFlow.increment() = update { it + 1 } + + protected fun MutableStateFlow.decrement() = update { it - 1 } + private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> throwable.printStackTraceDebug() if (throwable !is CancellationException) { errorEvent.call(throwable) } } - - protected fun MutableStateFlow.increment() = update { it + 1 } - - protected fun MutableStateFlow.decrement() = update { it - 1 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index 0f230118a..363e944c3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -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 { 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) = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index bb0c8b43b..f9252b55c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -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>(emptyList()) - private val loadingData = MutableStateFlow(false) val onDownloadStarted = MutableEventFlow() + val query = savedStateHandle.get(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(MultiSearchActivity.EXTRA_QUERY).orEmpty()) val list: StateFlow> = 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): Set { + val snapshot = listData.value ?: return emptySet() val result = HashSet(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) { @@ -116,13 +104,14 @@ class MultiSearchViewModel @Inject constructor( } } - private suspend fun searchImpl(q: String) = coroutineScope { + @CheckResult + private fun searchImpl(q: String): Flow> = 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?>(null) { list, item -> list.orEmpty() + item } + .filterNotNull() }