diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt index 9c81104a1..0cf8dfc1e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt @@ -18,6 +18,10 @@ import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.core.util.ext.getDisplaySize import com.google.android.material.R as materialR +@Deprecated( + "Use BaseAdaptiveSheet", + replaceWith = ReplaceWith("BaseAdaptiveSheet", "org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet"), +) abstract class BaseBottomSheet : BottomSheetDialogFragment() { var viewBinding: B? = null diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt new file mode 100644 index 000000000..bedd65148 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt @@ -0,0 +1,123 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.app.Dialog +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.sidesheet.SideSheetBehavior +import com.google.android.material.sidesheet.SideSheetCallback +import com.google.android.material.sidesheet.SideSheetDialog +import java.util.LinkedList + +sealed class AdaptiveSheetBehavior { + + @JvmField + protected val callbacks = LinkedList() + + abstract var state: Int + + abstract var isDraggable: Boolean + + open val isHideable: Boolean = true + + fun addCallback(callback: AdaptiveSheetCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: AdaptiveSheetCallback) { + callbacks.remove(callback) + } + + class Bottom( + private val delegate: BottomSheetBehavior<*>, + ) : AdaptiveSheetBehavior() { + + override var state: Int + get() = delegate.state + set(value) { + delegate.state = value + } + + override var isDraggable: Boolean + get() = delegate.isDraggable + set(value) { + delegate.isDraggable = value + } + + override val isHideable: Boolean + get() = delegate.isHideable + + var isFitToContents: Boolean + get() = delegate.isFitToContents + set(value) { + delegate.isFitToContents = value + } + + init { + delegate.addBottomSheetCallback( + object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + callbacks.forEach { it.onStateChanged(bottomSheet, newState) } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + callbacks.forEach { it.onSlide(bottomSheet, slideOffset) } + } + }, + ) + } + } + + class Side( + private val delegate: SideSheetBehavior<*>, + ) : AdaptiveSheetBehavior() { + + override var state: Int + get() = delegate.state + set(value) { + delegate.state = value + } + + override var isDraggable: Boolean + get() = delegate.isDraggable + set(value) { + delegate.isDraggable = value + } + + init { + delegate.addCallback( + object : SideSheetCallback() { + override fun onStateChanged(sheet: View, newState: Int) { + callbacks.forEach { it.onStateChanged(sheet, newState) } + } + + override fun onSlide(sheet: View, slideOffset: Float) { + callbacks.forEach { it.onSlide(sheet, slideOffset) } + } + }, + ) + } + } + + companion object { + + const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED + const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING + const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING + const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN + + fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) { + is BottomSheetDialog -> Bottom(dialog.behavior) + is SideSheetDialog -> Side(dialog.behavior) + else -> null + } + + fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) { + is BottomSheetBehavior<*> -> Bottom(behavior) + is SideSheetBehavior<*> -> Side(behavior) + else -> null + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt new file mode 100644 index 000000000..9abaebbc1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.view.View + +interface AdaptiveSheetCallback { + + /** + * Called when the sheet changes its state. + * + * @param sheet The sheet view. + * @param newState The new state. + */ + fun onStateChanged(sheet: View, newState: Int) + + /** + * Called when the sheet is being dragged. + * + * @param sheet The sheet view. + * @param slideOffset The new offset of this sheet. + */ + fun onSlide(sheet: View, slideOffset: Float) = Unit +} 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 new file mode 100644 index 000000000..26c6bfc9b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt @@ -0,0 +1,101 @@ +package org.koitharu.kotatsu.core.ui.sheet + +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 +import org.koitharu.kotatsu.core.util.ext.parents +import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding + +class AdaptiveSheetHeaderBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback { + + private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this) + private var sheetBehavior: AdaptiveSheetBehavior? = null + + var title: CharSequence? + get() = binding.textViewTitle.text + set(value) { + binding.textViewTitle.text = value + } + + val isExpanded: Boolean + get() = binding.dragHandle.isGone + + init { + orientation = VERTICAL + binding.buttonClose.setOnClickListener { dismissSheet() } + context.withStyledAttributes( + attrs, + R.styleable.AdaptiveSheetHeaderBar, defStyleAttr, + ) { + title = getText(R.styleable.AdaptiveSheetHeaderBar_title) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + dispatchInsets(ViewCompat.getRootWindowInsets(this)) + setBottomSheetBehavior(findParentSheetBehavior()) + } + + override fun onDetachedFromWindow() { + setBottomSheetBehavior(null) + 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?) { + + } + + private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) { + binding.dragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom + binding.layoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side + sheetBehavior?.removeCallback(this) + sheetBehavior = behavior + behavior?.addCallback(this) + } + + private fun dismissSheet() { + sheetBehavior?.state = AdaptiveSheetBehavior.STATE_HIDDEN + } + + private fun findParentSheetBehavior(): AdaptiveSheetBehavior? { + for (p in parents) { + val layoutParams = (p as? View)?.layoutParams + if (layoutParams is CoordinatorLayout.LayoutParams) { + AdaptiveSheetBehavior.from(layoutParams)?.let { + return it + } + } + } + return null + } +} 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 new file mode 100644 index 000000000..43e02e81d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -0,0 +1,161 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.activity.ComponentDialog +import androidx.activity.OnBackPressedDispatcher +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.view.updateLayoutParams +import androidx.viewbinding.ViewBinding +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.sidesheet.SideSheetDialog +import org.koitharu.kotatsu.R +import com.google.android.material.R as materialR + +abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { + + private var waitingForDismissAllowingStateLoss = false + + var viewBinding: B? = null + private set + + @Deprecated("", ReplaceWith("requireViewBinding()")) + protected val binding: B + get() = requireViewBinding() + + protected val behavior: AdaptiveSheetBehavior? + get() = AdaptiveSheetBehavior.from(dialog) + + val isExpanded: Boolean + get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED + + val onBackPressedDispatcher: OnBackPressedDispatcher + get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher + + final override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val binding = onCreateViewBinding(inflater, container) + viewBinding = binding + return binding.root + } + + final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val binding = requireViewBinding() + onViewBindingCreated(binding, savedInstanceState) + } + + override fun onDestroyView() { + viewBinding = null + super.onDestroyView() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + return if (context.resources.getBoolean(R.bool.is_tablet)) { + SideSheetDialog(context, theme) + } else { + BottomSheetDialog(context, theme) + } + } + + fun addSheetCallback(callback: AdaptiveSheetCallback) { + val b = behavior ?: return + b.addCallback(callback) + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) + ?: dialog?.findViewById(materialR.id.coordinator) + if (rootView != null) { + callback.onStateChanged(rootView, b.state) + } + } + + protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B + + protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit + + protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { + val b = behavior ?: return + if (isExpanded) { + b.state = BottomSheetBehavior.STATE_EXPANDED + } + if (b is AdaptiveSheetBehavior.Bottom) { + b.isFitToContents = !isExpanded + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) + rootView?.updateLayoutParams { + height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT + } + } + b.isDraggable = !isLocked + } + + fun requireViewBinding(): B = checkNotNull(viewBinding) { + "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." + } + + override fun dismiss() { + if (!tryDismissWithAnimation(false)) { + super.dismiss() + } + } + + override fun dismissAllowingStateLoss() { + if (!tryDismissWithAnimation(true)) { + super.dismissAllowingStateLoss() + } + } + + /** + * Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible, + * false otherwise. + */ + private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean { + val shouldDismissWithAnimation = when (val dialog = dialog) { + is BottomSheetDialog -> dialog.dismissWithAnimation + is SideSheetDialog -> dialog.isDismissWithSheetAnimationEnabled + else -> false + } + val behavior = behavior ?: return false + return if (shouldDismissWithAnimation && behavior.isHideable) { + dismissWithAnimation(behavior, allowingStateLoss) + true + } else { + false + } + } + + private fun dismissWithAnimation(behavior: AdaptiveSheetBehavior, allowingStateLoss: Boolean) { + waitingForDismissAllowingStateLoss = allowingStateLoss + if (behavior.state == AdaptiveSheetBehavior.STATE_HIDDEN) { + dismissAfterAnimation() + } else { + behavior.addCallback(SheetDismissCallback()) + behavior.state = AdaptiveSheetBehavior.STATE_HIDDEN + } + } + + private fun dismissAfterAnimation() { + if (waitingForDismissAllowingStateLoss) { + super.dismissAllowingStateLoss() + } else { + super.dismiss() + } + } + + private inner class SheetDismissCallback : AdaptiveSheetCallback { + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + dismissAfterAnimation() + } + } + + override fun onSlide(sheet: View, slideOffset: Float) {} + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt index 354206ad4..1fa1dcf37 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt @@ -30,6 +30,7 @@ import com.google.android.material.R as materialR private const val THROTTLE_DELAY = 200L +@Deprecated("") class BottomSheetHeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt index 0f20ad3e0..4d333c345 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt @@ -18,11 +18,11 @@ import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.details.ui.model.MangaBranch -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapNotNullToSet -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet +import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity class DetailsMenuProvider( @@ -63,7 +63,7 @@ class DetailsMenuProvider( R.id.action_favourite -> { viewModel.manga.value?.let { - FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it) + FavouriteCategoriesSheet.show(activity.supportFragmentManager, it) } } @@ -105,7 +105,7 @@ class DetailsMenuProvider( R.id.action_scrobbling -> { viewModel.manga.value?.let { - ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it, null) + ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt index 7b73a0277..42644501a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt @@ -19,7 +19,7 @@ fun scrobblingInfoAD( { layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) }, ) { binding.root.setOnClickListener { - ScrobblingInfoBottomSheet.show(fragmentManager, bindingAdapterPosition) + ScrobblingInfoSheet.show(fragmentManager, bindingAdapterPosition) } bind { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt similarity index 93% rename from app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt index 045fb420d..7578154e1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt @@ -17,7 +17,7 @@ import androidx.fragment.app.activityViewModels import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.newImageRequest @@ -30,12 +30,12 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet +import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import javax.inject.Inject @AndroidEntryPoint -class ScrobblingInfoBottomSheet : - BaseBottomSheet(), +class ScrobblingInfoSheet : + BaseAdaptiveSheet(), AdapterView.OnItemSelectedListener, RatingBar.OnRatingBarChangeListener, View.OnClickListener, @@ -74,7 +74,7 @@ class ScrobblingInfoBottomSheet : menu = PopupMenu(binding.root.context, binding.buttonMenu).apply { inflate(R.menu.opt_scrobbling) - setOnMenuItemClickListener(this@ScrobblingInfoBottomSheet) + setOnMenuItemClickListener(this@ScrobblingInfoSheet) } } @@ -152,7 +152,7 @@ class ScrobblingInfoBottomSheet : R.id.action_edit -> { val manga = viewModel.manga.value ?: return false val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler - ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService) + ScrobblingSelectorSheet.show(parentFragmentManager, manga, scrobblerService) dismiss() } } @@ -164,7 +164,7 @@ class ScrobblingInfoBottomSheet : private const val TAG = "ScrobblingInfoBottomSheet" private const val ARG_INDEX = "index" - fun show(fm: FragmentManager, index: Int) = ScrobblingInfoBottomSheet().withArgs(1) { + fun show(fm: FragmentManager, index: Int) = ScrobblingInfoSheet().withArgs(1) { putInt(ARG_INDEX, index) }.show(fm, TAG) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt similarity index 62% rename from app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt index 983246319..7d5be8b7b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt @@ -2,34 +2,29 @@ package org.koitharu.kotatsu.favourites.ui.categories.select import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.ui.BaseBottomSheet import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding -import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga @AndroidEntryPoint -class FavouriteCategoriesBottomSheet : - BaseBottomSheet(), - OnListItemClickListener, - View.OnClickListener, - Toolbar.OnMenuItemClickListener { +class FavouriteCategoriesSheet : + BaseAdaptiveSheet(), + OnListItemClickListener { private val viewModel: MangaCategoriesViewModel by viewModels() @@ -40,13 +35,13 @@ class FavouriteCategoriesBottomSheet : container: ViewGroup?, ) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false) - override fun onViewBindingCreated(binding: SheetFavoriteCategoriesBinding, savedInstanceState: Bundle?) { + override fun onViewBindingCreated( + binding: SheetFavoriteCategoriesBinding, + savedInstanceState: Bundle?, + ) { super.onViewBindingCreated(binding, savedInstanceState) adapter = MangaCategoriesAdapter(this) binding.recyclerViewCategories.adapter = adapter - binding.buttonDone.setOnClickListener(this) - binding.headerBar.toolbar.setOnMenuItemClickListener(this) - viewModel.content.observe(viewLifecycleOwner, this::onContentChanged) viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) } @@ -56,25 +51,11 @@ class FavouriteCategoriesBottomSheet : super.onDestroyView() } - override fun onClick(v: View) { - when (v.id) { - R.id.button_done -> dismiss() - } - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext())) - else -> return false - } - return true - } - override fun onItemClick(item: MangaCategoryItem, view: View) { viewModel.setChecked(item.id, !item.isChecked) } - private fun onContentChanged(categories: List) { + private fun onContentChanged(categories: List) { adapter?.items = categories } @@ -89,11 +70,17 @@ class FavouriteCategoriesBottomSheet : fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga)) - fun show(fm: FragmentManager, manga: Collection) = FavouriteCategoriesBottomSheet().withArgs(1) { - putParcelableArrayList( - KEY_MANGA_LIST, - manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) }, - ) - }.show(fm, TAG) + fun show(fm: FragmentManager, manga: Collection) = + FavouriteCategoriesSheet().withArgs(1) { + putParcelableArrayList( + KEY_MANGA_LIST, + manga.mapTo(ArrayList(manga.size)) { + ParcelableManga( + it, + withChapters = false, + ) + }, + ) + }.show(fm, TAG) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt index 69180f1aa..ef5707f75 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus @@ -12,8 +13,10 @@ import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel import javax.inject.Inject @HiltViewModel @@ -23,17 +26,21 @@ class MangaCategoriesViewModel @Inject constructor( ) : BaseViewModel() { private val manga = requireNotNull(savedStateHandle.get>(KEY_MANGA_LIST)).map { it.manga } + private val header = CategoriesHeaderItem() - val content = combine( + val content: StateFlow> = combine( favouritesRepository.observeCategories(), observeCategoriesIds(), ) { all, checked -> - all.map { - MangaCategoryItem( - id = it.id, - name = it.title, - isChecked = it.id in checked, - ) + buildList(all.size + 1) { + add(header) + all.mapTo(this) { + MangaCategoryItem( + id = it.id, + name = it.title, + isChecked = it.id in checked, + ) + } } }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt new file mode 100644 index 000000000..cc878a1ac --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.favourites.ui.categories.select.adapter + +import android.view.View +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding +import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun categoriesHeaderAD() = adapterDelegateViewBinding( + { inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) }, +) { + + val onClickListener = View.OnClickListener { v -> + val intent = when (v.id) { + R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context) + R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context) + else -> return@OnClickListener + } + v.context.startActivity(intent) + } + + binding.buttonCreate.setOnClickListener(onClickListener) + binding.buttonManage.setOnClickListener(onClickListener) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt index 351037748..e0578cf56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt @@ -3,32 +3,39 @@ package org.koitharu.kotatsu.favourites.ui.categories.select.adapter import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel class MangaCategoriesAdapter( - clickListener: OnListItemClickListener -) : AsyncListDifferDelegationAdapter(DiffCallback()) { + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { delegatesManager.addDelegate(mangaCategoryAD(clickListener)) + .addDelegate(categoriesHeaderAD()) } - private class DiffCallback : DiffUtil.ItemCallback() { + private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem - ): Boolean = oldItem.id == newItem.id + oldItem: ListModel, + newItem: ListModel, + ): Boolean = when { + oldItem is MangaCategoryItem && newItem is MangaCategoryItem -> oldItem.id == newItem.id + oldItem is CategoriesHeaderItem && newItem is CategoriesHeaderItem -> oldItem == newItem + else -> false + } override fun areContentsTheSame( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem + oldItem: ListModel, + newItem: ListModel, ): Boolean = oldItem == newItem override fun getChangePayload( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem + oldItem: ListModel, + newItem: ListModel, ): Any? { - if (oldItem.isChecked != newItem.isChecked) { + if (oldItem is MangaCategoryItem && newItem is MangaCategoryItem && oldItem.isChecked != newItem.isChecked) { return newItem.isChecked } return super.getChangePayload(oldItem, newItem) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt index 3badc482b..05f4b7505 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt @@ -4,10 +4,11 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel fun mangaCategoryAD( - clickListener: OnListItemClickListener -) = adapterDelegateViewBinding( + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }, ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt new file mode 100644 index 000000000..cde85e768 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.favourites.ui.categories.select.model + +import org.koitharu.kotatsu.list.ui.model.ListModel + +class CategoriesHeaderItem : ListModel { + + override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt index 95447a2af..721176233 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.favourites.ui.categories.select.model +import org.koitharu.kotatsu.list.ui.model.ListModel + data class MangaCategoryItem( val id: Long, val name: String, - val isChecked: Boolean -) \ No newline at end of file + val isChecked: Boolean, +) : ListModel diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt index 2b7141385..75b18132d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt @@ -11,7 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.core.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.databinding.DialogListModeBinding @@ -19,7 +19,7 @@ import javax.inject.Inject @AndroidEntryPoint class ListModeBottomSheet : - BaseBottomSheet(), + BaseAdaptiveSheet(), Slider.OnChangeListener, MaterialButtonToggleGroup.OnButtonCheckedListener { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 146767467..aaf868a15 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -45,7 +45,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -296,7 +296,7 @@ abstract class MangaListFragment : } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems) + FavouriteCategoriesSheet.show(childFragmentManager, selectedItems) mode.finish() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt similarity index 93% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt index 568a79e1e..0c74a0d80 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt @@ -9,8 +9,8 @@ import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.BaseBottomSheet import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.ext.getParcelableCompat import org.koitharu.kotatsu.core.util.ext.withArgs @@ -23,7 +23,7 @@ import javax.inject.Inject import kotlin.math.roundToInt @AndroidEntryPoint -class ChaptersBottomSheet : BaseBottomSheet(), OnListItemClickListener { +class ChaptersSheet : BaseAdaptiveSheet(), OnListItemClickListener { @Inject lateinit var settings: AppSettings @@ -83,7 +83,7 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC fm: FragmentManager, chapters: List, currentId: Long, - ) = ChaptersBottomSheet().withArgs(2) { + ) = ChaptersSheet().withArgs(2) { putParcelable(ARG_CHAPTERS, ParcelableMangaChapters(chapters)) putLong(ARG_CURRENT_ID, currentId) }.show(fm, TAG) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 0b8ce9908..afc354213 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -52,7 +52,7 @@ import org.koitharu.kotatsu.core.util.ext.zipWithPrevious import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.reader.ui.config.ReaderConfigBottomSheet +import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener @@ -64,10 +64,10 @@ import javax.inject.Inject @AndroidEntryPoint class ReaderActivity : BaseFullscreenActivity(), - ChaptersBottomSheet.OnChapterChangeListener, + ChaptersSheet.OnChapterChangeListener, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, - ReaderConfigBottomSheet.Callback, + ReaderConfigSheet.Callback, ReaderControlDelegate.OnInteractionListener, OnApplyWindowInsetsListener, IdlingDetector.Callback { @@ -179,7 +179,7 @@ class ReaderActivity : } R.id.action_chapters -> { - ChaptersBottomSheet.show( + ChaptersSheet.show( supportFragmentManager, viewModel.manga?.chapters.orEmpty(), viewModel.getCurrentState()?.chapterId ?: 0L, @@ -207,7 +207,7 @@ class ReaderActivity : R.id.action_options -> { viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) val currentMode = readerManager.currentMode ?: return false - ReaderConfigBottomSheet.show(supportFragmentManager, currentMode) + ReaderConfigSheet.show(supportFragmentManager, currentMode) } else -> return super.onOptionsItemSelected(item) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt similarity index 91% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 3994526dd..a29154d6f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -23,7 +23,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.observeAsStateFlow -import org.koitharu.kotatsu.core.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.ScreenOrientationHelper import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope @@ -36,8 +36,8 @@ import org.koitharu.kotatsu.settings.SettingsActivity import javax.inject.Inject @AndroidEntryPoint -class ReaderConfigBottomSheet : - BaseBottomSheet(), +class ReaderConfigSheet : + BaseAdaptiveSheet(), ActivityResultCallback, View.OnClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener, @@ -59,11 +59,17 @@ class ReaderConfigBottomSheet : ?: ReaderMode.STANDARD } - override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetReaderConfigBinding { + override fun onCreateViewBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ): SheetReaderConfigBinding { return SheetReaderConfigBinding.inflate(inflater, container, false) } - override fun onViewBindingCreated(binding: SheetReaderConfigBinding, savedInstanceState: Bundle?) { + override fun onViewBindingCreated( + binding: SheetReaderConfigBinding, + savedInstanceState: Bundle?, + ) { super.onViewBindingCreated(binding, savedInstanceState) observeScreenOrientation() binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD @@ -127,7 +133,11 @@ class ReaderConfigBottomSheet : } } - override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) { + override fun onButtonChecked( + group: MaterialButtonToggleGroup?, + checkedId: Int, + isChecked: Boolean, + ) { if (!isChecked) { return } @@ -180,7 +190,7 @@ class ReaderConfigBottomSheet : private const val TAG = "ReaderConfigBottomSheet" private const val ARG_MODE = "mode" - fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigBottomSheet().withArgs(1) { + fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigSheet().withArgs(1) { putInt(ARG_MODE, mode.id) }.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 72293932c..c6ac60cd5 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 @@ -14,12 +14,13 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.BaseBottomSheet import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.ScrollListenerInvalidationObserver import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar +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.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf @@ -31,14 +32,13 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.TargetScrollObserver -import org.koitharu.kotatsu.util.LoggingAdapterDataObserver import javax.inject.Inject @AndroidEntryPoint class PagesThumbnailsSheet : - BaseBottomSheet(), - OnListItemClickListener, - BottomSheetHeaderBar.OnExpansionChangeListener { + BaseAdaptiveSheet(), + AdaptiveSheetCallback, + OnListItemClickListener { private val viewModel by viewModels() @@ -64,11 +64,6 @@ class PagesThumbnailsSheet : override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) spanResolver = MangaListSpanResolver(binding.root.resources) - with(binding.headerBar) { - title = viewModel.title - subtitle = null - addOnExpansionChangeListener(this@PagesThumbnailsSheet) - } thumbnailsAdapter = PageThumbnailAdapter( coil = coil, lifecycleOwner = viewLifecycleOwner, @@ -87,14 +82,11 @@ class PagesThumbnailsSheet : ScrollListenerInvalidationObserver(this, checkNotNull(scrollListener)), ) thumbnailsAdapter?.registerAdapterDataObserver(TargetScrollObserver(this)) - thumbnailsAdapter?.registerAdapterDataObserver(LoggingAdapterDataObserver("THUMB")) } viewModel.thumbnails.observe(viewLifecycleOwner) { thumbnailsAdapter?.setItems(it, listCommitCallback) } - viewModel.branch.observe(viewLifecycleOwner) { - onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded) - } + viewModel.branch.observe(viewLifecycleOwner, ::updateTitle) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) } @@ -118,13 +110,17 @@ class PagesThumbnailsSheet : dismiss() } - override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { - if (isExpanded) { - headerBar.subtitle = viewModel.branch.value + override fun onStateChanged(sheet: View, newState: Int) { + viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED + } + + private fun updateTitle(branch: String?) { + val mangaName = viewModel.manga.title + viewBinding?.headerBar?.title = if (branch != null) { + getString(R.string.manga_branch_title_template, mangaName, branch) } else { - headerBar.subtitle = null + mangaName } - viewBinding?.recyclerView?.isFastScrollerEnabled = isExpanded } private inner class ScrollListener : BoundsScrollListener(3, 3) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt index 30e7ccb75..8116560fa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt @@ -42,7 +42,6 @@ class PagesThumbnailsViewModel @Inject constructor( val thumbnails = MutableStateFlow>(emptyList()) val branch = MutableStateFlow(null) - val title = manga.title init { loadingJob = launchJob(Dispatchers.Default) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt similarity index 97% rename from app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt index 65e79be5e..f01a9fed4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt @@ -16,9 +16,9 @@ import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.parser.MangaIntent -import org.koitharu.kotatsu.core.ui.BaseBottomSheet import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition import org.koitharu.kotatsu.core.util.ext.getDisplayMessage @@ -35,8 +35,8 @@ import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerSelec import javax.inject.Inject @AndroidEntryPoint -class ScrobblingSelectorBottomSheet : - BaseBottomSheet(), +class ScrobblingSelectorSheet : + BaseAdaptiveSheet(), OnListItemClickListener, PaginationScrollListener.Callback, View.OnClickListener, @@ -63,7 +63,7 @@ class ScrobblingSelectorBottomSheet : with(binding.recyclerView) { adapter = listAdapter addItemDecoration(decoration) - addOnScrollListener(PaginationScrollListener(4, this@ScrobblingSelectorBottomSheet)) + addOnScrollListener(PaginationScrollListener(4, this@ScrobblingSelectorSheet)) } binding.buttonDone.setOnClickListener(this) initOptionsMenu() @@ -209,7 +209,7 @@ class ScrobblingSelectorBottomSheet : private const val ARG_SCROBBLER = "scrobbler" fun show(fm: FragmentManager, manga: Manga, scrobblerService: ScrobblerService?) = - ScrobblingSelectorBottomSheet().withArgs(2) { + ScrobblingSelectorSheet().withArgs(2) { putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false)) if (scrobblerService != null) { putInt(ARG_SCROBBLER, scrobblerService.id) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index c8c75b2dd..36a1125fc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -27,7 +27,7 @@ import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -159,7 +159,7 @@ class MultiSearchActivity : } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(supportFragmentManager, collectSelectedItems()) + FavouriteCategoriesSheet.show(supportFragmentManager, collectSelectedItems()) mode.finish() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt index 942ef03ee..c9ad94181 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt @@ -4,22 +4,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.ui.BaseBottomSheet import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.SheetBaseBinding @AndroidEntryPoint class TrackerCategoriesConfigSheet : - BaseBottomSheet(), - OnListItemClickListener, - View.OnClickListener { + BaseAdaptiveSheet(), + OnListItemClickListener { private val viewModel by viewModels() @@ -30,8 +28,6 @@ class TrackerCategoriesConfigSheet : override fun onViewBindingCreated(binding: SheetBaseBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) binding.headerBar.setTitle(R.string.favourites_categories) - binding.buttonDone.isVisible = true - binding.buttonDone.setOnClickListener(this) val adapter = TrackerCategoriesConfigAdapter(this) binding.recyclerView.adapter = adapter @@ -42,10 +38,6 @@ class TrackerCategoriesConfigSheet : viewModel.toggleItem(item) } - override fun onClick(v: View?) { - dismiss() - } - companion object { private const val TAG = "TrackerCategoriesConfigSheet" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt index a5c5a942e..204dd7599 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt @@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.flattenTo @@ -63,7 +63,7 @@ class ShelfSelectionCallback( } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller)) + FavouriteCategoriesSheet.show(fragmentManager, collectSelectedItems(controller)) mode.finish() true } diff --git a/app/src/main/res/layout/dialog_list_mode.xml b/app/src/main/res/layout/dialog_list_mode.xml index 3f3b25272..b30e21a89 100644 --- a/app/src/main/res/layout/dialog_list_mode.xml +++ b/app/src/main/res/layout/dialog_list_mode.xml @@ -8,7 +8,7 @@ android:animateLayoutChanges="true" android:orientation="vertical"> - diff --git a/app/src/main/res/layout/item_categories_header.xml b/app/src/main/res/layout/item_categories_header.xml new file mode 100644 index 000000000..62af75fc9 --- /dev/null +++ b/app/src/main/res/layout/item_categories_header.xml @@ -0,0 +1,34 @@ + + + +