From f0a4fa4e950d80ea2d99aaf132f1ffee684d402b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 31 May 2023 15:17:24 +0300 Subject: [PATCH] Update bottom sheets --- .../core/ui/list/fastscroll/FastScroller.kt | 10 +- .../core/ui/sheet/AdaptiveSheetHeaderBar.kt | 37 +++----- .../core/ui/sheet/BaseAdaptiveSheet.kt | 18 +++- .../koitharu/kotatsu/core/util/ext/Theme.kt | 17 ++++ .../kotatsu/list/ui/adapter/ListHeaderAD.kt | 10 ++ .../kotatsu/list/ui/filter/FilterAdapter.kt | 42 +++++++-- .../list/ui/filter/FilterAdapterDelegates.kt | 42 +++------ .../list/ui/filter/FilterBottomSheet.kt | 91 ------------------- .../list/ui/filter/FilterCoordinator.kt | 18 ++-- .../list/ui/filter/FilterDiffCallback.kt | 40 ++++---- .../kotatsu/list/ui/filter/FilterItem.kt | 62 +++++++++++-- .../list/ui/filter/FilterSheetFragment.kt | 61 +++++++++++++ .../ui/thumbnails/PagesThumbnailsSheet.kt | 1 + .../remotelist/ui/RemoteListFragment.kt | 4 +- .../remotelist/ui/RemoteListViewModel.kt | 3 +- .../ui/config/ScrobblerConfigActivity.kt | 6 +- .../ui/selector/ScrobblingSelectorSheet.kt | 23 ++--- .../res/layout/item_checkable_multiple.xml | 14 +++ .../main/res/layout/item_checkable_single.xml | 14 +++ .../main/res/layout/item_header_single.xml | 21 +++++ .../layout/layout_sheet_header_adaptive.xml | 8 +- app/src/main/res/layout/sheet_filter.xml | 26 ++++-- app/src/main/res/layout/sheet_scrobbling.xml | 1 + .../res/layout/sheet_scrobbling_selector.xml | 57 +++++++----- app/src/main/res/menu/opt_filter.xml | 13 --- app/src/main/res/values/attrs.xml | 1 + 26 files changed, 377 insertions(+), 263 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterSheetFragment.kt create mode 100644 app/src/main/res/layout/item_checkable_multiple.xml create mode 100644 app/src/main/res/layout/item_checkable_single.xml create mode 100644 app/src/main/res/layout/item_header_single.xml delete mode 100644 app/src/main/res/menu/opt_filter.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt index d7eca512d..63480c8d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt @@ -56,6 +56,7 @@ class FastScroller @JvmOverloads constructor( private var bubbleHeight = 0 private var handleHeight = 0 private var viewHeight = 0 + private var offset = 0 private var hideScrollbar = true private var showBubble = true private var showBubbleAlways = false @@ -137,6 +138,7 @@ class FastScroller @JvmOverloads constructor( bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL) val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize) binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + offset = getDimensionPixelOffset(R.styleable.FastScroller_scrollerOffset, offset) } setTrackColor(trackColor) @@ -248,7 +250,7 @@ class FastScroller @JvmOverloads constructor( layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply { height = 0 - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } } @@ -256,13 +258,13 @@ class FastScroller @JvmOverloads constructor( height = LayoutParams.MATCH_PARENT anchorGravity = GravityCompat.END anchorId = recyclerViewId - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { height = LayoutParams.MATCH_PARENT gravity = GravityCompat.END - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply { @@ -270,7 +272,7 @@ class FastScroller @JvmOverloads constructor( addRule(RelativeLayout.ALIGN_TOP, recyclerViewId) addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId) addRule(RelativeLayout.ALIGN_END, recyclerViewId) - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt index 26c6bfc9b..0e062a844 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt @@ -4,14 +4,11 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View -import android.view.WindowInsets import android.widget.LinearLayout import androidx.annotation.AttrRes import androidx.annotation.StringRes import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.withStyledAttributes -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isGone import androidx.core.view.isVisible import org.koitharu.kotatsu.R @@ -28,17 +25,17 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor( private var sheetBehavior: AdaptiveSheetBehavior? = null var title: CharSequence? - get() = binding.textViewTitle.text + get() = binding.shTextViewTitle.text set(value) { - binding.textViewTitle.text = value + binding.shTextViewTitle.text = value } - val isExpanded: Boolean - get() = binding.dragHandle.isGone + val isTitleVisible: Boolean + get() = binding.shLayoutSidesheet.isVisible init { orientation = VERTICAL - binding.buttonClose.setOnClickListener { dismissSheet() } + binding.shButtonClose.setOnClickListener { dismissSheet() } context.withStyledAttributes( attrs, R.styleable.AdaptiveSheetHeaderBar, defStyleAttr, @@ -49,8 +46,13 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - dispatchInsets(ViewCompat.getRootWindowInsets(this)) - setBottomSheetBehavior(findParentSheetBehavior()) + if (isInEditMode) { + val isTabled = resources.getBoolean(R.bool.is_tablet) + binding.shDragHandle.isGone = isTabled + binding.shLayoutSidesheet.isVisible = isTabled + } else { + setBottomSheetBehavior(findParentSheetBehavior()) + } } override fun onDetachedFromWindow() { @@ -58,26 +60,17 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor( super.onDetachedFromWindow() } - override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets { - dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null) - return super.onApplyWindowInsets(insets) - } - override fun onStateChanged(sheet: View, newState: Int) { } fun setTitle(@StringRes resId: Int) { - binding.textViewTitle.setText(resId) - } - - private fun dispatchInsets(insets: WindowInsetsCompat?) { - + binding.shTextViewTitle.setText(resId) } private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) { - binding.dragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom - binding.layoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side + binding.shDragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom + binding.shLayoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side sheetBehavior?.removeCallback(this) sheetBehavior = behavior behavior?.addCallback(this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt index 43e02e81d..ba6b5be97 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -20,6 +20,7 @@ import com.google.android.material.R as materialR abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { private var waitingForDismissAllowingStateLoss = false + private var isFitToContentsDisabled = false var viewBinding: B? = null private set @@ -87,15 +88,28 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { b.state = BottomSheetBehavior.STATE_EXPANDED } if (b is AdaptiveSheetBehavior.Bottom) { - b.isFitToContents = !isExpanded + b.isFitToContents = !isFitToContentsDisabled && !isExpanded val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) rootView?.updateLayoutParams { - height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT + height = if (isFitToContentsDisabled || isExpanded) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } } } b.isDraggable = !isLocked } + protected fun disableFitToContents() { + isFitToContentsDisabled = true + val b = behavior as? AdaptiveSheetBehavior.Bottom ?: return + b.isFitToContents = false + dialog?.findViewById(materialR.id.design_bottom_sheet)?.updateLayoutParams { + height = LayoutParams.MATCH_PARENT + } + } + fun requireViewBinding(): B = checkNotNull(viewBinding) { "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt index 102b9bdb1..dae151af2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt @@ -5,6 +5,7 @@ import android.graphics.Color import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import androidx.annotation.Px import androidx.core.content.res.use import androidx.core.graphics.ColorUtils @@ -22,6 +23,22 @@ fun Context.getThemeColor( it.getColor(0, fallback) } +@Px +fun Context.getThemeDimensionPixelSize( + @AttrRes resId: Int, + @ColorInt fallback: Int = 0, +) = obtainStyledAttributes(intArrayOf(resId)).use { + it.getDimensionPixelSize(0, fallback) +} + +@Px +fun Context.getThemeDimensionPixelOffset( + @AttrRes resId: Int, + @ColorInt fallback: Int = 0, +) = obtainStyledAttributes(intArrayOf(resId)).use { + it.getDimensionPixelOffset(0, fallback) +} + @ColorInt fun Context.getThemeColor( @AttrRes resId: Int, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index 51a58e2e0..8076dc68b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding +import org.koitharu.kotatsu.databinding.ItemHeaderSingleBinding import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel @@ -22,3 +23,12 @@ fun listHeaderAD( binding.buttonMore.setTextAndVisible(item.buttonTextRes) } } + +fun listSimpleHeaderAD() = adapterDelegateViewBinding( + { inflater, parent -> ItemHeaderSingleBinding.inflate(inflater, parent, false) }, +) { + + bind { + binding.textViewTitle.text = item.getText(context) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index 6b1fe569f..0be19df8c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -1,21 +1,45 @@ package org.koitharu.kotatsu.list.ui.filter +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.list.ui.adapter.listSimpleHeaderAD +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD +import org.koitharu.kotatsu.list.ui.model.ListModel class FilterAdapter( listener: OnFilterChangedListener, - listListener: ListListener, -) : AsyncListDifferDelegationAdapter( - FilterDiffCallback(), - filterSortDelegate(listener), - filterTagDelegate(listener), - filterHeaderDelegate(), - filterLoadingDelegate(), - filterErrorDelegate(), -) { + listListener: ListListener, +) : AsyncListDifferDelegationAdapter(FilterDiffCallback()), FastScroller.SectionIndexer { init { + delegatesManager + .addDelegate(filterSortDelegate(listener)) + .addDelegate(filterTagDelegate(listener)) + .addDelegate(listSimpleHeaderAD()) + .addDelegate(loadingStateAD()) + .addDelegate(loadingFooterAD()) + .addDelegate(filterErrorDelegate()) differ.addListListener(listListener) } + + override fun getSectionText(context: Context, position: Int): CharSequence? { + val list = items + for (i in (0..position).reversed()) { + val item = list.getOrNull(i) ?: continue + if (item is FilterItem.Tag) { + return item.tag.title.firstOrNull()?.toString() + } + } + return null + } + + companion object { + + const val ITEM_TYPE_HEADER = 0 + const val ITEM_TYPE_SORT = 1 + const val ITEM_TYPE_TAG = 2 + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index 5dcb3a21a..3a6d26892 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -1,64 +1,48 @@ package org.koitharu.kotatsu.list.ui.filter import android.widget.TextView -import androidx.core.view.isVisible import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.model.titleRes -import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding -import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding +import org.koitharu.kotatsu.core.util.ext.setChecked +import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding +import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding +import org.koitharu.kotatsu.list.ui.model.ListModel fun filterSortDelegate( listener: OnFilterChangedListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) }, ) { itemView.setOnClickListener { listener.onSortItemClick(item) } - bind { + bind { payloads -> binding.root.setText(item.order.titleRes) - binding.root.isChecked = item.isSelected + binding.root.setChecked(item.isSelected, payloads.isNotEmpty()) } } fun filterTagDelegate( listener: OnFilterChangedListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, ) { itemView.setOnClickListener { listener.onTagItemClick(item) } - bind { + bind { payloads -> binding.root.text = item.tag.title - binding.root.isChecked = item.isChecked + binding.root.setChecked(item.isChecked, payloads.isNotEmpty()) } } -fun filterHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }, -) { - - bind { - binding.textViewTitle.setText(item.titleResId) - binding.badge.isVisible = if (item.counter == 0) { - false - } else { - binding.badge.text = item.counter.toString() - true - } - } -} - -fun filterLoadingDelegate() = adapterDelegate(R.layout.item_loading_footer) {} - -fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { +fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { bind { (itemView as TextView).setText(item.textResId) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt deleted file mode 100644 index a00341a55..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.ViewGroup -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.LinearLayoutManager -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.BaseBottomSheet -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.remotelist.ui.RemoteListViewModel - -class FilterBottomSheet : - BaseBottomSheet(), - MenuItem.OnActionExpandListener, - SearchView.OnQueryTextListener, - AsyncListDiffer.ListListener { - - private val viewModel by parentFragmentViewModels() - private var collapsibleActionViewCallback: CollapseActionViewCallback? = null - - override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { - return SheetFilterBinding.inflate(inflater, container, false) - } - - override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - val adapter = FilterAdapter(viewModel, this) - binding.recyclerView.adapter = adapter - viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) - initOptionsMenu() - } - - override fun onDestroyView() { - super.onDestroyView() - collapsibleActionViewCallback = null - } - - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - setExpanded(isExpanded = true, isLocked = true) - collapsibleActionViewCallback?.onMenuItemActionExpand(item) - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - val searchView = (item.actionView as? SearchView) ?: return false - searchView.setQuery("", false) - searchView.post { setExpanded(isExpanded = false, isLocked = false) } - collapsibleActionViewCallback?.onMenuItemActionCollapse(item) - return true - } - - override fun onQueryTextSubmit(query: String?): Boolean = false - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.filterSearch(newText?.trim().orEmpty()) - return true - } - - override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { - if (currentList.size > previousList.size && view != null) { - (requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) - } - } - - private fun initOptionsMenu() { - requireViewBinding().headerBar.inflateMenu(R.menu.opt_filter) - val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also { - onBackPressedDispatcher.addCallback(it) - } - } - - companion object { - - private const val TAG = "FilterBottomSheet" - - fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index 0ff57d7a3..fa3db4b0b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -18,6 +18,10 @@ 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.list.ui.model.ListHeader +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.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -39,8 +43,8 @@ class FilterCoordinator( } private var availableTagsDeferred = loadTagsAsync() - val items: StateFlow> = getItemsFlow() - .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading)) + val items: StateFlow> = getItemsFlow() + .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { observeState() @@ -115,19 +119,19 @@ class FilterCoordinator( allTags: TagsWrapper, state: FilterState, query: String, - ): List { + ): List { val sortOrders = repository.sortOrders.sortedBy { it.ordinal } val tags = mergeTags(state.tags, allTags.tags).toList() - val list = ArrayList(tags.size + sortOrders.size + 3) + val list = ArrayList(tags.size + sortOrders.size + 3) if (query.isEmpty()) { if (sortOrders.isNotEmpty()) { - list.add(FilterItem.Header(R.string.sort_order, 0)) + list.add(ListHeader(R.string.sort_order, 0, null)) sortOrders.mapTo(list) { FilterItem.Sort(it, isSelected = it == state.sortOrder) } } if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) { - list.add(FilterItem.Header(R.string.genres, state.tags.size)) + list.add(ListHeader(R.string.genres, 0, null)) tags.mapTo(list) { FilterItem.Tag(it, isChecked = it in state.tags) } @@ -135,7 +139,7 @@ class FilterCoordinator( if (allTags.isError) { list.add(FilterItem.Error(R.string.filter_load_error)) } else if (allTags.isLoading) { - list.add(FilterItem.Loading) + list.add(LoadingFooter()) } } else { tags.mapNotNullTo(list) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt index 4549c46cf..72004fdc8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt @@ -1,59 +1,51 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.recyclerview.widget.DiffUtil +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel -class FilterDiffCallback : DiffUtil.ItemCallback() { +class FilterDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { return when { oldItem === newItem -> true oldItem.javaClass != newItem.javaClass -> false - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.titleResId == newItem.titleResId + oldItem is ListHeader && newItem is ListHeader -> { + oldItem == newItem } + oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.tag == newItem.tag } + oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { oldItem.order == newItem.order } + oldItem is FilterItem.Error && newItem is FilterItem.Error -> { oldItem.textResId == newItem.textResId } + else -> false } } - override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { - return when { - oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.counter == newItem.counter - } - oldItem is FilterItem.Error && newItem is FilterItem.Error -> true - oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { - oldItem.isChecked == newItem.isChecked - } - oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { - oldItem.isSelected == newItem.isSelected - } - else -> false - } + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return oldItem == newItem } - override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? { + override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { val hasPayload = when { oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { oldItem.isChecked != newItem.isChecked } + oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { oldItem.isSelected != newItem.isSelected } - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.counter != newItem.counter - } + else -> false } return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt index bbef939cb..3ca5825b9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt @@ -1,29 +1,71 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.annotation.StringRes +import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder -sealed interface FilterItem { - - class Header( - @StringRes val titleResId: Int, - val counter: Int, - ) : FilterItem +sealed interface FilterItem : ListModel { class Sort( val order: SortOrder, val isSelected: Boolean, - ) : FilterItem + ) : FilterItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sort + + if (order != other.order) return false + return isSelected == other.isSelected + } + + override fun hashCode(): Int { + var result = order.hashCode() + result = 31 * result + isSelected.hashCode() + return result + } + } class Tag( val tag: MangaTag, val isChecked: Boolean, - ) : FilterItem + ) : FilterItem { - object Loading : FilterItem + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Tag + + if (tag != other.tag) return false + return isChecked == other.isChecked + } + + override fun hashCode(): Int { + var result = tag.hashCode() + result = 31 * result + isChecked.hashCode() + return result + } + } class Error( @StringRes val textResId: Int, - ) : FilterItem + ) : FilterItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Error + + return textResId == other.textResId + } + + override fun hashCode(): Int { + return textResId + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterSheetFragment.kt new file mode 100644 index 000000000..aa3240c27 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterSheetFragment.kt @@ -0,0 +1,61 @@ +package org.koitharu.kotatsu.list.ui.filter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.AsyncListDiffer +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(), + AdaptiveSheetCallback, + AsyncListDiffer.ListListener { + + private val viewModel by parentFragmentViewModels() + private var collapsibleActionViewCallback: CollapseActionViewCallback? = null + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { + return SheetFilterBinding.inflate(inflater, container, false) + } + + override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + addSheetCallback(this) + val adapter = FilterAdapter(viewModel, this) + binding.recyclerView.adapter = adapter + viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) + } + + override fun onDestroyView() { + super.onDestroyView() + collapsibleActionViewCallback = null + } + + override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { + if (currentList.size > previousList.size && view != null) { + (requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) + } + } + + override fun onStateChanged(sheet: View, newState: Int) { + viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED + } + + companion object { + + private const val TAG = "FilterBottomSheet" + + fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index c6ac60cd5..4f49f9bad 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -63,6 +63,7 @@ class PagesThumbnailsSheet : override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) + addSheetCallback(this) spanResolver = MangaListSpanResolver(binding.root.resources) thumbnailsAdapter = PageThumbnailAdapter( coil = coil, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index c441f6974..f02a86c7c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -16,7 +16,7 @@ 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.list.ui.MangaListFragment -import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet +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 @@ -42,7 +42,7 @@ class RemoteListFragment : MangaListFragment() { } override fun onFilterClick(view: View?) { - FilterBottomSheet.show(childFragmentManager) + FilterSheetFragment.show(childFragmentManager) } override fun onEmptyActionClick() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index ee0890f4f..3379c7463 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -34,6 +34,7 @@ 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 @@ -68,7 +69,7 @@ class RemoteListViewModel @Inject constructor( private val listError = MutableStateFlow(null) private var loadingJob: Job? = null - val filterItems: StateFlow> + val filterItems: StateFlow> get() = filter.items override val content = combine( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt index ac1784635..0afdf0dcb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.View import androidx.activity.viewModels import androidx.core.graphics.Insets -import androidx.core.view.isVisible import androidx.core.view.updatePadding import coil.ImageLoader import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -29,6 +28,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.ui.config.adapter.ScrobblingMangaAdapter import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import javax.inject.Inject +import com.google.android.material.R as materialR @AndroidEntryPoint class ScrobblerConfigActivity : BaseActivity(), @@ -115,11 +115,11 @@ class ScrobblerConfigActivity : BaseActivity(), private fun onUserChanged(user: ScrobblerUser?) { if (user == null) { viewBinding.imageViewAvatar.disposeImageRequest() - viewBinding.imageViewAvatar.isVisible = false + viewBinding.imageViewAvatar.setImageResource(materialR.drawable.abc_ic_menu_overflow_material) return } - viewBinding.imageViewAvatar.isVisible = true viewBinding.imageViewAvatar.newImageRequest(this, user.avatar) + ?.placeholder(R.drawable.bg_badge_empty) ?.enqueueWith(coil) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt index f01a9fed4..ba4a89330 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.widget.SearchView -import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import coil.ImageLoader @@ -58,6 +57,7 @@ class ScrobblingSelectorSheet : override fun onViewBindingCreated(binding: SheetScrobblingSelectorBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) + disableFitToContents() val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this, this) val decoration = ScrobblerMangaSelectionDecoration(binding.root.context) with(binding.recyclerView) { @@ -84,9 +84,7 @@ class ScrobblingSelectorSheet : tab.select() } } - viewModel.searchQuery.observe(viewLifecycleOwner) { - binding.headerBar.subtitle = it - } + viewModel.searchQuery.observe(viewLifecycleOwner, ::onSearchQueryChanged) } override fun onDestroyView() { @@ -135,7 +133,7 @@ class ScrobblingSelectorSheet : return false } viewModel.search(query) - requireViewBinding().headerBar.menu.findItem(R.id.action_search)?.collapseActionView() + requireViewBinding().toolbar.menu.findItem(R.id.action_search)?.collapseActionView() return true } @@ -155,10 +153,14 @@ class ScrobblingSelectorSheet : } private fun openSearch() { - val menuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search) ?: return + val menuItem = requireViewBinding().toolbar.menu.findItem(R.id.action_search) ?: return menuItem.expandActionView() } + private fun onSearchQueryChanged(query: String?) { + + } + private fun onError(e: Throwable) { Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() if (viewModel.isEmpty) { @@ -167,8 +169,8 @@ class ScrobblingSelectorSheet : } private fun initOptionsMenu() { - requireViewBinding().headerBar.inflateMenu(R.menu.opt_shiki_selector) - val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search) + requireViewBinding().toolbar.inflateMenu(R.menu.opt_shiki_selector) + val searchMenuItem = requireViewBinding().toolbar.menu.findItem(R.id.action_search) searchMenuItem.setOnActionExpandListener(this) val searchView = searchMenuItem.actionView as SearchView searchView.setOnQueryTextListener(this) @@ -182,10 +184,6 @@ class ScrobblingSelectorSheet : private fun initTabs() { val entries = viewModel.availableScrobblers val tabs = requireViewBinding().tabs - if (entries.size <= 1) { - tabs.isVisible = false - return - } val selectedId = arguments?.getInt(ARG_SCROBBLER, -1) ?: -1 tabs.removeAllTabs() tabs.clearOnTabSelectedListeners() @@ -200,7 +198,6 @@ class ScrobblingSelectorSheet : tab.select() } } - tabs.isVisible = true } companion object { diff --git a/app/src/main/res/layout/item_checkable_multiple.xml b/app/src/main/res/layout/item_checkable_multiple.xml new file mode 100644 index 000000000..0c00efe6f --- /dev/null +++ b/app/src/main/res/layout/item_checkable_multiple.xml @@ -0,0 +1,14 @@ + + diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml new file mode 100644 index 000000000..8a847b23f --- /dev/null +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -0,0 +1,14 @@ + + diff --git a/app/src/main/res/layout/item_header_single.xml b/app/src/main/res/layout/item_header_single.xml new file mode 100644 index 000000000..13b0a59f3 --- /dev/null +++ b/app/src/main/res/layout/item_header_single.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/layout/layout_sheet_header_adaptive.xml b/app/src/main/res/layout/layout_sheet_header_adaptive.xml index 27acda644..0f5bead1c 100644 --- a/app/src/main/res/layout/layout_sheet_header_adaptive.xml +++ b/app/src/main/res/layout/layout_sheet_header_adaptive.xml @@ -9,14 +9,14 @@ tools:parentTag="android.widget.LinearLayout"> - - + android:layout_height="match_parent"> + + + diff --git a/app/src/main/res/layout/sheet_scrobbling.xml b/app/src/main/res/layout/sheet_scrobbling.xml index d8a223c70..9728fdfdf 100644 --- a/app/src/main/res/layout/sheet_scrobbling.xml +++ b/app/src/main/res/layout/sheet_scrobbling.xml @@ -69,6 +69,7 @@ style="?android:attr/actionOverflowButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="-6dp" android:layout_marginEnd="8dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/open_in_browser" diff --git a/app/src/main/res/layout/sheet_scrobbling_selector.xml b/app/src/main/res/layout/sheet_scrobbling_selector.xml index 14d166361..c6327ef49 100644 --- a/app/src/main/res/layout/sheet_scrobbling_selector.xml +++ b/app/src/main/res/layout/sheet_scrobbling_selector.xml @@ -4,38 +4,53 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:orientation="vertical"> - + app:title="@string/tracking" /> -