From ee10b013a1b4f6cff5fb463a14716271228a4489 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 8 Dec 2024 19:26:18 +0200 Subject: [PATCH] Branch selection in chapters list --- .../kotatsu/core/model/QuickFilter.kt | 13 ++++++ .../core/util/LocaleStringComparator.kt | 35 ++++++++++++++++ .../details/domain/BranchComparator.kt | 5 ++- .../kotatsu/details/ui/ChaptersMapper.kt | 2 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 + .../details/ui/DetailsBottomSheetCallback.kt | 16 ++++++++ .../ui/pager/ChaptersPagesViewModel.kt | 16 ++++++++ .../ui/pager/chapters/ChaptersFragment.kt | 20 ++++++++- .../kotatsu/list/domain/ListFilterOption.kt | 14 +++++++ .../list/domain/MangaListQuickFilter.kt | 11 +---- app/src/main/res/layout/fragment_chapters.xml | 41 ++++++++++++++++--- 11 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/model/QuickFilter.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleStringComparator.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsBottomSheetCallback.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/QuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/QuickFilter.kt new file mode 100644 index 000000000..c91e32810 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/QuickFilter.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.core.model + +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.list.domain.ListFilterOption + +fun ListFilterOption.toChipModel(isChecked: Boolean) = ChipsView.ChipModel( + title = titleText, + titleResId = titleResId, + icon = iconResId, + iconData = getIconData(), + isChecked = isChecked, + data = this, +) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleStringComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleStringComparator.kt new file mode 100644 index 000000000..4bf0be081 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleStringComparator.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.core.util + +import androidx.core.os.LocaleListCompat +import org.koitharu.kotatsu.core.util.ext.iterator + +class LocaleStringComparator : Comparator { + + private val deviceLocales: List + + init { + val localeList = LocaleListCompat.getAdjustedDefault() + deviceLocales = buildList(localeList.size() + 1) { + add(null) + val set = HashSet(localeList.size() + 1) + set.add(null) + for (locale in localeList) { + val lang = locale.getDisplayLanguage(locale).lowercase() + if (set.add(lang)) { + add(lang) + } + } + } + } + + override fun compare(a: String?, b: String?): Int { + val indexA = deviceLocales.indexOf(a?.lowercase()) + val indexB = deviceLocales.indexOf(b?.lowercase()) + return when { + indexA < 0 && indexB < 0 -> compareValues(a, b) + indexA < 0 -> 1 + indexB < 0 -> -1 + else -> compareValues(indexA, indexB) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt index 4d93a464c..e891b547f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt @@ -1,8 +1,11 @@ package org.koitharu.kotatsu.details.domain +import org.koitharu.kotatsu.core.util.LocaleStringComparator import org.koitharu.kotatsu.details.ui.model.MangaBranch class BranchComparator : Comparator { - override fun compare(o1: MangaBranch, o2: MangaBranch): Int = compareValues(o1.name, o2.name) + private val delegate = LocaleStringComparator() + + override fun compare(o1: MangaBranch, o2: MangaBranch): Int = delegate.compare(o1.name, o2.name) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt index 732c5af0e..5042f95fa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt @@ -67,7 +67,7 @@ fun MangaDetails.mapChapters( return result } -fun List.withVolumeHeaders(context: Context): List { +fun List.withVolumeHeaders(context: Context): MutableList { var prevVolume = 0 val result = ArrayList((size * 1.4).toInt()) for (item in 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 577ffa907..e93dddd24 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 @@ -42,6 +42,7 @@ import coil3.size.Precision import coil3.size.Scale import coil3.transform.RoundedCornersTransformation import coil3.util.CoilUtils +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -167,6 +168,7 @@ class DetailsActivity : TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) viewBinding.containerBottomSheet?.let { sheet -> onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet)) + BottomSheetBehavior.from(sheet).addBottomSheetCallback(DetailsBottomSheetCallback(viewBinding.swipeRefreshLayout)) } TitleExpandListener(viewBinding.textViewTitle).attach() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsBottomSheetCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsBottomSheetCallback.kt new file mode 100644 index 000000000..d351a9b15 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsBottomSheetCallback.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.details.ui + +import android.view.View +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior + +class DetailsBottomSheetCallback( + private val swipeRefreshLayout: SwipeRefreshLayout, +) : BottomSheetBehavior.BottomSheetCallback() { + + override fun onStateChanged(bottomSheet: View, newState: Int) { + swipeRefreshLayout.isEnabled = newState == BottomSheetBehavior.STATE_COLLAPSED + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index b7e20b006..0a1b8b4ab 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -19,14 +19,17 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.plus import okio.FileNotFoundException import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.core.model.toChipModel import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.LocaleStringComparator import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.core.util.ext.requireValue +import org.koitharu.kotatsu.core.util.ext.sortedWithSafe import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -36,6 +39,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.download.ui.worker.DownloadTask import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga @@ -119,6 +123,18 @@ abstract class ChaptersPagesViewModel( (if (reversed) list.asReversed() else list).filterSearch(query) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + val quickFilter = combine( + mangaDetails, + selectedBranch, + ) { details, branch -> + val branches = details?.chapters?.keys?.sortedWithSafe(LocaleStringComparator()).orEmpty() + if (branches.size > 1) { + branches.map { ListFilterOption.Branch(it).toChipModel(it == branch) } + } else { + emptyList() + } + } + init { launchJob(Dispatchers.Default) { localStorageChanges diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt index 0c82e7c83..ca719a40d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt @@ -6,10 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.ancestors +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull @@ -22,6 +24,7 @@ import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.ext.dismissParentDialog import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate @@ -34,6 +37,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel import org.koitharu.kotatsu.details.ui.withVolumeHeaders +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder @@ -45,7 +49,7 @@ import kotlin.math.roundToInt @AndroidEntryPoint class ChaptersFragment : BaseFragment(), - OnListItemClickListener { + OnListItemClickListener, ChipsView.OnChipClickListener { private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this) @@ -86,11 +90,13 @@ class ChaptersFragment : adapter = chaptersAdapter ChapterGridSpanHelper.attach(this) } + binding.chipsFilter.onChipClickListener = this viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.chapters .map { it.withVolumeHeaders(requireContext()) } .flowOn(Dispatchers.Default) .observe(viewLifecycleOwner, this::onChaptersChanged) + viewModel.quickFilter.observe(viewLifecycleOwner, this::onFilterChanged) viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { binding.textViewHolder.isVisible = it } @@ -128,6 +134,11 @@ class ChaptersFragment : return selectionController?.onItemContextClick(view, item.chapter.id) ?: false } + override fun onChipClick(chip: Chip, data: Any?) { + if (data !is ListFilterOption.Branch) return + viewModel.setSelectedBranch(data.titleText) + } + override fun onWindowInsetsChanged(insets: Insets) = Unit private fun onChaptersChanged(list: List) { @@ -148,6 +159,13 @@ class ChaptersFragment : } } + private fun onFilterChanged(list: List) { + viewBinding?.chipsFilter?.run { + setChips(list) + isGone = list.isEmpty() + } + } + private suspend fun onSelectChapter(chapterId: Long) { if (!isResumed) { view?.ancestors?.firstNotNullOfOrNull { it as? ViewPager2 }?.setCurrentItem(0, true) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt index 71f6a4cf0..8239ca66c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt @@ -60,6 +60,20 @@ sealed interface ListFilterOption { get() = name } + data class Branch( + override val titleText: String?, + ) : ListFilterOption { + + override val titleResId: Int + get() = if (titleText == null) R.string.system_default else 0 + + override val iconResId: Int + get() = R.drawable.ic_language + + override val groupKey: String + get() = "_branch" + } + data class Tag( val tag: MangaTag ) : ListFilterOption { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt index bb1631667..ad9d2dcf6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt @@ -3,8 +3,8 @@ package org.koitharu.kotatsu.list.domain import androidx.collection.ArraySet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import org.koitharu.kotatsu.core.model.toChipModel import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.list.ui.model.QuickFilter import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy @@ -52,14 +52,7 @@ abstract class MangaListQuickFilter( return null } val availableOptions = availableFilterOptions.getOrNull()?.map { option -> - ChipsView.ChipModel( - title = option.titleText, - titleResId = option.titleResId, - icon = option.iconResId, - iconData = option.getIconData(), - isChecked = option in selectedOptions, - data = option, - ) + option.toChipModel(isChecked = option in selectedOptions) }.orEmpty() return if (availableOptions.isNotEmpty()) { QuickFilter(availableOptions) diff --git a/app/src/main/res/layout/fragment_chapters.xml b/app/src/main/res/layout/fragment_chapters.xml index 82f23d2d1..be25f31fd 100644 --- a/app/src/main/res/layout/fragment_chapters.xml +++ b/app/src/main/res/layout/fragment_chapters.xml @@ -1,15 +1,44 @@ - + + + + + + @@ -30,7 +59,7 @@ android:id="@+id/textView_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" + android:layout_centerInParent="true" android:layout_marginStart="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal" android:layout_marginEnd="@dimen/margin_normal" @@ -42,4 +71,4 @@ android:visibility="gone" tools:visibility="visible" /> - +