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 package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.util.LocaleStringComparator
import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.details.ui.model.MangaBranch
class BranchComparator : Comparator<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 return result
} }
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> { fun List<ChapterListItem>.withVolumeHeaders(context: Context): MutableList<ListModel> {
var prevVolume = 0 var prevVolume = 0
val result = ArrayList<ListModel>((size * 1.4).toInt()) val result = ArrayList<ListModel>((size * 1.4).toInt())
for (item in this) { for (item in this) {

View File

@@ -42,6 +42,7 @@ import coil3.size.Precision
import coil3.size.Scale import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
import coil3.util.CoilUtils import coil3.util.CoilUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -167,6 +168,7 @@ class DetailsActivity :
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
viewBinding.containerBottomSheet?.let { sheet -> viewBinding.containerBottomSheet?.let { sheet ->
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet)) onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet))
BottomSheetBehavior.from(sheet).addBottomSheetCallback(DetailsBottomSheetCallback(viewBinding.swipeRefreshLayout))
} }
TitleExpandListener(viewBinding.textViewTitle).attach() 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 kotlinx.coroutines.plus
import okio.FileNotFoundException import okio.FileNotFoundException
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository 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.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction 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.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.requireValue 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.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.ui.DetailsActivity 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.DownloadTask
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository 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.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -119,6 +123,18 @@ abstract class ChaptersPagesViewModel(
(if (reversed) list.asReversed() else list).filterSearch(query) (if (reversed) list.asReversed() else list).filterSearch(query)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) }.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 { init {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
localStorageChanges localStorageChanges

View File

@@ -6,10 +6,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ancestors import androidx.core.view.ancestors
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull 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.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper 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.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate 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.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.details.ui.withVolumeHeaders 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.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
@@ -45,7 +49,7 @@ import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem> { OnListItemClickListener<ChapterListItem>, ChipsView.OnChipClickListener {
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this) private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
@@ -86,11 +90,13 @@ class ChaptersFragment :
adapter = chaptersAdapter adapter = chaptersAdapter
ChapterGridSpanHelper.attach(this) ChapterGridSpanHelper.attach(this)
} }
binding.chipsFilter.onChipClickListener = this
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters viewModel.chapters
.map { it.withVolumeHeaders(requireContext()) } .map { it.withVolumeHeaders(requireContext()) }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.observe(viewLifecycleOwner, this::onChaptersChanged) .observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.quickFilter.observe(viewLifecycleOwner, this::onFilterChanged)
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it binding.textViewHolder.isVisible = it
} }
@@ -128,6 +134,11 @@ class ChaptersFragment :
return selectionController?.onItemContextClick(view, item.chapter.id) ?: false 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 override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun onChaptersChanged(list: List<ListModel>) { 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) { private suspend fun onSelectChapter(chapterId: Long) {
if (!isResumed) { if (!isResumed) {
view?.ancestors?.firstNotNullOfOrNull { it as? ViewPager2 }?.setCurrentItem(0, true) view?.ancestors?.firstNotNullOfOrNull { it as? ViewPager2 }?.setCurrentItem(0, true)

View File

@@ -60,6 +60,20 @@ sealed interface ListFilterOption {
get() = name 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( data class Tag(
val tag: MangaTag val tag: MangaTag
) : ListFilterOption { ) : ListFilterOption {

View File

@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.list.domain
import androidx.collection.ArraySet import androidx.collection.ArraySet
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import org.koitharu.kotatsu.core.model.toChipModel
import org.koitharu.kotatsu.core.prefs.AppSettings 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.list.ui.model.QuickFilter
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
@@ -52,14 +52,7 @@ abstract class MangaListQuickFilter(
return null return null
} }
val availableOptions = availableFilterOptions.getOrNull()?.map { option -> val availableOptions = availableFilterOptions.getOrNull()?.map { option ->
ChipsView.ChipModel( option.toChipModel(isChecked = option in selectedOptions)
title = option.titleText,
titleResId = option.titleResId,
icon = option.iconResId,
iconData = option.getIconData(),
isChecked = option in selectedOptions,
data = option,
)
}.orEmpty() }.orEmpty()
return if (availableOptions.isNotEmpty()) { return if (availableOptions.isNotEmpty()) {
QuickFilter(availableOptions) QuickFilter(availableOptions)

View File

@@ -1,15 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters" android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent" 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:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:scrollIndicators="top" android:scrollIndicators="top"
@@ -21,7 +50,7 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_centerInParent="true"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -30,7 +59,7 @@
android:id="@+id/textView_holder" android:id="@+id/textView_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_centerInParent="true"
android:layout_marginStart="@dimen/margin_normal" android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal" android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal" android:layout_marginEnd="@dimen/margin_normal"
@@ -42,4 +71,4 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </RelativeLayout>