Merge branch 'feature/bs-filter' into devel

pull/120/head
Koitharu 4 years ago
commit 6a965ddb28
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -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.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga 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.core.prefs.ReaderMode
import org.koitharu.kotatsu.utils.ext.mapToSet
class MangaDataRepository(private val db: MangaDatabase) { class MangaDataRepository(private val db: MangaDatabase) {
@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) {
db.mangaDao.upsert(MangaEntity.from(manga), tags) db.mangaDao.upsert(MangaEntity.from(manga), tags)
} }
} }
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).mapToSet {
it.toMangaTag()
}
}
} }

@ -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,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()
}
}

@ -6,8 +6,8 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao @Dao
abstract class TagsDao { abstract class TagsDao {
@Query("SELECT * FROM tags") @Query("SELECT * FROM tags WHERE source = :source")
abstract suspend fun getAllTags(): List<TagEntity> abstract suspend fun findTags(source: String): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(tag: TagEntity): Long abstract suspend fun insert(tag: TagEntity): Long

@ -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,7 +4,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
abstract class RemoteMangaRepository( abstract class RemoteMangaRepository(
@ -20,8 +19,6 @@ abstract class RemoteMangaRepository(
val title: String val title: String
get() = source.title get() = source.title
override val sortOrders: Set<SortOrder> get() = emptySet()
override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain() override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain()
override suspend fun getTags(): Set<MangaTag> = emptySet() override suspend fun getTags(): Set<MangaTag> = emptySet()

@ -17,6 +17,8 @@ class ExHentaiRepository(
override val source = MangaSource.EXHENTAI override val source = MangaSource.EXHENTAI
override val sortOrders: Set<SortOrder> = emptySet()
override val defaultDomain: String override val defaultDomain: String
get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED

@ -85,7 +85,7 @@ class HistoryListViewModel(
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1) val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
var prevDate: DateTimeAgo? = null var prevDate: DateTimeAgo? = null
if (!grouped) { if (!grouped) {
result += ListHeader(null, R.string.history) result += ListHeader(null, R.string.history, null)
} }
for ((manga, history) in list) { for ((manga, history) in list) {
if (grouped) { if (grouped) {

@ -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,16 +59,14 @@ 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,
clickListener = this, clickListener = this,
onRetryClick = ::resolveException, onRetryClick = ::resolveException,
onTagRemoveClick = viewModel::onRemoveFilterTag onTagRemoveClick = viewModel::onRemoveFilterTag,
onFilterClickListener = this::onFilterClick,
) )
filterAdapter = FilterAdapter2(viewModel)
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
@ -89,17 +79,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 +92,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 +108,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 +173,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
@ -238,6 +192,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
} }
protected open fun onFilterClick() = Unit
private fun onGridScaleChanged(scale: Float) { private fun onGridScaleChanged(scale: Float) {
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
spanResolver.setGridSize(scale, binding.recyclerView) spanResolver.setGridSize(scale, binding.recyclerView)

@ -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,16 @@ package org.koitharu.kotatsu.list.ui.adapter
import android.widget.TextView import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(R.layout.item_header) { fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(
layout = R.layout.item_header,
on = { item, _, _ -> item is ListHeader && item.sortOrder == null },
) {
bind { bind {
val textView = (itemView as TextView) val textView = (itemView as TextView)
@ -17,3 +22,24 @@ fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(R.layout.item_header
} }
} }
} }
fun listHeaderWithFilterAD(
onFilterClickListener: () -> Unit,
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderWithFilterBinding>(
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)
}
}

@ -20,6 +20,7 @@ class MangaListAdapter(
clickListener: OnListItemClickListener<Manga>, clickListener: OnListItemClickListener<Manga>,
onRetryClick: (Throwable) -> Unit, onRetryClick: (Throwable) -> Unit,
onTagRemoveClick: (MangaTag) -> Unit, onTagRemoveClick: (MangaTag) -> Unit,
onFilterClickListener: () -> Unit,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {
@ -41,6 +42,7 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick))
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener))
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
@ -79,5 +81,6 @@ class MangaListAdapter(
const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_FILTER = 10 const val ITEM_TYPE_FILTER = 10
const val ITEM_TYPE_HEADER_FILTER = 11
} }
} }

@ -2,11 +2,13 @@ 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(),
filterErrorDelegate(),
) )

@ -1,6 +1,9 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.list.ui.filter
import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
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
@ -45,3 +48,12 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, Filte
binding.root.setText(item.titleResId) binding.root.setText(item.titleResId)
} }
} }
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
bind {
(itemView as TextView).setText(item.textResId)
}
}

@ -0,0 +1,84 @@
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.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
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 sharedViewModel<FilterViewModel>(
owner = { from(requireParentFragment(), requireParentFragment()) }
) {
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
@ -16,13 +17,18 @@ class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
oldItem.order == newItem.order oldItem.order == newItem.order
} }
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
oldItem.textResId == newItem.textResId
}
else -> false else -> false
} }
} }
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when { return when {
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
oldItem is FilterItem.Header && newItem is FilterItem.Header -> 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 is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.isChecked == newItem.isChecked oldItem.isChecked == newItem.isChecked
} }

