Update filter header ui

This commit is contained in:
Koitharu
2023-06-01 10:44:49 +03:00
parent f0a4fa4e95
commit 84f41810c5
36 changed files with 591 additions and 390 deletions

View File

@@ -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.connectivityManager
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.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges
@@ -64,6 +66,9 @@ interface AppModule {
@Binds
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
@Binds
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
companion object {
@Provides

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
package org.koitharu.kotatsu.list.ui.filter
package org.koitharu.kotatsu.filter.ui
import android.content.Context
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
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.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.list.ui.filter
package org.koitharu.kotatsu.filter.ui
import android.widget.TextView
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.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.model.ListModel
fun filterSortDelegate(

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.filter
package org.koitharu.kotatsu.filter.ui
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.Dispatchers
import kotlinx.coroutines.async
@@ -17,7 +19,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
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.ListModel
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.util.SuspendLazy
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 java.text.Collator
import java.util.LinkedList
import java.util.Locale
import java.util.TreeSet
import javax.inject.Inject
class FilterCoordinator(
private val repository: RemoteMangaRepository,
@ViewModelScoped
class FilterCoordinator @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
dataRepository: MangaDataRepository,
private val coroutineScope: CoroutineScope,
) : OnFilterChangedListener {
private val searchRepository: MangaSearchRepository,
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 var searchQuery = MutableStateFlow("")
private val localTags = SuspendLazy {
@@ -43,13 +60,23 @@ class FilterCoordinator(
}
private var availableTagsDeferred = loadTagsAsync()
val items: StateFlow<List<ListModel>> = getItemsFlow()
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
override val filterItems: StateFlow<List<ListModel>> = getItemsFlow()
.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 {
observeState()
}
override fun applyFilter(tags: Set<MangaTag>) {
setTags(tags)
}
override fun onSortItemClick(item: FilterItem.Sort) {
currentState.update { oldValue ->
FilterState(item.order, oldValue.tags)
@@ -95,6 +122,14 @@ class FilterCoordinator(
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(
getTagsAsFlow(),
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
private fun buildFilterList(
allTags: TagsWrapper,

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.filter
package org.koitharu.kotatsu.filter.ui
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.ListModel

View File

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

View File

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

View File

@@ -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.view.LayoutInflater
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.AdaptiveSheetCallback
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.parentFragmentViewModels
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
class FilterSheetFragment :
BaseAdaptiveSheet<SheetFilterBinding>(),
AdaptiveSheetCallback,
AsyncListDiffer.ListListener<ListModel> {
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
private val owner by lazy(LazyThreadSafetyMode.NONE) {
FilterOwner.from(requireActivity())
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false)
@@ -32,14 +31,13 @@ class FilterSheetFragment :
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addSheetCallback(this)
val adapter = FilterAdapter(viewModel, this)
val adapter = FilterAdapter(owner, this)
binding.recyclerView.adapter = adapter
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
}
owner.filterItems.observe(viewLifecycleOwner, adapter::setItems)
override fun onDestroyView() {
super.onDestroyView()
collapsibleActionViewCallback = null
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.recyclerView.scrollIndicators = 0
}
}
override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) {

View File

@@ -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 {
fun onSortItemClick(item: FilterItem.Sort)
fun onTagItemClick(item: FilterItem.Tag)
}
}

View File

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

View File

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

View File

@@ -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.SortOrder
@@ -15,9 +15,7 @@ class FilterState(
other as FilterState
if (sortOrder != other.sortOrder) return false
if (tags != other.tags) return false
return true
return tags == other.tags
}
override fun hashCode(): Int {
@@ -25,4 +23,4 @@ class FilterState(
result = 31 * result + tags.hashCode()
return result
}
}
}

View File

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

View File

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

View File

@@ -5,8 +5,8 @@ import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
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.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
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)
}
}

View File

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

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal
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.ext.deleteAwait
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.util.ext.printStackTraceDebug
import java.io.File
import java.util.EnumSet
import javax.inject.Inject
import javax.inject.Singleton
@@ -37,11 +39,20 @@ private const val MAX_PARALLELISM = 4
class LocalMangaRepository @Inject constructor(
private val storageManager: LocalStorageManager,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
private val settings: AppSettings,
) : MangaRepository {
override val source = MangaSource.LOCAL
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> {
if (offset > 0) {
return emptyList()
@@ -137,8 +148,6 @@ class LocalMangaRepository @Inject constructor(
}.firstOrNull()?.getManga()
}
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
override suspend fun getPageUrl(page: MangaPage) = page.url
override suspend fun getTags() = emptySet<MangaTag>()

View File

@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toFile
import androidx.core.net.toUri
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.ext.addMenuProvider
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.filter.ui.FilterSheetFragment
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>()
@@ -35,11 +37,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
}
override fun onFilterClick(view: View?) {
super.onFilterClick(view)
val menu = PopupMenu(requireContext(), view ?: requireViewBinding().recyclerView)
menu.inflate(R.menu.popup_order)
menu.setOnMenuItemClickListener(this)
menu.show()
FilterSheetFragment.show(childFragmentManager)
}
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) {
MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.delete_manga)
@@ -96,6 +83,8 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
companion object {
fun newInstance() = LocalListFragment()
fun newInstance() = LocalListFragment().withArgs(1) {
putSerializable(RemoteListFragment.ARG_SOURCE, MangaSource.LOCAL) // required by FilterCoordinator
}
}
}

View File

@@ -1,123 +1,57 @@
package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
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.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.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
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.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
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.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.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
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 org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import javax.inject.Inject
@HiltViewModel
class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator,
tagHighlighter: MangaTagHighlighter,
settings: AppSettings,
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 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 {
onRefresh()
launchJob(Dispatchers.Default) {
localStorageChanges
.collectLatest {
if (refreshJob?.isActive != true) {
doRefresh()
}
.collect {
loadList(filter.snapshot(), append = false).join()
}
}
}
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>) {
launchLoadingJob(Dispatchers.Default) {
deleteLocalMangaUseCase(ids)
@@ -125,60 +59,12 @@ class LocalListViewModel @Inject constructor(
}
}
private suspend fun doRefresh() {
try {
listError.value = null
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
} catch (e: CancellationException) {
throw e
} 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 fun createEmptyState(canResetFilter: Boolean): EmptyState {
return 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,
)
}
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
}
}
}

View File

@@ -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.withArgs
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.filter.FilterSheetFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.SearchActivity
@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.settings.SettingsActivity
@AndroidEntryPoint
class RemoteListFragment : MangaListFragment() {
public override val viewModel by viewModels<RemoteListViewModel>()
override val viewModel by viewModels<RemoteListViewModel>()
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)

View File

@@ -9,7 +9,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
@@ -18,23 +17,18 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
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.MangaTagHighlighter
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
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.require
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.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.ListHeader2
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.LinkedList
import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L
@HiltViewModel
class RemoteListViewModel @Inject constructor(
open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val searchRepository: MangaSearchRepository,
settings: AppSettings,
dataRepository: MangaDataRepository,
private val filter: FilterCoordinator,
private val tagHighlighter: MangaTagHighlighter,
settings: AppSettings,
listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), OnFilterChangedListener {
) : MangaListViewModel(settings, downloadScheduler), FilterOwner by filter {
val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE)
private val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
private val filter = FilterCoordinator(repository, dataRepository, viewModelScope)
private val repository = mangaRepositoryFactory.create(source)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val hasNextPage = MutableStateFlow(false)
private val listError = MutableStateFlow<Throwable?>(null)
private var loadingJob: Job? = null
val filterItems: StateFlow<List<ListModel>>
get() = filter.items
override val content = combine(
mangaList,
listMode,
createHeaderFlow(),
listError,
hasNextPage,
) { list, mode, header, error, hasNext ->
) { list, mode, error, hasNext ->
buildList(list?.size?.plus(2) ?: 2) {
add(header)
when {
list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true))
list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(header.hasSelectedTags))
list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags))
else -> {
list.toUi(this, mode, tagHighlighter)
list.toUi(this, mode, listExtraProvider, tagHighlighter)
when {
error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter())
@@ -117,37 +103,23 @@ class RemoteListViewModel @Inject constructor(
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() {
if (hasNextPage.value && listError.value == null) {
loadList(filter.snapshot(), append = true)
}
}
fun filterSearch(query: String) = filter.performSearch(query)
fun resetFilter() = filter.reset()
override fun onUpdateFilter(tags: Set<MangaTag>) {
applyFilter(tags)
}
fun applyFilter(tags: Set<MangaTag>) {
filter.setTags(tags)
}
private fun loadList(filterState: FilterState, append: Boolean) {
if (loadingJob?.isActive == true) {
return
protected fun loadList(filterState: FilterState, append: Boolean): Job {
loadingJob?.let {
if (it.isActive) return it
}
loadingJob = launchLoadingJob(Dispatchers.Default) {
return launchLoadingJob(Dispatchers.Default) {
try {
listError.value = null
val list = repository.getList(
@@ -170,61 +142,13 @@ class RemoteListViewModel @Inject constructor(
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,
textPrimary = R.string.nothing_found,
textSecondary = 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
}
}

View File

@@ -3,7 +3,10 @@ package org.koitharu.kotatsu.search.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
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.core.model.parcelable.ParcelableMangaTags
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.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.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -22,15 +32,15 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
@AndroidEntryPoint
class MangaListActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner {
BaseActivity<ActivityMangaListBinding>(),
AppBarOwner, View.OnClickListener {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source
@@ -38,7 +48,28 @@ class MangaListActivity :
finishAfterTransition()
return
}
viewBinding.chipSort?.setOnClickListener(this)
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
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
@@ -52,24 +83,46 @@ class MangaListActivity :
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) {
runOnCommit(ApplyFilterRunnable(fragment, tags))
}
runOnCommit { initFilter() }
}
} else {
initFilter()
}
}
private fun initFilter() {
if (viewBinding.containerFilter != null) {
if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) {
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)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
}
private class ApplyFilterRunnable(
private val fragment: RemoteListFragment,
private val fragment: MangaListFragment,
private val tags: Set<MangaTag>,
) : Runnable {
override fun run() {
fragment.viewModel.applyFilter(tags)
checkNotNull(FilterOwner.find(fragment)) {
"Cannot find FilterOwner"
}.applyFilter(tags)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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