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.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 }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user