From ae8b48d7332e1c660923bb1b6d180877ff6aa5b0 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 16 Jan 2024 11:15:56 +0200 Subject: [PATCH] Migrate favorites to ViewPager2 --- .../kotatsu/core/ui/util/PopupMenuMediator.kt | 38 +++++++++++ .../kotatsu/details/ui/DetailsActivity.kt | 1 + .../ui/container/FavouriteTabModel.kt | 2 +- .../FavouriteTabPopupMenuProvider.kt | 54 ++++++++++++++++ .../container/FavouritesContainerAdapter.kt | 47 +++++++++----- .../container/FavouritesContainerAdapter2.kt | 55 ---------------- .../container/FavouritesContainerFragment.kt | 20 ++++-- .../container/FavouritesContainerViewModel.kt | 64 +++++++++++++++++-- .../FavouritesTabConfigurationStrategy.kt | 19 ++++++ .../ui/list/FavouritesListFragment.kt | 10 --- .../ui/list/FavouritesListMenuProvider.kt | 30 --------- .../layout/fragment_favourites_container.xml | 2 +- app/src/main/res/menu/popup_fav_tab.xml | 18 ++++++ app/src/main/res/menu/popup_fav_tab_all.xml | 9 +++ app/src/main/res/values/strings.xml | 3 +- 15 files changed, 245 insertions(+), 127 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/PopupMenuMediator.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabPopupMenuProvider.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter2.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesTabConfigurationStrategy.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt create mode 100644 app/src/main/res/menu/popup_fav_tab.xml create mode 100644 app/src/main/res/menu/popup_fav_tab_all.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/PopupMenuMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/PopupMenuMediator.kt new file mode 100644 index 000000000..558931d3a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/PopupMenuMediator.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.core.ui.util + +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat + +class PopupMenuMediator( + private val provider: MenuProvider, +) : View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener { + + override fun onLongClick(v: View): Boolean { + val menu = PopupMenu(v.context, v) + provider.onCreateMenu(menu.menu, menu.menuInflater) + provider.onPrepareMenu(menu.menu) + if (!menu.menu.hasVisibleItems()) { + return false + } + menu.setOnMenuItemClickListener(this) + menu.setOnDismissListener(this) + menu.show() + return true + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return provider.onMenuItemSelected(item) + } + + override fun onDismiss(menu: PopupMenu) { + provider.onMenuClosed(menu.menu) + } + + fun attach(view: View) { + view.setOnLongClickListener(this) + view.setOnContextClickListenerCompat(this) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index afdbcd194..2957c0bb2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -365,6 +365,7 @@ class DetailsActivity : private fun initPager() { viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false val adapter = DetailsPagerAdapter(this) + viewBinding.pager.offscreenPageLimit = 1 viewBinding.pager.adapter = adapter TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach() viewBinding.pager.setCurrentItem(settings.defaultDetailsTab, false) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabModel.kt index 684c1fa72..b68fbc85b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabModel.kt @@ -4,7 +4,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel data class FavouriteTabModel( val id: Long, - val title: String, + val title: String?, ) : ListModel { override fun areItemsTheSame(other: ListModel): Boolean { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabPopupMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabPopupMenuProvider.kt new file mode 100644 index 000000000..995d5c616 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouriteTabPopupMenuProvider.kt @@ -0,0 +1,54 @@ +package org.koitharu.kotatsu.favourites.ui.container + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity +import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID + +class FavouriteTabPopupMenuProvider( + private val context: Context, + private val viewModel: FavouritesContainerViewModel, + private val categoryId: Long +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + val menuResId = if (categoryId == NO_ID) { + R.menu.popup_fav_tab_all + } else { + R.menu.popup_fav_tab + } + menuInflater.inflate(menuResId, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.action_hide -> viewModel.hide(categoryId) + R.id.action_edit -> context.startActivity( + FavouritesCategoryEditActivity.newIntent(context, categoryId), + ) + + R.id.action_delete -> confirmDelete() + + else -> return false + } + return true + } + + private fun confirmDelete() { + MaterialAlertDialogBuilder( + context, + com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered, + ).setMessage(R.string.categories_delete_confirm) + .setTitle(R.string.remove_category) + .setIcon(R.drawable.ic_delete) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.remove) { _, _ -> + viewModel.deleteCategory(categoryId) + }.show() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter.kt index c9daa207d..8c2e0e4cd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter.kt @@ -1,33 +1,46 @@ package org.koitharu.kotatsu.favourites.ui.container import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.recyclerview.widget.AdapterListUpdateCallback +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.viewpager2.adapter.FragmentStateAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.FlowCollector +import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment -import org.koitharu.kotatsu.parsers.util.replaceWith +import org.koitharu.kotatsu.list.ui.ListModelDiffCallback +import kotlin.coroutines.suspendCoroutine -@Suppress("DEPRECATION") -class FavouritesContainerAdapter( - fm: FragmentManager -) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT), +class FavouritesContainerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment), FlowCollector> { - private val dataSet = ArrayList() + private val differ = AsyncListDiffer( + AdapterListUpdateCallback(this), + AsyncDifferConfig.Builder(ListModelDiffCallback()) + .setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor()) + .build(), + ) - override fun getCount(): Int = dataSet.size + override fun getItemCount(): Int = differ.currentList.size - override fun getItem(position: Int): Fragment { - val item = dataSet[position] + override fun getItemId(position: Int): Long { + return differ.currentList[position].id + } + + override fun containsItem(itemId: Long): Boolean { + return differ.currentList.any { x -> x.id == itemId } + } + + override fun createFragment(position: Int): Fragment { + val item = differ.currentList[position] return FavouritesListFragment.newInstance(item.id) } - override fun getPageTitle(position: Int): CharSequence { - return dataSet[position].title + override suspend fun emit(value: List) = suspendCoroutine { cont -> + differ.submitList(value, ContinuationResumeRunnable(cont)) } - override suspend fun emit(value: List) { - dataSet.replaceWith(value) - notifyDataSetChanged() - } + fun getItem(position: Int): FavouriteTabModel = differ.currentList[position] } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter2.kt deleted file mode 100644 index 4afab1c9f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerAdapter2.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.koitharu.kotatsu.favourites.ui.container - -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.AdapterListUpdateCallback -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.flow.FlowCollector -import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable -import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment -import org.koitharu.kotatsu.list.ui.ListModelDiffCallback -import kotlin.coroutines.suspendCoroutine - -// FIXME migrate to ViewPager2 in FavouritesContainerFragment -class FavouritesContainerAdapter2(fragment: Fragment) : - FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle), - TabConfigurationStrategy, - FlowCollector> { - - private val differ = AsyncListDiffer( - AdapterListUpdateCallback(this), - AsyncDifferConfig.Builder(ListModelDiffCallback()) - .setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor()) - .build(), - ) - - override fun getItemCount(): Int = differ.currentList.size - - override fun getItemId(position: Int): Long { - return differ.currentList[position].id - } - - override fun containsItem(itemId: Long): Boolean { - return differ.currentList.any { x -> x.id == itemId } - } - - override fun createFragment(position: Int): Fragment { - val item = differ.currentList[position] - return FavouritesListFragment.newInstance(item.id) - } - - override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { - val item = differ.currentList[position] - tab.text = item.title - tab.tag = item - } - - override suspend fun emit(value: List) = suspendCoroutine { cont -> - differ.submitList(value, ContinuationResumeRunnable(cont)) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerFragment.kt index efce0c7aa..5b9889514 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerFragment.kt @@ -12,14 +12,18 @@ import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.viewModels import coil.ImageLoader +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.util.ActionModeListener +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.setTabsEnabled import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding @@ -43,15 +47,21 @@ class FavouritesContainerFragment : BaseFragment() + private val categoriesStateFlow = favouritesRepository.observeCategoriesForLibrary() .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - val categories = categoriesStateFlow.filterNotNull() - .mapItems { FavouriteTabModel(it.id, it.title) } - .distinctUntilChanged() - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + val categories = combine( + categoriesStateFlow.filterNotNull(), + observeAllFavouritesVisibility(), + ) { list, showAll -> + list.toUi(showAll) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) val isEmpty = categoriesStateFlow.map { it?.isEmpty() == true }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) + + private fun List.toUi(showAll: Boolean): List { + if (isEmpty()) { + return emptyList() + } + val result = ArrayList(if (showAll) size + 1 else size) + if (showAll) { + result.add(FavouriteTabModel(NO_ID, null)) + } + mapTo(result) { FavouriteTabModel(it.id, it.title) } + return result + } + + fun hide(categoryId: Long) { + launchJob(Dispatchers.Default) { + if (categoryId == NO_ID) { + settings.isAllFavouritesVisible = false + } else { + favouritesRepository.updateCategory(categoryId, isVisibleInLibrary = false) + val reverse = ReversibleHandle { + favouritesRepository.updateCategory(categoryId, isVisibleInLibrary = true) + } + onActionDone.call(ReversibleAction(R.string.category_hidden_done, reverse)) + } + } + } + + fun deleteCategory(categoryId: Long) { + launchJob(Dispatchers.Default) { + favouritesRepository.removeCategories(setOf(categoryId)) + } + } + + private fun observeAllFavouritesVisibility() = settings.observeAsFlow( + key = AppSettings.KEY_ALL_FAVOURITES_VISIBLE, + valueProducer = { isAllFavouritesVisible }, + ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesTabConfigurationStrategy.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesTabConfigurationStrategy.kt new file mode 100644 index 000000000..c3fc540b1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesTabConfigurationStrategy.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.favourites.ui.container + +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.util.PopupMenuMediator + +class FavouritesTabConfigurationStrategy( + private val adapter: FavouritesContainerAdapter, + private val viewModel: FavouritesContainerViewModel, +) : TabConfigurationStrategy { + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + val item = adapter.getItem(position) + tab.text = item.title ?: tab.view.context.getString(R.string.all_favourites) + tab.tag = item + PopupMenuMediator(FavouriteTabPopupMenuProvider(tab.view.context, viewModel, item.id)).attach(tab.view) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index 3575f51f6..43b7dd4ff 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.favourites.ui.list -import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View @@ -11,10 +10,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.list.ListSelectionController -import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal import org.koitharu.kotatsu.core.util.ext.withArgs -import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource @@ -29,13 +26,6 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis val categoryId get() = viewModel.categoryId - override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - if (viewModel.categoryId != NO_ID) { - addMenuProvider(FavouritesListMenuProvider(binding.root.context, viewModel)) - } - } - override fun onScrolledToEnd() = Unit override fun onFilterClick(view: View?) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt deleted file mode 100644 index ca134d8e8..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.koitharu.kotatsu.favourites.ui.list - -import android.content.Context -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.core.view.MenuProvider -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity - -class FavouritesListMenuProvider( - private val context: Context, - private val viewModel: FavouritesListViewModel, -) : MenuProvider { - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.opt_favourites, menu) - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return when (menuItem.itemId) { - R.id.action_edit -> { - context.startActivity(FavouritesCategoryEditActivity.newIntent(context, viewModel.categoryId)) - true - } - - else -> false - } - } -} diff --git a/app/src/main/res/layout/fragment_favourites_container.xml b/app/src/main/res/layout/fragment_favourites_container.xml index 53d453eb4..553ec595e 100644 --- a/app/src/main/res/layout/fragment_favourites_container.xml +++ b/app/src/main/res/layout/fragment_favourites_container.xml @@ -14,7 +14,7 @@ app:tabGravity="start" app:tabMode="scrollable" /> - + + + + + + + + + diff --git a/app/src/main/res/menu/popup_fav_tab_all.xml b/app/src/main/res/menu/popup_fav_tab_all.xml new file mode 100644 index 000000000..6b8d5811b --- /dev/null +++ b/app/src/main/res/menu/popup_fav_tab_all.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8261386fc..a819a68b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -558,4 +558,5 @@ Default tab Mark as completed Mark selected manga as completely read?\n\nWarning: current reading progress will be lost. - \ No newline at end of file + This category was hidden from the main screen and is accessible through Menu → Manage categories +