Bulk favorite for GlobalSearchScreen & MigrateSearchScreen
This commit is contained in:
parent
8d46ab8fc2
commit
c30a8873a2
6 changed files with 445 additions and 35 deletions
|
|
@ -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<Manga>,
|
||||
// KMK <--
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
|
|
@ -107,6 +130,9 @@ internal fun GlobalSearchContent(
|
|||
getManga = getManga,
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
// KMK -->
|
||||
selection = selection,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
is SearchItemResult.Error -> {
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_bulk_select),
|
||||
icon = Icons.Outlined.Checklist,
|
||||
onClick = toggleBulkSelectionMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
// KMK <--
|
||||
)
|
||||
if (progress in 1..<total) {
|
||||
LinearProgressIndicator(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -7,8 +8,12 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
|
|||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
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.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
|
||||
|
||||
class MigrateSearchScreen(private val mangaId: Long, private val validSources: List<Long>) : 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<MigrationListScreen>()
|
||||
.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<MigrationListScreen>()
|
||||
.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 <--
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SearchScreenModel.State>(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<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()
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
|
||||
sealed interface 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,
|
||||
|
|
@ -200,6 +410,11 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue