Bookmarks selection

This commit is contained in:
Koitharu
2024-05-06 15:05:25 +03:00
parent 6676ab82b4
commit 46ded4af0d
12 changed files with 118 additions and 64 deletions

View File

@@ -122,7 +122,7 @@
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
android:label="@string/favourites" />
<activity
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
android:name="org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity"
android:label="@string/bookmarks" />
<activity
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"

View File

@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@AndroidEntryPoint
class BookmarksActivity :
class AllBookmarksActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner,
SnackbarOwner {
@@ -35,7 +35,7 @@ class BookmarksActivity :
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, BookmarksFragment::class.java, null)
replace(R.id.container, AllBookmarksFragment::class.java, null)
}
}
}
@@ -49,6 +49,6 @@ class BookmarksActivity :
companion object {
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
}
}

View File

@@ -17,7 +17,7 @@ import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment
@@ -42,7 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import javax.inject.Inject
@AndroidEntryPoint
class BookmarksFragment :
class AllBookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
@@ -55,7 +55,7 @@ class BookmarksFragment :
@Inject
lateinit var settings: AppSettings
private val viewModel by viewModels<BookmarksViewModel>()
private val viewModel by viewModels<AllBookmarksViewModel>()
private var bookmarksAdapter: BookmarksAdapter? = null
private var selectionController: ListSelectionController? = null
@@ -213,6 +213,6 @@ class BookmarksFragment :
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
),
)
fun newInstance() = BookmarksFragment()
fun newInstance() = AllBookmarksFragment()
}
}

View File

@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@HiltViewModel
class BookmarksViewModel @Inject constructor(
class AllBookmarksViewModel @Inject constructor(
private val repository: BookmarksRepository,
) : BaseViewModel() {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader

View File

@@ -1,19 +1,36 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) : BaseListAdapter<Bookmark>() {
headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
return findHeader(position)?.getText(context)
}
}

View File

@@ -1,36 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
return findHeader(position)?.getText(context)
}
}

View File

@@ -5,7 +5,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.details.ui.pager.bookmarks.MangaBookmarksFragment
import org.koitharu.kotatsu.details.ui.pager.bookmarks.BookmarksFragment
import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment
import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment
@@ -19,8 +19,8 @@ class ChaptersPagesAdapter(
override fun createFragment(position: Int): Fragment = when (position) {
0 -> ChaptersFragment()
1 -> if (isPagesTabEnabled) PagesFragment() else MangaBookmarksFragment()
2 -> MangaBookmarksFragment()
1 -> if (isPagesTabEnabled) PagesFragment() else BookmarksFragment()
2 -> BookmarksFragment()
else -> throw IllegalArgumentException("Invalid position $position")
}

View File

@@ -2,23 +2,33 @@ package org.koitharu.kotatsu.details.ui.pager.bookmarks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
import org.koitharu.kotatsu.bookmarks.ui.BookmarksSelectionDecoration
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.list.ui.GridSpanResolver
@@ -29,11 +39,11 @@ import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import javax.inject.Inject
@AndroidEntryPoint
class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark> {
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark>, ListSelectionController.Callback2 {
private val activityViewModel by activityViewModels<DetailsViewModel>()
private val viewModel by viewModels<MangaBookmarksViewModel>()
private val viewModel by viewModels<BookmarksViewModel>()
@Inject
lateinit var coil: ImageLoader
@@ -43,6 +53,7 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
private var bookmarksAdapter: BookmarksAdapter? = null
private var spanResolver: GridSpanResolver? = null
private var selectionController: ListSelectionController? = null
private val spanSizeLookup = SpanSizeLookup()
private val listCommitCallback = Runnable {
@@ -61,10 +72,16 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
override fun onViewBindingCreated(binding: FragmentMangaBookmarksBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
spanResolver = GridSpanResolver(binding.root.resources)
selectionController = ListSelectionController(
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = BookmarksSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
)
bookmarksAdapter = BookmarksAdapter(
coil = coil,
lifecycleOwner = viewLifecycleOwner,
clickListener = this@MangaBookmarksFragment,
clickListener = this@BookmarksFragment,
headerClickListener = null,
)
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization
@@ -78,13 +95,21 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
it.spanSizeLookup = spanSizeLookup
it.spanCount = checkNotNull(spanResolver).spanCount
}
selectionController?.attachToRecyclerView(this)
}
viewModel.content.observe(viewLifecycleOwner) { bookmarksAdapter?.setItems(it, listCommitCallback) }
viewModel.onError.observeEvent(
viewLifecycleOwner,
SnackbarErrorObserver(binding.recyclerView, this),
)
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
override fun onDestroyView() {
spanResolver = null
bookmarksAdapter = null
selectionController = null
spanSizeLookup.invalidateCache()
super.onDestroyView()
}
@@ -92,6 +117,9 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.pageId) == true) {
return
}
val listener = findParentCallback(ReaderNavigationCallback::class.java)
if (listener != null && listener.onBookmarkSelected(item)) {
dismissParentDialog()
@@ -105,6 +133,40 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
}
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemLongClick(item.pageId) ?: false
}
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
requireViewBinding().recyclerView.invalidateItemDecorations()
}
override fun onCreateActionMode(
controller: ListSelectionController,
mode: ActionMode,
menu: Menu,
): Boolean {
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
return true
}
override fun onActionItemClicked(
controller: ListSelectionController,
mode: ActionMode,
item: MenuItem,
): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
val ids = selectionController?.snapshot() ?: return false
viewModel.removeBookmarks(ids)
mode.finish()
true
}
else -> false
}
}
private fun onGridScaleChanged(scale: Float) {
spanSizeLookup.invalidateCache()
spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)

