Fix chapters selections

This commit is contained in:
Koitharu
2024-08-26 14:17:51 +03:00
parent 6b786084cf
commit d588e8d941
17 changed files with 170 additions and 178 deletions

View File

@@ -46,7 +46,7 @@ class AllBookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(), BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener, ListStateHolderListener,
OnListItemClickListener<Bookmark>, OnListItemClickListener<Bookmark>,
ListSelectionController.Callback2, ListSelectionController.Callback,
FastScroller.FastScrollListener, ListHeaderClickListener { FastScroller.FastScrollListener, ListHeaderClickListener {
@Inject @Inject

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.ui.list
import androidx.recyclerview.widget.RecyclerView
abstract class BaseListSelectionCallback(
protected val recyclerView: RecyclerView,
) : ListSelectionController.Callback {
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
recyclerView.invalidateItemDecorations()
}
}

View File

@@ -25,7 +25,7 @@ class ListSelectionController(
private val appCompatDelegate: AppCompatDelegate, private val appCompatDelegate: AppCompatDelegate,
private val decoration: AbstractSelectionItemDecoration, private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner, private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2, private val callback: Callback,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider { ) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
@@ -130,43 +130,7 @@ class ListSelectionController(
notifySelectionChanged() notifySelectionChanged()
} }
@Deprecated("") interface Callback {
interface Callback : Callback2 {
fun onSelectionChanged(count: Int)
fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
fun onDestroyActionMode(mode: ActionMode) = Unit
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
onSelectionChanged(count)
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(
controller: ListSelectionController,
mode: ActionMode,
item: MenuItem,
): Boolean = onActionItemClicked(mode, item)
override fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) {
onDestroyActionMode(mode)
}
}
interface Callback2 {
fun onSelectionChanged(controller: ListSelectionController, count: Int) fun onSelectionChanged(controller: ListSelectionController, count: Int)

View File

@@ -93,8 +93,8 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
} }
override fun onActionModeStarted(mode: ActionMode) { override fun onActionModeStarted(mode: ActionMode) {
expandAndLock()
viewBinding?.toolbar?.menuView?.isVisible = false viewBinding?.toolbar?.menuView?.isVisible = false
view?.post(::expandAndLock)
} }
override fun onActionModeFinished(mode: ActionMode) { override fun onActionModeFinished(mode: ActionMode) {

View File

@@ -40,7 +40,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(), class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark>, ListSelectionController.Callback2 { OnListItemClickListener<Bookmark>, ListSelectionController.Callback {
private val activityViewModel by activityViewModels<DetailsViewModel>() private val activityViewModel by activityViewModels<DetailsViewModel>()
private val viewModel by viewModels<BookmarksViewModel>() private val viewModel by viewModels<BookmarksViewModel>()

View File

@@ -2,11 +2,8 @@ package org.koitharu.kotatsu.details.ui.pager.chapters
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ancestors import androidx.core.view.ancestors
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -14,14 +11,12 @@ import androidx.fragment.app.activityViewModels
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.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
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
@@ -32,8 +27,6 @@ import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.findParentCallback import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.toCollection
import org.koitharu.kotatsu.core.util.ext.toSet
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
@@ -42,7 +35,6 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.withVolumeHeaders import org.koitharu.kotatsu.details.ui.withVolumeHeaders
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.local.ui.LocalChaptersRemoveService
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -50,8 +42,7 @@ import kotlin.math.roundToInt
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>, OnListItemClickListener<ChapterListItem> {
ListSelectionController.Callback2 {
private val viewModel by activityViewModels<DetailsViewModel>() private val viewModel by activityViewModels<DetailsViewModel>()
@@ -70,7 +61,7 @@ class ChaptersFragment :
appCompatDelegate = checkNotNull(findAppCompatDelegate()), appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = ChaptersSelectionDecoration(binding.root.context), decoration = ChaptersSelectionDecoration(binding.root.context),
registryOwner = this, registryOwner = this,
callback = this, callback = ChaptersSelectionCallback(viewModel, binding.recyclerViewChapters),
) )
viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView -> viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView ->
binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) { binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) {
@@ -127,126 +118,6 @@ class ChaptersFragment :
return selectionController?.onItemLongClick(item.chapter.id) ?: false return selectionController?.onItemLongClick(item.chapter.id) ?: false
} }
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
viewModel.download(selectionController?.snapshot())
mode.finish()
true
}
R.id.action_delete -> {
val ids = selectionController?.peekCheckedIds()
val manga = viewModel.manga.value
when {
ids == null || ids.isEmpty() || manga == null -> Unit
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
else -> {
LocalChaptersRemoveService.start(requireContext(), manga, ids.toSet())
Snackbar.make(
requireViewBinding().recyclerViewChapters,
R.string.chapters_will_removed_background,
Snackbar.LENGTH_LONG,
).show()
}
}
mode.finish()
true
}
R.id.action_select_range -> {
val items = chaptersAdapter?.items ?: return false
val ids = controller.peekCheckedIds().toCollection(HashSet())
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()) {
ids.addAll(buffer)
buffer.clear()
}
} else if (isAdding) {
buffer.add(x.chapter.id)
}
}
controller.addAll(ids)
true
}
R.id.action_select_all -> {
val ids = chaptersAdapter?.items?.mapNotNull {
if (it is ChapterListItem) {
it.chapter.id
} else {
null
}
} ?: return false
controller.addAll(ids)
true
}
R.id.action_mark_current -> {
val ids = controller.peekCheckedIds()
if (ids.size == 1) {
viewModel.markChapterAsCurrent(ids.first())
} else {
return false
}
mode.finish()
true
}
else -> false
}
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
return true
}
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().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) ->
val isLocal = x.isDownloaded || x.chapter.source == LocalMangaSource
if (isLocal) canSave = false else canDelete = false
}
menu.findItem(R.id.action_save).isVisible = canSave
menu.findItem(R.id.action_delete).isVisible = canDelete
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
menu.findItem(R.id.action_mark_current).isVisible = items.size == 1
mode.title = items.size.toString()
var hasGap = false
for (i in 0 until items.size - 1) {
if (items[i].index + 1 != items[i + 1].index) {
hasGap = true
break
}
}
menu.findItem(R.id.action_select_range).isVisible = hasGap
return true
}
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
viewBinding?.recyclerViewChapters?.invalidateItemDecorations()
}
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun onChaptersChanged(list: List<ListModel>) { private fun onChaptersChanged(list: List<ListModel>) {

View File

@@ -0,0 +1,122 @@
package org.koitharu.kotatsu.details.ui.pager.chapters
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.toCollection
import org.koitharu.kotatsu.core.util.ext.toSet
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
class ChaptersSelectionCallback(
private val viewModel: DetailsViewModel,
recyclerView: RecyclerView,
) : BaseListSelectionCallback(recyclerView) {
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
return true
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
val selectedIds = controller.peekCheckedIds()
val allItems = viewModel.chapters.value
val items = allItems.withIndex().filter { it.value.chapter.id in selectedIds }
var canSave = true
var canDelete = true
items.forEach { (_, x) ->
val isLocal = x.isDownloaded || x.chapter.source == LocalMangaSource
if (isLocal) canSave = false else canDelete = false
}
menu.findItem(R.id.action_save).isVisible = canSave
menu.findItem(R.id.action_delete).isVisible = canDelete
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
menu.findItem(R.id.action_mark_current).isVisible = items.size == 1
mode.title = items.size.toString()
var hasGap = false
for (i in 0 until items.size - 1) {
if (items[i].index + 1 != items[i + 1].index) {
hasGap = true
break
}
}
menu.findItem(R.id.action_select_range).isVisible = hasGap
return true
}
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
viewModel.download(controller.snapshot())
mode.finish()
true
}
R.id.action_delete -> {
val ids = controller.peekCheckedIds()
val manga = viewModel.manga.value
when {
ids.isEmpty() || manga == null -> Unit
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
else -> {
LocalChaptersRemoveService.start(recyclerView.context, manga, ids.toSet())
Snackbar.make(
recyclerView,
R.string.chapters_will_removed_background,
Snackbar.LENGTH_LONG,
).show()
}
}
mode.finish()
true
}
R.id.action_select_range -> {
val items = viewModel.chapters.value
val ids = controller.peekCheckedIds().toCollection(HashSet())
val buffer = HashSet<Long>()
var isAdding = false
for (x in items) {
if (x.chapter.id in ids) {
isAdding = true
if (buffer.isNotEmpty()) {
ids.addAll(buffer)
buffer.clear()
}
} else if (isAdding) {
buffer.add(x.chapter.id)
}
}
controller.addAll(ids)
true
}
R.id.action_select_all -> {
val ids = viewModel.chapters.value.map {
it.chapter.id
}
controller.addAll(ids)
true
}
R.id.action_mark_current -> {
val ids = controller.peekCheckedIds()
if (ids.size == 1) {
viewModel.markChapterAsCurrent(ids.first())
} else {
return false
}
mode.finish()
true
}
else -> false
}
}
}

