Add action for empty list state

pull/123/head
Koitharu 4 years ago
parent d7f60fa95a
commit 179b08b96a
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -36,13 +36,14 @@ class FavouritesListViewModel(
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> listOf(
EmptyState( EmptyState(
R.drawable.ic_heart_outline, icon = R.drawable.ic_heart_outline,
R.string.text_empty_holder_primary, textPrimary = R.string.text_empty_holder_primary,
if (categoryId == 0L) { textSecondary = if (categoryId == 0L) {
R.string.you_have_not_favourites_yet R.string.you_have_not_favourites_yet
} else { } else {
R.string.favourites_category_empty R.string.favourites_category_empty
} },
actionStringRes = 0,
) )
) )
else -> list.toUi(mode, this) else -> list.toUi(mode, this)

@ -45,7 +45,14 @@ class HistoryListViewModel(
createListModeFlow() createListModeFlow()
) { list, grouped, mode -> ) { list, grouped, mode ->
when { when {
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_history, R.string.text_history_holder_primary, R.string.text_history_holder_secondary)) list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_history,
textPrimary = R.string.text_history_holder_primary,
textSecondary = R.string.text_history_holder_secondary,
actionStringRes = 0,
)
)
else -> mapList(list, grouped, mode) else -> mapList(list, grouped, mode)
} }
}.onFirst { }.onFirst {

@ -15,17 +15,18 @@ import kotlinx.coroutines.launch
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.main.ui.MainActivity
@ -33,7 +34,7 @@ import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(), abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, PaginationScrollListener.Callback, MangaListListener,
SwipeRefreshLayout.OnRefreshListener { SwipeRefreshLayout.OnRefreshListener {
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
@ -62,10 +63,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
listAdapter = MangaListAdapter( listAdapter = MangaListAdapter(
coil = get(), coil = get(),
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
clickListener = this, listener = this,
onRetryClick = ::resolveException,
onTagRemoveClick = viewModel::onRemoveFilterTag,
onFilterClickListener = this::onFilterClick,
) )
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
@ -192,7 +190,17 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
} }
protected open fun onFilterClick() = Unit override fun onFilterClick() = Unit
override fun onEmptyActionClick() = Unit
override fun onRetryClick(error: Throwable) {
resolveException(error)
}
override fun onTagRemoveClick(tag: MangaTag) {
viewModel.onRemoveFilterTag(tag)
}
private fun onGridScaleChanged(scale: Float) { private fun onGridScaleChanged(scale: Float) {
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()

@ -1,23 +1,23 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.databinding.ItemCurrentFilterBinding
import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun currentFilterAD( fun currentFilterAD(
onTagRemoveClick: (MangaTag) -> Unit, listener: MangaListListener,
) = adapterDelegateViewBinding<CurrentFilterModel, ListModel, ItemCurrentFilterBinding>( ) = adapterDelegate<CurrentFilterModel, ListModel>(R.layout.item_current_filter) {
{ inflater, parent -> ItemCurrentFilterBinding.inflate(inflater, parent, false) }
) {
binding.chipsTags.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> val chipGroup = itemView as ChipsView
onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data ->
listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
} }
bind { bind {
binding.chipsTags.setChips(item.chips) chipGroup.setChips(item.chips)
} }
} }

@ -4,20 +4,20 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyStateListAD() = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>( fun emptyStateListAD(
listener: MangaListListener,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }
) { ) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind { bind {
with(binding.icon) { binding.icon.setImageResource(item.icon)
setImageResource(item.icon) binding.textPrimary.setText(item.textPrimary)
} binding.textSecondary.setTextAndVisible(item.textSecondary)
with(binding.textPrimary) { binding.buttonRetry.setTextAndVisible(item.actionStringRes)
setText(item.textPrimary)
}
with(binding.textSecondary) {
setText(item.textSecondary)
}
} }
} }

