Save state of selection in lists
This commit is contained in:
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
import androidx.appcompat.widget.ActionBarContextView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -82,9 +83,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||||
// ActivityCompat.recreate(this)
|
ActivityCompat.recreate(this)
|
||||||
throw RuntimeException("Test crash")
|
return true
|
||||||
// return true
|
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.savedstate.SavedStateRegistry
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
|
||||||
|
private const val KEY_SELECTION = "selection"
|
||||||
|
private const val PROVIDER_NAME = "selection_decoration"
|
||||||
|
|
||||||
|
class ListSelectionController(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val decoration: AbstractSelectionItemDecoration,
|
||||||
|
private val registryOwner: SavedStateRegistryOwner,
|
||||||
|
private val callback: Callback,
|
||||||
|
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
|
||||||
|
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
private val stateEventObserver = StateEventObserver()
|
||||||
|
|
||||||
|
val count: Int
|
||||||
|
get() = decoration.checkedItemsCount
|
||||||
|
|
||||||
|
fun snapshot(): Set<Long> {
|
||||||
|
return peekCheckedIds().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun peekCheckedIds(): Set<Long> {
|
||||||
|
return decoration.checkedItemsIds
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
decoration.clearSelection()
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(ids: Collection<Long>) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoration.checkAll(ids)
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachToRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
recyclerView.addItemDecoration(decoration)
|
||||||
|
registryOwner.lifecycle.addObserver(stateEventObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveState(): Bundle {
|
||||||
|
val bundle = Bundle(1)
|
||||||
|
bundle.putLongArray(KEY_SELECTION, peekCheckedIds().toLongArray())
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemClick(id: Long): Boolean {
|
||||||
|
if (decoration.checkedItemsCount != 0) {
|
||||||
|
decoration.toggleItemChecked(id)
|
||||||
|
if (decoration.checkedItemsCount == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
notifySelectionChanged()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemLongClick(id: Long): Boolean {
|
||||||
|
startActionMode()
|
||||||
|
return actionMode?.also {
|
||||||
|
decoration.setItemIsChecked(id, true)
|
||||||
|
notifySelectionChanged()
|
||||||
|
} != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return callback.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return callback.onPrepareActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return callback.onActionItemClicked(mode, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
callback.onDestroyActionMode(mode)
|
||||||
|
clear()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startActionMode() {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifySelectionChanged() {
|
||||||
|
val count = decoration.checkedItemsCount
|
||||||
|
callback.onSelectionChanged(count)
|
||||||
|
if (count == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreState(ids: Collection<Long>) {
|
||||||
|
if (ids.isEmpty() || decoration.checkedItemsCount != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoration.checkAll(ids)
|
||||||
|
startActionMode()
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : ActionMode.Callback {
|
||||||
|
|
||||||
|
fun onSelectionChanged(count: Int)
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateEventObserver : LifecycleEventObserver {
|
||||||
|
|
||||||
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
if (event == Lifecycle.Event.ON_CREATE) {
|
||||||
|
val registry = registryOwner.savedStateRegistry
|
||||||
|
registry.registerSavedStateProvider(PROVIDER_NAME, this@ListSelectionController)
|
||||||
|
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
|
||||||
|
if (state != null) {
|
||||||
|
restoreState(state.getLongArray(KEY_SELECTION)?.toList().orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import android.os.Bundle
|
|||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
@@ -16,6 +15,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
||||||
@@ -34,16 +34,15 @@ import kotlin.math.roundToInt
|
|||||||
class ChaptersFragment :
|
class ChaptersFragment :
|
||||||
BaseFragment<FragmentChaptersBinding>(),
|
BaseFragment<FragmentChaptersBinding>(),
|
||||||
OnListItemClickListener<ChapterListItem>,
|
OnListItemClickListener<ChapterListItem>,
|
||||||
ActionMode.Callback,
|
|
||||||
AdapterView.OnItemSelectedListener,
|
AdapterView.OnItemSelectedListener,
|
||||||
MenuItem.OnActionExpandListener,
|
MenuItem.OnActionExpandListener,
|
||||||
SearchView.OnQueryTextListener {
|
SearchView.OnQueryTextListener,
|
||||||
|
ListSelectionController.Callback {
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||||
|
|
||||||
private var chaptersAdapter: ChaptersAdapter? = null
|
private var chaptersAdapter: ChaptersAdapter? = null
|
||||||
private var actionMode: ActionMode? = null
|
private var selectionController: ListSelectionController? = null
|
||||||
private var selectionDecoration: ChaptersSelectionDecoration? = null
|
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -53,9 +52,14 @@ class ChaptersFragment :
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
chaptersAdapter = ChaptersAdapter(this)
|
chaptersAdapter = ChaptersAdapter(this)
|
||||||
selectionDecoration = ChaptersSelectionDecoration(view.context)
|
selectionController = ListSelectionController(
|
||||||
|
activity = requireActivity(),
|
||||||
|
decoration = ChaptersSelectionDecoration(view.context),
|
||||||
|
registryOwner = this,
|
||||||
|
callback = this,
|
||||||
|
)
|
||||||
with(binding.recyclerViewChapters) {
|
with(binding.recyclerViewChapters) {
|
||||||
addItemDecoration(selectionDecoration!!)
|
checkNotNull(selectionController).attachToRecyclerView(this)
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = chaptersAdapter
|
adapter = chaptersAdapter
|
||||||
}
|
}
|
||||||
@@ -74,20 +78,13 @@ class ChaptersFragment :
|
|||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
chaptersAdapter = null
|
chaptersAdapter = null
|
||||||
selectionDecoration = null
|
selectionController = null
|
||||||
binding.spinnerBranches?.adapter = null
|
binding.spinnerBranches?.adapter = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
if (selectionController?.onItemClick(item.chapter.id) == true) {
|
||||||
selectionDecoration?.toggleItemChecked(item.chapter.id)
|
|
||||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
|
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
|
||||||
@@ -106,14 +103,7 @@ class ChaptersFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
|
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
|
||||||
if (actionMode == null) {
|
return selectionController?.onItemLongClick(item.chapter.id) ?: false
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
return actionMode?.also {
|
|
||||||
selectionDecoration?.setItemIsChecked(item.chapter.id, true)
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
it.invalidate()
|
|
||||||
} != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
@@ -122,13 +112,13 @@ class ChaptersFragment :
|
|||||||
DownloadService.start(
|
DownloadService.start(
|
||||||
context ?: return false,
|
context ?: return false,
|
||||||
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
|
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
|
||||||
selectionDecoration?.checkedItemsIds?.toSet()
|
selectionController?.snapshot(),
|
||||||
)
|
)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
val ids = selectionDecoration?.checkedItemsIds
|
val ids = selectionController?.peekCheckedIds()
|
||||||
val manga = viewModel.manga.value
|
val manga = viewModel.manga.value
|
||||||
when {
|
when {
|
||||||
ids.isNullOrEmpty() || manga == null -> Unit
|
ids.isNullOrEmpty() || manga == null -> Unit
|
||||||
@@ -147,9 +137,7 @@ class ChaptersFragment :
|
|||||||
}
|
}
|
||||||
R.id.action_select_all -> {
|
R.id.action_select_all -> {
|
||||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
||||||
selectionDecoration?.checkAll(ids)
|
selectionController?.addAll(ids)
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
mode.invalidate()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
@@ -169,7 +157,7 @@ class ChaptersFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val selectedIds = selectionDecoration?.checkedItemsIds ?: return false
|
val selectedIds = selectionController?.peekCheckedIds() ?: return false
|
||||||
val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty()
|
val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty()
|
||||||
menu.findItem(R.id.action_save).isVisible = items.none { x ->
|
menu.findItem(R.id.action_save).isVisible = items.none { x ->
|
||||||
x.chapter.source == MangaSource.LOCAL
|
x.chapter.source == MangaSource.LOCAL
|
||||||
@@ -181,10 +169,8 @@ class ChaptersFragment :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
override fun onSelectionChanged(count: Int) {
|
||||||
selectionDecoration?.clearSelection()
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
binding.recyclerViewChapters.invalidateItemDecorations()
|
||||||
actionMode = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
|
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
@@ -18,6 +17,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
|
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
|
||||||
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
|
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
||||||
@@ -46,12 +46,11 @@ abstract class MangaListFragment :
|
|||||||
PaginationScrollListener.Callback,
|
PaginationScrollListener.Callback,
|
||||||
MangaListListener,
|
MangaListListener,
|
||||||
SwipeRefreshLayout.OnRefreshListener,
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
ActionMode.Callback {
|
ListSelectionController.Callback {
|
||||||
|
|
||||||
private var listAdapter: MangaListAdapter? = null
|
private var listAdapter: MangaListAdapter? = null
|
||||||
private var paginationListener: PaginationScrollListener? = null
|
private var paginationListener: PaginationScrollListener? = null
|
||||||
private var selectionDecoration: MangaSelectionDecoration? = null
|
private var selectionController: ListSelectionController? = null
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
private val spanResolver = MangaListSpanResolver()
|
private val spanResolver = MangaListSpanResolver()
|
||||||
private val spanSizeLookup = SpanSizeLookup()
|
private val spanSizeLookup = SpanSizeLookup()
|
||||||
private val listCommitCallback = Runnable {
|
private val listCommitCallback = Runnable {
|
||||||
@@ -62,7 +61,7 @@ abstract class MangaListFragment :
|
|||||||
protected abstract val viewModel: MangaListViewModel
|
protected abstract val viewModel: MangaListViewModel
|
||||||
|
|
||||||
protected val selectedItemsIds: Set<Long>
|
protected val selectedItemsIds: Set<Long>
|
||||||
get() = selectionDecoration?.checkedItemsIds?.toSet().orEmpty()
|
get() = selectionController?.snapshot().orEmpty()
|
||||||
|
|
||||||
protected val selectedItems: Set<Manga>
|
protected val selectedItems: Set<Manga>
|
||||||
get() = collectSelectedItems()
|
get() = collectSelectedItems()
|
||||||
@@ -79,12 +78,17 @@ abstract class MangaListFragment :
|
|||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
listener = this,
|
listener = this,
|
||||||
)
|
)
|
||||||
selectionDecoration = MangaSelectionDecoration(view.context)
|
selectionController = ListSelectionController(
|
||||||
|
activity = requireActivity(),
|
||||||
|
decoration = MangaSelectionDecoration(view.context),
|
||||||
|
registryOwner = this,
|
||||||
|
callback = this,
|
||||||
|
)
|
||||||
paginationListener = PaginationScrollListener(4, this)
|
paginationListener = PaginationScrollListener(4, this)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = listAdapter
|
adapter = listAdapter
|
||||||
addItemDecoration(selectionDecoration!!)
|
checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView)
|
||||||
addOnScrollListener(paginationListener!!)
|
addOnScrollListener(paginationListener!!)
|
||||||
}
|
}
|
||||||
with(binding.swipeRefreshLayout) {
|
with(binding.swipeRefreshLayout) {
|
||||||
@@ -105,34 +109,19 @@ abstract class MangaListFragment :
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
listAdapter = null
|
listAdapter = null
|
||||||
paginationListener = null
|
paginationListener = null
|
||||||
selectionDecoration = null
|
selectionController = null
|
||||||
spanSizeLookup.invalidateCache()
|
spanSizeLookup.invalidateCache()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, view: View) {
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
if (selectionController?.onItemClick(item.id) != true) {
|
||||||
selectionDecoration?.toggleItemChecked(item.id)
|
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
binding.recyclerView.invalidateItemDecorations()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
||||||
if (actionMode == null) {
|
return selectionController?.onItemLongClick(item.id) ?: false
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
return actionMode?.also {
|
|
||||||
selectionDecoration?.setItemIsChecked(item.id, true)
|
|
||||||
binding.recyclerView.invalidateItemDecorations()
|
|
||||||
it.invalidate()
|
|
||||||
} != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@@ -249,7 +238,7 @@ abstract class MangaListFragment :
|
|||||||
addOnLayoutChangeListener(spanResolver)
|
addOnLayoutChangeListener(spanResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectionDecoration?.let { addItemDecoration(it) }
|
selectionController?.attachToRecyclerView(binding.recyclerView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +248,7 @@ abstract class MangaListFragment :
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.title = selectionDecoration?.checkedItemsCount?.toString()
|
mode.title = selectionController?.count?.toString()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,9 +258,7 @@ abstract class MangaListFragment :
|
|||||||
val ids = listAdapter?.items?.mapNotNull {
|
val ids = listAdapter?.items?.mapNotNull {
|
||||||
(it as? MangaItemModel)?.id
|
(it as? MangaItemModel)?.id
|
||||||
} ?: return false
|
} ?: return false
|
||||||
selectionDecoration?.checkAll(ids)
|
selectionController?.addAll(ids)
|
||||||
binding.recyclerView.invalidateItemDecorations()
|
|
||||||
mode.invalidate()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_share -> {
|
R.id.action_share -> {
|
||||||
@@ -293,14 +280,12 @@ abstract class MangaListFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onSelectionChanged(count: Int) {
|
||||||
selectionDecoration?.clearSelection()
|
|
||||||
binding.recyclerView.invalidateItemDecorations()
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
actionMode = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun collectSelectedItems(): Set<Manga> {
|
private fun collectSelectedItems(): Set<Manga> {
|
||||||
val checkedIds = selectionDecoration?.checkedItemsIds ?: return emptySet()
|
val checkedIds = selectionController?.peekCheckedIds() ?: return emptySet()
|
||||||
val items = listAdapter?.items ?: return emptySet()
|
val items = listAdapter?.items ?: return emptySet()
|
||||||
val result = ArraySet<Manga>(checkedIds.size)
|
val result = ArraySet<Manga>(checkedIds.size)
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
|
|||||||
@@ -145,8 +145,15 @@ class MainActivity :
|
|||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
drawerToggle?.isDrawerIndicatorEnabled =
|
val isSearchOpened = isSearchOpened()
|
||||||
drawer?.getDrawerLockMode(GravityCompat.START) == DrawerLayout.LOCK_MODE_UNLOCKED
|
adjustDrawerLock(isSearchOpened)
|
||||||
|
if (isSearchOpened) {
|
||||||
|
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||||
|
scrollFlags = SCROLL_FLAG_NO_SCROLL
|
||||||
|
}
|
||||||
|
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
|
||||||
|
binding.appbar.updatePadding(left = 0, right = 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -396,29 +403,31 @@ class MainActivity :
|
|||||||
|
|
||||||
private fun onSearchOpened() {
|
private fun onSearchOpened() {
|
||||||
TransitionManager.beginDelayedTransition(binding.appbar)
|
TransitionManager.beginDelayedTransition(binding.appbar)
|
||||||
drawerToggle?.isDrawerIndicatorEnabled = false
|
|
||||||
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
|
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||||
scrollFlags = SCROLL_FLAG_NO_SCROLL
|
scrollFlags = SCROLL_FLAG_NO_SCROLL
|
||||||
}
|
}
|
||||||
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
|
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
|
||||||
binding.appbar.updatePadding(left = 0, right = 0)
|
binding.appbar.updatePadding(left = 0, right = 0)
|
||||||
adjustDrawerLock()
|
adjustDrawerLock(isSearchOpened = true)
|
||||||
adjustFabVisibility(isSearchOpened = true)
|
adjustFabVisibility(isSearchOpened = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSearchClosed() {
|
private fun onSearchClosed() {
|
||||||
TransitionManager.beginDelayedTransition(binding.appbar)
|
TransitionManager.beginDelayedTransition(binding.appbar)
|
||||||
drawerToggle?.isDrawerIndicatorEnabled = true
|
|
||||||
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
|
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||||
scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS
|
scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS
|
||||||
}
|
}
|
||||||
binding.appbar.background = null
|
binding.appbar.background = null
|
||||||
val padding = resources.getDimensionPixelOffset(R.dimen.margin_normal)
|
val padding = resources.getDimensionPixelOffset(R.dimen.margin_normal)
|
||||||
binding.appbar.updatePadding(left = padding, right = padding)
|
binding.appbar.updatePadding(left = padding, right = padding)
|
||||||
adjustDrawerLock()
|
adjustDrawerLock(isSearchOpened = false)
|
||||||
adjustFabVisibility(isSearchOpened = false)
|
adjustFabVisibility(isSearchOpened = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isSearchOpened(): Boolean {
|
||||||
|
return supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true
|
||||||
|
}
|
||||||
|
|
||||||
private fun onFirstStart() {
|
private fun onFirstStart() {
|
||||||
lifecycleScope.launchWhenResumed {
|
lifecycleScope.launchWhenResumed {
|
||||||
val isUpdateSupported = withContext(Dispatchers.Default) {
|
val isUpdateSupported = withContext(Dispatchers.Default) {
|
||||||
@@ -440,7 +449,7 @@ class MainActivity :
|
|||||||
private fun adjustFabVisibility(
|
private fun adjustFabVisibility(
|
||||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
|
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
|
||||||
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY),
|
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY),
|
||||||
isSearchOpened: Boolean = supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true,
|
isSearchOpened: Boolean = isSearchOpened(),
|
||||||
) {
|
) {
|
||||||
val fab = binding.fab
|
val fab = binding.fab
|
||||||
if (isResumeEnabled && !isSearchOpened && topFragment is HistoryListFragment) {
|
if (isResumeEnabled && !isSearchOpened && topFragment is HistoryListFragment) {
|
||||||
@@ -454,12 +463,15 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun adjustDrawerLock() {
|
private fun adjustDrawerLock(
|
||||||
|
isSearchOpened: Boolean = isSearchOpened(),
|
||||||
|
) {
|
||||||
val drawer = drawer ?: return
|
val drawer = drawer ?: return
|
||||||
val isLocked = actionModeDelegate.isActionModeStarted || (drawerToggle?.isDrawerIndicatorEnabled == false)
|
val isLocked = actionModeDelegate.isActionModeStarted || isSearchOpened
|
||||||
drawer.setDrawerLockMode(
|
drawer.setDrawerLockMode(
|
||||||
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED
|
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED
|
||||||
)
|
)
|
||||||
|
drawerToggle?.isDrawerIndicatorEnabled = !isLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class VoiceInputCallback : ActivityResultCallback<String?> {
|
private inner class VoiceInputCallback : ActivityResultCallback<String?> {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
|
|||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
@@ -32,14 +33,14 @@ import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
|||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.findViewsByType
|
import org.koitharu.kotatsu.utils.ext.findViewsByType
|
||||||
|
|
||||||
class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener, ActionMode.Callback {
|
class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener,
|
||||||
|
ListSelectionController.Callback {
|
||||||
|
|
||||||
private val viewModel by viewModel<MultiSearchViewModel> {
|
private val viewModel by viewModel<MultiSearchViewModel> {
|
||||||
parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty())
|
parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty())
|
||||||
}
|
}
|
||||||
private lateinit var adapter: MultiSearchAdapter
|
private lateinit var adapter: MultiSearchAdapter
|
||||||
private lateinit var selectionDecoration: MangaSelectionDecoration
|
private lateinit var selectionController: ListSelectionController
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -51,7 +52,13 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sizeResolver = ItemSizeResolver(resources, get())
|
val sizeResolver = ItemSizeResolver(resources, get())
|
||||||
selectionDecoration = MangaSelectionDecoration(this)
|
val selectionDecoration = MangaSelectionDecoration(this)
|
||||||
|
selectionController = ListSelectionController(
|
||||||
|
activity = this,
|
||||||
|
decoration = selectionDecoration,
|
||||||
|
registryOwner = this,
|
||||||
|
callback = this,
|
||||||
|
)
|
||||||
adapter = MultiSearchAdapter(
|
adapter = MultiSearchAdapter(
|
||||||
lifecycleOwner = this,
|
lifecycleOwner = this,
|
||||||
coil = get(),
|
coil = get(),
|
||||||
@@ -90,29 +97,14 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, view: View) {
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
if (selectionDecoration.checkedItemsCount != 0) {
|
if (!selectionController.onItemClick(item.id)) {
|
||||||
selectionDecoration.toggleItemChecked(item.id)
|
val intent = DetailsActivity.newIntent(this, item)
|
||||||
if (selectionDecoration.checkedItemsCount == 0) {
|
startActivity(intent)
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
invalidateItemDecorations()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val intent = DetailsActivity.newIntent(this, item)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
||||||
if (actionMode == null) {
|
return selectionController.onItemLongClick(item.id)
|
||||||
actionMode = startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
return actionMode?.also {
|
|
||||||
selectionDecoration.setItemIsChecked(item.id, true)
|
|
||||||
invalidateItemDecorations()
|
|
||||||
it.invalidate()
|
|
||||||
} != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) {
|
override fun onRetryClick(error: Throwable) {
|
||||||
@@ -131,7 +123,7 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.title = selectionDecoration.checkedItemsCount.toString()
|
mode.title = selectionController.count.toString()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,22 +148,16 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onSelectionChanged(count: Int) {
|
||||||
selectionDecoration.clearSelection()
|
|
||||||
invalidateItemDecorations()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectSelectedItems(): Set<Manga> {
|
|
||||||
return viewModel.getItems(selectionDecoration.checkedItemsIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateItemDecorations() {
|
|
||||||
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
|
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
|
||||||
it.invalidateItemDecorations()
|
it.invalidateItemDecorations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun collectSelectedItems(): Set<Manga> {
|
||||||
|
return viewModel.getItems(selectionController.peekCheckedIds())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_QUERY = "query"
|
private const val EXTRA_QUERY = "query"
|
||||||
|
|||||||
Reference in New Issue
Block a user