@ -19,4 +19,10 @@ sealed interface FilterItem {
val tag: MangaTag, val tag: MangaTag,
val isChecked: Boolean, val isChecked: Boolean,
) : FilterItem ) : FilterItem
object Loading : FilterItem
class Error(
@StringRes val textResId: Int,
) : 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,114 @@
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.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.*
class FilterViewModel(
private val repository: RemoteMangaRepository,
dataRepository: MangaDataRepository,
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 localTagsDeferred = viewModelScope.async(Dispatchers.Default) {
dataRepository.findTags(repository.source)
}
private var availableTagsDeferred = loadTagsAsync()
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 = tryLoadTags()
val localTags = localTagsDeferred.await()
val sortOrders = repository.sortOrders
val list = ArrayList<FilterItem>(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 == null || tags.isNotEmpty() || selectedTags.isNotEmpty()) {
list.add(FilterItem.Header(R.string.genres))
val mappedTags = TreeSet<FilterItem.Tag>(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)
if (tags == null) {
list.add(FilterItem.Error(R.string.filter_load_error))
}
}
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()
}
}
private suspend fun tryLoadTags(): Set<MangaTag>? {
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()
}
}

@ -1,8 +1,10 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.SortOrder
data class ListHeader( data class ListHeader(
val text: CharSequence?, val text: CharSequence?,
@StringRes val textRes: Int, @StringRes val textRes: Int,
val sortOrder: SortOrder?,
) : ListModel ) : ListModel

@ -32,7 +32,7 @@ class LocalListViewModel(
val importProgress = MutableLiveData<Progress?>(null) val importProgress = MutableLiveData<Progress?>(null)
private val listError = MutableStateFlow<Throwable?>(null) private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null) private val mangaList = MutableStateFlow<List<Manga>?>(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 private var importJob: Job? = null
override val content = combine( override val content = combine(

@ -4,12 +4,26 @@ 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.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 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(
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
settings = get(),
)
}
viewModel { params ->
FilterViewModel(
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
dataRepository = get(),
state = 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,26 @@ class RemoteListFragment : MangaListFragment() {
) )
true true
} }
R.id.action_filter -> {
onFilterClick()
true
}
else -> super.onOptionsItemSelected(item) 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(
result.getParcelable(FilterBottomSheet.ARG_STATE) ?: return
)
}
}
companion object { companion object {
private const val ARG_SOURCE = "provider" private const val ARG_SOURCE = "provider"

@ -9,38 +9,43 @@ 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.parser.MangaRepository import org.koitharu.kotatsu.core.model.MangaTag
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
class RemoteListViewModel( class RemoteListViewModel(
private val repository: MangaRepository, private val repository: RemoteMangaRepository,
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)
private var loadingJob: Job? = 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( override val content = combine(
mangaList, mangaList,
createListModeFlow(), createListModeFlow(),
headerModel,
listError, listError,
hasNextPage hasNextPage
) { list, mode, error, hasNext -> ) { list, mode, header, error, hasNext ->
when { when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty)) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty))
else -> { else -> {
val result = ArrayList<ListModel>(list.size + 3) val result = ArrayList<ListModel>(list.size + 3)
result += headerModel result += header
createFilterModel()?.let { result.add(it) } createFilterModel()?.let { result.add(it) }
list.toUi(result, mode) list.toUi(result, mode)
when { when {
@ -54,7 +59,6 @@ class RemoteListViewModel(
init { init {
loadList(false) loadList(false)
loadFilter()
} }
override fun onRefresh() { override fun onRefresh() {
@ -65,12 +69,28 @@ 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
headerModel.value = ListHeader(repository.title, 0, newFilter.sortOrder)
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 +100,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 +118,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"

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
<TextView
android:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/textView_filter"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
tools:text="@tools:sample/lorem[21]" />
<TextView
android:id="@+id/textView_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/list_selector"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:drawableEndCompat="@drawable/ic_drop_down"
app:drawableTint="?android:attr/textColorSecondary"
tools:ignore="RtlSymmetry"
tools:text="@string/popular" />
</RelativeLayout>

@ -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"

@ -261,4 +261,5 @@
<string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string> <string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string>
<string name="enabled">Включено</string> <string name="enabled">Включено</string>
<string name="disabled">Выключено</string> <string name="disabled">Выключено</string>
<string name="filter_load_error">Не удалось загрузить список жанров</string>
</resources> </resources>

@ -263,4 +263,5 @@
<string name="exclude_nsfw_from_suggestions">Do not suggest NSFW manga</string> <string name="exclude_nsfw_from_suggestions">Do not suggest NSFW manga</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="filter_load_error">Unable to load genres list</string>
</resources> </resources>
Loading…
Cancel
Save