@ -7,13 +7,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
fun errorFooterAD( fun errorFooterAD(
onRetryClick: (Throwable) -> Unit listener: MangaListListener,
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>( ) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }
) { ) {
binding.root.setOnClickListener { binding.root.setOnClickListener {
onRetryClick(item.exception) listener.onRetryClick(item.exception)
} }
bind { bind {

@ -8,13 +8,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
fun errorStateListAD( fun errorStateListAD(
onRetryClick: (Throwable) -> Unit listener: MangaListListener,
) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>( ) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>(
{ inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }
) { ) {
binding.buttonRetry.setOnClickListener { binding.buttonRetry.setOnClickListener {
onRetryClick(item.exception) listener.onRetryClick(item.exception)
} }
bind { bind {

@ -24,14 +24,14 @@ fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(
} }
fun listHeaderWithFilterAD( fun listHeaderWithFilterAD(
onFilterClickListener: () -> Unit, listener: MangaListListener,
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderWithFilterBinding>( ) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderWithFilterBinding>(
viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) }, viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) },
on = { item, _, _ -> item is ListHeader && item.sortOrder != null }, on = { item, _, _ -> item is ListHeader && item.sortOrder != null },
) { ) {
binding.textViewFilter.setOnClickListener { binding.textViewFilter.setOnClickListener {
onFilterClickListener() listener.onFilterClick()
} }
bind { bind {

@ -4,9 +4,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
@ -17,32 +14,29 @@ import kotlin.jvm.internal.Intrinsics
class MangaListAdapter( class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>, listener: MangaListListener,
onRetryClick: (Throwable) -> Unit,
onTagRemoveClick: (MangaTag) -> Unit,
onFilterClickListener: () -> Unit,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {
delegatesManager delegatesManager
.addDelegate( .addDelegate(
ITEM_TYPE_MANGA_LIST, ITEM_TYPE_MANGA_LIST,
mangaListItemAD(coil, lifecycleOwner, clickListener) mangaListItemAD(coil, lifecycleOwner, listener)
) )
.addDelegate( .addDelegate(
ITEM_TYPE_MANGA_LIST_DETAILED, ITEM_TYPE_MANGA_LIST_DETAILED,
mangaListDetailedItemAD(coil, lifecycleOwner, clickListener) mangaListDetailedItemAD(coil, lifecycleOwner, listener)
) )
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, clickListener)) .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD()) .addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(onRetryClick)) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(onRetryClick)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(listener))
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener)) .addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(listener))
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.list.ui.adapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTag
interface MangaListListener : OnListItemClickListener<Manga> {
fun onRetryClick(error: Throwable)
fun onTagRemoveClick(tag: MangaTag)
fun onFilterClick()
fun onEmptyActionClick()
}

@ -24,10 +24,15 @@ class FilterBottomSheet : BaseBottomSheet<SheetFilterBinding>() {
) { ) {
parametersOf( parametersOf(
requireArguments().getParcelable<MangaSource>(ARG_SOURCE), requireArguments().getParcelable<MangaSource>(ARG_SOURCE),
requireArguments().getParcelable<FilterState>(ARG_STATE),
) )
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val state = requireArguments().getParcelable<FilterState>(ARG_STATE)
viewModel.updateState(state)
}
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false) return SheetFilterBinding.inflate(inflater, container, false)
} }

