mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-13 20:48:56 +01:00
Use Compose for Library screen (#7557)
- Move Pager to Compose - Move AppBar to Compose - Use Stable interface for state - Use pills for no. of manga in category instead of (x)
This commit is contained in:
@@ -1,208 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.library.components.LibraryComfortableGrid
|
||||
import eu.kanade.presentation.library.components.LibraryCompactGrid
|
||||
import eu.kanade.presentation.library.components.LibraryCoverOnlyGrid
|
||||
import eu.kanade.presentation.library.components.LibraryList
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* This adapter stores the categories from the library, used with a ViewPager.
|
||||
*
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class LibraryAdapter(
|
||||
private val presenter: LibraryPresenter,
|
||||
private val onClickManga: (LibraryManga) -> Unit,
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = mutableStateListOf()
|
||||
private set
|
||||
|
||||
/**
|
||||
* The number of manga in each category.
|
||||
* List order must be the same as [categories]
|
||||
*/
|
||||
private var itemsPerCategory: List<Int> = emptyList()
|
||||
|
||||
private var boundViews = arrayListOf<View>()
|
||||
|
||||
/**
|
||||
* Pair of category and size of category
|
||||
*/
|
||||
fun updateCategories(new: List<Pair<Category, Int>>) {
|
||||
var updated = false
|
||||
|
||||
val newCategories = new.map { it.first }
|
||||
if (categories != newCategories) {
|
||||
categories = newCategories
|
||||
updated = true
|
||||
}
|
||||
|
||||
val newItemsPerCategory = new.map { it.second }
|
||||
if (itemsPerCategory !== newItemsPerCategory) {
|
||||
itemsPerCategory = newItemsPerCategory
|
||||
updated = true
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun inflateView(container: ViewGroup, viewType: Int): View {
|
||||
val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a view with a position.
|
||||
*
|
||||
* @param view the view to bind.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun bindView(view: View, position: Int) {
|
||||
(view as ComposeView).apply {
|
||||
setComposeContent {
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
|
||||
val category = presenter.categories[position]
|
||||
val displayMode = presenter.getDisplayMode(index = position)
|
||||
val mangaList by presenter.getMangaForCategory(categoryId = category.id)
|
||||
|
||||
val onClickManga = { manga: LibraryManga ->
|
||||
if (presenter.hasSelection().not()) {
|
||||
onClickManga(manga)
|
||||
} else {
|
||||
presenter.toggleSelection(manga)
|
||||
}
|
||||
}
|
||||
val onLongClickManga = { manga: LibraryManga ->
|
||||
presenter.toggleSelection(manga)
|
||||
}
|
||||
|
||||
SwipeRefresh(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
state = rememberSwipeRefreshState(isRefreshing = false),
|
||||
onRefresh = {
|
||||
if (LibraryUpdateService.start(context, category)) {
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
},
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
when (displayMode) {
|
||||
DisplayModeSetting.LIST -> {
|
||||
LibraryList(
|
||||
items = mangaList,
|
||||
selection = presenter.selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
)
|
||||
}
|
||||
DisplayModeSetting.COMPACT_GRID -> {
|
||||
LibraryCompactGrid(
|
||||
items = mangaList,
|
||||
columns = presenter.columns,
|
||||
selection = presenter.selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
)
|
||||
}
|
||||
DisplayModeSetting.COMFORTABLE_GRID -> {
|
||||
LibraryComfortableGrid(
|
||||
items = mangaList,
|
||||
columns = presenter.columns,
|
||||
selection = presenter.selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
)
|
||||
}
|
||||
DisplayModeSetting.COVER_ONLY_GRID -> {
|
||||
LibraryCoverOnlyGrid(
|
||||
items = mangaList,
|
||||
columns = presenter.columns,
|
||||
selection = presenter.selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boundViews.add(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycles a view.
|
||||
*
|
||||
* @param view the view to recycle.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
boundViews.remove(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of categories.
|
||||
*
|
||||
* @return the number of categories or 0 if the list is null.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return categories.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display for a category.
|
||||
*
|
||||
* @param position the position of the element.
|
||||
* @return the title to display.
|
||||
*/
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return if (!preferences.categoryNumberOfItems().get()) {
|
||||
categories[position].name
|
||||
} else {
|
||||
categories[position].let { "${it.name} (${itemsPerCategory[position]})" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewType(position: Int): Int = -1
|
||||
}
|
||||
@@ -1,240 +1,119 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.fredporciuncula.flow.preferences.Preference
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.toDbCategory
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.presentation.library.LibraryScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.preference.asHotFlow
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
||||
import eu.kanade.tachiyomi.widget.EmptyView
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import reactivecircus.flowbinding.viewpager.pageSelections
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LibraryController(
|
||||
bundle: Bundle? = null,
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||
) : FullComposeController<LibraryPresenter>(bundle),
|
||||
RootController,
|
||||
TabbedController,
|
||||
ActionModeWithToolbar.Callback,
|
||||
ChangeMangaCategoriesDialog.Listener,
|
||||
DeleteLibraryMangasDialog.Listener {
|
||||
|
||||
/**
|
||||
* Position of the active category.
|
||||
*/
|
||||
private var activeCategory: Int = preferences.lastUsedCategory().get()
|
||||
|
||||
/**
|
||||
* Action mode for selections.
|
||||
*/
|
||||
private var actionMode: ActionModeWithToolbar? = null
|
||||
|
||||
private var mangaMap: LibraryMap = emptyMap()
|
||||
|
||||
private var adapter: LibraryAdapter? = null
|
||||
|
||||
/**
|
||||
* Sheet containing filter/sort/display items.
|
||||
*/
|
||||
private var settingsSheet: LibrarySettingsSheet? = null
|
||||
|
||||
private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||
|
||||
private var mangaCountVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||
|
||||
private var tabsVisibilitySubscription: Subscription? = null
|
||||
|
||||
private var mangaCountVisibilitySubscription: Subscription? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
retainViewMode = RetainViewMode.RETAIN_DETACH
|
||||
}
|
||||
|
||||
private var currentTitle: String? = null
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
setTitle()
|
||||
}
|
||||
}
|
||||
override fun createPresenter(): LibraryPresenter = LibraryPresenter()
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return currentTitle ?: resources?.getString(R.string.label_library)
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val context = LocalContext.current
|
||||
LibraryScreen(
|
||||
presenter = presenter,
|
||||
onMangaClicked = ::openManga,
|
||||
onGlobalSearchClicked = {
|
||||
router.pushController(GlobalSearchController(presenter.query))
|
||||
},
|
||||
onChangeCategoryClicked = ::showMangaCategoriesDialog,
|
||||
onMarkAsReadClicked = { markReadStatus(true) },
|
||||
onMarkAsUnreadClicked = { markReadStatus(false) },
|
||||
onDownloadClicked = ::downloadUnreadChapters,
|
||||
onDeleteClicked = ::showDeleteMangaDialog,
|
||||
onClickFilter = ::showSettingsSheet,
|
||||
onClickRefresh = {
|
||||
if (LibraryUpdateService.start(context)) {
|
||||
context.toast(R.string.updating_library)
|
||||
}
|
||||
},
|
||||
onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) },
|
||||
onClickSelectAll = { presenter.selectAll(presenter.activeCategory) },
|
||||
onClickUnselectAll = ::clearSelection,
|
||||
)
|
||||
LaunchedEffect(presenter.selectionMode) {
|
||||
val activity = (activity as? MainActivity) ?: return@LaunchedEffect
|
||||
activity.showBottomNav(presenter.selectionMode.not())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
val showCategoryTabs = preferences.categoryTabs().get()
|
||||
val currentCategory = adapter?.categories?.getOrNull(binding.libraryPager.currentItem)
|
||||
|
||||
var title = if (showCategoryTabs) {
|
||||
resources?.getString(R.string.label_library)
|
||||
} else {
|
||||
currentCategory?.name
|
||||
override fun handleBack(): Boolean {
|
||||
if (presenter.selection.isNotEmpty()) {
|
||||
presenter.clearSelection()
|
||||
return true
|
||||
}
|
||||
|
||||
if (preferences.categoryNumberOfItems().get()) {
|
||||
if (!showCategoryTabs || adapter?.categories?.size == 1) {
|
||||
title += " (${mangaMap[currentCategory?.id]?.size ?: 0})"
|
||||
}
|
||||
}
|
||||
|
||||
currentTitle = title
|
||||
return false
|
||||
}
|
||||
|
||||
override fun createPresenter(): LibraryPresenter {
|
||||
return LibraryPresenter()
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = LibraryAdapter(
|
||||
presenter = presenter,
|
||||
onClickManga = {
|
||||
openManga(it.id!!)
|
||||
},
|
||||
)
|
||||
|
||||
getColumnsPreferenceForCurrentOrientation()
|
||||
.asHotFlow { presenter.columns = it }
|
||||
.launchIn(viewScope)
|
||||
|
||||
binding.libraryPager.adapter = adapter
|
||||
binding.libraryPager.pageSelections()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
preferences.lastUsedCategory().set(it)
|
||||
activeCategory = it
|
||||
updateTitle()
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
if (adapter!!.categories.isNotEmpty()) {
|
||||
createActionModeIfNeeded()
|
||||
}
|
||||
|
||||
settingsSheet = LibrarySettingsSheet(router) { group ->
|
||||
when (group) {
|
||||
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
|
||||
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
||||
is LibrarySettingsSheet.Display.DisplayGroup -> {
|
||||
val delay = if (preferences.categorizedDisplaySettings().get()) 125L else 0L
|
||||
|
||||
Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
reattachAdapter()
|
||||
}
|
||||
}
|
||||
is LibrarySettingsSheet.Display.DisplayGroup -> {}
|
||||
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
|
||||
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
|
||||
is LibrarySettingsSheet.Display.TabsGroup -> {} // onTabsSettingsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnGlobalSearch.clicks()
|
||||
.onEach {
|
||||
router.pushController(GlobalSearchController(presenter.query))
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
|
||||
private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
preferences.portraitColumns()
|
||||
} else {
|
||||
preferences.landscapeColumns()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
(activity as? MainActivity)?.binding?.tabs?.setupWithViewPager(binding.libraryPager)
|
||||
presenter.subscribeLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter = null
|
||||
settingsSheet?.sheetScope?.cancel()
|
||||
settingsSheet = null
|
||||
tabsVisibilitySubscription?.unsubscribe()
|
||||
tabsVisibilitySubscription = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun configureTabs(tabs: TabLayout): Boolean {
|
||||
with(tabs) {
|
||||
isVisible = false
|
||||
tabGravity = TabLayout.GRAVITY_START
|
||||
tabMode = TabLayout.MODE_SCROLLABLE
|
||||
}
|
||||
tabsVisibilitySubscription?.unsubscribe()
|
||||
tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
|
||||
tabs.isVisible = visible
|
||||
}
|
||||
mangaCountVisibilitySubscription?.unsubscribe()
|
||||
mangaCountVisibilitySubscription = mangaCountVisibilityRelay.subscribe {
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun cleanupTabs(tabs: TabLayout) {
|
||||
tabsVisibilitySubscription?.unsubscribe()
|
||||
tabsVisibilitySubscription = null
|
||||
}
|
||||
|
||||
fun showSettingsSheet() {
|
||||
if (adapter?.categories?.isNotEmpty() == true) {
|
||||
adapter?.categories?.get(binding.libraryPager.currentItem)?.let { category ->
|
||||
if (presenter.categories.isNotEmpty()) {
|
||||
presenter.categories[presenter.activeCategory].let { category ->
|
||||
settingsSheet?.show(category.toDbCategory())
|
||||
}
|
||||
} else {
|
||||
@@ -242,61 +121,6 @@ class LibraryController(
|
||||
}
|
||||
}
|
||||
|
||||
fun onNextLibraryUpdate(categories: List<Category>, mangaMap: LibraryMap) {
|
||||
val view = view ?: return
|
||||
val adapter = adapter ?: return
|
||||
|
||||
// Show empty view if needed
|
||||
if (mangaMap.isNotEmpty()) {
|
||||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(
|
||||
R.string.information_empty_library,
|
||||
listOf(
|
||||
EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) {
|
||||
activity?.openInBrowser("https://tachiyomi.org/help/guides/getting-started")
|
||||
},
|
||||
),
|
||||
)
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
|
||||
// Get the current active category.
|
||||
val activeCat = if (adapter.categories.isNotEmpty()) {
|
||||
binding.libraryPager.currentItem
|
||||
} else {
|
||||
activeCategory
|
||||
}
|
||||
|
||||
// Set the categories
|
||||
adapter.updateCategories(categories.map { it to (mangaMap[it.id]?.size ?: 0) })
|
||||
|
||||
// Restore active category.
|
||||
binding.libraryPager.setCurrentItem(activeCat, false)
|
||||
|
||||
// Trigger display of tabs
|
||||
onTabsSettingsChanged(firstLaunch = true)
|
||||
|
||||
// Delay the scroll position to allow the view to be properly measured.
|
||||
view.post {
|
||||
if (isAttached) {
|
||||
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
|
||||
}
|
||||
}
|
||||
|
||||
presenter.loadedManga.clear()
|
||||
mangaMap.forEach {
|
||||
presenter.loadedManga[it.key] = it.value
|
||||
}
|
||||
presenter.loadedMangaFlow.value = presenter.loadedManga
|
||||
|
||||
// Send the manga map to child fragments after the adapter is updated.
|
||||
this.mangaMap = mangaMap
|
||||
|
||||
// Finally update the title
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
private fun onFilterChanged() {
|
||||
presenter.requestFilterUpdate()
|
||||
activity?.invalidateOptionsMenu()
|
||||
@@ -306,146 +130,17 @@ class LibraryController(
|
||||
presenter.requestBadgesUpdate()
|
||||
}
|
||||
|
||||
private fun onTabsSettingsChanged(firstLaunch: Boolean = false) {
|
||||
if (!firstLaunch) {
|
||||
mangaCountVisibilityRelay.call(preferences.categoryNumberOfItems().get())
|
||||
}
|
||||
tabsVisibilityRelay.call(preferences.categoryTabs().get() && (adapter?.categories?.size ?: 0) > 1)
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
private fun onSortChanged() {
|
||||
presenter.requestSortUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reattaches the adapter to the view pager to recreate fragments
|
||||
*/
|
||||
private fun reattachAdapter() {
|
||||
val adapter = adapter ?: return
|
||||
|
||||
val position = binding.libraryPager.currentItem
|
||||
|
||||
adapter.recycle = false
|
||||
binding.libraryPager.adapter = adapter
|
||||
binding.libraryPager.currentItem = position
|
||||
adapter.recycle = true
|
||||
}
|
||||
|
||||
fun createActionModeIfNeeded() {
|
||||
val activity = activity
|
||||
if (actionMode == null && activity is MainActivity) {
|
||||
actionMode = activity.startActionModeAndToolbar(this)
|
||||
activity.showBottomNav(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
|
||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||
menu.findItem(R.id.action_filter).icon?.mutate()
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
presenter.query = query
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
if (presenter.query.isNotEmpty()) {
|
||||
binding.btnGlobalSearch.isVisible = true
|
||||
binding.btnGlobalSearch.text =
|
||||
resources?.getString(R.string.action_global_search_query, presenter.query)
|
||||
} else {
|
||||
binding.btnGlobalSearch.isVisible = false
|
||||
}
|
||||
presenter.searchQuery = query
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val settingsSheet = settingsSheet ?: return
|
||||
|
||||
val filterItem = menu.findItem(R.id.action_filter)
|
||||
|
||||
// Tint icon if there's a filter active
|
||||
if (settingsSheet.filters.hasActiveFilters()) {
|
||||
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
|
||||
filterItem.icon?.setTint(filterColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_search -> expandActionViewFromInteraction = true
|
||||
R.id.action_filter -> showSettingsSheet()
|
||||
R.id.action_update_library -> {
|
||||
activity?.let {
|
||||
if (LibraryUpdateService.start(it)) {
|
||||
it.toast(R.string.updating_library)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the action mode, forcing it to refresh its content.
|
||||
*/
|
||||
fun invalidateActionMode() {
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.generic_selection, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
|
||||
menuInflater.inflate(R.menu.library_selection, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = presenter.selection.size
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = count.toString()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
|
||||
if (presenter.hasSelection().not()) return
|
||||
toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
|
||||
presenter.selection.any { presenter.loadedManga.values.any { it.any { it.isLocal } } }
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_move_to_category -> showMangaCategoriesDialog()
|
||||
R.id.action_download_unread -> downloadUnreadChapters()
|
||||
R.id.action_mark_as_read -> markReadStatus(true)
|
||||
R.id.action_mark_as_unread -> markReadStatus(false)
|
||||
R.id.action_delete -> showDeleteMangaDialog()
|
||||
R.id.action_select_all -> selectAllCategoryManga()
|
||||
R.id.action_select_inverse -> selectInverseCategoryManga()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// Clear all the manga selections and notify child views.
|
||||
presenter.clearSelection()
|
||||
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters()
|
||||
}
|
||||
|
||||
private fun openManga(mangaId: Long) {
|
||||
@@ -461,7 +156,6 @@ class LibraryController(
|
||||
*/
|
||||
fun clearSelection() {
|
||||
presenter.clearSelection()
|
||||
invalidateActionMode()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -496,13 +190,13 @@ class LibraryController(
|
||||
private fun downloadUnreadChapters() {
|
||||
val mangas = presenter.selection.toList()
|
||||
presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
private fun markReadStatus(read: Boolean) {
|
||||
val mangas = presenter.selection.toList()
|
||||
presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
private fun showDeleteMangaDialog() {
|
||||
@@ -512,28 +206,11 @@ class LibraryController(
|
||||
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
||||
presenter.setMangaCategories(mangas, addCategories, removeCategories)
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.clearSelection()
|
||||
}
|
||||
|
||||
override fun deleteMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
|
||||
presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
private fun selectAllCategoryManga() {
|
||||
presenter.selectAll(binding.libraryPager.currentItem)
|
||||
}
|
||||
|
||||
private fun selectInverseCategoryManga() {
|
||||
presenter.invertSelection(binding.libraryPager.currentItem)
|
||||
}
|
||||
|
||||
override fun onSearchViewQueryTextChange(newText: String?) {
|
||||
// Ignore events if this controller isn't at the top to avoid query being reset
|
||||
if (router.backstack.lastOrNull()?.controller == this) {
|
||||
presenter.query = newText ?: ""
|
||||
presenter.searchQuery = newText ?: ""
|
||||
performSearch()
|
||||
}
|
||||
presenter.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.core.util.asObservable
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
@@ -25,6 +27,10 @@ import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.presentation.library.LibraryState
|
||||
import eu.kanade.presentation.library.LibraryStateImpl
|
||||
import eu.kanade.presentation.library.components.LibraryToolbarTitle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
@@ -39,14 +45,16 @@ import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -70,6 +78,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
|
||||
* Presenter of [LibraryController].
|
||||
*/
|
||||
class LibraryPresenter(
|
||||
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
@@ -83,31 +92,27 @@ class LibraryPresenter(
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
) : BasePresenter<LibraryController>() {
|
||||
) : BasePresenter<LibraryController>(), LibraryState by state {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = mutableStateListOf()
|
||||
var loadedManga by mutableStateOf(emptyMap<Long, List<LibraryItem>>())
|
||||
private set
|
||||
|
||||
var loadedManga = mutableStateMapOf<Long, List<LibraryItem>>()
|
||||
private set
|
||||
|
||||
val loadedMangaFlow = MutableStateFlow(loadedManga)
|
||||
|
||||
var searchQuery by mutableStateOf(query)
|
||||
|
||||
val selection: MutableList<LibraryManga> = mutableStateListOf()
|
||||
|
||||
val isPerCategory by preferences.categorizedDisplaySettings().asState()
|
||||
|
||||
var columns by mutableStateOf(0)
|
||||
|
||||
var currentDisplayMode by preferences.libraryDisplayMode().asState()
|
||||
|
||||
val tabVisibility by preferences.categoryTabs().asState()
|
||||
|
||||
val mangaCountVisibility by preferences.categoryNumberOfItems().asState()
|
||||
|
||||
var activeCategory: Int by preferences.lastUsedCategory().asState()
|
||||
|
||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
||||
|
||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
@@ -123,7 +128,7 @@ class LibraryPresenter(
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
private var librarySubscription: Subscription? = null
|
||||
private var librarySubscription: Job? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
@@ -135,22 +140,31 @@ class LibraryPresenter(
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
// TODO: Move this to a coroutine world
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.apply { setBadges(mangaMap) }
|
||||
}
|
||||
.combineLatest(getFilterObservable()) { lib, tracks ->
|
||||
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
|
||||
}
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, (categories, mangaMap) ->
|
||||
view.onNextLibraryUpdate(categories, mangaMap)
|
||||
},)
|
||||
/**
|
||||
* TODO: Move this to a coroutine world
|
||||
* - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
|
||||
* - Create new db view and new query to just fetch the current category save as needed to instance variable
|
||||
* - Fetch badges to maps and retrive as needed instead of fetching all of them at once
|
||||
*/
|
||||
if (librarySubscription == null || librarySubscription!!.isCancelled) {
|
||||
librarySubscription = presenterScope.launchIO {
|
||||
getLibraryObservable()
|
||||
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.apply { setBadges(mangaMap) }
|
||||
}
|
||||
.combineLatest(getFilterObservable()) { lib, tracks ->
|
||||
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks))
|
||||
}
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.asFlow()
|
||||
.collectLatest {
|
||||
state.isLoading = false
|
||||
loadedManga = it.mangaMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +411,7 @@ class LibraryPresenter(
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryObservable(): Observable<Library> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga ->
|
||||
return combine(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0)) {
|
||||
arrayListOf(Category.default(context)) + dbCategories
|
||||
} else {
|
||||
@@ -411,9 +425,9 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
this.categories = categories
|
||||
state.categories = categories
|
||||
Library(categories, libraryManga)
|
||||
}
|
||||
}.asObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,8 +435,8 @@ class LibraryPresenter(
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return getCategories.subscribe().asObservable()
|
||||
private fun getCategoriesObservable(): Flow<List<Category>> {
|
||||
return getCategories.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,8 +445,8 @@ class LibraryPresenter(
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
private fun getLibraryMangasObservable(): Observable<LibraryMap> {
|
||||
return getLibraryManga.subscribe().asObservable()
|
||||
private fun getLibraryMangasObservable(): Flow<LibraryMap> {
|
||||
return getLibraryManga.subscribe()
|
||||
.map { list ->
|
||||
list.map { libraryManga ->
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
@@ -447,7 +461,8 @@ class LibraryPresenter(
|
||||
* @return an observable of tracked manga.
|
||||
*/
|
||||
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
|
||||
return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
|
||||
return filterTriggerRelay.observeOn(Schedulers.io())
|
||||
.combineLatest(getTracksObservable()) { _, tracks -> tracks }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,7 +473,7 @@ class LibraryPresenter(
|
||||
private fun getTracksObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
|
||||
// TODO: Move this to domain/data layer
|
||||
return getTracks.subscribe()
|
||||
.asObservable().map { tracks ->
|
||||
.map { tracks ->
|
||||
tracks
|
||||
.groupBy { it.mangaId }
|
||||
.mapValues { tracksForMangaId ->
|
||||
@@ -468,6 +483,7 @@ class LibraryPresenter(
|
||||
}
|
||||
}
|
||||
}
|
||||
.asObservable()
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -497,7 +513,7 @@ class LibraryPresenter(
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.let { remove(it) }
|
||||
librarySubscription?.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,14 +626,50 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State<List<LibraryItem>> {
|
||||
fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State<Int?> {
|
||||
return produceState<Int?>(initialValue = null, loadedManga) {
|
||||
value = loadedManga[categoryId]?.size
|
||||
}
|
||||
}
|
||||
|
||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
return (if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns()).asState()
|
||||
}
|
||||
|
||||
// TODO: This is good but should we separate title from count or get categories with count from db
|
||||
@Composable
|
||||
fun getToolbarTitle(): androidx.compose.runtime.State<LibraryToolbarTitle> {
|
||||
val category = categories.getOrNull(activeCategory)
|
||||
|
||||
val defaultTitle = stringResource(id = R.string.label_library)
|
||||
val default = remember { LibraryToolbarTitle(defaultTitle) }
|
||||
|
||||
return produceState(initialValue = default, category, mangaCountVisibility, tabVisibility) {
|
||||
val title = if (tabVisibility.not()) category?.name ?: defaultTitle else defaultTitle
|
||||
|
||||
value = when {
|
||||
category == null -> default
|
||||
(tabVisibility.not() && mangaCountVisibility.not()) -> LibraryToolbarTitle(title)
|
||||
tabVisibility.not() && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size)
|
||||
(tabVisibility && categories.size > 1) && mangaCountVisibility -> LibraryToolbarTitle(title)
|
||||
tabVisibility && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size)
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaForCategory(page: Int): androidx.compose.runtime.State<List<LibraryItem>> {
|
||||
val categoryId = remember(categories) {
|
||||
categories.getOrNull(page)?.id ?: -1
|
||||
}
|
||||
val unfiltered = loadedManga[categoryId] ?: emptyList()
|
||||
|
||||
return derivedStateOf {
|
||||
val query = searchQuery
|
||||
if (query.isNotBlank()) {
|
||||
if (query.isNullOrBlank().not()) {
|
||||
unfiltered.filter {
|
||||
it.filter(query)
|
||||
it.filter(query!!)
|
||||
}
|
||||
} else {
|
||||
unfiltered
|
||||
@@ -626,9 +678,9 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDisplayMode(index: Int): DisplayModeSetting {
|
||||
fun getDisplayMode(index: Int): androidx.compose.runtime.State<DisplayModeSetting> {
|
||||
val category = categories[index]
|
||||
return remember {
|
||||
return derivedStateOf {
|
||||
if (isPerCategory.not() || category.id == 0L) {
|
||||
currentDisplayMode
|
||||
} else {
|
||||
@@ -642,34 +694,30 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selection.clear()
|
||||
state.selection = emptyList()
|
||||
}
|
||||
|
||||
fun toggleSelection(manga: LibraryManga) {
|
||||
val mutableList = state.selection.toMutableList()
|
||||
if (selection.fastAny { it.id == manga.id }) {
|
||||
selection.remove(manga)
|
||||
mutableList.remove(manga)
|
||||
} else {
|
||||
selection.add(manga)
|
||||
mutableList.add(manga)
|
||||
}
|
||||
view?.invalidateActionMode()
|
||||
view?.createActionModeIfNeeded()
|
||||
state.selection = mutableList
|
||||
}
|
||||
|
||||
fun selectAll(index: Int) {
|
||||
val category = categories[index]
|
||||
val items = loadedManga[category.id] ?: emptyList()
|
||||
selection.addAll(items.filterNot { it.manga in selection }.map { it.manga })
|
||||
view?.createActionModeIfNeeded()
|
||||
view?.invalidateActionMode()
|
||||
state.selection = state.selection.toMutableList().apply {
|
||||
addAll(items.filterNot { it.manga in selection }.map { it.manga })
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection(index: Int) {
|
||||
val category = categories[index]
|
||||
val items = (loadedManga[category.id] ?: emptyList()).map { it.manga }
|
||||
val invert = items.filterNot { it in selection }
|
||||
selection.removeAll(items)
|
||||
selection.addAll(invert)
|
||||
view?.createActionModeIfNeeded()
|
||||
view?.invalidateActionMode()
|
||||
state.selection = items.filterNot { it in selection }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,9 +488,13 @@ class MainActivity : BaseActivity() {
|
||||
return
|
||||
}
|
||||
val backstackSize = router.backstackSize
|
||||
if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
||||
val startScreen = router.getControllerWithTag("$startScreenId")
|
||||
if (backstackSize == 1 && startScreen == null) {
|
||||
// Return to start screen
|
||||
moveToStartScreen()
|
||||
setSelectedNavItem(startScreenId)
|
||||
} else if (startScreen != null && router.handleBack()) {
|
||||
// Clear selection for Library screen
|
||||
} else if (shouldHandleExitConfirmation()) {
|
||||
// Exit confirmation (resets after 2 seconds)
|
||||
lifecycleScope.launchUI { resetExitConfirmation() }
|
||||
|
||||
Reference in New Issue
Block a user