diff --git a/app/build.gradle b/app/build.gradle index 02789436f..14a32d8dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:8e7d7e0bde') { + implementation('com.github.KotatsuApp:kotatsu-parsers:fcaa0ea442') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksAdapter.kt index 93447eb35..250d57f7e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarksAdapter.kt @@ -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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt index 8b8cec8a4..6f04932b6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt @@ -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 : AsyncListDifferDelegationAdapter( fun removeListListener(listListener: ListListener) { 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 + } } 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 f525f7c8e..3390c4d66 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 @@ -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.withVolumeHeaders(context: Context): List { + var prevVolume = 0 + val result = ArrayList((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 +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt index f309ef29d..feba337f5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt @@ -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, -) = adapterDelegateViewBinding( +) = adapterDelegateViewBinding( { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }, ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt index 2cc6f1f9f..2cdfbd26c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt @@ -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, -) : BaseListAdapter(), FastScroller.SectionIndexer { +) : BaseListAdapter(), 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) } } 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 c78196d1b..aeaaa09a2 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 @@ -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() 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> { x -> + val value = x.value + @Suppress("UNCHECKED_CAST") + if (value is ChapterListItem && value.chapter.id in selectedIds) { + x as IndexedValue + } 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) { + private fun onChaptersChanged(list: List) { 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( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt index 323eba0d3..426d5754e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt @@ -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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index 1ec2dacd8..a75ff38cf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt index 30b609152..e32516fe2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt @@ -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(), 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 } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt index a519b4a4c..b9df32ce2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt @@ -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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt index e39f5055d..2e65e20fc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt @@ -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) } } diff --git a/app/src/main/res/layout/fragment_chapters.xml b/app/src/main/res/layout/fragment_chapters.xml index 884fc6126..82f23d2d1 100644 --- a/app/src/main/res/layout/fragment_chapters.xml +++ b/app/src/main/res/layout/fragment_chapters.xml @@ -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" /> diff --git a/app/src/main/res/layout/sheet_chapters.xml b/app/src/main/res/layout/sheet_chapters.xml index 378496dc1..ed8566f42 100644 --- a/app/src/main/res/layout/sheet_chapters.xml +++ b/app/src/main/res/layout/sheet_chapters.xml @@ -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" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 61b1b3443..ce3d9b64a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -562,4 +562,6 @@ Approximate reading time Approximate remaining time %1$s %2$s + Volume %d + Unknown volume