@ -11,28 +11,24 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.replaceWith
import java.util.* import java.util.*
class FilterViewModel( class FilterViewModel(
private val repository: RemoteMangaRepository, private val repository: RemoteMangaRepository,
dataRepository: MangaDataRepository, dataRepository: MangaDataRepository,
state: FilterState,
) : BaseViewModel(), OnFilterChangedListener { ) : BaseViewModel(), OnFilterChangedListener {
val filter = MutableLiveData<List<FilterItem>>() val filter = MutableLiveData<List<FilterItem>>()
val result = MutableLiveData<FilterState>() val result = MutableLiveData<FilterState>()
private var job: Job? = null private var job: Job? = null
private var selectedSortOrder: SortOrder? = state.sortOrder private var selectedSortOrder: SortOrder? = repository.sortOrders.firstOrNull()
private val selectedTags = HashSet(state.tags) private val selectedTags = HashSet<MangaTag>()
private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) { private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) {
dataRepository.findTags(repository.source) dataRepository.findTags(repository.source)
} }
private var availableTagsDeferred = loadTagsAsync() private var availableTagsDeferred = loadTagsAsync()
init {
showFilter()
}
override fun onSortItemClick(item: FilterItem.Sort) { override fun onSortItemClick(item: FilterItem.Sort) {
selectedSortOrder = item.order selectedSortOrder = item.order
updateFilters() updateFilters()
@ -49,6 +45,18 @@ class FilterViewModel(
} }
} }
fun updateState(state: FilterState?) {
if (state != null) {
selectedSortOrder = state.sortOrder
selectedTags.replaceWith(state.tags)
}
if (job == null) {
showFilter()
} else {
updateFilters()
}
}
@AnyThread @AnyThread
private fun updateFilters() { private fun updateFilters() {
val previousJob = job val previousJob = job

@ -6,5 +6,6 @@ import androidx.annotation.StringRes
data class EmptyState( data class EmptyState(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@StringRes val textPrimary: Int, @StringRes val textPrimary: Int,
@StringRes val textSecondary: Int @StringRes val textSecondary: Int,
@StringRes val actionStringRes: Int,
) : ListModel ) : ListModel

@ -62,6 +62,21 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun onEmptyActionClick() {
try {
importCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
Snackbar.make(
binding.recyclerView,
R.string.operation_not_supported,
Snackbar.LENGTH_SHORT
).show()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_local, menu) inflater.inflate(R.menu.opt_local, menu)
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
@ -70,18 +85,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_import -> { R.id.action_import -> {
try { onEmptyActionClick()
importCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
Snackbar.make(
binding.recyclerView,
R.string.operation_not_supported,
Snackbar.LENGTH_SHORT
).show()
}
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

@ -45,9 +45,10 @@ class LocalListViewModel(
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf( list.isEmpty() -> listOf(
EmptyState( EmptyState(
R.drawable.ic_storage, icon = R.drawable.ic_storage,
R.string.text_local_holder_primary, textPrimary = R.string.text_local_holder_primary,
R.string.text_local_holder_secondary textSecondary = R.string.text_local_holder_secondary,
actionStringRes = R.string._import,
) )
) )
else -> ArrayList<ListModel>(list.size + 1).apply { else -> ArrayList<ListModel>(list.size + 1).apply {

@ -23,7 +23,6 @@ val remoteListModule
FilterViewModel( FilterViewModel(
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository, repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
dataRepository = get(), dataRepository = get(),
state = params.get(),
) )
} }
} }

@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet
import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.parcelableArgument
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@ -65,6 +66,10 @@ class RemoteListFragment : MangaListFragment(), FragmentResultListener {
FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) FilterBottomSheet.show(childFragmentManager, source, viewModel.filter)
} }
override fun onEmptyActionClick() {
viewModel.applyFilter(FilterState(viewModel.filter.sortOrder, emptySet()))
}
override fun onFragmentResult(requestKey: String, result: Bundle) { override fun onFragmentResult(requestKey: String, result: Bundle) {
when (requestKey) { when (requestKey) {
FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter( FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter(

@ -42,7 +42,7 @@ class RemoteListViewModel(
when { when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty)) list.isEmpty() -> createEmptyState()
else -> { else -> {
val result = ArrayList<ListModel>(list.size + 3) val result = ArrayList<ListModel>(list.size + 3)
result += header result += header
@ -126,4 +126,13 @@ class RemoteListViewModel(
CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) })
} }
} }
private fun createEmptyState() = listOf(
EmptyState(
icon = R.drawable.ic_book_cross,
textPrimary = R.string.nothing_found,
textSecondary = 0,
actionStringRes = if (filter.tags.isEmpty()) 0 else R.string.reset_filter,
)
)
} }

@ -33,7 +33,14 @@ class SearchViewModel(
when { when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_search, R.string.nothing_found, R.string.text_search_holder_secondary)) list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_book_search,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
)
)
else -> { else -> {
val result = ArrayList<ListModel>(list.size + 1) val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode) list.toUi(result, mode)

@ -13,7 +13,6 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.onFirst import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.*
class GlobalSearchViewModel( class GlobalSearchViewModel(
private val query: String, private val query: String,
@ -35,7 +34,14 @@ class GlobalSearchViewModel(
when { when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_search, R.string.nothing_found, R.string.text_search_holder_secondary)) list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_book_search,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
)
)
else -> { else -> {
val result = ArrayList<ListModel>(list.size + 1) val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode) list.toUi(result, mode)

@ -24,11 +24,14 @@ class SuggestionsViewModel(
createListModeFlow() createListModeFlow()
) { list, mode -> ) { list, mode ->
when { when {
list.isEmpty() -> listOf(EmptyState( list.isEmpty() -> listOf(
icon = R.drawable.ic_book_cross, EmptyState(
textPrimary = R.string.nothing_found, icon = R.drawable.ic_book_cross,
textSecondary = R.string.text_suggestion_holder, textPrimary = R.string.nothing_found,
)) textSecondary = R.string.text_suggestion_holder,
actionStringRes = 0,
)
)
else -> buildList<ListModel>(list.size + 1) { else -> buildList<ListModel>(list.size + 1) {
add(headerModel) add(headerModel)
list.toUi(this, mode) list.toUi(this, mode)

@ -10,12 +10,13 @@ import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.databinding.FragmentFeedBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter
@ -25,7 +26,7 @@ import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListener.Callback, class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListener.Callback,
OnListItemClickListener<Manga> { MangaListListener {
private val viewModel by viewModel<FeedViewModel>() private val viewModel by viewModel<FeedViewModel>()
@ -114,6 +115,14 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
) )
} }
override fun onRetryClick(error: Throwable) = Unit
override fun onTagRemoveClick(tag: MangaTag) = Unit
override fun onFilterClick() = Unit
override fun onEmptyActionClick() = Unit
private fun onListChanged(list: List<ListModel>) { private fun onListChanged(list: List<ListModel>) {
feedAdapter?.items = list feedAdapter?.items = list
} }

@ -37,7 +37,14 @@ class FeedViewModel(
hasNextPage hasNextPage
) { list, isHasNextPage -> ) { list, isHasNextPage ->
when { when {
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_feed, R.string.text_empty_holder_primary, R.string.text_feed_holder)) list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_feed,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.text_feed_holder,
actionStringRes = 0,
)
)
isHasNextPage -> list + LoadingFooter isHasNextPage -> list + LoadingFooter
else -> list else -> list
} }

@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.adapter.* import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@ -15,17 +13,17 @@ import kotlin.jvm.internal.Intrinsics
class FeedAdapter( class FeedAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga> listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {
delegatesManager delegatesManager
.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, clickListener)) .addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD {}) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD {}) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.isGone import androidx.core.view.isGone
var TextView.textAndVisible: CharSequence? var TextView.textAndVisible: CharSequence?
@ -17,4 +18,14 @@ var TextView.drawableStart: Drawable?
set(value) { set(value) {
val dr = compoundDrawablesRelative val dr = compoundDrawablesRelative
setCompoundDrawablesRelativeWithIntrinsicBounds(value, dr[1], dr[2], dr[3]) setCompoundDrawablesRelativeWithIntrinsicBounds(value, dr[1], dr[2], dr[3])
} }
fun TextView.setTextAndVisible(@StringRes textResId: Int) {
if (textResId == 0) {
text = null
isGone = true
} else {
setText(textResId)
isGone = text.isNullOrEmpty()
}
}

@ -1,15 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView <org.koitharu.kotatsu.base.ui.widgets.ChipsView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chips_tags"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
app:closeIconEnabled="true" />
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:closeIconEnabled="true"
app:singleLine="true" />
</HorizontalScrollView>

@ -34,4 +34,14 @@
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[15]" /> tools:text="@tools:sample/lorem[15]" />
<Button
android:id="@+id/button_retry"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
tools:text="@string/try_again"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>

@ -264,4 +264,5 @@
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="filter_load_error">Unable to load genres list</string> <string name="filter_load_error">Unable to load genres list</string>
<string name="reset_filter">Reset filter</string>
</resources> </resources>
Loading…
Cancel
Save