Move filter into bottom sheet
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
@@ -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) }
|
||||||
|
) { }
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ sealed interface FilterItem {
|
|||||||
val tag: MangaTag,
|
val tag: MangaTag,
|
||||||
val isChecked: Boolean,
|
val isChecked: Boolean,
|
||||||
) : FilterItem
|
) : FilterItem
|
||||||
|
|
||||||
|
object Loading : FilterItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
33
app/src/main/res/layout/sheet_filter.xml
Normal file
33
app/src/main/res/layout/sheet_filter.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user