separate BulkSelection into standalone BulkFavoriteScreenModel
This commit is contained in:
parent
42022bf661
commit
cc5b6a1d5f
6 changed files with 419 additions and 386 deletions
|
|
@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||||
import eu.kanade.presentation.components.SelectionToolbar
|
import eu.kanade.presentation.components.SelectionToolbar
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.SearchItemResult
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
|
|
@ -24,9 +24,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchScreen(
|
fun GlobalSearchScreen(
|
||||||
// KMK -->
|
|
||||||
screenModel: GlobalSearchScreenModel,
|
|
||||||
// KMK <--
|
|
||||||
state: SearchScreenModel.State,
|
state: SearchScreenModel.State,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
|
|
@ -37,15 +34,20 @@ fun GlobalSearchScreen(
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
|
// KMK -->
|
||||||
|
bulkFavoriteState: BulkFavoriteScreenModel.State,
|
||||||
|
toggleSelectionMode: () -> Unit,
|
||||||
|
addFavorite: () -> Unit,
|
||||||
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.selectionMode)
|
if (bulkFavoriteState.selectionMode)
|
||||||
SelectionToolbar(
|
SelectionToolbar(
|
||||||
selectedCount = state.selection.size,
|
selectedCount = bulkFavoriteState.selection.size,
|
||||||
onClickClearSelection = screenModel::toggleSelectionMode,
|
onClickClearSelection = toggleSelectionMode,
|
||||||
onChangeCategoryClicked = screenModel::addFavorite,
|
onChangeCategoryClicked = addFavorite,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -62,7 +64,7 @@ fun GlobalSearchScreen(
|
||||||
onToggleResults = onToggleResults,
|
onToggleResults = onToggleResults,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
toggleBulkSelectionMode = screenModel::toggleSelectionMode,
|
toggleBulkSelectionMode = toggleSelectionMode,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -75,7 +77,7 @@ fun GlobalSearchScreen(
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
selection = state.selection,
|
selection = bulkFavoriteState.selection,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import androidx.compose.runtime.State
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||||
import eu.kanade.presentation.components.SelectionToolbar
|
import eu.kanade.presentation.components.SelectionToolbar
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.SearchScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
@ -13,9 +13,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSearchScreen(
|
fun MigrateSearchScreen(
|
||||||
// KMK -->
|
|
||||||
screenModel: MigrateSearchScreenModel,
|
|
||||||
// KMK <--
|
|
||||||
state: SearchScreenModel.State,
|
state: SearchScreenModel.State,
|
||||||
fromSourceId: Long?,
|
fromSourceId: Long?,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
|
|
@ -27,15 +24,20 @@ fun MigrateSearchScreen(
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
|
// KMK -->
|
||||||
|
bulkFavoriteState: BulkFavoriteScreenModel.State,
|
||||||
|
toggleSelectionMode: () -> Unit,
|
||||||
|
addFavorite: () -> Unit,
|
||||||
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.selectionMode)
|
if (bulkFavoriteState.selectionMode)
|
||||||
SelectionToolbar(
|
SelectionToolbar(
|
||||||
selectedCount = state.selection.size,
|
selectedCount = bulkFavoriteState.selection.size,
|
||||||
onClickClearSelection = screenModel::toggleSelectionMode,
|
onClickClearSelection = toggleSelectionMode,
|
||||||
onChangeCategoryClicked = screenModel::addFavorite,
|
onChangeCategoryClicked = addFavorite,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -52,7 +54,7 @@ fun MigrateSearchScreen(
|
||||||
onToggleResults = onToggleResults,
|
onToggleResults = onToggleResults,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
toggleBulkSelectionMode = screenModel::toggleSelectionMode
|
toggleBulkSelectionMode = toggleSelectionMode
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +68,7 @@ fun MigrateSearchScreen(
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
selection = state.selection,
|
selection = bulkFavoriteState.selection,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,8 @@ import eu.kanade.presentation.browse.MigrateSearchScreen
|
||||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
||||||
import eu.kanade.presentation.util.Screen
|
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.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.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
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()
|
val dialogState by dialogScreenModel.state.collectAsState()
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
BackHandler(enabled = state.selectionMode) {
|
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||||
when {
|
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||||
state.selectionMode -> screenModel.toggleSelectionMode()
|
|
||||||
}
|
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||||
|
bulkFavoriteScreenModel.toggleSelectionMode()
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
MigrateSearchScreen(
|
MigrateSearchScreen(
|
||||||
// KMK -->
|
|
||||||
screenModel = screenModel,
|
|
||||||
// KMK <--
|
|
||||||
state = state,
|
state = state,
|
||||||
fromSourceId = state.fromSourceId,
|
fromSourceId = state.fromSourceId,
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
|
|
@ -55,8 +53,8 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
||||||
},
|
},
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.selectionMode) {
|
if (bulkFavoriteState.selectionMode) {
|
||||||
screenModel.toggleSelection(it)
|
bulkFavoriteScreenModel.toggleSelection(it)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -71,39 +69,44 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||||
|
// KMK -->
|
||||||
|
bulkFavoriteState = bulkFavoriteState,
|
||||||
|
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||||
|
addFavorite = bulkFavoriteScreenModel::addFavorite,
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val onDismissRequest = { screenModel.setDialog(null) }
|
val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||||
when (val dialog = state.dialog) {
|
when (val dialog = bulkFavoriteState.dialog) {
|
||||||
is SearchScreenModel.Dialog.ChangeMangasCategory -> {
|
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||||
ChangeCategoryDialog(
|
ChangeCategoryDialog(
|
||||||
initialSelection = dialog.initialSelection,
|
initialSelection = dialog.initialSelection,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||||
onConfirm = { include, exclude ->
|
onConfirm = { include, exclude ->
|
||||||
screenModel.setMangaCategories(dialog.mangas, include, exclude)
|
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchScreenModel.Dialog.AllowDuplicate -> {
|
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||||
AllowDuplicateDialog(
|
AllowDuplicateDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onAllowAllDuplicate = {
|
onAllowAllDuplicate = {
|
||||||
screenModel.addFavoriteDuplicate()
|
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||||
},
|
},
|
||||||
onSkipAllDuplicate = {
|
onSkipAllDuplicate = {
|
||||||
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||||
},
|
},
|
||||||
onOpenManga = {
|
onOpenManga = {
|
||||||
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
||||||
},
|
},
|
||||||
onAllowDuplicate = {
|
onAllowDuplicate = {
|
||||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||||
},
|
},
|
||||||
onSkipDuplicate = {
|
onSkipDuplicate = {
|
||||||
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||||
},
|
},
|
||||||
duplicatedName = dialog.duplicatedManga.second.title,
|
duplicatedName = dialog.duplicatedManga.second.title,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
||||||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||||
import eu.kanade.presentation.util.Screen
|
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.browse.source.browse.BrowseSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
|
@ -50,10 +51,11 @@ class GlobalSearchScreen(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
BackHandler(enabled = state.selectionMode) {
|
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||||
when {
|
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||||
state.selectionMode -> screenModel.toggleSelectionMode()
|
|
||||||
}
|
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||||
|
bulkFavoriteScreenModel.backHandler()
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
|
|
@ -77,9 +79,6 @@ class GlobalSearchScreen(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalSearchScreen(
|
GlobalSearchScreen(
|
||||||
// KMK -->
|
|
||||||
screenModel = screenModel,
|
|
||||||
// KMK <--
|
|
||||||
state = state,
|
state = state,
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||||
|
|
@ -92,96 +91,101 @@ class GlobalSearchScreen(
|
||||||
},
|
},
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.selectionMode)
|
if (bulkFavoriteState.selectionMode)
|
||||||
screenModel.toggleSelection(it)
|
bulkFavoriteScreenModel.toggleSelection(it)
|
||||||
else
|
else
|
||||||
// KMK <--
|
// KMK <--
|
||||||
navigator.push(MangaScreen(it.id, true))
|
navigator.push(MangaScreen(it.id, true))
|
||||||
},
|
},
|
||||||
onLongClickItem = { manga ->
|
onLongClickItem = { manga ->
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (state.selectionMode)
|
if (bulkFavoriteState.selectionMode)
|
||||||
// KMK <--
|
// KMK <--
|
||||||
navigator.push(MangaScreen(manga.id, true))
|
navigator.push(MangaScreen(manga.id, true))
|
||||||
// KMK -->
|
// KMK -->
|
||||||
else
|
else
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga)
|
||||||
when {
|
when {
|
||||||
manga.favorite -> screenModel.setDialog(SearchScreenModel.Dialog.RemoveManga(manga))
|
manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga))
|
||||||
duplicateManga != null -> screenModel.setDialog(
|
duplicateManga != null -> bulkFavoriteScreenModel.setDialog(
|
||||||
SearchScreenModel.Dialog.AddDuplicateManga(
|
BulkFavoriteScreenModel.Dialog.AddDuplicateManga(
|
||||||
manga,
|
manga,
|
||||||
duplicateManga,
|
duplicateManga,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else -> screenModel.addFavorite(manga)
|
else -> bulkFavoriteScreenModel.addFavorite(manga)
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
},
|
},
|
||||||
|
// KMK -->
|
||||||
|
bulkFavoriteState = bulkFavoriteState,
|
||||||
|
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||||
|
addFavorite = bulkFavoriteScreenModel::addFavorite,
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val onDismissRequest = { screenModel.setDialog(null) }
|
val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||||
when (val dialog = state.dialog) {
|
when (val dialog = bulkFavoriteState.dialog) {
|
||||||
is SearchScreenModel.Dialog.AddDuplicateManga -> {
|
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchScreenModel.Dialog.RemoveManga -> {
|
is BulkFavoriteScreenModel.Dialog.RemoveManga -> {
|
||||||
RemoveMangaDialog(
|
RemoveMangaDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
screenModel.changeMangaFavorite(dialog.manga)
|
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||||
},
|
},
|
||||||
mangaToRemove = dialog.manga,
|
mangaToRemove = dialog.manga,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchScreenModel.Dialog.ChangeMangaCategory -> {
|
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> {
|
||||||
ChangeCategoryDialog(
|
ChangeCategoryDialog(
|
||||||
initialSelection = dialog.initialSelection,
|
initialSelection = dialog.initialSelection,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||||
onConfirm = { include, _ ->
|
onConfirm = { include, _ ->
|
||||||
screenModel.changeMangaFavorite(dialog.manga)
|
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||||
screenModel.moveMangaToCategories(dialog.manga, include)
|
bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchScreenModel.Dialog.ChangeMangasCategory -> {
|
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||||
ChangeCategoryDialog(
|
ChangeCategoryDialog(
|
||||||
initialSelection = dialog.initialSelection,
|
initialSelection = dialog.initialSelection,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||||
onConfirm = { include, exclude ->
|
onConfirm = { include, exclude ->
|
||||||
screenModel.setMangaCategories(dialog.mangas, include, exclude)
|
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchScreenModel.Dialog.AllowDuplicate -> {
|
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||||
AllowDuplicateDialog(
|
AllowDuplicateDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onAllowAllDuplicate = {
|
onAllowAllDuplicate = {
|
||||||
screenModel.addFavoriteDuplicate()
|
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||||
},
|
},
|
||||||
onSkipAllDuplicate = {
|
onSkipAllDuplicate = {
|
||||||
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||||
},
|
},
|
||||||
onOpenManga = {
|
onOpenManga = {
|
||||||
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
||||||
},
|
},
|
||||||
onAllowDuplicate = {
|
onAllowDuplicate = {
|
||||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||||
},
|
},
|
||||||
onSkipDuplicate = {
|
onSkipDuplicate = {
|
||||||
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||||
},
|
},
|
||||||
duplicatedName = dialog.duplicatedManga.second.title,
|
duplicatedName = dialog.duplicatedManga.second.title,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,15 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.produceState
|
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.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.manga.model.toDomainManga
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.track.interactor.AddTracks
|
|
||||||
import eu.kanade.presentation.util.ioCoroutineScope
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.PersistentMap
|
||||||
import kotlinx.collections.immutable.mutate
|
import kotlinx.collections.immutable.mutate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toPersistentMap
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -32,29 +20,16 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
abstract class SearchScreenModel(
|
abstract class SearchScreenModel(
|
||||||
|
|
@ -64,16 +39,6 @@ abstract class SearchScreenModel(
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||||
private val getManga: GetManga = 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) {
|
) : StateScreenModel<SearchScreenModel.State>(initialState) {
|
||||||
|
|
||||||
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||||
|
|
@ -228,280 +193,6 @@ abstract class SearchScreenModel(
|
||||||
updateItems(newItems)
|
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
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val fromSourceId: Long? = null,
|
val fromSourceId: Long? = null,
|
||||||
|
|
@ -509,11 +200,6 @@ abstract class SearchScreenModel(
|
||||||
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
|
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
|
||||||
val onlyShowHasResults: Boolean = false,
|
val onlyShowHasResults: Boolean = false,
|
||||||
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
|
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 progress: Int = items.count { it.value !is SearchItemResult.Loading }
|
||||||
val total: Int = items.size
|
val total: Int = items.size
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue