separate BulkSelection into standalone BulkFavoriteScreenModel

This commit is contained in:
Cuong M. Tran 2024-03-09 11:00:40 +07:00 committed by Cuong Tran
parent 42022bf661
commit cc5b6a1d5f
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
6 changed files with 419 additions and 386 deletions

View file

@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.presentation.components.SelectionToolbar
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
@ -24,9 +24,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
@Composable
fun GlobalSearchScreen(
// KMK -->
screenModel: GlobalSearchScreenModel,
// KMK <--
state: SearchScreenModel.State,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
@ -37,15 +34,20 @@ fun GlobalSearchScreen(
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
// KMK -->
bulkFavoriteState: BulkFavoriteScreenModel.State,
toggleSelectionMode: () -> Unit,
addFavorite: () -> Unit,
// KMK <--
) {
Scaffold(
topBar = { scrollBehavior ->
// KMK -->
if (state.selectionMode)
if (bulkFavoriteState.selectionMode)
SelectionToolbar(
selectedCount = state.selection.size,
onClickClearSelection = screenModel::toggleSelectionMode,
onChangeCategoryClicked = screenModel::addFavorite,
selectedCount = bulkFavoriteState.selection.size,
onClickClearSelection = toggleSelectionMode,
onChangeCategoryClicked = addFavorite,
)
else
// KMK <--
@ -62,7 +64,7 @@ fun GlobalSearchScreen(
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
// KMK -->
toggleBulkSelectionMode = screenModel::toggleSelectionMode,
toggleBulkSelectionMode = toggleSelectionMode,
// KMK <--
)
},
@ -75,7 +77,7 @@ fun GlobalSearchScreen(
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
// KMK -->
selection = state.selection,
selection = bulkFavoriteState.selection,
// KMK <--
)
}

View file

@ -5,7 +5,7 @@ import androidx.compose.runtime.State
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.presentation.components.SelectionToolbar
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.domain.manga.model.Manga
@ -13,9 +13,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
@Composable
fun MigrateSearchScreen(
// KMK -->
screenModel: MigrateSearchScreenModel,
// KMK <--
state: SearchScreenModel.State,
fromSourceId: Long?,
navigateUp: () -> Unit,
@ -27,15 +24,20 @@ fun MigrateSearchScreen(
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
// KMK -->
bulkFavoriteState: BulkFavoriteScreenModel.State,
toggleSelectionMode: () -> Unit,
addFavorite: () -> Unit,
// KMK <--
) {
Scaffold(
topBar = { scrollBehavior ->
// KMK -->
if (state.selectionMode)
if (bulkFavoriteState.selectionMode)
SelectionToolbar(
selectedCount = state.selection.size,
onClickClearSelection = screenModel::toggleSelectionMode,
onChangeCategoryClicked = screenModel::addFavorite,
selectedCount = bulkFavoriteState.selection.size,
onClickClearSelection = toggleSelectionMode,
onChangeCategoryClicked = addFavorite,
)
else
// KMK <--
@ -52,7 +54,7 @@ fun MigrateSearchScreen(
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
// KMK -->
toggleBulkSelectionMode = screenModel::toggleSelectionMode
toggleBulkSelectionMode = toggleSelectionMode
// KMK <--
)
},
@ -66,7 +68,7 @@ fun MigrateSearchScreen(
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
// KMK -->
selection = state.selection,
selection = bulkFavoriteState.selection,
// KMK <--
)
}

View file

@ -0,0 +1,336 @@
package eu.kanade.tachiyomi.ui.browse
import androidx.compose.runtime.Immutable
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
class BulkFavoriteScreenModel(
initialState: State = State(),
private val sourceManager: SourceManager = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
) : StateScreenModel<BulkFavoriteScreenModel.State>(initialState) {
fun backHandler() {
toggleSelectionMode()
}
fun toggleSelectionMode() {
if (state.value.selectionMode)
clearSelection()
mutableState.update { it.copy(selectionMode = !it.selectionMode) }
}
private fun clearSelection() {
mutableState.update { it.copy(selection = persistentListOf()) }
}
fun toggleSelection(manga: Manga) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
if (list.fastAny { it.id == manga.id }) {
list.removeAll { it.id == manga.id }
} else {
list.add(manga)
}
}
state.copy(selection = newSelection)
}.also {
if (state.value.selection.isEmpty())
toggleSelectionMode()
}
}
fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch {
val mangaWithDup = getDuplicateLibraryManga(startIdx)
if (mangaWithDup != null)
setDialog(Dialog.AllowDuplicate(mangaWithDup))
else
addFavoriteDuplicate()
}
}
fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch {
val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
setMangasCategories(mangaList, listOf(defaultCategory.id), emptyList())
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
// Automatic 'Default' or no categories
setMangasCategories(mangaList, emptyList(), emptyList())
}
else -> {
// Get indexes of the common categories to preselect.
val common = getCommonCategories(mangaList)
// Get indexes of the mix categories to preselect.
val mix = getMixCategories(mangaList)
val preselected = categories
.map {
when (it) {
in common -> CheckboxState.State.Checked(it)
in mix -> CheckboxState.TriState.Exclude(it)
else -> CheckboxState.State.None(it)
}
}
.toImmutableList()
setDialog(Dialog.ChangeMangasCategory(mangaList, preselected))
}
}
}
}
private suspend fun getNotDuplicateLibraryMangas(): List<Manga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, Manga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/**
* Bulk update categories of manga using old and new common categories.
*
* @param mangaList the list of manga to move.
* @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas.
*/
fun setMangasCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
screenModelScope.launchNonCancellable {
mangaList.fastForEach { manga ->
val categoryIds = getCategories.await(manga.id)
.map { it.id }
.subtract(removeCategories.toSet())
.plus(addCategories)
.toList()
moveMangaToCategoriesAndAddToLibrary(manga, categoryIds)
}
}
toggleSelectionMode()
}
private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List<Long>) {
moveMangaToCategory(manga.id, categories)
if (manga.favorite) return
screenModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true)
}
}
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds)
}
}
/**
* Returns the common categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas
.map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2) }
}
/**
* Returns the mix (non-common) categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common)
}
/**
* Get user categories.
*
* @return List of categories, not including the default category
*/
suspend fun getCategories(): List<Category> {
return getCategories.subscribe()
.firstOrNull()
?.filterNot { it.isSystemCategory }
.orEmpty()
}
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga).getOrNull(0)
}
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
)
}
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
val source = sourceManager.getOrStub(manga.source)
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
},
)
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
if (!new.favorite) {
new = new.removeCovers(coverCache)
} else {
setMangaDefaultChapterFlags.await(manga)
addTracks.bindEnhancedTrackers(manga, source)
}
updateManga.await(new.toMangaUpdate())
}
}
fun addFavorite(manga: Manga) {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
moveMangaToCategories(manga, defaultCategory)
changeMangaFavorite(manga)
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
moveMangaToCategories(manga)
changeMangaFavorite(manga)
}
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
setDialog(
Dialog.ChangeMangaCategory(
manga,
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
),
)
}
}
}
}
fun setDialog(dialog: Dialog?) {
mutableState.update {
it.copy(dialog = dialog)
}
}
interface Dialog {
data class RemoveManga(val manga: Manga) : Dialog
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class ChangeMangaCategory(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog
data class ChangeMangasCategory(
val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class AllowDuplicate(val duplicatedManga: Pair<Int, Manga>) : Dialog
}
@Immutable
data class State (
val dialog: Dialog? = null,
val selection: PersistentList<Manga> = persistentListOf(),
val selectionMode: Boolean = false,
)
}

View file

@ -11,8 +11,8 @@ import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
@ -29,17 +29,15 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
val dialogState by dialogScreenModel.state.collectAsState()
// KMK -->
BackHandler(enabled = state.selectionMode) {
when {
state.selectionMode -> screenModel.toggleSelectionMode()
}
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
BackHandler(enabled = bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelectionMode()
}
// KMK <--
MigrateSearchScreen(
// KMK -->
screenModel = screenModel,
// KMK <--
state = state,
fromSourceId = state.fromSourceId,
navigateUp = navigator::pop,
@ -55,8 +53,8 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
},
onClickItem = {
// KMK -->
if (state.selectionMode) {
screenModel.toggleSelection(it)
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(it)
}
else
// KMK <--
@ -71,39 +69,44 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
}
},
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
// KMK -->
bulkFavoriteState = bulkFavoriteState,
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
addFavorite = bulkFavoriteScreenModel::addFavorite,
// KMK <--
)
// KMK -->
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is SearchScreenModel.Dialog.ChangeMangasCategory -> {
val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
when (val dialog = bulkFavoriteState.dialog) {
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, exclude ->
screenModel.setMangaCategories(dialog.mangas, include, exclude)
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
},
)
}
is SearchScreenModel.Dialog.AllowDuplicate -> {
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
AllowDuplicateDialog(
onDismissRequest = onDismissRequest,
onAllowAllDuplicate = {
screenModel.addFavoriteDuplicate()
bulkFavoriteScreenModel.addFavoriteDuplicate()
},
onSkipAllDuplicate = {
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
},
onOpenManga = {
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
},
onAllowDuplicate = {
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
},
onSkipDuplicate = {
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
},
duplicatedName = dialog.duplicatedManga.second.title,
)

View file

@ -20,6 +20,7 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
@ -50,10 +51,11 @@ class GlobalSearchScreen(
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
BackHandler(enabled = state.selectionMode) {
when {
state.selectionMode -> screenModel.toggleSelectionMode()
}
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
BackHandler(enabled = bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.backHandler()
}
// KMK <--
@ -77,9 +79,6 @@ class GlobalSearchScreen(
}
} else {
GlobalSearchScreen(
// KMK -->
screenModel = screenModel,
// KMK <--
state = state,
navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery,
@ -92,96 +91,101 @@ class GlobalSearchScreen(
},
onClickItem = {
// KMK -->
if (state.selectionMode)
screenModel.toggleSelection(it)
if (bulkFavoriteState.selectionMode)
bulkFavoriteScreenModel.toggleSelection(it)
else
// KMK <--
navigator.push(MangaScreen(it.id, true))
},
onLongClickItem = { manga ->
// KMK -->
if (state.selectionMode)
if (bulkFavoriteState.selectionMode)
// KMK <--
navigator.push(MangaScreen(manga.id, true))
// KMK -->
else
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(SearchScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog(
SearchScreenModel.Dialog.AddDuplicateManga(
manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> bulkFavoriteScreenModel.setDialog(
BulkFavoriteScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
)
else -> screenModel.addFavorite(manga)
else -> bulkFavoriteScreenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
// KMK <--
},
// KMK -->
bulkFavoriteState = bulkFavoriteState,
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
addFavorite = bulkFavoriteScreenModel::addFavorite,
// KMK <--
)
}
// KMK -->
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is SearchScreenModel.Dialog.AddDuplicateManga -> {
val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
when (val dialog = bulkFavoriteState.dialog) {
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
)
}
is SearchScreenModel.Dialog.RemoveManga -> {
is BulkFavoriteScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is SearchScreenModel.Dialog.ChangeMangaCategory -> {
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
is SearchScreenModel.Dialog.ChangeMangasCategory -> {
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, exclude ->
screenModel.setMangaCategories(dialog.mangas, include, exclude)
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
},
)
}
is SearchScreenModel.Dialog.AllowDuplicate -> {
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
AllowDuplicateDialog(
onDismissRequest = onDismissRequest,
onAllowAllDuplicate = {
screenModel.addFavoriteDuplicate()
bulkFavoriteScreenModel.addFavoriteDuplicate()
},
onSkipAllDuplicate = {
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
},
onOpenManga = {
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
},
onAllowDuplicate = {
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
},
onSkipDuplicate = {
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
},
duplicatedName = dialog.duplicatedManga.second.title,
)

View file

@ -3,27 +3,15 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.Job
@ -32,29 +20,16 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.concurrent.Executors
abstract class SearchScreenModel(
@ -64,16 +39,6 @@ abstract class SearchScreenModel(
private val extensionManager: ExtensionManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
// KMK -->
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
// KMK <--
) : StateScreenModel<SearchScreenModel.State>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
@ -228,280 +193,6 @@ abstract class SearchScreenModel(
updateItems(newItems)
}
// KMK -->
fun toggleSelectionMode() {
if (state.value.selectionMode)
clearSelection()
mutableState.update { it.copy(selectionMode = !it.selectionMode) }
}
private fun clearSelection() {
mutableState.update { it.copy(selection = persistentListOf()) }
}
fun toggleSelection(manga: Manga) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
if (list.fastAny { it.id == manga.id }) {
list.removeAll { it.id == manga.id }
} else {
list.add(manga)
}
}
state.copy(selection = newSelection)
}.also {
if (state.value.selection.isEmpty())
toggleSelectionMode()
}
}
fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch {
val mangaWithDup = getDuplicateLibraryManga(startIdx)
if (mangaWithDup != null)
setDialog(Dialog.AllowDuplicate(mangaWithDup))
else
addFavoriteDuplicate()
}
}
fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch {
val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
setMangaCategories(mangaList, listOf(defaultCategory.id), emptyList())
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
// Automatic 'Default' or no categories
setMangaCategories(mangaList, emptyList(), emptyList())
}
else -> {
// Get indexes of the common categories to preselect.
val common = getCommonCategories(mangaList)
// Get indexes of the mix categories to preselect.
val mix = getMixCategories(mangaList)
val preselected = categories
.map {
when (it) {
in common -> CheckboxState.State.Checked(it)
in mix -> CheckboxState.TriState.Exclude(it)
else -> CheckboxState.State.None(it)
}
}
.toImmutableList()
setDialog(Dialog.ChangeMangasCategory(mangaList, preselected))
}
}
}
}
private suspend fun getNotDuplicateLibraryMangas(): List<Manga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, Manga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/**
* Bulk update categories of manga using old and new common categories.
*
* @param mangaList the list of manga to move.
* @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas.
*/
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
screenModelScope.launchNonCancellable {
mangaList.fastForEach { manga ->
val categoryIds = getCategories.await(manga.id)
.map { it.id }
.subtract(removeCategories.toSet())
.plus(addCategories)
.toList()
moveMangaToCategoriesAndAddToLibrary(manga, categoryIds)
}
}
toggleSelectionMode()
}
private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List<Long>) {
moveMangaToCategory(manga.id, categories)
if (manga.favorite) return
screenModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true)
}
}
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds)
}
}
/**
* Returns the common categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas
.map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2) }
}
/**
* Returns the mix (non-common) categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common)
}
/**
* Get user categories.
*
* @return List of categories, not including the default category
*/
suspend fun getCategories(): List<Category> {
return getCategories.subscribe()
.firstOrNull()
?.filterNot { it.isSystemCategory }
.orEmpty()
}
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga).getOrNull(0)
}
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
)
}
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
val source = sourceManager.getOrStub(manga.source)
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
},
)
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
if (!new.favorite) {
new = new.removeCovers(coverCache)
} else {
setMangaDefaultChapterFlags.await(manga)
addTracks.bindEnhancedTrackers(manga, source)
}
updateManga.await(new.toMangaUpdate())
}
}
fun addFavorite(manga: Manga) {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
moveMangaToCategories(manga, defaultCategory)
changeMangaFavorite(manga)
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
moveMangaToCategories(manga)
changeMangaFavorite(manga)
}
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
setDialog(
Dialog.ChangeMangaCategory(
manga,
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
),
)
}
}
}
}
fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) }
}
sealed interface Dialog {
data class RemoveManga(val manga: Manga) : Dialog
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class ChangeMangaCategory(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog
data class ChangeMangasCategory(
val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class AllowDuplicate(val duplicatedManga: Pair<Int, Manga>) : Dialog
}
// KMK <--
@Immutable
data class State(
val fromSourceId: Long? = null,
@ -509,11 +200,6 @@ abstract class SearchScreenModel(
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
// KMK -->
val dialog: Dialog? = null,
val selection: PersistentList<Manga> = persistentListOf(),
val selectionMode: Boolean = false,
// KMK <--
) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size