Move filter into bottom sheet

This commit is contained in:
Koitharu
2022-03-02 21:05:57 +02:00
parent 28a4d4164e
commit 238bc89be9
21 changed files with 298 additions and 241 deletions

View File

@@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() {
} }
} }
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
throwable.printStackTrace() throwable.printStackTrace()
} }

View File

@@ -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<MangaTag>,
) : Parcelable

View File

@@ -4,13 +4,9 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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.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.filter.FilterAdapter2
import org.koitharu.kotatsu.list.ui.filter.FilterItem
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
@@ -43,7 +37,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
SwipeRefreshLayout.OnRefreshListener { SwipeRefreshLayout.OnRefreshListener {
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
private var filterAdapter: FilterAdapter2? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private val spanResolver = MangaListSpanResolver() private val spanResolver = MangaListSpanResolver()
private val spanSizeLookup = SpanSizeLookup() private val spanSizeLookup = SpanSizeLookup()
@@ -51,7 +44,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
} }
open val isSwipeRefreshEnabled = true open val isSwipeRefreshEnabled = true
private var drawer: DrawerLayout? = null
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
@@ -67,8 +59,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
drawer = binding.root as? DrawerLayout
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
listAdapter = MangaListAdapter( listAdapter = MangaListAdapter(
coil = get(), coil = get(),
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
@@ -76,7 +66,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
onRetryClick = ::resolveException, onRetryClick = ::resolveException,
onTagRemoveClick = viewModel::onRemoveFilterTag onTagRemoveClick = viewModel::onRemoveFilterTag
) )
filterAdapter = FilterAdapter2(viewModel)
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
@@ -89,17 +78,12 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
setOnRefreshListener(this@MangaListFragment) setOnRefreshListener(this@MangaListFragment)
isEnabled = isSwipeRefreshEnabled isEnabled = isSwipeRefreshEnabled
} }
with(binding.recyclerViewFilter) {
setHasFixedSize(true)
adapter = filterAdapter
}
(parentFragment as? RecycledViewPoolHolder)?.let { (parentFragment as? RecycledViewPoolHolder)?.let {
binding.recyclerView.setRecycledViewPool(it.recycledViewPool) binding.recyclerView.setRecycledViewPool(it.recycledViewPool)
} }
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
@@ -107,9 +91,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
override fun onDestroyView() { override fun onDestroyView() {
drawer = null
listAdapter = null listAdapter = null
filterAdapter = null
paginationListener = null paginationListener = null
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
super.onDestroyView() super.onDestroyView()
@@ -125,19 +107,9 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
ListModeSelectDialog.show(childFragmentManager) ListModeSelectDialog.show(childFragmentManager)
true true
} }
R.id.action_filter -> {
drawer?.toggleDrawer(GravityCompat.END)
true
}
else -> super.onOptionsItemSelected(item) 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) { override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item)) startActivity(DetailsActivity.newIntent(context ?: return, item))
} }
@@ -200,27 +172,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
} }
protected fun onInitFilter(filter: List<FilterItem>) {
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) { override fun onWindowInsetsChanged(insets: Insets) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.recyclerViewFilter.updatePadding(
top = headerHeight,
bottom = insets.bottom
)
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right right = insets.right

View File

@@ -1,32 +1,22 @@
package org.koitharu.kotatsu.list.ui package org.koitharu.kotatsu.list.ui
import androidx.annotation.CallSuper
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel 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.model.MangaTag
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode 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.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel( abstract class MangaListViewModel(
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel(), OnFilterChangedListener { ) : BaseViewModel() {
abstract val content: LiveData<List<ListModel>> abstract val content: LiveData<List<ListModel>>
val filter = MutableLiveData<List<FilterItem>>()
val listMode = MutableLiveData<ListMode>() val listMode = MutableLiveData<ListMode>()
val gridScale = settings.observe() val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE } .filter { it == AppSettings.KEY_GRID_SIZE }
@@ -35,6 +25,8 @@ abstract class MangaListViewModel(
settings.gridSize / 100f settings.gridSize / 100f
} }
open fun onRemoveFilterTag(tag: MangaTag) = Unit
protected fun createListModeFlow() = settings.observe() protected fun createListModeFlow() = settings.observe()
.filter { it == AppSettings.KEY_LIST_MODE } .filter { it == AppSettings.KEY_LIST_MODE }
.map { settings.listMode } .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<FilterItem>(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 onRefresh()
abstract fun onRetry() abstract fun onRetry()

View File

@@ -2,11 +2,12 @@ package org.koitharu.kotatsu.list.ui.filter
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class FilterAdapter2( class FilterAdapter(
listener: OnFilterChangedListener, listener: OnFilterChangedListener,
) : AsyncListDifferDelegationAdapter<FilterItem>( ) : AsyncListDifferDelegationAdapter<FilterItem>(
FilterDiffCallback(), FilterDiffCallback(),
filterSortDelegate(listener), filterSortDelegate(listener),
filterTagDelegate(listener), filterTagDelegate(listener),
filterHeaderDelegate(), filterHeaderDelegate(),
filterLoadingDelegate(),
) )

View File

@@ -4,6 +4,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemLoadingFooterBinding
fun filterSortDelegate( fun filterSortDelegate(
listener: OnFilterChangedListener, listener: OnFilterChangedListener,
@@ -44,4 +45,8 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, Filte
bind { bind {
binding.root.setText(item.titleResId) binding.root.setText(item.titleResId)
} }
} }
fun filterLoadingDelegate() = adapterDelegateViewBinding<FilterItem.Loading, FilterItem, ItemLoadingFooterBinding>(
{ layoutInflater, parent -> ItemLoadingFooterBinding.inflate(layoutInflater, parent, false) }
) { }

View File

@@ -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<SheetFilterBinding>() {
private val viewModel by viewModel<FilterViewModel> {
parametersOf(
requireArguments().getParcelable<MangaSource>(ARG_SOURCE),
requireArguments().getParcelable<FilterState>(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)
}
}

View File

@@ -6,6 +6,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when { return when {
oldItem === newItem -> true
oldItem.javaClass != newItem.javaClass -> false oldItem.javaClass != newItem.javaClass -> false
oldItem is FilterItem.Header && newItem is FilterItem.Header -> { oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
oldItem.titleResId == newItem.titleResId oldItem.titleResId == newItem.titleResId
@@ -22,6 +23,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when { return when {
oldItem === newItem -> true
oldItem is FilterItem.Header && newItem is FilterItem.Header -> true oldItem is FilterItem.Header && newItem is FilterItem.Header -> true
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.isChecked == newItem.isChecked oldItem.isChecked == newItem.isChecked

View File

@@ -19,4 +19,6 @@ sealed interface FilterItem {
val tag: MangaTag, val tag: MangaTag,
val isChecked: Boolean, val isChecked: Boolean,
) : FilterItem ) : FilterItem
object Loading : FilterItem
} }

View File

@@ -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<MangaTag>,
) : Parcelable

View File

@@ -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<List<FilterItem>>()
val result = MutableLiveData<FilterState>()
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<FilterItem>(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<FilterItem.Tag>(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<FilterItem>(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()
}
}
}

View File

@@ -4,12 +4,17 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.list.ui.filter.FilterViewModel
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
val remoteListModule val remoteListModule
get() = module { get() = module {
viewModel { source -> viewModel { params ->
RemoteListViewModel(get(named(source.get<MangaSource>())), get()) RemoteListViewModel(get(named(params.get<MangaSource>())), get())
}
viewModel { params ->
FilterViewModel(get(named(params.get<MangaSource>())), params.get())
} }
} }

View File

@@ -1,18 +1,22 @@
package org.koitharu.kotatsu.remotelist.ui package org.koitharu.kotatsu.remotelist.ui
import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.fragment.app.FragmentResultListener
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R 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.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
class RemoteListFragment : MangaListFragment() { class RemoteListFragment : MangaListFragment(), FragmentResultListener {
override val viewModel by viewModel<RemoteListViewModel> { override val viewModel by viewModel<RemoteListViewModel> {
parametersOf(source) parametersOf(source)
@@ -20,6 +24,11 @@ class RemoteListFragment : MangaListFragment() {
private val source by parcelableArgument<MangaSource>(ARG_SOURCE) private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
childFragmentManager.setFragmentResultListener(FilterBottomSheet.REQUEST_KEY, viewLifecycleOwner, this)
}
override fun onScrolledToEnd() { override fun onScrolledToEnd() {
viewModel.loadNextPage() viewModel.loadNextPage()
} }
@@ -44,10 +53,22 @@ class RemoteListFragment : MangaListFragment() {
) )
true true
} }
R.id.action_filter -> {
FilterBottomSheet.show(childFragmentManager, source, viewModel.filter)
true
}
else -> super.onOptionsItemSelected(item) 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 { companion object {
private const val ARG_SOURCE = "provider" private const val ARG_SOURCE = "provider"

View File

@@ -9,11 +9,12 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R 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.Manga 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.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings 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.MangaListViewModel
import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -22,6 +23,8 @@ class RemoteListViewModel(
settings: AppSettings settings: AppSettings
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
var filter = FilterState(repository.sortOrders.firstOrNull(), emptySet())
private set
private val mangaList = MutableStateFlow<List<Manga>?>(null) private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val hasNextPage = MutableStateFlow(false) private val hasNextPage = MutableStateFlow(false)
private val listError = MutableStateFlow<Throwable?>(null) private val listError = MutableStateFlow<Throwable?>(null)
@@ -54,7 +57,6 @@ class RemoteListViewModel(
init { init {
loadList(false) loadList(false)
loadFilter()
} }
override fun onRefresh() { override fun onRefresh() {
@@ -65,12 +67,27 @@ class RemoteListViewModel(
loadList(append = !mangaList.value.isNullOrEmpty()) 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() { fun loadNextPage() {
if (hasNextPage.value && listError.value == null) { if (hasNextPage.value && listError.value == null) {
loadList(append = true) loadList(append = true)
} }
} }
fun applyFilter(newFilter: FilterState) {
filter = newFilter
mangaList.value = null
hasNextPage.value = false
loadList(false)
}
private fun loadList(append: Boolean) { private fun loadList(append: Boolean) {
if (loadingJob?.isActive == true) { if (loadingJob?.isActive == true) {
return return
@@ -80,8 +97,8 @@ class RemoteListViewModel(
listError.value = null listError.value = null
val list = repository.getList2( val list = repository.getList2(
offset = if (append) mangaList.value?.size ?: 0 else 0, offset = if (append) mangaList.value?.size ?: 0 else 0,
sortOrder = currentFilter.sortOrder, sortOrder = filter.sortOrder,
tags = currentFilter.tags, tags = filter.tags,
) )
if (!append) { if (!append) {
mangaList.value = list 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? { private fun createFilterModel(): CurrentFilterModel? {
val tags = currentFilter.tags val tags = filter.tags
return if (tags.isEmpty()) { return if (tags.isEmpty()) {
null null
} else { } else {
CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) 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()
}
}
}
}
} }

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="horizontal">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/grid_spacing_outer"
app:fastScrollEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<View
android:id="@+id/divider_filter"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?attr/colorOutline"
android:visibility="gone"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_filter"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="?android:windowBackground"
android:orientation="vertical"
android:scrollbars="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable"
tools:visibility="visible" />
</LinearLayout>

View File

@@ -1,39 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/grid_spacing_outer"
app:fastScrollEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_filter" android:id="@+id/recyclerView"
android:layout_width="240dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end"
android:background="?android:windowBackground"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:scrollbars="vertical" android:padding="@dimen/grid_spacing_outer"
app:fastScrollEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable" /> tools:listitem="@layout/item_manga_list" />
</androidx.drawerlayout.widget.DrawerLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground" android:background="?selectableItemBackground"
android:drawableStart="?android:listChoiceIndicatorMultiple" android:drawableStart="?android:listChoiceIndicatorMultiple"
android:drawablePadding="12dp" android:drawablePadding="12dp"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground" android:background="?selectableItemBackground"
android:drawableStart="?android:listChoiceIndicatorSingle" android:drawableStart="?android:listChoiceIndicatorSingle"
android:drawablePadding="12dp" android:drawablePadding="12dp"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navigationIcon="@drawable/ic_cross"
app:title="@string/filter" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
app:fastScrollEnabled="true"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable" />
</LinearLayout>

View File

@@ -9,9 +9,4 @@
android:title="@string/list_mode" android:title="@string/list_mode"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_filter"
android:orderInCategory="30"
android:title="@string/filter"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -3,6 +3,12 @@
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">
<item
android:id="@+id/action_filter"
android:orderInCategory="30"
android:title="@string/filter"
app:showAsAction="never" />
<item <item
android:id="@+id/action_source_settings" android:id="@+id/action_source_settings"
android:orderInCategory="50" android:orderInCategory="50"