View File

@@ -27,7 +27,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(), class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
DownloadItemListener, DownloadItemListener,
ListSelectionController.Callback2 { ListSelectionController.Callback {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader

View File

@@ -56,7 +56,7 @@ class ExploreFragment :
BaseFragment<FragmentExploreBinding>(), BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner, RecyclerViewOwner,
ExploreListEventListener, ExploreListEventListener,
OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback2 { OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader

View File

@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
class CategoriesSelectionCallback( class CategoriesSelectionCallback(
private val recyclerView: RecyclerView, private val recyclerView: RecyclerView,
private val viewModel: FavouritesCategoriesViewModel, private val viewModel: FavouritesCategoriesViewModel,
) : ListSelectionController.Callback2 { ) : ListSelectionController.Callback {
override fun onSelectionChanged(controller: ListSelectionController, count: Int) { override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
recyclerView.invalidateItemDecorations() recyclerView.invalidateItemDecorations()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.domain package org.koitharu.kotatsu.history.domain
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -24,6 +25,14 @@ class HistoryListQuickFilter @Inject constructor(
} }
add(ListFilterOption.Macro.COMPLETED) add(ListFilterOption.Macro.COMPLETED)
add(ListFilterOption.Macro.FAVORITE) add(ListFilterOption.Macro.FAVORITE)
add(
ListFilterOption.Inverted(
option = ListFilterOption.Macro.FAVORITE,
iconResId = R.drawable.ic_heart_off,
titleResId = R.string.not_in_favorites,
titleText = null,
),
)
if (!settings.isNsfwContentDisabled && !settings.isHistoryExcludeNsfw) { if (!settings.isNsfwContentDisabled && !settings.isHistoryExcludeNsfw) {
add(ListFilterOption.Macro.NSFW) add(ListFilterOption.Macro.NSFW)
} }

View File

@@ -68,7 +68,7 @@ abstract class MangaListFragment :
PaginationScrollListener.Callback, PaginationScrollListener.Callback,
MangaListListener, MangaListListener,
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
ListSelectionController.Callback2, ListSelectionController.Callback,
FastScroller.FastScrollListener { FastScroller.FastScrollListener {
@Inject @Inject

View File

@@ -45,7 +45,7 @@ import javax.inject.Inject
class MultiSearchActivity : class MultiSearchActivity :
BaseActivity<ActivitySearchMultiBinding>(), BaseActivity<ActivitySearchMultiBinding>(),
MangaListListener, MangaListListener,
ListSelectionController.Callback2 { ListSelectionController.Callback {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M2.39,1.73L1.11,3L3.19,5.08C2.45,6 2,7.19 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C14.32,19.24 15.14,18.5 15.9,17.79L20,22L21.27,20.73M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,7.74 4.22,7.06 4.61,6.5L14.5,16.37C13.74,17.06 12.95,17.78 12.1,18.55M8.3,5.1L6.33,3.13C6.7,3.05 7.1,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,10.84 20.69,12.92 18.47,15.27L17.06,13.86C18.91,11.88 20,10.2 20,8.5C20,6.5 18.5,5 16.5,5C15.1,5 13.74,5.83 13.11,7H10.89C10.38,6.06 9.39,5.34 8.3,5.1Z" />
</vector>

View File

@@ -15,10 +15,11 @@
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:bubbleSize="small"
android:padding="@dimen/list_spacing_normal" android:padding="@dimen/list_spacing_normal"
app:bubbleSize="small"
tools:layoutManager="org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager" tools:layoutManager="org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager"
tools:listitem="@layout/item_manga_list" /> tools:listitem="@layout/item_manga_list" />

View File

@@ -11,6 +11,7 @@
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/list_spacing_normal" android:padding="@dimen/list_spacing_normal"

View File

@@ -685,4 +685,5 @@
<string name="sfw">SFW</string> <string name="sfw">SFW</string>
<string name="skip_all">Skip all</string> <string name="skip_all">Skip all</string>
<string name="stuck">Stuck</string> <string name="stuck">Stuck</string>
<string name="not_in_favorites">Not in favoites</string>
</resources> </resources>