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>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
ListSelectionController.Callback2,
ListSelectionController.Callback,
FastScroller.FastScrollListener, ListHeaderClickListener {
@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 decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2,
private val callback: Callback,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
@@ -130,43 +130,7 @@ class ListSelectionController(
notifySelectionChanged()
}
@Deprecated("")
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 {
interface Callback {
fun onSelectionChanged(controller: ListSelectionController, count: Int)

View File

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

View File

@@ -40,7 +40,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark>, ListSelectionController.Callback2 {
OnListItemClickListener<Bookmark>, ListSelectionController.Callback {
private val activityViewModel by activityViewModels<DetailsViewModel>()
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.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.ancestors
import androidx.core.view.isVisible
@@ -14,14 +11,12 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
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.list.ListSelectionController
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.observe
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.details.ui.DetailsViewModel
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.list.ui.adapter.TypedListSpacingDecoration
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.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -50,8 +42,7 @@ import kotlin.math.roundToInt
class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>,
ListSelectionController.Callback2 {
OnListItemClickListener<ChapterListItem> {
private val viewModel by activityViewModels<DetailsViewModel>()
@@ -70,7 +61,7 @@ class ChaptersFragment :
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = ChaptersSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
callback = ChaptersSelectionCallback(viewModel, binding.recyclerViewChapters),
)
viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView ->
binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) {
@@ -127,126 +118,6 @@ class ChaptersFragment :
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
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
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
DownloadItemListener,
ListSelectionController.Callback2 {
ListSelectionController.Callback {
@Inject
lateinit var coil: ImageLoader

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.domain
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -24,6 +25,14 @@ class HistoryListQuickFilter @Inject constructor(
}
add(ListFilterOption.Macro.COMPLETED)
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) {
add(ListFilterOption.Macro.NSFW)
}

View File

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

View File

@@ -45,7 +45,7 @@ import javax.inject.Inject
class MultiSearchActivity :
BaseActivity<ActivitySearchMultiBinding>(),
MangaListListener,
ListSelectionController.Callback2 {
ListSelectionController.Callback {
@Inject
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:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
app:bubbleSize="small"
android:padding="@dimen/list_spacing_normal"
app:bubbleSize="small"
tools:layoutManager="org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager"
tools:listitem="@layout/item_manga_list" />

View File

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

View File

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