Branch selection in chapters list

This commit is contained in:
Koitharu
2024-12-08 19:26:18 +02:00
parent 8c79df3d35
commit ee10b013a1
11 changed files with 157 additions and 18 deletions

View File

@@ -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,
)

View File

@@ -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<String?> {
private val deviceLocales: List<String?>
init {
val localeList = LocaleListCompat.getAdjustedDefault()
deviceLocales = buildList(localeList.size() + 1) {
add(null)
val set = HashSet<String?>(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)
}
}
}

View File

@@ -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<MangaBranch> {
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)
}

View File

@@ -67,7 +67,7 @@ fun MangaDetails.mapChapters(
return result
}
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
fun List<ChapterListItem>.withVolumeHeaders(context: Context): MutableList<ListModel> {
var prevVolume = 0
val result = ArrayList<ListModel>((size * 1.4).toInt())
for (item in this) {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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

View File

@@ -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<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem> {
OnListItemClickListener<ChapterListItem>, 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<ListModel>) {
@@ -148,6 +159,13 @@ class ChaptersFragment :
}
}
private fun onFilterChanged(list: List<ChipsView.ChipModel>) {
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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -1,15 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView
android:id="@+id/scrollView_filter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/list_spacing"
android:scrollbars="none">
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="2dp"
android:paddingBottom="6dp"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
app:singleLine="true" />
</HorizontalScrollView>
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/scrollView_filter"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:clipToPadding="false"
android:orientation="vertical"
android:scrollIndicators="top"
@@ -21,7 +50,7 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
@@ -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" />
</FrameLayout>
</RelativeLayout>