Remove from library action

This commit is contained in:
Koitharu
2022-07-22 13:13:00 +03:00
parent 83886362be
commit 7c659371a9
10 changed files with 242 additions and 121 deletions

View File

@@ -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(
} }
} }
} }
} }

View File

@@ -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>(
} }
} }
} }
} }

View File

@@ -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()
} }
} }

View File

@@ -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)

View File

@@ -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()) }
} }

View File

@@ -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
} }

View File

@@ -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()))
}
}

View File

@@ -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

View File

@@ -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)
} }
} }

View File

@@ -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
} }
} }