Update filter header ui

pull/377/head
Koitharu 3 years ago
parent f0a4fa4e95
commit 84f41810c5
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -40,6 +40,8 @@ import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.activityManager import org.koitharu.kotatsu.core.util.ext.activityManager
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
@ -64,6 +66,9 @@ interface AppModule {
@Binds @Binds
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
@Binds
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
companion object { companion object {
@Provides @Provides

@ -22,6 +22,8 @@ interface MangaRepository {
val sortOrders: Set<SortOrder> val sortOrders: Set<SortOrder>
var defaultSortOrder: SortOrder
suspend fun getList(offset: Int, query: String): List<Manga> suspend fun getList(offset: Int, query: String): List<Manga>
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga>

@ -39,8 +39,8 @@ class RemoteMangaRepository(
override val sortOrders: Set<SortOrder> override val sortOrders: Set<SortOrder>
get() = parser.sortOrders get() = parser.sortOrders
var defaultSortOrder: SortOrder? override var defaultSortOrder: SortOrder
get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull() get() = getConfig().defaultSortOrder ?: sortOrders.first()
set(value) { set(value) {
getConfig().defaultSortOrder = value getConfig().defaultSortOrder = value
} }

@ -6,6 +6,7 @@ import android.content.res.ColorStateList
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View.OnClickListener import android.view.View.OnClickListener
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.getColorStateListOrThrow import androidx.core.content.res.getColorStateListOrThrow
import androidx.core.view.children import androidx.core.view.children
@ -101,6 +102,13 @@ class ChipsView @JvmOverloads constructor(
chip.setTextColor(tint ?: defaultChipTextColor) chip.setTextColor(tint ?: defaultChipTextColor)
chip.isClickable = onChipClickListener != null || model.isCheckable chip.isClickable = onChipClickListener != null || model.isCheckable
chip.isCheckable = model.isCheckable chip.isCheckable = model.isCheckable
if (model.icon == 0) {
chip.chipIcon = null
chip.isChipIconVisible = false
} else {
chip.setChipIconResource(model.icon)
chip.isChipIconVisible = true
}
chip.isChecked = model.isChecked chip.isChecked = model.isChecked
chip.tag = model.data chip.tag = model.data
} }
@ -134,6 +142,7 @@ class ChipsView @JvmOverloads constructor(
class ChipModel( class ChipModel(
@ColorRes val tint: Int, @ColorRes val tint: Int,
val title: CharSequence, val title: CharSequence,
@DrawableRes val icon: Int,
val isCheckable: Boolean, val isCheckable: Boolean,
val isChecked: Boolean, val isChecked: Boolean,
val data: Any? = null, val data: Any? = null,
@ -147,6 +156,7 @@ class ChipsView @JvmOverloads constructor(
if (tint != other.tint) return false if (tint != other.tint) return false
if (title != other.title) return false if (title != other.title) return false
if (icon != other.icon) return false
if (isCheckable != other.isCheckable) return false if (isCheckable != other.isCheckable) return false
if (isChecked != other.isChecked) return false if (isChecked != other.isChecked) return false
return data == other.data return data == other.data
@ -155,6 +165,7 @@ class ChipsView @JvmOverloads constructor(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = tint.hashCode() var result = tint.hashCode()
result = 31 * result + title.hashCode() result = 31 * result + title.hashCode()
result = 31 * result + icon.hashCode()
result = 31 * result + isCheckable.hashCode() result = 31 * result + isCheckable.hashCode()
result = 31 * result + isChecked.hashCode() result = 31 * result + isChecked.hashCode()
result = 31 * result + (data?.hashCode() ?: 0) result = 31 * result + (data?.hashCode() ?: 0)

@ -8,7 +8,7 @@ import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class RetainedLifecycleCoroutineScope( class RetainedLifecycleCoroutineScope(
private val lifecycle: RetainedLifecycle, val lifecycle: RetainedLifecycle,
) : CoroutineScope, RetainedLifecycle.OnClearedListener { ) : CoroutineScope, RetainedLifecycle.OnClearedListener {
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate

@ -8,9 +8,11 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.lifecycle.RetainedLifecycle
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -18,6 +20,9 @@ import kotlin.coroutines.resumeWithException
val processLifecycleScope: LifecycleCoroutineScope val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope inline get() = ProcessLifecycleOwner.get().lifecycleScope
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
inline get() = RetainedLifecycleCoroutineScope(this)
suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) { suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) {
if (currentState.isAtLeast(state)) { if (currentState.isAtLeast(state)) {
return return

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.annotation.SuppressLint
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.createViewModelLazy import androidx.fragment.app.createViewModelLazy
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
@MainThread @MainThread
@ -17,3 +19,7 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras }, extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory }, factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
) )
val ViewModelStore.values: Collection<ViewModel>
@SuppressLint("RestrictedApi")
get() = this.keys().mapNotNull { get(it) }

@ -284,6 +284,7 @@ class DetailsFragment :
ChipsView.ChipModel( ChipsView.ChipModel(
title = tag.title, title = tag.title,
tint = tagHighlighter.getTint(tag), tint = tagHighlighter.getTint(tag),
icon = 0,
data = tag, data = tag,
isCheckable = false, isCheckable = false,
isChecked = false, isChecked = false,

@ -1,9 +1,10 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import android.widget.TextView import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.core.util.ext.setChecked
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.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun filterSortDelegate( fun filterSortDelegate(

@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import kotlinx.coroutines.CoroutineScope import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -17,7 +19,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.filter.ui.model.FilterState
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
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@ -25,17 +33,26 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.text.Collator import java.text.Collator
import java.util.LinkedList
import java.util.Locale import java.util.Locale
import java.util.TreeSet import java.util.TreeSet
import javax.inject.Inject
class FilterCoordinator( @ViewModelScoped
private val repository: RemoteMangaRepository, class FilterCoordinator @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
dataRepository: MangaDataRepository, dataRepository: MangaDataRepository,
private val coroutineScope: CoroutineScope, private val searchRepository: MangaSearchRepository,
) : OnFilterChangedListener { lifecycle: ViewModelLifecycle,
) : FilterOwner {
private val coroutineScope = lifecycle.lifecycleScope
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))
private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet())) private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
private var searchQuery = MutableStateFlow("") private var searchQuery = MutableStateFlow("")
private val localTags = SuspendLazy { private val localTags = SuspendLazy {
@ -43,13 +60,23 @@ class FilterCoordinator(
} }
private var availableTagsDeferred = loadTagsAsync() private var availableTagsDeferred = loadTagsAsync()
val items: StateFlow<List<ListModel>> = getItemsFlow() override val filterItems: StateFlow<List<ListModel>> = getItemsFlow()
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn(
scope = coroutineScope + Dispatchers.Default,
started = SharingStarted.Lazily,
initialValue = FilterHeaderModel(emptyList(), repository.defaultSortOrder, false),
)
init { init {
observeState() observeState()
} }
override fun applyFilter(tags: Set<MangaTag>) {
setTags(tags)
}
override fun onSortItemClick(item: FilterItem.Sort) { override fun onSortItemClick(item: FilterItem.Sort) {
currentState.update { oldValue -> currentState.update { oldValue ->
FilterState(item.order, oldValue.tags) FilterState(item.order, oldValue.tags)
@ -95,6 +122,14 @@ class FilterCoordinator(
searchQuery.value = query searchQuery.value = query
} }
private fun getHeaderFlow() = combine(
observeState(),
observeAvailableTags(),
) { state, available ->
val chips = createChipsList(state, available.orEmpty())
FilterHeaderModel(chips, state.sortOrder, state.tags.isNotEmpty())
}
private fun getItemsFlow() = combine( private fun getItemsFlow() = combine(
getTagsAsFlow(), getTagsAsFlow(),
currentState, currentState,
@ -114,6 +149,48 @@ class FilterCoordinator(
} }
} }
private suspend fun createChipsList(
filterState: FilterState,
availableTags: Set<MangaTag>,
): List<ChipsView.ChipModel> {
val selectedTags = filterState.tags.toMutableSet()
var tags = searchRepository.getTagsSuggestion("", 6, repository.source)
if (tags.isEmpty()) {
tags = availableTags.take(6)
}
if (tags.isEmpty() && selectedTags.isEmpty()) {
return emptyList()
}
val result = LinkedList<ChipsView.ChipModel>()
for (tag in tags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = selectedTags.remove(tag),
data = tag,
)
if (model.isChecked) {
result.addFirst(model)
} else {
result.addLast(model)
}
}
for (tag in selectedTags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = true,
data = tag,
)
result.addFirst(model)
}
return result
}
@WorkerThread @WorkerThread
private fun buildFilterList( private fun buildFilterList(
allTags: TagsWrapper, allTags: TagsWrapper,

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.filter.ui.model.FilterItem
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

@ -0,0 +1,71 @@
package org.koitharu.kotatsu.filter.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import com.google.android.material.chip.Chip
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.parsers.model.MangaTag
import com.google.android.material.R as materialR
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
private val owner by lazy(LazyThreadSafetyMode.NONE) {
FilterOwner.from(requireActivity())
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.chipsTags.onChipClickListener = this
owner.header.observe(viewLifecycleOwner, ::onDataChanged)
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag
if (tag == null) {
FilterSheetFragment.show(parentFragmentManager)
} else {
owner.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked))
}
}
private fun onDataChanged(header: FilterHeaderModel) {
val binding = viewBinding ?: return
val chips = header.chips
if (chips.isEmpty()) {
binding.chipsTags.setChips(emptyList())
binding.root.isVisible = false
return
}
if (binding.root.context.isAnimationsEnabled) {
binding.scrollView.smoothScrollTo(0, 0)
} else {
binding.scrollView.scrollTo(0, 0)
}
binding.chipsTags.setChips(header.chips + moreTagsChip())
binding.root.isVisible = true
}
private fun moreTagsChip() = ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
)
}

@ -0,0 +1,32 @@
package org.koitharu.kotatsu.filter.ui
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.util.ext.values
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
interface FilterOwner : OnFilterChangedListener {
val filterItems: StateFlow<List<ListModel>>
val header: StateFlow<FilterHeaderModel>
fun applyFilter(tags: Set<MangaTag>)
companion object {
fun from(activity: FragmentActivity): FilterOwner {
for (f in activity.supportFragmentManager.fragments) {
return find(f) ?: continue
}
error("Cannot find FilterOwner")
}
fun find(fragment: Fragment): FilterOwner? {
return fragment.viewModelStore.values.firstNotNullOfOrNull { it as? FilterOwner }
}
}
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -10,20 +11,18 @@ import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentFragmentViewModels
import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
class FilterSheetFragment : class FilterSheetFragment :
BaseAdaptiveSheet<SheetFilterBinding>(), BaseAdaptiveSheet<SheetFilterBinding>(),
AdaptiveSheetCallback, AdaptiveSheetCallback,
AsyncListDiffer.ListListener<ListModel> { AsyncListDiffer.ListListener<ListModel> {
private val viewModel by parentFragmentViewModels<RemoteListViewModel>() private val owner by lazy(LazyThreadSafetyMode.NONE) {
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null FilterOwner.from(requireActivity())
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false) return SheetFilterBinding.inflate(inflater, container, false)
@ -32,14 +31,13 @@ class FilterSheetFragment :
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addSheetCallback(this) addSheetCallback(this)
val adapter = FilterAdapter(viewModel, this) val adapter = FilterAdapter(owner, this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) owner.filterItems.observe(viewLifecycleOwner, adapter::setItems)
}
override fun onDestroyView() { if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
super.onDestroyView() binding.recyclerView.scrollIndicators = 0
collapsibleActionViewCallback = null }
} }
override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) { override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) {

@ -1,8 +1,10 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui
import org.koitharu.kotatsu.filter.ui.model.FilterItem
interface OnFilterChangedListener { interface OnFilterChangedListener {
fun onSortItemClick(item: FilterItem.Sort) fun onSortItemClick(item: FilterItem.Sort)
fun onTagItemClick(item: FilterItem.Tag) fun onTagItemClick(item: FilterItem.Tag)
} }

@ -1,9 +1,10 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.filter.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
class ListHeader2( class FilterHeaderModel(
val chips: Collection<ChipsView.ChipModel>, val chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?, val sortOrder: SortOrder?,
val hasSelectedTags: Boolean, val hasSelectedTags: Boolean,
@ -13,7 +14,7 @@ class ListHeader2(
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as ListHeader2 other as FilterHeaderModel
if (chips != other.chips) return false if (chips != other.chips) return false
return sortOrder == other.sortOrder return sortOrder == other.sortOrder

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui.model
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.filter.ui.model
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
@ -15,9 +15,7 @@ class FilterState(
other as FilterState other as FilterState
if (sortOrder != other.sortOrder) return false if (sortOrder != other.sortOrder) return false
if (tags != other.tags) return false return tags == other.tags
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -25,4 +23,4 @@ class FilterState(
result = 31 * result + tags.hashCode() result = 31 * result + tags.hashCode()
return result return result
} }
} }

@ -0,0 +1,32 @@
package org.koitharu.kotatsu.list.domain
import dagger.Reusable
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject
@Reusable
class ListExtraProviderImpl @Inject constructor(
private val settings: AppSettings,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
) : ListExtraProvider {
override suspend fun getCounter(mangaId: Long): Int {
return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId)
} else {
0
}
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
}

@ -1,24 +1,20 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemHeader2Binding import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@Deprecated("")
fun listHeader2AD( fun listHeader2AD(
listener: MangaListListener, listener: MangaListListener,
) = adapterDelegateViewBinding<ListHeader2, ListModel, ItemHeader2Binding>( ) = adapterDelegateViewBinding<FilterHeaderModel, ListModel, FragmentFilterHeaderBinding>(
{ layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> FragmentFilterHeaderBinding.inflate(layoutInflater, parent, false) },
) { ) {
var ignoreChecking = false var ignoreChecking = false
binding.textViewFilter.setOnClickListener {
listener.onFilterClick(it)
}
binding.chipsTags.setOnCheckedStateChangeListener { _, _ -> binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
if (!ignoreChecking) { if (!ignoreChecking) {
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java)) listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
@ -36,6 +32,5 @@ fun listHeader2AD(
ignoreChecking = true ignoreChecking = true
binding.chipsTags.setChips(item.chips) // TODO use recyclerview binding.chipsTags.setChips(item.chips) // TODO use recyclerview
ignoreChecking = false ignoreChecking = false
binding.textViewFilter.setTextAndVisible(item.sortOrder?.titleRes ?: 0)
} }
} }

@ -5,8 +5,8 @@ import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
@ -82,7 +82,7 @@ open class MangaListAdapter(
} }
} }
is ListHeader2 -> Unit is FilterHeaderModel -> Unit
else -> super.getChangePayload(oldItem, newItem) else -> super.getChangePayload(oldItem, newItem)
} }
} }

@ -43,6 +43,7 @@ fun Manga.toListDetailedModel(
ChipsView.ChipModel( ChipsView.ChipModel(
tint = tagHighlighter?.getTint(it) ?: 0, tint = tagHighlighter?.getTint(it) ?: 0,
title = it.title, title = it.title,
icon = 0,
isCheckable = false, isCheckable = false,
isChecked = false, isChecked = false,
data = it, data = it,

@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeMutex import org.koitharu.kotatsu.core.util.CompositeMutex
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.input.LocalMangaInput
@ -28,6 +29,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.util.EnumSet
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -37,11 +39,20 @@ private const val MAX_PARALLELISM = 4
class LocalMangaRepository @Inject constructor( class LocalMangaRepository @Inject constructor(
private val storageManager: LocalStorageManager, private val storageManager: LocalStorageManager,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>, @LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
private val settings: AppSettings,
) : MangaRepository { ) : MangaRepository {
override val source = MangaSource.LOCAL override val source = MangaSource.LOCAL
private val locks = CompositeMutex<Long>() private val locks = CompositeMutex<Long>()
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
override var defaultSortOrder: SortOrder
get() = settings.localListOrder
set(value) {
settings.localListOrder = value
}
override suspend fun getList(offset: Int, query: String): List<Manga> { override suspend fun getList(offset: Int, query: String): List<Manga> {
if (offset > 0) { if (offset > 0) {
return emptyList() return emptyList()
@ -137,8 +148,6 @@ class LocalMangaRepository @Inject constructor(
}.firstOrNull()?.getManga() }.firstOrNull()?.getManga()
} }
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
override suspend fun getPageUrl(page: MangaPage) = page.url override suspend fun getPageUrl(page: MangaPage) = page.url
override suspend fun getTags() = emptySet<MangaTag>() override suspend fun getTags() = emptySet<MangaTag>()

@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -16,11 +15,14 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener { class LocalListFragment : MangaListFragment() {
override val viewModel by viewModels<LocalListViewModel>() override val viewModel by viewModels<LocalListViewModel>()
@ -35,11 +37,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
} }
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
super.onFilterClick(view) FilterSheetFragment.show(childFragmentManager)
val menu = PopupMenu(requireContext(), view ?: requireViewBinding().recyclerView)
menu.inflate(R.menu.popup_order)
menu.setOnMenuItemClickListener(this)
menu.show()
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
@ -67,17 +65,6 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
} }
} }
override fun onMenuItemClick(item: MenuItem): Boolean {
val order = when (item.itemId) {
R.id.action_order_new -> SortOrder.NEWEST
R.id.action_order_abs -> SortOrder.ALPHABETICAL
R.id.action_order_rating -> SortOrder.RATING
else -> return false
}
viewModel.setSortOrder(order)
return true
}
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) { private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
MaterialAlertDialogBuilder(context ?: return) MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
@ -96,6 +83,8 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
companion object { companion object {
fun newInstance() = LocalListFragment() fun newInstance() = LocalListFragment().withArgs(1) {
putSerializable(RemoteListFragment.ARG_SOURCE, MangaSource.LOCAL) // required by FilterCoordinator
}
} }
} }

@ -1,123 +1,57 @@
package org.koitharu.kotatsu.local.ui package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.viewModelScope import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LocalListViewModel @Inject constructor( class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository, savedStateHandle: SavedStateHandle,
private val historyRepository: HistoryRepository, mangaRepositoryFactory: MangaRepository.Factory,
private val trackingRepository: TrackingRepository, filter: FilterCoordinator,
private val settings: AppSettings, tagHighlighter: MangaTagHighlighter,
private val tagHighlighter: MangaTagHighlighter, settings: AppSettings,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { listExtraProvider: ListExtraProvider,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) : RemoteListViewModel(
savedStateHandle,
mangaRepositoryFactory,
filter,
tagHighlighter,
settings,
listExtraProvider,
downloadScheduler,
) {
val onMangaRemoved = MutableEventFlow<Unit>() val onMangaRemoved = MutableEventFlow<Unit>()
val sortOrder = MutableStateFlow(settings.localListOrder)
private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
private var refreshJob: Job? = null
override val content = combine(
mangaList,
listMode,
sortOrder,
selectedTags,
listError,
) { list, mode, order, tags, error ->
when {
error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_empty_local,
textPrimary = R.string.text_local_holder_primary,
textSecondary = R.string.text_local_holder_secondary,
actionStringRes = R.string._import,
),
)
else -> buildList(list.size + 1) {
add(createHeader(list, tags, order))
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
}
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init { init {
onRefresh()
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
localStorageChanges localStorageChanges
.collectLatest { .collect {
if (refreshJob?.isActive != true) { loadList(filter.snapshot(), append = false).join()
doRefresh()
}
} }
} }
} }
override fun onUpdateFilter(tags: Set<MangaTag>) {
selectedTags.value = tags
onRefresh()
}
override fun onRefresh() {
val prevJob = refreshJob
refreshJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
doRefresh()
}
}
override fun onRetry() = onRefresh()
fun setSortOrder(value: SortOrder) {
sortOrder.value = value
settings.localListOrder = value
onRefresh()
}
fun delete(ids: Set<Long>) { fun delete(ids: Set<Long>) {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
deleteLocalMangaUseCase(ids) deleteLocalMangaUseCase(ids)
@ -125,60 +59,12 @@ class LocalListViewModel @Inject constructor(
} }
} }
private suspend fun doRefresh() { override fun createEmptyState(canResetFilter: Boolean): EmptyState {
try { return EmptyState(
listError.value = null icon = R.drawable.ic_empty_local,
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value) textPrimary = R.string.text_local_holder_primary,
} catch (e: CancellationException) { textSecondary = R.string.text_local_holder_secondary,
throw e actionStringRes = R.string._import,
} catch (e: Throwable) {
listError.value = e
}
}
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
val tags = HashMap<MangaTag, Int>()
for (item in mangaList) {
for (tag in item.tags) {
tags[tag] = tags[tag]?.plus(1) ?: 1
}
}
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
val chips = LinkedList<ChipsView.ChipModel>()
for ((tag, _) in topTags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
isCheckable = true,
isChecked = tag in selectedTags,
data = tag,
)
if (model.isChecked) {
chips.addFirst(model)
} else {
chips.addLast(model)
}
}
return ListHeader2(
chips = chips,
sortOrder = order,
hasSelectedTags = selectedTags.isNotEmpty(),
) )
} }
override suspend fun getCounter(mangaId: Long): Int {
return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId)
} else {
0
}
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
} }

