Remove from library action
This commit is contained in:
@@ -12,9 +12,9 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.savedstate.SavedStateRegistry
|
import androidx.savedstate.SavedStateRegistry
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
private const val KEY_SELECTION = "selection"
|
private const val KEY_SELECTION = "selection"
|
||||||
private const val PROVIDER_NAME = "selection_decoration"
|
private const val PROVIDER_NAME = "selection_decoration"
|
||||||
@@ -159,7 +159,7 @@ class ListSelectionController(
|
|||||||
override fun onActionItemClicked(
|
override fun onActionItemClicked(
|
||||||
controller: ListSelectionController,
|
controller: ListSelectionController,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
item: MenuItem
|
item: MenuItem,
|
||||||
): Boolean = onActionItemClicked(mode, item)
|
): Boolean = onActionItemClicked(mode, item)
|
||||||
|
|
||||||
override fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) {
|
override fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) {
|
||||||
@@ -173,7 +173,10 @@ class ListSelectionController(
|
|||||||
|
|
||||||
fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
|
fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
|
fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.title = controller.count.toString()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean
|
fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean
|
||||||
|
|
||||||
@@ -197,4 +200,4 @@ class ListSelectionController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.savedstate.SavedStateRegistry
|
import androidx.savedstate.SavedStateRegistry
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
private const val PROVIDER_NAME = "selection_decoration_sectioned"
|
private const val PROVIDER_NAME = "selection_decoration_sectioned"
|
||||||
|
|
||||||
class SectionedSelectionController<T : Any>(
|
class SectionedSelectionController<T : Any>(
|
||||||
private val activity: Activity,
|
private val activity: Activity,
|
||||||
private val registryOwner: SavedStateRegistryOwner,
|
private val owner: SavedStateRegistryOwner,
|
||||||
private val callback: Callback<T>,
|
private val callback: Callback<T>,
|
||||||
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
|
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class SectionedSelectionController<T : Any>(
|
|||||||
get() = decorations.values.sumOf { it.checkedItemsCount }
|
get() = decorations.values.sumOf { it.checkedItemsCount }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registryOwner.lifecycle.addObserver(StateEventObserver())
|
owner.lifecycle.addObserver(StateEventObserver())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snapshot(): Map<T, Set<Long>> {
|
fun snapshot(): Map<T, Set<Long>> {
|
||||||
@@ -117,19 +117,19 @@ class SectionedSelectionController<T : Any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
return callback.onCreateActionMode(mode, menu)
|
return callback.onCreateActionMode(this, mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
return callback.onPrepareActionMode(mode, menu)
|
return callback.onPrepareActionMode(this, mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
return callback.onActionItemClicked(mode, item)
|
return callback.onActionItemClicked(this, mode, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
callback.onDestroyActionMode(mode)
|
callback.onDestroyActionMode(this, mode)
|
||||||
clear()
|
clear()
|
||||||
actionMode = null
|
actionMode = null
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ class SectionedSelectionController<T : Any>(
|
|||||||
|
|
||||||
private fun notifySelectionChanged() {
|
private fun notifySelectionChanged() {
|
||||||
val count = this.count
|
val count = this.count
|
||||||
callback.onSelectionChanged(count)
|
callback.onSelectionChanged(this, count)
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
actionMode?.finish()
|
actionMode?.finish()
|
||||||
} else {
|
} else {
|
||||||
@@ -173,27 +173,48 @@ class SectionedSelectionController<T : Any>(
|
|||||||
|
|
||||||
private fun getDecoration(section: T): AbstractSelectionItemDecoration {
|
private fun getDecoration(section: T): AbstractSelectionItemDecoration {
|
||||||
return decorations.getOrPut(section) {
|
return decorations.getOrPut(section) {
|
||||||
callback.onCreateItemDecoration(section)
|
callback.onCreateItemDecoration(this, section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback<T> : ListSelectionController.Callback {
|
interface Callback<T : Any> {
|
||||||
|
|
||||||
fun onCreateItemDecoration(section: T): AbstractSelectionItemDecoration
|
fun onSelectionChanged(controller: SectionedSelectionController<T>, count: Int)
|
||||||
|
|
||||||
|
fun onCreateActionMode(controller: SectionedSelectionController<T>, mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
|
fun onPrepareActionMode(controller: SectionedSelectionController<T>, mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.title = controller.count.toString()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDestroyActionMode(controller: SectionedSelectionController<T>, mode: ActionMode) = Unit
|
||||||
|
|
||||||
|
fun onActionItemClicked(
|
||||||
|
controller: SectionedSelectionController<T>,
|
||||||
|
mode: ActionMode,
|
||||||
|
item: MenuItem,
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
fun onCreateItemDecoration(
|
||||||
|
controller: SectionedSelectionController<T>,
|
||||||
|
section: T,
|
||||||
|
): AbstractSelectionItemDecoration
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class StateEventObserver : LifecycleEventObserver {
|
private inner class StateEventObserver : LifecycleEventObserver {
|
||||||
|
|
||||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
if (event == Lifecycle.Event.ON_CREATE) {
|
if (event == Lifecycle.Event.ON_CREATE) {
|
||||||
val registry = registryOwner.savedStateRegistry
|
val registry = owner.savedStateRegistry
|
||||||
registry.registerSavedStateProvider(PROVIDER_NAME, this@SectionedSelectionController)
|
registry.registerSavedStateProvider(PROVIDER_NAME, this@SectionedSelectionController)
|
||||||
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
|
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
|
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
|
||||||
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
|
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
|
||||||
restoreState(
|
restoreState(
|
||||||
state.keySet().associateWithTo(HashMap()) { state.getLongArray(it)?.toList().orEmpty() }
|
state.keySet()
|
||||||
|
.associateWithTo(HashMap()) { state.getLongArray(it)?.toList().orEmpty() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,4 +222,4 @@ class SectionedSelectionController<T : Any>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|||||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
|
|
||||||
class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHolderListener,
|
class BookmarksFragment :
|
||||||
OnListItemClickListener<Bookmark>, SectionedSelectionController.Callback<Manga> {
|
BaseFragment<FragmentListSimpleBinding>(),
|
||||||
|
ListStateHolderListener,
|
||||||
|
OnListItemClickListener<Bookmark>,
|
||||||
|
SectionedSelectionController.Callback<Manga> {
|
||||||
|
|
||||||
private val viewModel by viewModel<BookmarksViewModel>()
|
private val viewModel by viewModel<BookmarksViewModel>()
|
||||||
private var adapter: BookmarksGroupAdapter? = null
|
private var adapter: BookmarksGroupAdapter? = null
|
||||||
@@ -45,7 +48,7 @@ class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHo
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
selectionController = SectionedSelectionController(
|
selectionController = SectionedSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
registryOwner = this,
|
owner = this,
|
||||||
callback = this,
|
callback = this,
|
||||||
)
|
)
|
||||||
adapter = BookmarksGroupAdapter(
|
adapter = BookmarksGroupAdapter(
|
||||||
@@ -87,21 +90,24 @@ class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHo
|
|||||||
|
|
||||||
override fun onEmptyActionClick() = Unit
|
override fun onEmptyActionClick() = Unit
|
||||||
|
|
||||||
override fun onSelectionChanged(count: Int) {
|
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
|
||||||
binding.recyclerView.invalidateNestedItemDecorations()
|
binding.recyclerView.invalidateNestedItemDecorations()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(
|
||||||
|
controller: SectionedSelectionController<Manga>,
|
||||||
|
mode: ActionMode,
|
||||||
|
menu: Menu,
|
||||||
|
): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
|
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onActionItemClicked(
|
||||||
mode.title = selectionController?.count?.toString()
|
controller: SectionedSelectionController<Manga>,
|
||||||
return true
|
mode: ActionMode,
|
||||||
}
|
item: MenuItem,
|
||||||
|
): Boolean {
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_remove -> {
|
R.id.action_remove -> {
|
||||||
val ids = selectionController?.snapshot() ?: return false
|
val ids = selectionController?.snapshot() ?: return false
|
||||||
@@ -113,9 +119,10 @@ class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateItemDecoration(section: Manga): AbstractSelectionItemDecoration {
|
override fun onCreateItemDecoration(
|
||||||
return BookmarksSelectionDecoration(requireContext())
|
controller: SectionedSelectionController<Manga>,
|
||||||
}
|
section: Manga,
|
||||||
|
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(
|
||||||
@@ -135,7 +142,7 @@ class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHo
|
|||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
binding.recyclerView,
|
binding.recyclerView,
|
||||||
e.getDisplayMessage(resources),
|
e.getDisplayMessage(resources),
|
||||||
Snackbar.LENGTH_SHORT
|
Snackbar.LENGTH_SHORT,
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,4 +181,4 @@ class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHo
|
|||||||
|
|
||||||
fun newInstance() = BookmarksFragment()
|
fun newInstance() = BookmarksFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,15 +106,32 @@ abstract class FavouritesDao {
|
|||||||
|
|
||||||
/** DELETE **/
|
/** DELETE **/
|
||||||
|
|
||||||
suspend fun delete(mangaId: Long) = setDeletedAt(mangaId, System.currentTimeMillis())
|
suspend fun delete(mangaId: Long) = setDeletedAt(
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun delete(mangaId: Long, categoryId: Long) = setDeletedAt(mangaId, categoryId, System.currentTimeMillis())
|
suspend fun delete(mangaId: Long, categoryId: Long) = setDeletedAt(
|
||||||
|
categoryId = categoryId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun deleteAll(categoryId: Long) = setDeletedAtAll(categoryId, System.currentTimeMillis())
|
suspend fun deleteAll(categoryId: Long) = setDeletedAtAll(
|
||||||
|
categoryId = categoryId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun recover(mangaId: Long) = setDeletedAt(mangaId, 0L)
|
suspend fun recover(mangaId: Long) = setDeletedAt(
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = 0L,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun recover(mangaId: Long, categoryId: Long) = setDeletedAt(categoryId, mangaId, 0L)
|
suspend fun recover(categoryId: Long, mangaId: Long) = setDeletedAt(
|
||||||
|
categoryId = categoryId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = 0L,
|
||||||
|
)
|
||||||
|
|
||||||
@Query("DELETE FROM favourites WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
@Query("DELETE FROM favourites WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
||||||
abstract suspend fun gc(maxDeletionTime: Long)
|
abstract suspend fun gc(maxDeletionTime: Long)
|
||||||
@@ -139,7 +156,7 @@ abstract class FavouritesDao {
|
|||||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||||
|
|
||||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||||
abstract suspend fun setDeletedAt(mangaId: Long, categoryId: Long, deletedAt: Long)
|
abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
|
||||||
|
|
||||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
||||||
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ val libraryModule
|
|||||||
|
|
||||||
factory { LibraryRepository(get()) }
|
factory { LibraryRepository(get()) }
|
||||||
|
|
||||||
viewModel { LibraryViewModel(get(), get(), get(), get()) }
|
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { LibraryCategoriesConfigViewModel(get()) }
|
viewModel { LibraryCategoriesConfigViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.library.ui
|
package org.koitharu.kotatsu.library.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.view.ActionMode
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -12,32 +13,24 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
|
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
|
||||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||||
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
|
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
|
||||||
import org.koitharu.kotatsu.library.ui.adapter.LibraryListEventListener
|
import org.koitharu.kotatsu.library.ui.adapter.LibraryListEventListener
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.main.ui.BottomNavOwner
|
import org.koitharu.kotatsu.main.ui.BottomNavOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.flattenTo
|
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
|
||||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
|
||||||
|
|
||||||
class LibraryFragment :
|
class LibraryFragment :
|
||||||
BaseFragment<FragmentLibraryBinding>(),
|
BaseFragment<FragmentLibraryBinding>(),
|
||||||
LibraryListEventListener,
|
LibraryListEventListener {
|
||||||
SectionedSelectionController.Callback<LibrarySectionModel> {
|
|
||||||
|
|
||||||
private val viewModel by viewModel<LibraryViewModel>()
|
private val viewModel by viewModel<LibraryViewModel>()
|
||||||
private var adapter: LibraryAdapter? = null
|
private var adapter: LibraryAdapter? = null
|
||||||
@@ -52,8 +45,8 @@ class LibraryFragment :
|
|||||||
val sizeResolver = ItemSizeResolver(resources, get())
|
val sizeResolver = ItemSizeResolver(resources, get())
|
||||||
selectionController = SectionedSelectionController(
|
selectionController = SectionedSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
registryOwner = this,
|
owner = this,
|
||||||
callback = this,
|
callback = LibrarySelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
|
||||||
)
|
)
|
||||||
adapter = LibraryAdapter(
|
adapter = LibraryAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
@@ -111,62 +104,6 @@ class LibraryFragment :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_library, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.title = selectionController?.count?.toString()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
val ctx = context ?: return false
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_share -> {
|
|
||||||
ShareHelper(ctx).shareMangaLinks(collectSelectedItems())
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_favourite -> {
|
|
||||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, collectSelectedItems())
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_save -> {
|
|
||||||
DownloadService.confirmAndStart(ctx, collectSelectedItems())
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSelectionChanged(count: Int) {
|
|
||||||
binding.recyclerView.invalidateNestedItemDecorations()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateItemDecoration(section: LibrarySectionModel): AbstractSelectionItemDecoration {
|
|
||||||
return MangaSelectionDecoration(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectSelectedItemsMap(): Map<LibrarySectionModel, Set<Manga>> {
|
|
||||||
val snapshot = selectionController?.snapshot()
|
|
||||||
if (snapshot.isNullOrEmpty()) {
|
|
||||||
return emptyMap()
|
|
||||||
}
|
|
||||||
return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectSelectedItems(): Set<Manga> {
|
|
||||||
val snapshot = selectionController?.snapshot()
|
|
||||||
if (snapshot.isNullOrEmpty()) {
|
|
||||||
return emptySet()
|
|
||||||
}
|
|
||||||
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onListChanged(list: List<ListModel>) {
|
private fun onListChanged(list: List<ListModel>) {
|
||||||
adapter?.items = list
|
adapter?.items = list
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package org.koitharu.kotatsu.library.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
||||||
|
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.flattenTo
|
||||||
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
|
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||||
|
|
||||||
|
class LibrarySelectionCallback(
|
||||||
|
private val recyclerView: RecyclerView,
|
||||||
|
private val fragmentManager: FragmentManager,
|
||||||
|
private val viewModel: LibraryViewModel,
|
||||||
|
) : SectionedSelectionController.Callback<LibrarySectionModel> {
|
||||||
|
|
||||||
|
private val context: Context
|
||||||
|
get() = recyclerView.context
|
||||||
|
|
||||||
|
override fun onCreateActionMode(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
mode: ActionMode,
|
||||||
|
menu: Menu,
|
||||||
|
): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.mode_library, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
mode: ActionMode,
|
||||||
|
menu: Menu,
|
||||||
|
): Boolean {
|
||||||
|
menu.findItem(R.id.action_remove).isVisible =
|
||||||
|
controller.peekCheckedIds().count { (_, v) -> v.isNotEmpty() } == 1
|
||||||
|
return super.onPrepareActionMode(controller, mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
mode: ActionMode,
|
||||||
|
item: MenuItem,
|
||||||
|
): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.action_share -> {
|
||||||
|
ShareHelper(context).shareMangaLinks(collectSelectedItems(controller))
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_favourite -> {
|
||||||
|
FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller))
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_save -> {
|
||||||
|
DownloadService.confirmAndStart(context, collectSelectedItems(controller))
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_remove -> {
|
||||||
|
val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false
|
||||||
|
when (group) {
|
||||||
|
is LibrarySectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
||||||
|
is LibrarySectionModel.History -> viewModel.removeFromHistory(ids)
|
||||||
|
}
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectionChanged(controller: SectionedSelectionController<LibrarySectionModel>, count: Int) {
|
||||||
|
recyclerView.invalidateNestedItemDecorations()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateItemDecoration(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
section: LibrarySectionModel,
|
||||||
|
): AbstractSelectionItemDecoration = MangaSelectionDecoration(context)
|
||||||
|
|
||||||
|
private fun collectSelectedItemsMap(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
): Map<LibrarySectionModel, Set<Manga>> {
|
||||||
|
val snapshot = controller.peekCheckedIds()
|
||||||
|
if (snapshot.isEmpty()) {
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectSelectedItems(
|
||||||
|
controller: SectionedSelectionController<LibrarySectionModel>,
|
||||||
|
): Set<Manga> {
|
||||||
|
val snapshot = controller.peekCheckedIds()
|
||||||
|
if (snapshot.isEmpty()) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
@@ -32,6 +33,7 @@ private const val HISTORY_MAX_SEGMENTS = 2
|
|||||||
class LibraryViewModel(
|
class LibraryViewModel(
|
||||||
repository: LibraryRepository,
|
repository: LibraryRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val favouritesRepository: FavouritesRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel(), ListExtraProvider {
|
) : BaseViewModel(), ListExtraProvider {
|
||||||
@@ -59,6 +61,16 @@ class LibraryViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeFromFavourites(category: FavouriteCategory, ids: Set<Long>) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
val handle = favouritesRepository.removeFromCategory(category.id, ids)
|
||||||
|
onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeFromHistory(ids: Set<Long>) {
|
fun removeFromHistory(ids: Set<Long>) {
|
||||||
if (ids.isEmpty()) {
|
if (ids.isEmpty()) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
||||||
@@ -16,7 +17,7 @@ import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
|||||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
fun libraryGroupAD(
|
fun libraryGroupAD(
|
||||||
@@ -55,8 +56,7 @@ fun libraryGroupAD(
|
|||||||
|
|
||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
binding.recyclerView.clearItemDecorations()
|
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
||||||
}
|
}
|
||||||
binding.textViewTitle.text = item.getTitle(context.resources)
|
binding.textViewTitle.text = item.getTitle(context.resources)
|
||||||
@@ -66,5 +66,6 @@ fun libraryGroupAD(
|
|||||||
|
|
||||||
onViewRecycled {
|
onViewRecycled {
|
||||||
adapter.items = emptyList()
|
adapter.items = emptyList()
|
||||||
|
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||||
@@ -34,6 +35,15 @@ fun RecyclerView.clearItemDecorations() {
|
|||||||
suppressLayout(false)
|
suppressLayout(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.removeItemDecoration(cls: Class<out ItemDecoration>) {
|
||||||
|
repeat(itemDecorationCount) { i ->
|
||||||
|
if (cls.isInstance(getItemDecorationAt(i))) {
|
||||||
|
removeItemDecorationAt(i)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var RecyclerView.firstVisibleItemPosition: Int
|
var RecyclerView.firstVisibleItemPosition: Int
|
||||||
get() = (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition()
|
get() = (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition()
|
||||||
?: RecyclerView.NO_POSITION
|
?: RecyclerView.NO_POSITION
|
||||||
@@ -69,13 +79,15 @@ fun View.measureWidth(): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun ViewPager2.doOnPageChanged(crossinline callback: (Int) -> Unit) {
|
inline fun ViewPager2.doOnPageChanged(crossinline callback: (Int) -> Unit) {
|
||||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
registerOnPageChangeCallback(
|
||||||
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
super.onPageSelected(position)
|
super.onPageSelected(position)
|
||||||
callback(position)
|
callback(position)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ViewPager2.recyclerView: RecyclerView?
|
val ViewPager2.recyclerView: RecyclerView?
|
||||||
@@ -157,4 +169,4 @@ val View.parents: Sequence<ViewParent>
|
|||||||
yield(p)
|
yield(p)
|
||||||
p = p.parent
|
p = p.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user