Open filter from list header
This commit is contained in:
@@ -6,7 +6,10 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
class MangaDataRepository(private val db: MangaDatabase) {
|
||||
|
||||
@@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) {
|
||||
db.mangaDao.upsert(MangaEntity.from(manga), tags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
||||
return db.tagsDao.findTags(source.name).mapToSet {
|
||||
it.toMangaTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
abstract class TagsDao {
|
||||
|
||||
@Query("SELECT * FROM tags")
|
||||
abstract suspend fun getAllTags(): List<TagEntity>
|
||||
@Query("SELECT * FROM tags WHERE source = :source")
|
||||
abstract suspend fun findTags(source: String): List<TagEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(tag: TagEntity): Long
|
||||
|
||||
@@ -4,7 +4,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
|
||||
abstract class RemoteMangaRepository(
|
||||
@@ -20,8 +19,6 @@ abstract class RemoteMangaRepository(
|
||||
val title: String
|
||||
get() = source.title
|
||||
|
||||
override val sortOrders: Set<SortOrder> get() = emptySet()
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain()
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
@@ -17,6 +17,8 @@ class ExHentaiRepository(
|
||||
|
||||
override val source = MangaSource.EXHENTAI
|
||||
|
||||
override val sortOrders: Set<SortOrder> = emptySet()
|
||||
|
||||
override val defaultDomain: String
|
||||
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)
|
||||
var prevDate: DateTimeAgo? = null
|
||||
if (!grouped) {
|
||||
result += ListHeader(null, R.string.history)
|
||||
result += ListHeader(null, R.string.history, null)
|
||||
}
|
||||
for ((manga, history) in list) {
|
||||
if (grouped) {
|
||||
|
||||
@@ -64,7 +64,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
clickListener = this,
|
||||
onRetryClick = ::resolveException,
|
||||
onTagRemoveClick = viewModel::onRemoveFilterTag
|
||||
onTagRemoveClick = viewModel::onRemoveFilterTag,
|
||||
onFilterClickListener = this::onFilterClick,
|
||||
)
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
with(binding.recyclerView) {
|
||||
@@ -191,6 +192,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onFilterClick() = Unit
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver.setGridSize(scale, binding.recyclerView)
|
||||
|
||||
@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderWithFilterBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun listHeaderAD() = adapterDelegate<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 {
|
||||
val textView = (itemView as TextView)
|
||||
@@ -16,4 +21,25 @@ fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(R.layout.item_header
|
||||
textView.setText(item.textRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
onRetryClick: (Throwable) -> Unit,
|
||||
onTagRemoveClick: (MangaTag) -> Unit,
|
||||
onFilterClickListener: () -> Unit,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
@@ -41,6 +42,7 @@ class MangaListAdapter(
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
|
||||
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
|
||||
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick))
|
||||
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
@@ -79,5 +81,6 @@ class MangaListAdapter(
|
||||
const val ITEM_TYPE_EMPTY = 8
|
||||
const val ITEM_TYPE_HEADER = 9
|
||||
const val ITEM_TYPE_FILTER = 10
|
||||
const val ITEM_TYPE_HEADER_FILTER = 11
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
@@ -18,7 +19,9 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class FilterBottomSheet : BaseBottomSheet<SheetFilterBinding>() {
|
||||
|
||||
private val viewModel by viewModel<FilterViewModel> {
|
||||
private val viewModel by sharedViewModel<FilterViewModel>(
|
||||
owner = { from(requireParentFragment(), requireParentFragment()) }
|
||||
) {
|
||||
parametersOf(
|
||||
requireArguments().getParcelable<MangaSource>(ARG_SOURCE),
|
||||
requireArguments().getParcelable<FilterState>(ARG_STATE),
|
||||
|
||||
@@ -4,13 +4,15 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import java.util.*
|
||||
|
||||
class FilterViewModel(
|
||||
private val repository: MangaRepository,
|
||||
private val repository: RemoteMangaRepository,
|
||||
dataRepository: MangaDataRepository,
|
||||
state: FilterState,
|
||||
) : BaseViewModel(), OnFilterChangedListener {
|
||||
|
||||
@@ -22,6 +24,9 @@ class FilterViewModel(
|
||||
private val availableTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) {
|
||||
repository.getTags()
|
||||
}
|
||||
private val localTagsDeferred = viewModelScope.async(Dispatchers.Default + createErrorHandler()) {
|
||||
dataRepository.findTags(repository.source)
|
||||
}
|
||||
|
||||
init {
|
||||
showFilter()
|
||||
@@ -48,6 +53,7 @@ class FilterViewModel(
|
||||
job = launchJob(Dispatchers.Default) {
|
||||
previousJob?.cancelAndJoin()
|
||||
val tags = availableTagsDeferred.await()
|
||||
val localTags = localTagsDeferred.await()
|
||||
val sortOrders = repository.sortOrders
|
||||
val list = ArrayList<FilterItem>(sortOrders.size + tags.size + 2)
|
||||
list.add(FilterItem.Header(R.string.sort_order))
|
||||
@@ -57,6 +63,7 @@ class FilterViewModel(
|
||||
if (tags.isNotEmpty() || selectedTags.isNotEmpty()) {
|
||||
list.add(FilterItem.Header(R.string.genres))
|
||||
val mappedTags = TreeSet<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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
|
||||
data class ListHeader(
|
||||
val text: CharSequence?,
|
||||
@StringRes val textRes: Int,
|
||||
val sortOrder: SortOrder?,
|
||||
) : ListModel
|
||||
@@ -32,7 +32,7 @@ class LocalListViewModel(
|
||||
val importProgress = MutableLiveData<Progress?>(null)
|
||||
private val listError = MutableStateFlow<Throwable?>(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
|
||||
|
||||
override val content = combine(
|
||||
|
||||
@@ -4,6 +4,8 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterViewModel
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||
|
||||
@@ -11,10 +13,17 @@ val remoteListModule
|
||||
get() = module {
|
||||
|
||||
viewModel { params ->
|
||||
RemoteListViewModel(get(named(params.get<MangaSource>())), get())
|
||||
RemoteListViewModel(
|
||||
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
|
||||
settings = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel { params ->
|
||||
FilterViewModel(get(named(params.get<MangaSource>())), params.get())
|
||||
FilterViewModel(
|
||||
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
|
||||
dataRepository = get(),
|
||||
state = params.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,17 @@ class RemoteListFragment : MangaListFragment(), FragmentResultListener {
|
||||
true
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
FilterBottomSheet.show(childFragmentManager, source, viewModel.filter)
|
||||
onFilterClick()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilterClick() {
|
||||
FilterBottomSheet.show(childFragmentManager, source, viewModel.filter)
|
||||
}
|
||||
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
when (requestKey) {
|
||||
FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter(
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
@@ -19,7 +18,7 @@ import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
|
||||
class RemoteListViewModel(
|
||||
private val repository: MangaRepository,
|
||||
private val repository: RemoteMangaRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
@@ -29,21 +28,24 @@ class RemoteListViewModel(
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private var loadingJob: Job? = null
|
||||
private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0)
|
||||
private val headerModel = MutableStateFlow(
|
||||
ListHeader(repository.title, 0, filter.sortOrder)
|
||||
)
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
headerModel,
|
||||
listError,
|
||||
hasNextPage
|
||||
) { list, mode, error, hasNext ->
|
||||
) { list, mode, header, error, hasNext ->
|
||||
when {
|
||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty))
|
||||
else -> {
|
||||
val result = ArrayList<ListModel>(list.size + 3)
|
||||
result += headerModel
|
||||
result += header
|
||||
createFilterModel()?.let { result.add(it) }
|
||||
list.toUi(result, mode)
|
||||
when {
|
||||
@@ -83,6 +85,7 @@ class RemoteListViewModel(
|
||||
|
||||
fun applyFilter(newFilter: FilterState) {
|
||||
filter = newFilter
|
||||
headerModel.value = ListHeader(repository.title, 0, newFilter.sortOrder)
|
||||
mangaList.value = null
|
||||
hasNextPage.value = false
|
||||
loadList(false)
|
||||
|
||||
36
app/src/main/res/layout/item_header_with_filter.xml
Normal file
36
app/src/main/res/layout/item_header_with_filter.xml
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user