From 84e5400522f9983a656a02385170adc9906bce3a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jun 2023 14:36:33 +0300 Subject: [PATCH] Download options dialog --- .../org/koitharu/kotatsu/core/model/Manga.kt | 4 + .../core/ui/widgets/TwoLinesItemView.kt | 16 ++- .../details/ui/ChaptersBottomSheetMediator.kt | 1 + .../kotatsu/details/ui/ChaptersFragment.kt | 4 + .../kotatsu/details/ui/DetailsMenuProvider.kt | 56 +++-------- .../kotatsu/details/ui/DetailsViewModel.kt | 9 ++ .../details/ui/DownloadDialogHelper.kt | 64 ++++++++++++ .../download/ui/dialog/DownloadOption.kt | 99 +++++++++++++++++++ .../download/ui/dialog/DownloadOptionAD.kt | 27 +++++ app/src/main/res/drawable/ic_list_end.xml | 12 +++ app/src/main/res/drawable/ic_list_next.xml | 12 +++ app/src/main/res/drawable/ic_list_start.xml | 12 +++ app/src/main/res/drawable/ic_select_group.xml | 11 +++ .../main/res/layout/item_download_option.xml | 16 +++ app/src/main/res/menu/opt_details.xml | 2 +- app/src/main/res/values/strings.xml | 7 ++ 16 files changed, 307 insertions(+), 45 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt create mode 100644 app/src/main/res/drawable/ic_list_end.xml create mode 100644 app/src/main/res/drawable/ic_list_next.xml create mode 100644 app/src/main/res/drawable/ic_list_start.xml create mode 100644 app/src/main/res/drawable/ic_select_group.xml create mode 100644 app/src/main/res/layout/item_download_option.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index ce0b3e7f1..6314c0c6e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -8,10 +8,14 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet +@JvmName("mangaIds") fun Collection.ids() = mapToSet { it.id } fun Collection.distinctById() = distinctBy { it.id } +@JvmName("chaptersIds") +fun Collection.ids() = mapToSet { it.id } + fun Collection.countChaptersByBranch(): Int { if (size <= 1) { return size diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt index 37058bac2..8bf6e5a49 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt @@ -24,6 +24,7 @@ import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding @SuppressLint("RestrictedApi") @@ -35,6 +36,18 @@ class TwoLinesItemView @JvmOverloads constructor( private val binding = ViewTwoLinesItemBinding.inflate(LayoutInflater.from(context), this) + var title: CharSequence? + get() = binding.title.text + set(value) { + binding.title.text = value + } + + var subtitle: CharSequence? + get() = binding.subtitle.textAndVisible + set(value) { + binding.subtitle.textAndVisible = value + } + init { var textColors: ColorStateList? = null context.withStyledAttributes( @@ -76,8 +89,7 @@ class TwoLinesItemView @JvmOverloads constructor( } fun setIconResource(@DrawableRes resId: Int) { - val icon = if (resId != 0) ContextCompat.getDrawable(context, resId) else null - binding.icon.setImageDrawable(icon) + binding.icon.setImageResource(resId) } private fun createShapeDrawable(ta: TypedArray): InsetDrawable { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt index 727ff31f3..8187b7fdf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt @@ -30,6 +30,7 @@ class ChaptersBottomSheetMediator( } override fun onActionModeStarted(mode: ActionMode) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED lock() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index d1b6aa21d..dc9998095 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter @@ -66,6 +67,9 @@ class ChaptersFragment : viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { binding.textViewHolder.isVisible = it } + viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) { + selectionController?.onItemLongClick(it) + } } override fun onDestroyView() { 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 a289f55c2..4832e24a7 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 @@ -16,12 +16,11 @@ import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.os.AppShortcutManager +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ShareHelper -import org.koitharu.kotatsu.details.ui.model.MangaBranch +import org.koitharu.kotatsu.download.ui.dialog.DownloadOption 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.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity @@ -30,7 +29,7 @@ class DetailsMenuProvider( private val viewModel: DetailsViewModel, private val snackbarHost: View, private val appShortcutManager: AppShortcutManager, -) : MenuProvider { +) : MenuProvider, OnListItemClickListener { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_details, menu) @@ -44,7 +43,7 @@ class DetailsMenuProvider( menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity) menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable menu.findItem(R.id.action_favourite).setIcon( - if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline, + if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline, ) } @@ -80,15 +79,7 @@ class DetailsMenuProvider( } R.id.action_save -> { - viewModel.manga.value?.let { - val chaptersCount = it.chapters?.size ?: 0 - val branches = viewModel.branches.value.orEmpty() - if (chaptersCount > 5 || branches.size > 1) { - showSaveConfirmation(it, chaptersCount, branches) - } else { - viewModel.download(null) - } - } + DownloadDialogHelper(snackbarHost, viewModel).show(this) } R.id.action_browser -> { @@ -125,35 +116,16 @@ class DetailsMenuProvider( return true } - private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List) { - val dialogBuilder = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.save_manga) - .setNegativeButton(android.R.string.cancel, null) - if (branches.size > 1) { - val items = Array(branches.size) { i -> branches[i].name.orEmpty() } - val currentBranch = branches.indexOfFirst { it.isSelected } - val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch } - dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked -> - checkedIndices[i] = checked - }.setPositiveButton(R.string.save) { _, _ -> - val selectedBranches = branches.mapIndexedNotNullTo(HashSet()) { i, b -> - if (checkedIndices[i]) b.name else null - } - val chaptersIds = manga.chapters?.mapNotNullToSet { c -> - if (c.branch in selectedBranches) c.id else null - } - viewModel.download(chaptersIds) - } - } else { - dialogBuilder.setMessage( - activity.getString( - R.string.large_manga_save_confirm, - activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount), - ), - ).setPositiveButton(R.string.save) { _, _ -> - viewModel.download(null) + override fun onItemClick(item: DownloadOption, view: View) { + val chaptersIds: Set? = when (item) { + is DownloadOption.WholeManga -> null + is DownloadOption.SelectionHint -> { + viewModel.startChaptersSelection() + return } + + else -> item.chaptersIds } - dialogBuilder.show() + viewModel.download(chaptersIds) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index ea7e8eed0..38b041a6e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -84,6 +84,7 @@ class DetailsViewModel @Inject constructor( val onShowToast = MutableEventFlow() val onShowTip = MutableEventFlow() + val onSelectChapter = MutableEventFlow() val onDownloadStarted = MutableEventFlow() val manga = doubleManga.map { it?.any } @@ -290,6 +291,14 @@ class DetailsViewModel @Inject constructor( } } + fun startChaptersSelection() { + val chapters = chapters.value + val chapter = chapters.find { + it.isUnread && !it.isDownloaded + } ?: chapters.firstOrNull() ?: return + onSelectChapter.call(chapter.chapter.id) + } + fun onButtonTipClosed() { settings.closeTip(DetailsActivity.TIP_BUTTON) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt new file mode 100644 index 000000000..df456252f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt @@ -0,0 +1,64 @@ +package org.koitharu.kotatsu.details.ui + +import android.content.DialogInterface +import android.view.View +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.ids +import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.download.ui.dialog.DownloadOption +import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD + +class DownloadDialogHelper( + private val host: View, + private val viewModel: DetailsViewModel, +) { + + fun show(callback: OnListItemClickListener) { + val branch = viewModel.selectedBranchValue + val allChapters = viewModel.manga.value?.chapters ?: return + val branchChapters = viewModel.manga.value?.getChapters(branch).orEmpty() + val history = viewModel.history.value + + val options = buildList { + add(DownloadOption.WholeManga(allChapters.ids())) + if (branch != null && branchChapters.isNotEmpty()) { + add(DownloadOption.AllChapters(branch, branchChapters.ids())) + } + + if (history != null) { + val unreadChapters = branchChapters.takeLastWhile { it.id != history.chapterId } + if (unreadChapters.isNotEmpty() && unreadChapters.size < branchChapters.size) { + add(DownloadOption.AllUnreadChapters(unreadChapters.ids(), branch)) + if (unreadChapters.size > 5) { + add(DownloadOption.NextUnreadChapters(unreadChapters.take(5).ids())) + if (unreadChapters.size > 10) { + add(DownloadOption.NextUnreadChapters(unreadChapters.take(10).ids())) + } + } + } + } else { + if (branchChapters.size > 5) { + add(DownloadOption.FirstChapters(branchChapters.take(5).ids())) + if (branchChapters.size > 10) { + add(DownloadOption.FirstChapters(branchChapters.take(10).ids())) + } + } + } + add(DownloadOption.SelectionHint()) + } + var dialog: DialogInterface? = null + val listener = OnListItemClickListener { item, _ -> + callback.onItemClick(item, host) + dialog?.dismiss() + } + dialog = RecyclerViewAlertDialog.Builder(host.context) + .addAdapterDelegate(downloadOptionAD(listener)) + .setCancelable(true) + .setTitle(R.string.download) + .setNegativeButton(android.R.string.cancel) + .setItems(options) + .create() + .also { it.show() } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt new file mode 100644 index 000000000..ae9bf076a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt @@ -0,0 +1,99 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import android.content.res.Resources +import androidx.annotation.DrawableRes +import org.koitharu.kotatsu.R +import java.util.Locale +import com.google.android.material.R as materialR + +sealed interface DownloadOption { + + val chaptersIds: Set + + @get:DrawableRes + val iconResId: Int + + val chaptersCount: Int + get() = chaptersIds.size + + fun getLabel(resources: Resources): CharSequence + + class AllChapters( + val branch: String, + override val chaptersIds: Set, + ) : DownloadOption { + + override val iconResId = R.drawable.ic_select_group + + override fun getLabel(resources: Resources): CharSequence { + return resources.getString(R.string.download_option_all_chapters, branch) + } + } + + class WholeManga( + override val chaptersIds: Set, + ) : DownloadOption { + + override val iconResId = materialR.drawable.abc_ic_menu_selectall_mtrl_alpha + + override fun getLabel(resources: Resources): CharSequence { + return resources.getString(R.string.download_option_whole_manga) + } + } + + class FirstChapters( + override val chaptersIds: Set, + ) : DownloadOption { + + override val iconResId = R.drawable.ic_list_start + + override fun getLabel(resources: Resources): CharSequence { + return resources.getString( + R.string.download_option_first_n_chapters, + resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) + .lowercase(Locale.getDefault()), + ) + } + } + + class AllUnreadChapters( + override val chaptersIds: Set, + val branch: String?, + ) : DownloadOption { + + override val iconResId = R.drawable.ic_list_end + + override fun getLabel(resources: Resources): CharSequence { + return if (branch == null) { + resources.getString(R.string.download_option_all_unread) + } else { + resources.getString(R.string.download_option_all_unread_b, branch) + } + } + } + + class NextUnreadChapters( + override val chaptersIds: Set, + ) : DownloadOption { + + override val iconResId = R.drawable.ic_list_next + + override fun getLabel(resources: Resources): CharSequence { + return resources.getString( + R.string.download_option_next_unread_n_chapters, + resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) + .lowercase(Locale.getDefault()), + ) + } + } + + class SelectionHint : DownloadOption { + + override val chaptersIds: Set = emptySet() + override val iconResId = R.drawable.ic_tap + + override fun getLabel(resources: Resources): CharSequence { + return resources.getString(R.string.download_option_manual_selection) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt new file mode 100644 index 000000000..3a277787f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ItemDownloadOptionBinding + +fun downloadOptionAD( + onClickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemDownloadOptionBinding.inflate(layoutInflater, parent, false) }, +) { + + binding.root.setOnClickListener { v -> onClickListener.onItemClick(item, v) } + + bind { + with(binding.root) { + title = item.getLabel(resources) + subtitle = if (item.chaptersCount == 0) null else resources.getQuantityString( + R.plurals.chapters, + item.chaptersCount, + item.chaptersCount, + ) + setIconResource(item.iconResId) + } + } +} diff --git a/app/src/main/res/drawable/ic_list_end.xml b/app/src/main/res/drawable/ic_list_end.xml new file mode 100644 index 000000000..2e7f20ad1 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_end.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_list_next.xml b/app/src/main/res/drawable/ic_list_next.xml new file mode 100644 index 000000000..eb82e2891 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_next.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_list_start.xml b/app/src/main/res/drawable/ic_list_start.xml new file mode 100644 index 000000000..ca31a1d05 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_start.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_select_group.xml b/app/src/main/res/drawable/ic_select_group.xml new file mode 100644 index 000000000..9705aa565 --- /dev/null +++ b/app/src/main/res/drawable/ic_select_group.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/item_download_option.xml b/app/src/main/res/layout/item_download_option.xml new file mode 100644 index 000000000..195b02ace --- /dev/null +++ b/app/src/main/res/layout/item_download_option.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/menu/opt_details.xml b/app/src/main/res/menu/opt_details.xml index 2c5586c5b..fb64e11bc 100644 --- a/app/src/main/res/menu/opt_details.xml +++ b/app/src/main/res/menu/opt_details.xml @@ -20,7 +20,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16701719e..3c350fc05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -438,4 +438,11 @@ Animate page switching Press and hold the Read button to see more options Clear cookies for specified domain only. In most cases will invalidate authorization + All chapters with translation %s + The whole manga + First %s + Next unread %s + All unread chapters + All unread chapters (%s) + Select chapters manually