Show errors in global search results
This commit is contained in:
@@ -8,6 +8,7 @@ class MultiSearchListModel(
|
||||
val source: MangaSource,
|
||||
val hasMore: Boolean,
|
||||
val list: List<MangaItemModel>,
|
||||
val error: Throwable?,
|
||||
) : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -19,14 +20,14 @@ class MultiSearchListModel(
|
||||
if (source != other.source) return false
|
||||
if (hasMore != other.hasMore) return false
|
||||
if (list != other.list) return false
|
||||
|
||||
return true
|
||||
return error == other.error
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.hashCode()
|
||||
result = 31 * result + hasMore.hashCode()
|
||||
result = 31 * result + list.hashCode()
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CompositeException
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -30,7 +30,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -52,20 +51,17 @@ class MultiSearchViewModel @Inject constructor(
|
||||
private var searchJob: Job? = null
|
||||
private val listData = MutableStateFlow<List<MultiSearchListModel>>(emptyList())
|
||||
private val loadingData = MutableStateFlow(false)
|
||||
private var listError = MutableStateFlow<Throwable?>(null)
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val query = MutableStateFlow(savedStateHandle.get<String>(MultiSearchActivity.EXTRA_QUERY).orEmpty())
|
||||
val list: StateFlow<List<ListModel>> = combine(
|
||||
listData,
|
||||
loadingData,
|
||||
listError,
|
||||
) { list, loading, error ->
|
||||
) { list, loading ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
when {
|
||||
loading -> LoadingState
|
||||
error != null -> error.toErrorState(canRetry = true)
|
||||
else -> EmptyState(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.nothing_found,
|
||||
@@ -101,15 +97,12 @@ class MultiSearchViewModel @Inject constructor(
|
||||
searchJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
try {
|
||||
listError.value = null
|
||||
listData.value = emptyList()
|
||||
loadingData.value = true
|
||||
query.value = q
|
||||
searchImpl(q)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
} finally {
|
||||
loadingData.value = false
|
||||
}
|
||||
@@ -129,35 +122,28 @@ class MultiSearchViewModel @Inject constructor(
|
||||
val deferredList = sources.map { source ->
|
||||
async(dispatcher) {
|
||||
runCatchingCancellable {
|
||||
val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
|
||||
.toUi(ListMode.GRID, extraProvider)
|
||||
if (list.isNotEmpty()) {
|
||||
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list)
|
||||
} else {
|
||||
null
|
||||
withTimeout(8_000) {
|
||||
mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
|
||||
.toUi(ListMode.GRID, extraProvider)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = { list ->
|
||||
if (list.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list, null)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTraceDebug()
|
||||
MultiSearchListModel(source, true, emptyList(), error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val errors = ArrayList<Throwable>()
|
||||
for (deferred in deferredList) {
|
||||
deferred.await()
|
||||
.onSuccess { item ->
|
||||
if (item != null) {
|
||||
listData.update { x -> x + item }
|
||||
}
|
||||
}.onFailure {
|
||||
errors.add(it)
|
||||
}
|
||||
}
|
||||
if (listData.value.isEmpty()) {
|
||||
when (errors.size) {
|
||||
0 -> Unit
|
||||
1 -> throw errors[0]
|
||||
else -> throw CompositeException(errors)
|
||||
deferred.await()?.let { item ->
|
||||
listData.update { x -> x + item }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.search.ui.multi.adapter
|
||||
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||
@@ -10,6 +11,8 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
@@ -46,5 +49,7 @@ fun searchResultsAD(
|
||||
binding.buttonMore.isVisible = item.hasMore
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.items = item.list
|
||||
binding.recyclerView.isGone = item.list.isEmpty()
|
||||
binding.textViewError.textAndVisible = item.error?.getDisplayMessage(context.resources)
|
||||
}
|
||||
}
|
||||
|
||||
11
app/src/main/res/drawable/ic_error_small.xml
Normal file
11
app/src/main/res/drawable/ic_error_small.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/error"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11 15h2v2h-2zm0-8h2v6h-2zm0.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
|
||||
</vector>
|
||||
@@ -47,4 +47,21 @@
|
||||
android:paddingHorizontal="@dimen/grid_spacing"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/textView_error"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/recyclerView"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginHorizontal="@dimen/grid_spacing"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="@dimen/grid_spacing"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_error_small"
|
||||
tools:text="@tools:sample/lorem[6]"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
Reference in New Issue
Block a user