diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 80e13f03c..a7d390f34 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -10,7 +10,9 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem 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.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter @@ -22,6 +24,9 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun GlobalSearchScreen( + // KMK --> + screenModel: GlobalSearchScreenModel, + // KMK <-- state: SearchScreenModel.State, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, @@ -35,19 +40,31 @@ fun GlobalSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - sourceFilter = state.sourceFilter, - onChangeSearchFilter = onChangeSearchFilter, - onlyShowHasResults = state.onlyShowHasResults, - onToggleResults = onToggleResults, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (state.selectionMode) + SelectionToolbar( + selectedCount = state.selection.size, + onClickClearSelection = screenModel::toggleSelectionMode, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, + scrollBehavior = scrollBehavior, + // KMK --> + toggleBulkSelectionMode = screenModel::toggleSelectionMode + // KMK <-- + ) }, ) { paddingValues -> GlobalSearchContent( @@ -57,6 +74,9 @@ fun GlobalSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = state.selection, + // KMK <-- ) } } @@ -70,6 +90,9 @@ internal fun GlobalSearchContent( onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, + // KMK --> + selection: List, + // KMK <-- ) { LazyColumn( contentPadding = contentPadding, @@ -107,6 +130,9 @@ internal fun GlobalSearchContent( getManga = getManga, onClick = onClickItem, onLongClick = onLongClickItem, + // KMK --> + selection = selection, + // KMK <-- ) } is SearchItemResult.Error -> { diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 31abb596c..d32f3ffdd 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -11,6 +12,9 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun MigrateSearchScreen( + // KMK --> + screenModel: MigrateSearchScreenModel, + // KMK <-- state: SearchScreenModel.State, fromSourceId: Long?, navigateUp: () -> Unit, @@ -25,19 +29,31 @@ fun MigrateSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - sourceFilter = state.sourceFilter, - onChangeSearchFilter = onChangeSearchFilter, - onlyShowHasResults = state.onlyShowHasResults, - onToggleResults = onToggleResults, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (state.selectionMode) + eu.kanade.presentation.components.SelectionToolbar( + selectedCount = state.selection.size, + onClickClearSelection = screenModel::toggleSelectionMode, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, + scrollBehavior = scrollBehavior, + // KMK --> + toggleBulkSelectionMode = screenModel::toggleSelectionMode + // KMK <-- + ) }, ) { paddingValues -> GlobalSearchContent( @@ -48,6 +64,9 @@ fun MigrateSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = state.selection, + // KMK <-- ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index fa61696ae..3dbc87fce 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.PushPin @@ -26,8 +27,11 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter +import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -45,6 +49,9 @@ fun GlobalSearchToolbar( onlyShowHasResults: Boolean, onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, + // KMK --> + toggleBulkSelectionMode: () -> Unit, + // KMK <-- ) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Box { @@ -55,6 +62,23 @@ fun GlobalSearchToolbar( onClickCloseSearch = navigateUp, navigateUp = navigateUp, scrollBehavior = scrollBehavior, + // KMK --> + actions = { + AppBarActions( + actions = persistentListOf().builder() + .apply { + add( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleBulkSelectionMode, + ), + ) + } + .build(), + ) + }, + // KMK <-- ) if (progress in 1..) : Screen() { @@ -23,7 +28,18 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) } val dialogState by dialogScreenModel.state.collectAsState() + // KMK --> + BackHandler(enabled = state.selectionMode) { + when { + state.selectionMode -> screenModel.toggleSelectionMode() + } + } + // KMK <-- + MigrateSearchScreen( + // KMK --> + screenModel = screenModel, + // KMK <-- state = state, fromSourceId = state.fromSourceId, navigateUp = navigator::pop, @@ -38,15 +54,62 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L // SY <-- }, onClickItem = { - // SY --> - navigator.items - .filterIsInstance() - .last() - .newSelectedItem = mangaId to it.id - navigator.popUntil { it is MigrationListScreen } - // SY <-- + // KMK --> + if (state.selectionMode) { + screenModel.toggleSelection(it) + } + else + // KMK <-- + { + // SY --> + navigator.items + .filterIsInstance() + .last() + .newSelectedItem = mangaId to it.id + navigator.popUntil { it is MigrationListScreen } + // SY <-- + } }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, ) + + // KMK --> + val onDismissRequest = { screenModel.setDialog(null) } + when (val dialog = state.dialog) { + is SearchScreenModel.Dialog.ChangeMangasCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + is SearchScreenModel.Dialog.AllowDuplicate -> { + AllowDuplicateDialog( + onDismissRequest = onDismissRequest, + onAllowAllDuplicate = { + screenModel.addFavoriteDuplicate() + }, + onSkipAllDuplicate = { + screenModel.addFavoriteDuplicate(skipAllDuplicates = true) + }, + onOpenManga = { + navigator.push(MangaScreen(dialog.duplicatedManga.second.id)) + }, + onAllowDuplicate = { + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1) + }, + onSkipDuplicate = { + screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first) + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first) + }, + duplicatedName = dialog.duplicatedManga.second.title, + ) + } + else -> {} + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index a09595c4d..da382a574 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -11,8 +12,11 @@ import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.GlobalSearchScreen +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.source.browse.BrowseSourceScreen +import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import tachiyomi.presentation.core.screens.LoadingScreen @@ -36,6 +40,14 @@ class GlobalSearchScreen( mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1) } + // KMK --> + BackHandler(enabled = state.selectionMode) { + when { + state.selectionMode -> screenModel.toggleSelectionMode() + } + } + // KMK <-- + if (showSingleLoadingScreen) { LoadingScreen() @@ -56,6 +68,9 @@ class GlobalSearchScreen( } } else { GlobalSearchScreen( + // KMK --> + screenModel = screenModel, + // KMK <-- state = state, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, @@ -66,9 +81,57 @@ class GlobalSearchScreen( onClickSource = { navigator.push(BrowseSourceScreen(it.id, state.searchQuery)) }, - onClickItem = { navigator.push(MangaScreen(it.id, true)) }, - onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, + onClickItem = { + // KMK --> + if (state.selectionMode) + screenModel.toggleSelection(it) + else + // KMK <-- + navigator.push(MangaScreen(it.id, true)) + }, + onLongClickItem = { + navigator.push(MangaScreen(it.id, true)) + }, ) } + + // KMK --> + val onDismissRequest = { screenModel.setDialog(null) } + when (val dialog = state.dialog) { + is SearchScreenModel.Dialog.ChangeMangasCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + is SearchScreenModel.Dialog.AllowDuplicate -> { + AllowDuplicateDialog( + onDismissRequest = onDismissRequest, + onAllowAllDuplicate = { + screenModel.addFavoriteDuplicate() + }, + onSkipAllDuplicate = { + screenModel.addFavoriteDuplicate(skipAllDuplicates = true) + }, + onOpenManga = { + navigator.push(MangaScreen(dialog.duplicatedManga.second.id)) + }, + onAllowDuplicate = { + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1) + }, + onSkipDuplicate = { + screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first) + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first) + }, + duplicatedName = dialog.duplicatedManga.second.title, + ) + } + else -> {} + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index fe1fe79d1..8fbede554 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -3,15 +3,24 @@ 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.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.CatalogueSource +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 @@ -20,10 +29,19 @@ 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.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.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 @@ -39,6 +57,13 @@ 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(), + // KMK <-- ) : StateScreenModel(initialState) { private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() @@ -193,6 +218,191 @@ 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) + } + } + + 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 { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + 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, addCategories: List, removeCategories: List) { + 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) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + 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): Collection { + 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): Collection { + 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 { + return getCategories.subscribe() + .firstOrNull() + ?.filterNot { it.isSystemCategory } + .orEmpty() + } + + fun setDialog(dialog: Dialog?) { + mutableState.update { it.copy(dialog = dialog) } + } + + sealed interface Dialog { + data class ChangeMangasCategory( + val mangas: List, + val initialSelection: ImmutableList>, + ) : Dialog + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog + } + // KMK <-- + @Immutable data class State( val fromSourceId: Long? = null, @@ -200,6 +410,11 @@ abstract class SearchScreenModel( val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, val items: PersistentMap = persistentMapOf(), + // KMK --> + val dialog: Dialog? = null, + val selection: PersistentList = persistentListOf(), + val selectionMode: Boolean = false, + // KMK <-- ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size