@ -15,8 +15,8 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.filter.FilterSheetFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
@ -25,7 +25,7 @@ import org.koitharu.kotatsu.settings.SettingsActivity
@AndroidEntryPoint @AndroidEntryPoint
class RemoteListFragment : MangaListFragment() { class RemoteListFragment : MangaListFragment() {
public override val viewModel by viewModels<RemoteListViewModel>() override val viewModel by viewModels<RemoteListViewModel>()
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)

@ -9,7 +9,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -18,23 +17,18 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
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.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.model.FilterState
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator
import org.koitharu.kotatsu.list.ui.filter.FilterItem
import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorFooter
@ -43,50 +37,42 @@ import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L
@HiltViewModel @HiltViewModel
class RemoteListViewModel @Inject constructor( open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
private val searchRepository: MangaSearchRepository, private val filter: FilterCoordinator,
settings: AppSettings,
dataRepository: MangaDataRepository,
private val tagHighlighter: MangaTagHighlighter, private val tagHighlighter: MangaTagHighlighter,
settings: AppSettings,
listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), OnFilterChangedListener { ) : MangaListViewModel(settings, downloadScheduler), FilterOwner by filter {
val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE) val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE)
private val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository private val repository = mangaRepositoryFactory.create(source)
private val filter = FilterCoordinator(repository, dataRepository, viewModelScope)
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
val filterItems: StateFlow<List<ListModel>>
get() = filter.items
override val content = combine( override val content = combine(
mangaList, mangaList,
listMode, listMode,
createHeaderFlow(),
listError, listError,
hasNextPage, hasNextPage,
) { list, mode, header, error, hasNext -> ) { list, mode, error, hasNext ->
buildList(list?.size?.plus(2) ?: 2) { buildList(list?.size?.plus(2) ?: 2) {
add(header)
when { when {
list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true))
list == null -> add(LoadingState) list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(header.hasSelectedTags)) list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags))
else -> { else -> {
list.toUi(this, mode, tagHighlighter) list.toUi(this, mode, listExtraProvider, tagHighlighter)
when { when {
error != null -> add(error.toErrorFooter()) error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter()) hasNext -> add(LoadingFooter())
@ -117,37 +103,23 @@ class RemoteListViewModel @Inject constructor(
loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty()) loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty())
} }
override fun onSortItemClick(item: FilterItem.Sort) {
filter.onSortItemClick(item)
}
override fun onTagItemClick(item: FilterItem.Tag) {
filter.onTagItemClick(item)
}
fun loadNextPage() { fun loadNextPage() {
if (hasNextPage.value && listError.value == null) { if (hasNextPage.value && listError.value == null) {
loadList(filter.snapshot(), append = true) loadList(filter.snapshot(), append = true)
} }
} }
fun filterSearch(query: String) = filter.performSearch(query)
fun resetFilter() = filter.reset() fun resetFilter() = filter.reset()
override fun onUpdateFilter(tags: Set<MangaTag>) { override fun onUpdateFilter(tags: Set<MangaTag>) {
applyFilter(tags) applyFilter(tags)
} }
fun applyFilter(tags: Set<MangaTag>) { protected fun loadList(filterState: FilterState, append: Boolean): Job {
filter.setTags(tags) loadingJob?.let {
} if (it.isActive) return it
private fun loadList(filterState: FilterState, append: Boolean) {
if (loadingJob?.isActive == true) {
return
} }
loadingJob = launchLoadingJob(Dispatchers.Default) { return launchLoadingJob(Dispatchers.Default) {
try { try {
listError.value = null listError.value = null
val list = repository.getList( val list = repository.getList(
@ -170,61 +142,13 @@ class RemoteListViewModel @Inject constructor(
errorEvent.call(e) errorEvent.call(e)
} }
} }
} }.also { loadingJob = it }
} }
private fun createEmptyState(canResetFilter: Boolean) = EmptyState( protected open fun createEmptyState(canResetFilter: Boolean) = EmptyState(
icon = R.drawable.ic_empty_common, icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found, textPrimary = R.string.nothing_found,
textSecondary = 0, textSecondary = 0,
actionStringRes = if (canResetFilter) R.string.reset_filter else 0, actionStringRes = if (canResetFilter) R.string.reset_filter else 0,
) )
private fun createHeaderFlow() = combine(
filter.observeState(),
filter.observeAvailableTags(),
) { state, available ->
val chips = createChipsList(state, available.orEmpty())
ListHeader2(chips, state.sortOrder, state.tags.isNotEmpty())
}
private suspend fun createChipsList(
filterState: FilterState,
availableTags: Set<MangaTag>,
): List<ChipsView.ChipModel> {
val selectedTags = filterState.tags.toMutableSet()
var tags = searchRepository.getTagsSuggestion("", 6, repository.source)
if (tags.isEmpty()) {
tags = availableTags.take(6)
}
if (tags.isEmpty() && selectedTags.isEmpty()) {
return emptyList()
}
val result = LinkedList<ChipsView.ChipModel>()
for (tag in tags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
isCheckable = true,
isChecked = selectedTags.remove(tag),
data = tag,
)
if (model.isChecked) {
result.addFirst(model)
} else {
result.addLast(model)
}
}
for (tag in selectedTags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
isCheckable = true,
isChecked = true,
data = tag,
)
result.addFirst(model)
}
return result
}
} }

