Add volume headers to chapters list
This commit is contained in:
@@ -13,7 +13,6 @@ import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class BookmarksAdapter(
|
||||
@@ -32,13 +31,6 @@ class BookmarksAdapter(
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is ListHeader) {
|
||||
return item.getText(context)
|
||||
}
|
||||
}
|
||||
return null
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.FlowCollector
|
||||
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@@ -35,4 +36,15 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
|
||||
fun removeListListener(listListener: ListListener<T>) {
|
||||
differ.removeListListener(listListener)
|
||||
}
|
||||
|
||||
fun findHeader(position: Int): ListHeader? {
|
||||
val snapshot = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = snapshot.getOrNull(i) ?: continue
|
||||
if (item is ListHeader) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.Context
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
fun MangaDetails.mapChapters(
|
||||
@@ -61,3 +65,22 @@ fun MangaDetails.mapChapters(
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
var prevVolume = 0
|
||||
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
||||
for (item in this) {
|
||||
val chapter = item.chapter
|
||||
if (chapter.volume != prevVolume) {
|
||||
val text = if (chapter.volume == 0) {
|
||||
context.getString(R.string.volume_unknown)
|
||||
} else {
|
||||
context.getString(R.string.volume_, chapter.volume)
|
||||
}
|
||||
result.add(ListHeader(text))
|
||||
prevVolume = chapter.volume
|
||||
}
|
||||
result.add(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun chapterListItemAD(
|
||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
||||
) = adapterDelegateViewBinding<ChapterListItem, ListModel, ItemChapterBinding>(
|
||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
|
||||
@@ -5,22 +5,20 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ChaptersAdapter(
|
||||
onItemClickListener: OnListItemClickListener<ChapterListItem>,
|
||||
) : BaseListAdapter<ChapterListItem>(), FastScroller.SectionIndexer {
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return items[position].chapter.id
|
||||
addDelegate(ListItemType.CHAPTER, chapterListItemAD(onItemClickListener))
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(null))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val item = items.getOrNull(position) ?: return null
|
||||
return item.chapter.number.toString()
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
@@ -26,6 +29,9 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
@@ -57,13 +63,17 @@ class ChaptersFragment :
|
||||
callback = this,
|
||||
)
|
||||
with(binding.recyclerViewChapters) {
|
||||
addItemDecoration(TypedListSpacingDecoration(context, true))
|
||||
checkNotNull(selectionController).attachToRecyclerView(this)
|
||||
setHasFixedSize(true)
|
||||
isNestedScrollingEnabled = false
|
||||
adapter = chaptersAdapter
|
||||
}
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
viewModel.chapters
|
||||
.map { it.withVolumeHeaders(requireContext()) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
|
||||
binding.textViewHolder.isVisible = it
|
||||
}
|
||||
@@ -144,6 +154,9 @@ class ChaptersFragment :
|
||||
val buffer = HashSet<Long>()
|
||||
var isAdding = false
|
||||
for (x in items) {
|
||||
if (x !is ChapterListItem) {
|
||||
continue
|
||||
}
|
||||
if (x.chapter.id in ids) {
|
||||
isAdding = true
|
||||
if (buffer.isNotEmpty()) {
|
||||
@@ -159,7 +172,13 @@ class ChaptersFragment :
|
||||
}
|
||||
|
||||
R.id.action_select_all -> {
|
||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
||||
val ids = chaptersAdapter?.items?.mapNotNull {
|
||||
if (it is ChapterListItem) {
|
||||
it.chapter.id
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: return false
|
||||
controller.addAll(ids)
|
||||
true
|
||||
}
|
||||
@@ -183,7 +202,15 @@ class ChaptersFragment :
|
||||
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
||||
val selectedIds = selectionController?.peekCheckedIds() ?: return false
|
||||
val allItems = chaptersAdapter?.items.orEmpty()
|
||||
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
|
||||
val items = allItems.withIndex().mapNotNull<IndexedValue<ListModel>, IndexedValue<ChapterListItem>> { x ->
|
||||
val value = x.value
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (value is ChapterListItem && value.chapter.id in selectedIds) {
|
||||
x as IndexedValue<ChapterListItem>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
var canSave = true
|
||||
var canDelete = true
|
||||
items.forEach { (_, x) ->
|
||||
@@ -212,10 +239,10 @@ class ChaptersFragment :
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
private fun onChaptersChanged(list: List<ChapterListItem>) {
|
||||
private fun onChaptersChanged(list: List<ListModel>) {
|
||||
val adapter = chaptersAdapter ?: return
|
||||
if (adapter.itemCount == 0) {
|
||||
val position = list.indexOfFirst { it.isCurrent } - 1
|
||||
val position = list.indexOfFirst { it is ChapterListItem && it.isCurrent } - 1
|
||||
if (position > 0) {
|
||||
val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
|
||||
adapter.setItems(
|
||||
|
||||
@@ -6,7 +6,6 @@ import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
|
||||
class HistoryListAdapter(
|
||||
@@ -17,13 +16,6 @@ class HistoryListAdapter(
|
||||
) : MangaListAdapter(coil, lifecycleOwner, listener, sizeResolver), FastScroller.SectionIndexer {
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is ListHeader) {
|
||||
return item.getText(context)
|
||||
}
|
||||
}
|
||||
return null
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,5 @@ class TypedListSpacingDecoration(
|
||||
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|
||||
|| this == ListItemType.FILTER_SORT
|
||||
|| this == ListItemType.FILTER_TAG
|
||||
|| this == ListItemType.CHAPTER
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
@@ -15,9 +17,13 @@ import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.databinding.SheetChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.mapChapters
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -37,32 +43,48 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val chapters = viewModel.manga?.allChapters
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
val manga = viewModel.manga
|
||||
if (manga == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val currentId = viewModel.getCurrentState()?.chapterId ?: 0L
|
||||
val currentPosition = chapters.indexOfFirst { it.id == currentId }
|
||||
val items = chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
isCurrent = index == currentPosition,
|
||||
isUnread = index > currentPosition,
|
||||
isNew = false,
|
||||
isDownloaded = false,
|
||||
isBookmarked = false,
|
||||
)
|
||||
val state = viewModel.getCurrentState()
|
||||
val currentChapter = state?.let { manga.allChapters.findById(it.chapterId) }
|
||||
val chapters = manga.mapChapters(
|
||||
history = state?.let {
|
||||
MangaHistory(
|
||||
createdAt = Instant.now(),
|
||||
updatedAt = Instant.now(),
|
||||
chapterId = it.chapterId,
|
||||
page = it.page,
|
||||
scroll = it.scroll,
|
||||
percent = PROGRESS_NONE,
|
||||
)
|
||||
},
|
||||
newCount = 0,
|
||||
branch = currentChapter?.branch,
|
||||
bookmarks = listOf(),
|
||||
).withVolumeHeaders(binding.root.context)
|
||||
if (chapters.isEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val currentPosition = if (currentChapter != null) {
|
||||
chapters.indexOfFirst { it is ChapterListItem && it.chapter.id == currentChapter.id }
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.recyclerView.context, true))
|
||||
binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter ->
|
||||
if (currentPosition >= 0) {
|
||||
val targetPosition = (currentPosition - 1).coerceAtLeast(0)
|
||||
val offset =
|
||||
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
|
||||
adapter.setItems(
|
||||
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
|
||||
chapters, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
|
||||
)
|
||||
} else {
|
||||
adapter.items = items
|
||||
adapter.items = chapters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
|
||||
@@ -24,13 +23,6 @@ class PageThumbnailAdapter(
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is ListHeader) {
|
||||
return item.getText(context)
|
||||
}
|
||||
}
|
||||
return null
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
|
||||
@@ -45,13 +44,6 @@ class FeedAdapter(
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is ListHeader) {
|
||||
return item.getText(context)
|
||||
}
|
||||
}
|
||||
return null
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollIndicators="top"
|
||||
app:bubbleSize="small"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_chapter" />
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
android:layout_below="@id/headerBar"
|
||||
android:orientation="vertical"
|
||||
android:scrollIndicators="top"
|
||||
app:bubbleSize="small"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_chapter" />
|
||||
|
||||
@@ -562,4 +562,6 @@
|
||||
<string name="approximate_reading_time">Approximate reading time</string>
|
||||
<string name="approximate_remaining_time">Approximate remaining time</string>
|
||||
<string name="remaining_time_pattern">%1$s %2$s</string>
|
||||
<string name="volume_">Volume %d</string>
|
||||
<string name="volume_unknown">Unknown volume</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user