From 238bc89be9bcae537630d4bfcd529014d56132fc Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Mar 2022 21:05:57 +0200 Subject: [PATCH 1/3] Move filter into bottom sheet --- .../koitharu/kotatsu/base/ui/BaseViewModel.kt | 2 +- .../kotatsu/core/model/MangaFilter.kt | 10 --- .../kotatsu/list/ui/MangaListFragment.kt | 47 ---------- .../kotatsu/list/ui/MangaListViewModel.kt | 71 +-------------- .../{FilterAdapter2.kt => FilterAdapter.kt} | 3 +- .../list/ui/filter/FilterAdapterDelegates.kt | 7 +- .../list/ui/filter/FilterBottomSheet.kt | 81 +++++++++++++++++ .../list/ui/filter/FilterDiffCallback.kt | 2 + .../kotatsu/list/ui/filter/FilterItem.kt | 2 + .../kotatsu/list/ui/filter/FilterState.kt | 12 +++ .../kotatsu/list/ui/filter/FilterViewModel.kt | 89 +++++++++++++++++++ .../kotatsu/remotelist/RemoteListModule.kt | 9 +- .../remotelist/ui/RemoteListFragment.kt | 23 ++++- .../remotelist/ui/RemoteListViewModel.kt | 49 +++++----- .../main/res/layout-w600dp/fragment_list.xml | 50 ----------- app/src/main/res/layout/fragment_list.xml | 34 ++----- .../res/layout/item_checkable_multiple.xml | 2 +- .../main/res/layout/item_checkable_single.xml | 2 +- app/src/main/res/layout/sheet_filter.xml | 33 +++++++ app/src/main/res/menu/opt_list.xml | 5 -- app/src/main/res/menu/opt_list_remote.xml | 6 ++ 21 files changed, 298 insertions(+), 241 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt rename app/src/main/java/org/koitharu/kotatsu/list/ui/filter/{FilterAdapter2.kt => FilterAdapter.kt} (86%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt delete mode 100644 app/src/main/res/layout-w600dp/fragment_list.xml create mode 100644 app/src/main/res/layout/sheet_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt index b3df5277a..8d1c5e279 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt @@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() { } } - private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> + protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> if (BuildConfig.DEBUG) { throwable.printStackTrace() } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt deleted file mode 100644 index 498492f24..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.koitharu.kotatsu.core.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MangaFilter( - val sortOrder: SortOrder?, - val tags: Set, -) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index a91d3924b..01f40436b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -4,13 +4,9 @@ import android.os.Bundle import android.view.* import androidx.annotation.CallSuper import androidx.appcompat.widget.PopupMenu -import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.view.GravityCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.core.view.updatePadding -import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -30,8 +26,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter -import org.koitharu.kotatsu.list.ui.filter.FilterAdapter2 -import org.koitharu.kotatsu.list.ui.filter.FilterItem import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.MainActivity @@ -43,7 +37,6 @@ abstract class MangaListFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener { private var listAdapter: MangaListAdapter? = null - private var filterAdapter: FilterAdapter2? = null private var paginationListener: PaginationScrollListener? = null private val spanResolver = MangaListSpanResolver() private val spanSizeLookup = SpanSizeLookup() @@ -51,7 +44,6 @@ abstract class MangaListFragment : BaseFragment(), spanSizeLookup.invalidateCache() } open val isSwipeRefreshEnabled = true - private var drawer: DrawerLayout? = null protected abstract val viewModel: MangaListViewModel @@ -67,8 +59,6 @@ abstract class MangaListFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - drawer = binding.root as? DrawerLayout - drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) listAdapter = MangaListAdapter( coil = get(), lifecycleOwner = viewLifecycleOwner, @@ -76,7 +66,6 @@ abstract class MangaListFragment : BaseFragment(), onRetryClick = ::resolveException, onTagRemoveClick = viewModel::onRemoveFilterTag ) - filterAdapter = FilterAdapter2(viewModel) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { setHasFixedSize(true) @@ -89,17 +78,12 @@ abstract class MangaListFragment : BaseFragment(), setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } - with(binding.recyclerViewFilter) { - setHasFixedSize(true) - adapter = filterAdapter - } (parentFragment as? RecycledViewPoolHolder)?.let { binding.recyclerView.setRecycledViewPool(it.recycledViewPool) } viewModel.content.observe(viewLifecycleOwner, ::onListChanged) - viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter) viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) @@ -107,9 +91,7 @@ abstract class MangaListFragment : BaseFragment(), } override fun onDestroyView() { - drawer = null listAdapter = null - filterAdapter = null paginationListener = null spanSizeLookup.invalidateCache() super.onDestroyView() @@ -125,19 +107,9 @@ abstract class MangaListFragment : BaseFragment(), ListModeSelectDialog.show(childFragmentManager) true } - R.id.action_filter -> { - drawer?.toggleDrawer(GravityCompat.END) - true - } else -> super.onOptionsItemSelected(item) } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.action_filter).isVisible = drawer != null && - drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED - super.onPrepareOptionsMenu(menu) - } - override fun onItemClick(item: Manga, view: View) { startActivity(DetailsActivity.newIntent(context ?: return, item)) } @@ -200,27 +172,8 @@ abstract class MangaListFragment : BaseFragment(), } } - protected fun onInitFilter(filter: List) { - filterAdapter?.items = filter - drawer?.setDrawerLockMode( - if (filter.isEmpty()) { - DrawerLayout.LOCK_MODE_LOCKED_CLOSED - } else { - DrawerLayout.LOCK_MODE_UNLOCKED - } - ) ?: binding.dividerFilter?.let { - it.isGone = filter.isEmpty() - binding.recyclerViewFilter.isVisible = it.isVisible - } - activity?.invalidateOptionsMenu() - } - override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top - binding.recyclerViewFilter.updatePadding( - top = headerHeight, - bottom = insets.bottom - ) binding.root.updatePadding( left = insets.left, right = insets.right diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index e2a463f4e..6a04449b6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -1,32 +1,22 @@ package org.koitharu.kotatsu.list.ui -import androidx.annotation.CallSuper import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.list.domain.AvailableFilters -import org.koitharu.kotatsu.list.ui.filter.FilterItem -import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct abstract class MangaListViewModel( private val settings: AppSettings, -) : BaseViewModel(), OnFilterChangedListener { +) : BaseViewModel() { abstract val content: LiveData> - val filter = MutableLiveData>() val listMode = MutableLiveData() val gridScale = settings.observe() .filter { it == AppSettings.KEY_GRID_SIZE } @@ -35,6 +25,8 @@ abstract class MangaListViewModel( settings.gridSize / 100f } + open fun onRemoveFilterTag(tag: MangaTag) = Unit + protected fun createListModeFlow() = settings.observe() .filter { it == AppSettings.KEY_LIST_MODE } .map { settings.listMode } @@ -46,63 +38,6 @@ abstract class MangaListViewModel( } } - protected var currentFilter: MangaFilter = MangaFilter(null, emptySet()) - private set(value) { - field = value - onFilterChanged() - } - protected var availableFilters: AvailableFilters? = null - private var filterJob: Job? = null - - final override fun onSortItemClick(item: FilterItem.Sort) { - currentFilter = currentFilter.copy(sortOrder = item.order) - } - - final override fun onTagItemClick(item: FilterItem.Tag) { - val tags = if (item.isChecked) { - currentFilter.tags - item.tag - } else { - currentFilter.tags + item.tag - } - currentFilter = currentFilter.copy(tags = tags) - } - - fun onRemoveFilterTag(tag: MangaTag) { - val tags = currentFilter.tags - if (tag !in tags) { - return - } - currentFilter = currentFilter.copy(tags = tags - tag) - } - - @CallSuper - open fun onFilterChanged() { - val previousJob = filterJob - filterJob = launchJob(Dispatchers.Default) { - previousJob?.cancelAndJoin() - filter.postValue( - availableFilters?.run { - val list = ArrayList(size + 2) - if (sortOrders.isNotEmpty()) { - val selectedSort = currentFilter.sortOrder ?: sortOrders.first() - list += FilterItem.Header(R.string.sort_order) - sortOrders.sortedBy { it.ordinal }.mapTo(list) { - FilterItem.Sort(it, isSelected = it == selectedSort) - } - } - if (tags.isNotEmpty()) { - list += FilterItem.Header(R.string.genres) - tags.sortedBy { it.title }.mapTo(list) { - FilterItem.Tag(it, isChecked = it in currentFilter.tags) - } - } - ensureActive() - list - }.orEmpty() - ) - } - } - abstract fun onRefresh() abstract fun onRetry() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt rename to app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 67b4d3585..3af68c0f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -2,11 +2,12 @@ package org.koitharu.kotatsu.list.ui.filter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -class FilterAdapter2( +class FilterAdapter( listener: OnFilterChangedListener, ) : AsyncListDifferDelegationAdapter( FilterDiffCallback(), filterSortDelegate(listener), filterTagDelegate(listener), filterHeaderDelegate(), + filterLoadingDelegate(), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index 8b926d768..10a6cafd8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -4,6 +4,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding +import org.koitharu.kotatsu.databinding.ItemLoadingFooterBinding fun filterSortDelegate( listener: OnFilterChangedListener, @@ -44,4 +45,8 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemLoadingFooterBinding.inflate(layoutInflater, parent, false) } +) { } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt new file mode 100644 index 000000000..c47d7ac00 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.list.ui.filter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.databinding.SheetFilterBinding +import org.koitharu.kotatsu.utils.ext.withArgs + +class FilterBottomSheet : BaseBottomSheet() { + + private val viewModel by viewModel { + parametersOf( + requireArguments().getParcelable(ARG_SOURCE), + requireArguments().getParcelable(ARG_STATE), + ) + } + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { + return SheetFilterBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.setNavigationOnClickListener { dismiss() } + if (!resources.getBoolean(R.bool.is_tablet)) { + binding.toolbar.navigationIcon = null + } + val adapter = FilterAdapter(viewModel) + binding.recyclerView.adapter = adapter + viewModel.filter.observe(viewLifecycleOwner, adapter::setItems) + viewModel.result.observe(viewLifecycleOwner) { + parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(ARG_STATE to it)) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also { + val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also + behavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + binding.toolbar.setNavigationIcon(R.drawable.ic_cross) + } else { + binding.toolbar.navigationIcon = null + } + } + } + ) + } + + companion object { + + const val REQUEST_KEY = "filter" + + const val ARG_STATE = "state" + private const val TAG = "FilterBottomSheet" + private const val ARG_SOURCE = "source" + + fun show( + fm: FragmentManager, + source: MangaSource, + state: FilterState, + ) = FilterBottomSheet().withArgs(2) { + putParcelable(ARG_SOURCE, source) + putParcelable(ARG_STATE, state) + }.show(fm, TAG) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt index 1ccd4e813..d72cadf7c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt @@ -6,6 +6,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { + oldItem === newItem -> true oldItem.javaClass != newItem.javaClass -> false oldItem is FilterItem.Header && newItem is FilterItem.Header -> { oldItem.titleResId == newItem.titleResId @@ -22,6 +23,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { + oldItem === newItem -> true oldItem is FilterItem.Header && newItem is FilterItem.Header -> true oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.isChecked == newItem.isChecked diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt index a74d93b1d..8117f5afd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt @@ -19,4 +19,6 @@ sealed interface FilterItem { val tag: MangaTag, val isChecked: Boolean, ) : FilterItem + + object Loading : FilterItem } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt new file mode 100644 index 000000000..1c1c8a9cf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.list.ui.filter + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.core.model.SortOrder + +@Parcelize +class FilterState( + val sortOrder: SortOrder?, + val tags: Set, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt new file mode 100644 index 000000000..7341e836a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -0,0 +1,89 @@ +package org.koitharu.kotatsu.list.ui.filter + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.core.parser.MangaRepository +import java.util.* + +class FilterViewModel( + private val repository: MangaRepository, + state: FilterState, +) : BaseViewModel(), OnFilterChangedListener { + + val filter = MutableLiveData>() + val result = MutableLiveData() + private var job: Job? = null + private var selectedSortOrder: SortOrder? = state.sortOrder + private val selectedTags = HashSet(state.tags) + private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + repository.getTags() + } + + init { + showFilter() + } + + override fun onSortItemClick(item: FilterItem.Sort) { + selectedSortOrder = item.order + updateFilters() + } + + override fun onTagItemClick(item: FilterItem.Tag) { + val isModified = if (item.isChecked) { + selectedTags.remove(item.tag) + } else { + selectedTags.add(item.tag) + } + if (isModified) { + updateFilters() + } + } + + private fun updateFilters() { + val previousJob = job + job = launchJob(Dispatchers.Default) { + previousJob?.cancelAndJoin() + val tags = availableTagsDeferred.await() + val sortOrders = repository.sortOrders + val list = ArrayList(sortOrders.size + tags.size + 2) + list.add(FilterItem.Header(R.string.sort_order)) + sortOrders.sortedBy { it.ordinal }.mapTo(list) { + FilterItem.Sort(it, isSelected = it == selectedSortOrder) + } + if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { + list.add(FilterItem.Header(R.string.genres)) + val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) + tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } + selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } + list.addAll(mappedTags) + } + ensureActive() + filter.postValue(list) + } + result.value = FilterState(selectedSortOrder, selectedTags) + } + + private fun showFilter() { + job = launchJob(Dispatchers.Default) { + val sortOrders = repository.sortOrders + val list = ArrayList(sortOrders.size + selectedTags.size + 3) + list.add(FilterItem.Header(R.string.sort_order)) + sortOrders.sortedBy { it.ordinal }.mapTo(list) { + FilterItem.Sort(it, isSelected = it == selectedSortOrder) + } + if (selectedTags.isNotEmpty()) { + list.add(FilterItem.Header(R.string.genres)) + selectedTags.sortedBy { it.title }.mapTo(list) { + FilterItem.Tag(it, isChecked = it in selectedTags) + } + } + list.add(FilterItem.Loading) + filter.postValue(list) + updateFilters() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt index 4555fe10e..b856248ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt @@ -4,12 +4,17 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.list.ui.filter.FilterViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel val remoteListModule get() = module { - viewModel { source -> - RemoteListViewModel(get(named(source.get())), get()) + viewModel { params -> + RemoteListViewModel(get(named(params.get())), get()) + } + + viewModel { params -> + FilterViewModel(get(named(params.get())), params.get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 5ae3a92da..ff951de7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,18 +1,22 @@ package org.koitharu.kotatsu.remotelist.ui +import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View +import androidx.fragment.app.FragmentResultListener import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.withArgs -class RemoteListFragment : MangaListFragment() { +class RemoteListFragment : MangaListFragment(), FragmentResultListener { override val viewModel by viewModel { parametersOf(source) @@ -20,6 +24,11 @@ class RemoteListFragment : MangaListFragment() { private val source by parcelableArgument(ARG_SOURCE) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.setFragmentResultListener(FilterBottomSheet.REQUEST_KEY, viewLifecycleOwner, this) + } + override fun onScrolledToEnd() { viewModel.loadNextPage() } @@ -44,10 +53,22 @@ class RemoteListFragment : MangaListFragment() { ) true } + R.id.action_filter -> { + FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + true + } else -> super.onOptionsItemSelected(item) } } + override fun onFragmentResult(requestKey: String, result: Bundle) { + when (requestKey) { + FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter( + result.getParcelable(FilterBottomSheet.ARG_STATE) ?: return + ) + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index e92616d4c..fc5acd438 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -9,11 +9,12 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.list.domain.AvailableFilters import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.filter.FilterState import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -22,6 +23,8 @@ class RemoteListViewModel( settings: AppSettings ) : MangaListViewModel(settings) { + var filter = FilterState(repository.sortOrders.firstOrNull(), emptySet()) + private set private val mangaList = MutableStateFlow?>(null) private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null) @@ -54,7 +57,6 @@ class RemoteListViewModel( init { loadList(false) - loadFilter() } override fun onRefresh() { @@ -65,12 +67,27 @@ class RemoteListViewModel( loadList(append = !mangaList.value.isNullOrEmpty()) } + override fun onRemoveFilterTag(tag: MangaTag) { + val tags = filter.tags + if (tag !in tags) { + return + } + applyFilter(FilterState(filter.sortOrder, tags - tag)) + } + fun loadNextPage() { if (hasNextPage.value && listError.value == null) { loadList(append = true) } } + fun applyFilter(newFilter: FilterState) { + filter = newFilter + mangaList.value = null + hasNextPage.value = false + loadList(false) + } + private fun loadList(append: Boolean) { if (loadingJob?.isActive == true) { return @@ -80,8 +97,8 @@ class RemoteListViewModel( listError.value = null val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, - sortOrder = currentFilter.sortOrder, - tags = currentFilter.tags, + sortOrder = filter.sortOrder, + tags = filter.tags, ) if (!append) { mangaList.value = list @@ -98,34 +115,12 @@ class RemoteListViewModel( } } - override fun onFilterChanged() { - super.onFilterChanged() - mangaList.value = null - hasNextPage.value = false - loadList(false) - } - private fun createFilterModel(): CurrentFilterModel? { - val tags = currentFilter.tags + val tags = filter.tags return if (tags.isEmpty()) { null } else { CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) } } - - private fun loadFilter() { - launchJob(Dispatchers.Default) { - try { - val sorts = repository.sortOrders - val tags = repository.getTags() - availableFilters = AvailableFilters(sorts, tags) - onFilterChanged() - } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - } - } - } } diff --git a/app/src/main/res/layout-w600dp/fragment_list.xml b/app/src/main/res/layout-w600dp/fragment_list.xml deleted file mode 100644 index c3041768c..000000000 --- a/app/src/main/res/layout-w600dp/fragment_list.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml index 93abec4b7..a4e4cb419 100644 --- a/app/src/main/res/layout/fragment_list.xml +++ b/app/src/main/res/layout/fragment_list.xml @@ -1,39 +1,21 @@ - - - - - - - + tools:listitem="@layout/item_manga_list" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_multiple.xml b/app/src/main/res/layout/item_checkable_multiple.xml index 7871b30a6..2feb5f5aa 100644 --- a/app/src/main/res/layout/item_checkable_multiple.xml +++ b/app/src/main/res/layout/item_checkable_multiple.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="?android:listPreferredItemHeightSmall" - android:background="?android:selectableItemBackground" + android:background="?selectableItemBackground" android:drawableStart="?android:listChoiceIndicatorMultiple" android:drawablePadding="12dp" android:gravity="center_vertical|start" diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml index a9c19ed8a..cec15830e 100644 --- a/app/src/main/res/layout/item_checkable_single.xml +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="?android:listPreferredItemHeightSmall" - android:background="?android:selectableItemBackground" + android:background="?selectableItemBackground" android:drawableStart="?android:listChoiceIndicatorSingle" android:drawablePadding="12dp" android:gravity="center_vertical|start" diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml new file mode 100644 index 000000000..b7343028b --- /dev/null +++ b/app/src/main/res/layout/sheet_filter.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list.xml b/app/src/main/res/menu/opt_list.xml index 09ef204b0..421f8a2c5 100644 --- a/app/src/main/res/menu/opt_list.xml +++ b/app/src/main/res/menu/opt_list.xml @@ -9,9 +9,4 @@ android:title="@string/list_mode" app:showAsAction="never" /> - \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list_remote.xml b/app/src/main/res/menu/opt_list_remote.xml index deb531840..5df3276f1 100644 --- a/app/src/main/res/menu/opt_list_remote.xml +++ b/app/src/main/res/menu/opt_list_remote.xml @@ -3,6 +3,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + Date: Thu, 3 Mar 2022 18:31:47 +0200 Subject: [PATCH 2/3] Open filter from list header --- .../base/domain/MangaDataRepository.kt | 9 ++++ .../base/ui/widgets/AnimatedToolbar.kt | 41 ------------------- .../koitharu/kotatsu/core/db/dao/TagsDao.kt | 4 +- .../core/parser/RemoteMangaRepository.kt | 3 -- .../core/parser/site/ExHentaiRepository.kt | 2 + .../history/ui/HistoryListViewModel.kt | 2 +- .../kotatsu/list/ui/MangaListFragment.kt | 5 ++- .../kotatsu/list/ui/adapter/ListHeaderAD.kt | 28 ++++++++++++- .../list/ui/adapter/MangaListAdapter.kt | 3 ++ .../list/ui/filter/FilterBottomSheet.kt | 7 +++- .../kotatsu/list/ui/filter/FilterViewModel.kt | 11 ++++- .../kotatsu/list/ui/model/ListHeader.kt | 2 + .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../kotatsu/remotelist/RemoteListModule.kt | 13 +++++- .../remotelist/ui/RemoteListFragment.kt | 6 ++- .../remotelist/ui/RemoteListViewModel.kt | 13 +++--- .../res/layout/item_header_with_filter.xml | 36 ++++++++++++++++ 17 files changed, 125 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt create mode 100644 app/src/main/res/layout/item_header_with_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt index 06f22090a..e51857f55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt @@ -6,7 +6,10 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.utils.ext.mapToSet class MangaDataRepository(private val db: MangaDatabase) { @@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) { db.mangaDao.upsert(MangaEntity.from(manga), tags) } } + + suspend fun findTags(source: MangaSource): Set { + return db.tagsDao.findTags(source.name).mapToSet { + it.toMangaTag() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt deleted file mode 100644 index f17b31f84..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.base.ui.widgets - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.View -import androidx.appcompat.widget.Toolbar -import androidx.core.view.isGone -import com.google.android.material.R -import com.google.android.material.appbar.MaterialToolbar -import java.lang.reflect.Field - -class AnimatedToolbar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.toolbarStyle, -) : MaterialToolbar(context, attrs, defStyleAttr) { - - private var navButtonView: View? = null - get() { - if (field == null) { - runCatching { - field = navButtonViewField?.get(this) as? View - } - } - return field - } - - override fun setNavigationIcon(icon: Drawable?) { - super.setNavigationIcon(icon) - navButtonView?.isGone = (icon == null) - } - - private companion object { - - val navButtonViewField: Field? = runCatching { - Toolbar::class.java.getDeclaredField("mNavButtonView") - .also { it.isAccessible = true } - }.getOrNull() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt index 0cd94ba37..7f9655d19 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt @@ -6,8 +6,8 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity @Dao abstract class TagsDao { - @Query("SELECT * FROM tags") - abstract suspend fun getAllTags(): List + @Query("SELECT * FROM tags WHERE source = :source") + abstract suspend fun findTags(source: String): List @Insert(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun insert(tag: TagEntity): Long diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 4a2c2be82..f66e4b14f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -4,7 +4,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.SourceSettings abstract class RemoteMangaRepository( @@ -20,8 +19,6 @@ abstract class RemoteMangaRepository( val title: String get() = source.title - override val sortOrders: Set get() = emptySet() - override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain() override suspend fun getTags(): Set = emptySet() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index 41b86750e..0077f3d8c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -17,6 +17,8 @@ class ExHentaiRepository( override val source = MangaSource.EXHENTAI + override val sortOrders: Set = emptySet() + override val defaultDomain: String get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 97fa18c4c..97664dd2f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -85,7 +85,7 @@ class HistoryListViewModel( val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1) var prevDate: DateTimeAgo? = null if (!grouped) { - result += ListHeader(null, R.string.history) + result += ListHeader(null, R.string.history, null) } for ((manga, history) in list) { if (grouped) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 01f40436b..00bd769fd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -64,7 +64,8 @@ abstract class MangaListFragment : BaseFragment(), lifecycleOwner = viewLifecycleOwner, clickListener = this, onRetryClick = ::resolveException, - onTagRemoveClick = viewModel::onRemoveFilterTag + onTagRemoveClick = viewModel::onRemoveFilterTag, + onFilterClickListener = this::onFilterClick, ) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { @@ -191,6 +192,8 @@ abstract class MangaListFragment : BaseFragment(), } } + protected open fun onFilterClick() = Unit + private fun onGridScaleChanged(scale: Float) { spanSizeLookup.invalidateCache() spanResolver.setGridSize(scale, binding.recyclerView) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index 4d25060ac..53ac01484 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui.adapter import android.widget.TextView import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemHeaderWithFilterBinding import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel -fun listHeaderAD() = adapterDelegate(R.layout.item_header) { +fun listHeaderAD() = adapterDelegate( + layout = R.layout.item_header, + on = { item, _, _ -> item is ListHeader && item.sortOrder == null }, +) { bind { val textView = (itemView as TextView) @@ -16,4 +21,25 @@ fun listHeaderAD() = adapterDelegate(R.layout.item_header textView.setText(item.textRes) } } +} + +fun listHeaderWithFilterAD( + onFilterClickListener: () -> Unit, +) = adapterDelegateViewBinding( + viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) }, + on = { item, _, _ -> item is ListHeader && item.sortOrder != null }, +) { + + binding.textViewFilter.setOnClickListener { + onFilterClickListener() + } + + bind { + if (item.text != null) { + binding.textViewTitle.text = item.text + } else { + binding.textViewTitle.setText(item.textRes) + } + binding.textViewFilter.setText(requireNotNull(item.sortOrder).titleRes) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 61cd60c03..714f04473 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -20,6 +20,7 @@ class MangaListAdapter( clickListener: OnListItemClickListener, onRetryClick: (Throwable) -> Unit, onTagRemoveClick: (MangaTag) -> Unit, + onFilterClickListener: () -> Unit, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -41,6 +42,7 @@ class MangaListAdapter( .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) + .addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener)) } private class DiffCallback : DiffUtil.ItemCallback() { @@ -79,5 +81,6 @@ class MangaListAdapter( const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_FILTER = 10 + const val ITEM_TYPE_HEADER_FILTER = 11 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt index c47d7ac00..70aef4326 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -8,7 +8,8 @@ import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet @@ -18,7 +19,9 @@ import org.koitharu.kotatsu.utils.ext.withArgs class FilterBottomSheet : BaseBottomSheet() { - private val viewModel by viewModel { + private val viewModel by sharedViewModel( + owner = { from(requireParentFragment(), requireParentFragment()) } + ) { parametersOf( requireArguments().getParcelable(ARG_SOURCE), requireArguments().getParcelable(ARG_STATE), diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt index 7341e836a..0942ef35d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -4,13 +4,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.model.SortOrder -import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import java.util.* class FilterViewModel( - private val repository: MangaRepository, + private val repository: RemoteMangaRepository, + dataRepository: MangaDataRepository, state: FilterState, ) : BaseViewModel(), OnFilterChangedListener { @@ -22,6 +24,9 @@ class FilterViewModel( private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { repository.getTags() } + private val localTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + dataRepository.findTags(repository.source) + } init { showFilter() @@ -48,6 +53,7 @@ class FilterViewModel( job = launchJob(Dispatchers.Default) { previousJob?.cancelAndJoin() val tags = availableTagsDeferred.await() + val localTags = localTagsDeferred.await() val sortOrders = repository.sortOrders val list = ArrayList(sortOrders.size + tags.size + 2) list.add(FilterItem.Header(R.string.sort_order)) @@ -57,6 +63,7 @@ class FilterViewModel( if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres)) val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) + localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } list.addAll(mappedTags) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index 209c7227f..a14db0f3a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.list.ui.model import androidx.annotation.StringRes +import org.koitharu.kotatsu.core.model.SortOrder data class ListHeader( val text: CharSequence?, @StringRes val textRes: Int, + val sortOrder: SortOrder?, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 3f721355a..b68af3a0a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -32,7 +32,7 @@ class LocalListViewModel( val importProgress = MutableLiveData(null) private val listError = MutableStateFlow(null) private val mangaList = MutableStateFlow?>(null) - private val headerModel = ListHeader(null, R.string.local_storage) + private val headerModel = ListHeader(null, R.string.local_storage, null) private var importJob: Job? = null override val content = combine( diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt index b856248ba..4d35a857f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt @@ -4,6 +4,8 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.list.ui.filter.FilterViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel @@ -11,10 +13,17 @@ val remoteListModule get() = module { viewModel { params -> - RemoteListViewModel(get(named(params.get())), get()) + RemoteListViewModel( + repository = get(named(params.get())) as RemoteMangaRepository, + settings = get(), + ) } viewModel { params -> - FilterViewModel(get(named(params.get())), params.get()) + FilterViewModel( + repository = get(named(params.get())) as RemoteMangaRepository, + dataRepository = get(), + state = params.get(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index ff951de7b..04a1ffefe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -54,13 +54,17 @@ class RemoteListFragment : MangaListFragment(), FragmentResultListener { true } R.id.action_filter -> { - FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + onFilterClick() true } else -> super.onOptionsItemSelected(item) } } + override fun onFilterClick() { + FilterBottomSheet.show(childFragmentManager, source, viewModel.filter) + } + override fun onFragmentResult(requestKey: String, result: Bundle) { when (requestKey) { FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter( diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index fc5acd438..c914e709f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -10,7 +10,6 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -19,7 +18,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class RemoteListViewModel( - private val repository: MangaRepository, + private val repository: RemoteMangaRepository, settings: AppSettings ) : MangaListViewModel(settings) { @@ -29,21 +28,24 @@ class RemoteListViewModel( private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null) private var loadingJob: Job? = null - private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0) + private val headerModel = MutableStateFlow( + ListHeader(repository.title, 0, filter.sortOrder) + ) override val content = combine( mangaList, createListModeFlow(), + headerModel, listError, hasNextPage - ) { list, mode, error, hasNext -> + ) { list, mode, header, error, hasNext -> when { list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty)) else -> { val result = ArrayList(list.size + 3) - result += headerModel + result += header createFilterModel()?.let { result.add(it) } list.toUi(result, mode) when { @@ -83,6 +85,7 @@ class RemoteListViewModel( fun applyFilter(newFilter: FilterState) { filter = newFilter + headerModel.value = ListHeader(repository.title, 0, newFilter.sortOrder) mangaList.value = null hasNextPage.value = false loadList(false) diff --git a/app/src/main/res/layout/item_header_with_filter.xml b/app/src/main/res/layout/item_header_with_filter.xml new file mode 100644 index 000000000..05c7793ba --- /dev/null +++ b/app/src/main/res/layout/item_header_with_filter.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file From 3c64d6675e86bdc90293adbe837fed6b02cb38f1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Mar 2022 08:16:36 +0200 Subject: [PATCH 3/3] Handle filter loading errors --- .../kotatsu/list/ui/filter/FilterAdapter.kt | 1 + .../list/ui/filter/FilterAdapterDelegates.kt | 15 +++++--- .../list/ui/filter/FilterDiffCallback.kt | 6 +++- .../kotatsu/list/ui/filter/FilterItem.kt | 4 +++ .../kotatsu/list/ui/filter/FilterViewModel.kt | 34 ++++++++++++++----- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 3af68c0f4..19b3f11f7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -10,4 +10,5 @@ class FilterAdapter( filterTagDelegate(listener), filterHeaderDelegate(), filterLoadingDelegate(), + filterErrorDelegate(), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index 10a6cafd8..073de2c9d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -1,10 +1,12 @@ package org.koitharu.kotatsu.list.ui.filter +import android.widget.TextView +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding -import org.koitharu.kotatsu.databinding.ItemLoadingFooterBinding fun filterSortDelegate( listener: OnFilterChangedListener, @@ -47,6 +49,11 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemLoadingFooterBinding.inflate(layoutInflater, parent, false) } -) { } \ No newline at end of file +fun filterLoadingDelegate() = adapterDelegate(R.layout.item_loading_footer) {} + +fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { + + bind { + (itemView as TextView).setText(item.textResId) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt index d72cadf7c..73e3db315 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt @@ -17,14 +17,18 @@ class FilterDiffCallback : DiffUtil.ItemCallback() { oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { oldItem.order == newItem.order } + oldItem is FilterItem.Error && newItem is FilterItem.Error -> { + oldItem.textResId == newItem.textResId + } else -> false } } override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { return when { - oldItem === newItem -> true + oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true oldItem is FilterItem.Header && newItem is FilterItem.Header -> true + oldItem is FilterItem.Error && newItem is FilterItem.Error -> true oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.isChecked == newItem.isChecked } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt index 8117f5afd..75b29e60d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt @@ -21,4 +21,8 @@ sealed interface FilterItem { ) : FilterItem object Loading : FilterItem + + class Error( + @StringRes val textResId: Int, + ) : FilterItem } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt index 0942ef35d..06c4b029e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import java.util.* @@ -21,12 +22,10 @@ class FilterViewModel( private var job: Job? = null private var selectedSortOrder: SortOrder? = state.sortOrder private val selectedTags = HashSet(state.tags) - private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { - repository.getTags() - } - private val localTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) { + private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) { dataRepository.findTags(repository.source) } + private var availableTagsDeferred = loadTagsAsync() init { showFilter() @@ -52,21 +51,24 @@ class FilterViewModel( val previousJob = job job = launchJob(Dispatchers.Default) { previousJob?.cancelAndJoin() - val tags = availableTagsDeferred.await() + val tags = tryLoadTags() val localTags = localTagsDeferred.await() val sortOrders = repository.sortOrders - val list = ArrayList(sortOrders.size + tags.size + 2) + val list = ArrayList(sortOrders.size + (tags?.size ?: 1) + 2) list.add(FilterItem.Header(R.string.sort_order)) sortOrders.sortedBy { it.ordinal }.mapTo(list) { FilterItem.Sort(it, isSelected = it == selectedSortOrder) } - if (tags.isNotEmpty() || selectedTags.isNotEmpty()) { + if (tags == null || tags.isNotEmpty() || selectedTags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres)) val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title })) localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } - tags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } + tags?.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) } selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) } list.addAll(mappedTags) + if (tags == null) { + list.add(FilterItem.Error(R.string.filter_load_error)) + } } ensureActive() filter.postValue(list) @@ -93,4 +95,20 @@ class FilterViewModel( updateFilters() } } + + private suspend fun tryLoadTags(): Set? { + val shouldRetryOnError = availableTagsDeferred.isCompleted + val result = availableTagsDeferred.await() + if (result == null && shouldRetryOnError) { + availableTagsDeferred = loadTagsAsync() + return availableTagsDeferred.await() + } + return result + } + + private fun loadTagsAsync() = viewModelScope.async(Dispatchers.Default) { + kotlin.runCatching { + repository.getTags() + }.getOrNull() + } } \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3155e92f0..3966f6c81 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -253,4 +253,5 @@ Разрешить Запретить для NSFW Запретить всегда + Не удалось загрузить список жанров \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6adb549de..bd5d42803 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -255,4 +255,5 @@ Allow Block on NSFW Block always + Unable to load genres list \ No newline at end of file