@ -3,7 +3,10 @@ package org.koitharu.kotatsu.search.ui
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
@ -11,9 +14,16 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ActivityMangaListBinding
import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.local.ui.LocalListFragment import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -22,15 +32,15 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
@AndroidEntryPoint @AndroidEntryPoint
class MangaListActivity : class MangaListActivity :
BaseActivity<ActivityContainerBinding>(), BaseActivity<ActivityMangaListBinding>(),
AppBarOwner { AppBarOwner, View.OnClickListener {
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = viewBinding.appbar get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater)) setContentView(ActivityMangaListBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source
@ -38,7 +48,28 @@ class MangaListActivity :
finishAfterTransition() finishAfterTransition()
return return
} }
viewBinding.chipSort?.setOnClickListener(this)
title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title
initList(source, tags)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
viewBinding.cardFilter?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = marginStart + insets.bottom
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.chip_sort -> FilterSheetFragment.show(supportFragmentManager)
}
}
private fun initList(source: MangaSource, tags: Set<MangaTag>?) {
val fm = supportFragmentManager val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) { if (fm.findFragmentById(R.id.container) == null) {
fm.commit { fm.commit {
@ -52,24 +83,46 @@ class MangaListActivity :
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) {
runOnCommit(ApplyFilterRunnable(fragment, tags)) runOnCommit(ApplyFilterRunnable(fragment, tags))
} }
runOnCommit { initFilter() }
} }
} else {
initFilter()
} }
} }
override fun onWindowInsetsChanged(insets: Insets) { private fun initFilter() {
viewBinding.root.updatePadding( if (viewBinding.containerFilter != null) {
left = insets.left, if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) {
right = insets.right, supportFragmentManager.commit {
) setReorderingAllowed(true)
replace(R.id.container_filter, FilterSheetFragment::class.java, null)
}
}
} else if (viewBinding.containerFilterHeader != null) {
if (supportFragmentManager.findFragmentById(R.id.container_filter_header) == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container_filter_header, FilterHeaderFragment::class.java, null)
}
}
}
val chipSort = viewBinding.chipSort
if (chipSort != null) {
FilterOwner.from(this).header.observe(this) {
chipSort.setTextAndVisible(it.sortOrder?.titleRes ?: 0)
}
}
} }
private class ApplyFilterRunnable( private class ApplyFilterRunnable(
private val fragment: RemoteListFragment, private val fragment: MangaListFragment,
private val tags: Set<MangaTag>, private val tags: Set<MangaTag>,
) : Runnable { ) : Runnable {
override fun run() { override fun run() {
fragment.viewModel.applyFilter(tags) checkNotNull(FilterOwner.find(fragment)) {
"Cannot find FilterOwner"
}.applyFilter(tags)
} }
} }