View File

@@ -18,6 +18,9 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -26,12 +29,13 @@ import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@HiltViewModel
class MangaBookmarksViewModel @Inject constructor(
bookmarksRepository: BookmarksRepository,
class BookmarksViewModel @Inject constructor(
private val bookmarksRepository: BookmarksRepository,
settings: AppSettings,
) : BaseViewModel(), FlowCollector<Manga?> {
private val manga = MutableStateFlow<Manga?>(null)
val onActionDone = MutableEventFlow<ReversibleAction>()
val gridScale = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
@@ -50,7 +54,14 @@ class MangaBookmarksViewModel @Inject constructor(
manga.value = value
}
private suspend fun mapList(manga: Manga, bookmarks: List<Bookmark>): List<ListModel>? {
fun removeBookmarks(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
val handle = bookmarksRepository.removeBookmarks(ids)
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
}
}
private fun mapList(manga: Manga, bookmarks: List<Bookmark>): List<ListModel>? {
val chapters = manga.chapters ?: return null
val bookmarksMap = bookmarks.groupBy { it.chapterId }
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)

View File

@@ -19,7 +19,7 @@ import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.BaseFragment
@@ -133,7 +133,7 @@ class ExploreFragment :
override fun onClick(v: View) {
val intent = when (v.id) {
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context)
R.id.button_bookmarks -> AllBookmarksActivity.newIntent(v.context)
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
R.id.button_downloads -> DownloadsActivity.newIntent(v.context)
R.id.button_random -> {

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.NavItem
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
@@ -144,7 +144,7 @@ class MainNavigationDelegate(
R.id.nav_feed -> FeedFragment::class.java
R.id.nav_local -> LocalListFragment::class.java
R.id.nav_suggestions -> SuggestionsFragment::class.java
R.id.nav_bookmarks -> BookmarksFragment::class.java
R.id.nav_bookmarks -> AllBookmarksFragment::class.java
R.id.nav_updated -> UpdatesFragment::class.java
else -> return false
},
@@ -158,7 +158,7 @@ class MainNavigationDelegate(
is FeedFragment -> R.id.nav_feed
is LocalListFragment -> R.id.nav_local
is SuggestionsFragment -> R.id.nav_suggestions
is BookmarksFragment -> R.id.nav_bookmarks
is AllBookmarksFragment -> R.id.nav_bookmarks
is UpdatesFragment -> R.id.nav_updated
else -> 0
}