@ -135,6 +135,7 @@ class SearchSuggestionViewModel @Inject constructor(
ChipsView.ChipModel( ChipsView.ChipModel(
tint = 0, tint = 0,
title = tag.title, title = tag.title,
icon = 0,
data = tag, data = tag,
isCheckable = false, isCheckable = false,
isChecked = false, isChecked = false,

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M18 21L14 17H17V7H14L18 3L22 7H19V17H22M2 19V17H12V19M2 13V11H9V13M2 7V5H6V7H2Z" />
</vector>

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
tools:title="Title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/card_filter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:layout="@layout/fragment_list" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_filter"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/container"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintWidth_max="400dp"
app:layout_constraintWidth_min="320dp"
app:layout_constraintWidth_percent="0.35">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_filter"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/sheet_filter" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:toolbarId="@id/toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
android:gravity="bottom|end"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/toolbar_button_margin"
app:layout_collapseMode="parallax"
tools:ignore="RtlSymmetry">
<com.google.android.material.chip.Chip
android:id="@+id/chip_sort"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:chipIcon="@drawable/ic_sort"
app:chipIconEnabled="true"
app:closeIcon="@drawable/ic_expand_more"
app:closeIconEnabled="true"
app:layout_collapseMode="pin"
tools:text="@string/popular"
tools:visibility="visible" />
</LinearLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
tools:title="Title" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_filter_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
tools:layout="@layout/fragment_filter_header" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/scrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingHorizontal="12dp"
android:scrollbars="none">
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/margin_small"
app:selectionRequired="false"
app:singleLine="true"
app:singleSelection="false" />
</HorizontalScrollView>

@ -1,48 +0,0 @@
<?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">
<HorizontalScrollView
android:id="@+id/scrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/margin_small"
android:layout_toStartOf="@id/textView_filter"
android:scrollIndicators="start|end"
android:scrollbars="none"
tools:ignore="UnusedAttribute">
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/margin_small"
app:selectionRequired="false"
app:singleLine="true"
app:singleSelection="false" />
</HorizontalScrollView>
<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_expand_more"
app:drawableTint="?android:attr/textColorSecondary"
tools:ignore="RtlSymmetry"
tools:text="@string/popular" />
</RelativeLayout>

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_order_new"
android:title="@string/newest" />
<item
android:id="@+id/action_order_abs"
android:title="@string/by_name" />
<item
android:id="@+id/action_order_rating"
android:title="@string/by_rating" />
</menu>
Loading…
Cancel
Save