migrate the rest to BulkSelectionScreenModel
This commit is contained in:
parent
cc5b6a1d5f
commit
df229af0eb
17 changed files with 352 additions and 817 deletions
|
|
@ -71,6 +71,7 @@ fun FeedScreen(
|
|||
onClickManga: (Manga) -> Unit,
|
||||
// KMK -->
|
||||
onLongClickManga: (Manga) -> Unit,
|
||||
selection: List<Manga>,
|
||||
// KMK <--
|
||||
onRefresh: () -> Unit,
|
||||
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
|
||||
|
|
@ -126,7 +127,7 @@ fun FeedScreen(
|
|||
onClickManga = onClickManga,
|
||||
// KMK -->
|
||||
onLongClickManga = onLongClickManga,
|
||||
selection = state.selection,
|
||||
selection = selection,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
@ -239,7 +240,7 @@ fun <T> RadioSelector(
|
|||
options: ImmutableList<T>,
|
||||
selected: Int?,
|
||||
optionStrings: ImmutableList<String> = remember { options.map { it.toString() }.toImmutableList() },
|
||||
onSelectOption: (Int) -> Unit /* KMK --> */ = {} /* KMK <-- */,
|
||||
onSelectOption: (Int) -> Unit/* KMK --> */ = {},/* KMK <-- */
|
||||
) {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
optionStrings.forEachIndexed { index, option ->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.domain.source.model.installedExtension
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
|
|
@ -35,19 +37,21 @@ fun GlobalSearchScreen(
|
|||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
// KMK -->
|
||||
bulkFavoriteState: BulkFavoriteScreenModel.State,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
addFavorite: () -> Unit,
|
||||
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
) {
|
||||
// KMK -->
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
// KMK <--
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
SelectionToolbar(
|
||||
selectedCount = bulkFavoriteState.selection.size,
|
||||
onClickClearSelection = toggleSelectionMode,
|
||||
onChangeCategoryClicked = addFavorite,
|
||||
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
|
|
@ -64,7 +68,7 @@ fun GlobalSearchScreen(
|
|||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
// KMK -->
|
||||
toggleBulkSelectionMode = toggleSelectionMode,
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
// KMK <--
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package eu.kanade.presentation.browse
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.SelectionToolbar
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
|
|
@ -25,19 +27,21 @@ fun MigrateSearchScreen(
|
|||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
// KMK -->
|
||||
bulkFavoriteState: BulkFavoriteScreenModel.State,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
addFavorite: () -> Unit,
|
||||
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
) {
|
||||
// KMK -->
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
// KMK <--
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
SelectionToolbar(
|
||||
selectedCount = bulkFavoriteState.selection.size,
|
||||
onClickClearSelection = toggleSelectionMode,
|
||||
onChangeCategoryClicked = addFavorite,
|
||||
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
|
|
@ -54,7 +58,7 @@ fun MigrateSearchScreen(
|
|||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
// KMK -->
|
||||
toggleBulkSelectionMode = toggleSelectionMode
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode
|
||||
// KMK <--
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ package eu.kanade.presentation.browse
|
|||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Checklist
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
|
|
@ -14,11 +18,12 @@ 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.SourceSettingsButton
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.components.SelectionToolbar
|
||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedState
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
|
@ -104,18 +109,21 @@ fun SourceFeedScreen(
|
|||
// KMK -->
|
||||
sourceId: Long,
|
||||
onLongClickManga: (Manga) -> Unit,
|
||||
screenModel: SourceFeedScreenModel,
|
||||
screenState: SourceFeedState,
|
||||
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
) {
|
||||
// KMK -->
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
// KMK <--
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
// KMK -->
|
||||
if (screenState.selectionMode)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
SelectionToolbar(
|
||||
selectedCount = screenState.selection.size,
|
||||
onClickClearSelection = screenModel::clearSelection,
|
||||
onChangeCategoryClicked = screenModel::addFavorite,
|
||||
selectedCount = bulkFavoriteState.selection.size,
|
||||
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
|
|
@ -127,6 +135,7 @@ fun SourceFeedScreen(
|
|||
onClickSearch = onClickSearch,
|
||||
// KMK -->
|
||||
sourceId = sourceId,
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
// KMK <--
|
||||
)
|
||||
},
|
||||
|
|
@ -152,7 +161,7 @@ fun SourceFeedScreen(
|
|||
onClickManga = onClickManga,
|
||||
// KMK -->
|
||||
onLongClickManga = onLongClickManga,
|
||||
screenState = screenState,
|
||||
selection = bulkFavoriteState.selection,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
@ -173,7 +182,7 @@ fun SourceFeedList(
|
|||
onClickManga: (Manga) -> Unit,
|
||||
// KMK -->
|
||||
onLongClickManga: (Manga) -> Unit,
|
||||
screenState: SourceFeedState,
|
||||
selection: List<Manga>,
|
||||
// KMK <--
|
||||
) {
|
||||
ScrollbarLazyColumn(
|
||||
|
|
@ -208,7 +217,7 @@ fun SourceFeedList(
|
|||
onClickManga = onClickManga,
|
||||
// KMK -->
|
||||
onLongClickManga = onLongClickManga,
|
||||
selection = screenState.selection,
|
||||
selection = selection,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
@ -257,6 +266,7 @@ fun SourceFeedToolbar(
|
|||
onClickSearch: (String) -> Unit,
|
||||
// KMK -->
|
||||
sourceId: Long,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
SearchToolbar(
|
||||
|
|
@ -269,6 +279,19 @@ fun SourceFeedToolbar(
|
|||
placeholderText = stringResource(MR.strings.action_search_hint),
|
||||
// KMK -->
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_bulk_select),
|
||||
icon = Icons.Outlined.Checklist,
|
||||
onClick = toggleSelectionMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
persistentListOf(
|
||||
SourceSettingsButton(sourceId),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ fun BrowseSourceToolbar(
|
|||
onSearch: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
// KMK -->
|
||||
toggleBulkSelectionMode: () -> Unit,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
// Avoid capturing unstable source in actions lambda
|
||||
|
|
@ -81,7 +81,7 @@ fun BrowseSourceToolbar(
|
|||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_bulk_select),
|
||||
icon = Icons.Outlined.Checklist,
|
||||
onClick = toggleBulkSelectionMode,
|
||||
onClick = toggleSelectionMode,
|
||||
),
|
||||
)
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fun GlobalSearchToolbar(
|
|||
onToggleResults: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
// KMK -->
|
||||
toggleBulkSelectionMode: () -> Unit,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
|
|
@ -71,7 +71,7 @@ fun GlobalSearchToolbar(
|
|||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_bulk_select),
|
||||
icon = Icons.Outlined.Checklist,
|
||||
onClick = toggleBulkSelectionMode,
|
||||
onClick = toggleSelectionMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.zIndex
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -40,7 +40,7 @@ fun TabbedScreen(
|
|||
searchQuery: String? = null,
|
||||
onChangeSearchQuery: (String?) -> Unit = {},
|
||||
// KMK -->
|
||||
screenModel: FeedScreenModel,
|
||||
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -48,7 +48,7 @@ fun TabbedScreen(
|
|||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// KMK -->
|
||||
val screenState by screenModel.state.collectAsState()
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
// KMK <--
|
||||
|
||||
LaunchedEffect(startIndex) {
|
||||
|
|
@ -62,11 +62,11 @@ fun TabbedScreen(
|
|||
val tab = tabs[state.currentPage]
|
||||
val searchEnabled = tab.searchEnabled
|
||||
// KMK -->
|
||||
if (screenState.selectionMode)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
SelectionToolbar(
|
||||
selectedCount = screenState.selection.size,
|
||||
onClickClearSelection = screenModel::clearSelection,
|
||||
onChangeCategoryClicked = screenModel::addFavorite,
|
||||
selectedCount = bulkFavoriteState.selection.size,
|
||||
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import eu.kanade.presentation.util.Tab
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
|
||||
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.feed.feedTab
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||
|
|
@ -67,7 +66,7 @@ data class BrowseTab(
|
|||
val extensionsState by extensionsScreenModel.state.collectAsState()
|
||||
|
||||
// KMK -->
|
||||
val feedScreenModel = rememberScreenModel { FeedScreenModel() }
|
||||
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||
// KMK <--
|
||||
|
||||
TabbedScreen(
|
||||
|
|
@ -81,7 +80,7 @@ data class BrowseTab(
|
|||
)
|
||||
} else if (feedTabInFront) {
|
||||
persistentListOf(
|
||||
feedTab(/* KMK --> */feedScreenModel/* KMK <-- */),
|
||||
feedTab(/* KMK --> */bulkFavoriteScreenModel/* KMK <-- */),
|
||||
sourcesTab(),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
|
|
@ -89,7 +88,7 @@ data class BrowseTab(
|
|||
} else {
|
||||
persistentListOf(
|
||||
sourcesTab(),
|
||||
feedTab(/* KMK --> */feedScreenModel/* KMK <-- */),
|
||||
feedTab(/* KMK --> */bulkFavoriteScreenModel/* KMK <-- */),
|
||||
extensionsTab(extensionsScreenModel),
|
||||
migrateSourceTab(),
|
||||
)
|
||||
|
|
@ -99,7 +98,7 @@ data class BrowseTab(
|
|||
searchQuery = extensionsState.searchQuery,
|
||||
onChangeSearchQuery = extensionsScreenModel::search,
|
||||
// KMK -->
|
||||
screenModel = feedScreenModel,
|
||||
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.State
|
||||
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
|
||||
|
|
@ -16,8 +14,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
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.asCoroutineDispatcher
|
||||
|
|
@ -27,7 +23,6 @@ import kotlinx.coroutines.channels.Channel
|
|||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
|
@ -35,15 +30,9 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.common.preference.CheckboxState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
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.source.interactor.CountFeedSavedSearchGlobal
|
||||
|
|
@ -76,12 +65,6 @@ open class FeedScreenModel(
|
|||
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
|
||||
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
||||
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
||||
// KMK -->
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<FeedScreenState>(FeedScreenState()) {
|
||||
|
||||
private val _events = Channel<Event>(Int.MAX_VALUE)
|
||||
|
|
@ -322,188 +305,10 @@ open class FeedScreenModel(
|
|||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
fun clearSelection() {
|
||||
mutableState.update { it.copy(selection = persistentListOf()) }
|
||||
}
|
||||
|
||||
fun toggleSelection(manga: DomainManga) {
|
||||
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<DomainManga> {
|
||||
return state.value.selection.filterNot { manga ->
|
||||
getDuplicateLibraryManga.await(manga).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, DomainManga>? {
|
||||
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<DomainManga>, 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)
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
private suspend fun getCommonCategories(mangas: List<DomainManga>): 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<DomainManga>): 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)
|
||||
}
|
||||
|
||||
private fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
sealed class Dialog {
|
||||
data class AddFeed(val options: ImmutableList<CatalogueSource>) : Dialog()
|
||||
data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList<SavedSearch?>) : Dialog()
|
||||
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
||||
// KMK -->
|
||||
data class ChangeMangasCategory(
|
||||
val mangas: List<DomainManga>,
|
||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
) : Dialog()
|
||||
data class AllowDuplicate(val duplicatedManga: Pair<Int, DomainManga>) : Dialog()
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
|
|
@ -515,9 +320,6 @@ open class FeedScreenModel(
|
|||
data class FeedScreenState(
|
||||
val dialog: FeedScreenModel.Dialog? = null,
|
||||
val items: List<FeedItemUI>? = null,
|
||||
// KMK -->
|
||||
val selection: PersistentList<DomainManga> = persistentListOf(),
|
||||
// KMK <--
|
||||
) {
|
||||
val isLoading
|
||||
get() = items == null
|
||||
|
|
@ -527,10 +329,6 @@ data class FeedScreenState(
|
|||
|
||||
val isLoadingItems
|
||||
get() = items?.fastAny { it.results == null } != false
|
||||
|
||||
// KMK -->
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
const val MAX_FEED_ITEMS = 20
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ package eu.kanade.tachiyomi.ui.browse.feed
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.Checklist
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.stack.StackEvent
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
|
|
@ -20,10 +21,13 @@ import eu.kanade.presentation.browse.FeedAddDialog
|
|||
import eu.kanade.presentation.browse.FeedAddSearchDialog
|
||||
import eu.kanade.presentation.browse.FeedDeleteConfirmDialog
|
||||
import eu.kanade.presentation.browse.FeedScreen
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
||||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||
|
|
@ -31,6 +35,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
|
@ -39,16 +44,26 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||
@Composable
|
||||
fun Screen.feedTab(
|
||||
// KMK -->
|
||||
screenModel: FeedScreenModel
|
||||
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
): TabContent {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
/* KMK -->
|
||||
val screenModel = rememberScreenModel { FeedScreenModel() }
|
||||
KMK <-- */
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
// KMK -->
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||
bulkFavoriteScreenModel.backHandler()
|
||||
}
|
||||
|
||||
LaunchedEffect(bulkFavoriteState.selectionMode) {
|
||||
HomeScreen.showBottomNav(!bulkFavoriteState.selectionMode)
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
DisposableEffect(navigator.lastEvent) {
|
||||
|
|
@ -65,46 +80,24 @@ fun Screen.feedTab(
|
|||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
when {
|
||||
state.selectionMode -> screenModel.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.selectionMode, state.dialog) {
|
||||
HomeScreen.showBottomNav(!state.selectionMode)
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
return TabContent(
|
||||
titleRes = SYMR.strings.feed,
|
||||
actions =
|
||||
// KMK -->
|
||||
if (state.selectionMode)
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.Bookmark,
|
||||
onClick = { },
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Filled.Bookmark,
|
||||
onClick = { },
|
||||
),
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_add),
|
||||
icon = Icons.Outlined.Add,
|
||||
onClick = {
|
||||
screenModel.openAddDialog()
|
||||
},
|
||||
),
|
||||
actions = persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_add),
|
||||
icon = Icons.Outlined.Add,
|
||||
onClick = {
|
||||
screenModel.openAddDialog()
|
||||
},
|
||||
),
|
||||
// KMK -->
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_bulk_select),
|
||||
icon = Icons.Outlined.Checklist,
|
||||
onClick = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
),
|
||||
// KMK <--
|
||||
),
|
||||
content = { contentPadding, snackbarHostState ->
|
||||
FeedScreen(
|
||||
state = state,
|
||||
|
|
@ -136,29 +129,38 @@ fun Screen.feedTab(
|
|||
onClickDelete = screenModel::openDeleteDialog,
|
||||
onClickManga = { manga ->
|
||||
// KMK -->
|
||||
if (state.selectionMode)
|
||||
screenModel.toggleSelection(manga)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
bulkFavoriteScreenModel.toggleSelection(manga)
|
||||
else
|
||||
// KMK <--
|
||||
// KMK <--
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
},
|
||||
// KMK -->
|
||||
onLongClickManga = { manga ->
|
||||
if (state.selectionMode) {
|
||||
if (!bulkFavoriteState.selectionMode)
|
||||
scope.launchIO {
|
||||
val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga))
|
||||
duplicateManga != null -> bulkFavoriteScreenModel.setDialog(
|
||||
BulkFavoriteScreenModel.Dialog.AddDuplicateManga(
|
||||
manga,
|
||||
duplicateManga,
|
||||
),
|
||||
)
|
||||
else -> bulkFavoriteScreenModel.addFavorite(manga)
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
else
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
} else {
|
||||
screenModel.toggleSelection(manga)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
},
|
||||
selection = bulkFavoriteState.selection,
|
||||
// KMK <--
|
||||
onRefresh = screenModel::init,
|
||||
getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) },
|
||||
)
|
||||
|
||||
// KMK -->
|
||||
val onDismissRequest = screenModel::dismissDialog
|
||||
// KMK <--
|
||||
state.dialog?.let { dialog ->
|
||||
when (dialog) {
|
||||
is FeedScreenModel.Dialog.AddFeed -> {
|
||||
|
|
@ -194,43 +196,75 @@ fun Screen.feedTab(
|
|||
},
|
||||
)
|
||||
}
|
||||
// KMK -->
|
||||
is FeedScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
screenModel.setMangaCategories(dialog.mangas, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is FeedScreenModel.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,
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||
when (val dialog = bulkFavoriteState.dialog) {
|
||||
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.RemoveManga -> {
|
||||
RemoveMangaDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = {
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
mangaToRemove = dialog.manga,
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, _ ->
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||
AllowDuplicateDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onAllowAllDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||
},
|
||||
onSkipAllDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||
},
|
||||
onOpenManga = {
|
||||
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
||||
},
|
||||
onAllowDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||
},
|
||||
onSkipDuplicate = {
|
||||
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||
},
|
||||
duplicatedName = dialog.duplicatedManga.second.title,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
val internalErrString = stringResource(MR.strings.internal_error)
|
||||
val tooManyFeedsString = stringResource(SYMR.strings.too_many_in_feed)
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -53,12 +53,10 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
|||
},
|
||||
onClickItem = {
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode) {
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
bulkFavoriteScreenModel.toggleSelection(it)
|
||||
}
|
||||
else
|
||||
// KMK <--
|
||||
{
|
||||
else {
|
||||
// KMK <--
|
||||
// SY -->
|
||||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
|
|
@ -70,9 +68,7 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
|||
},
|
||||
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||
// KMK -->
|
||||
bulkFavoriteState = bulkFavoriteState,
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
addFavorite = bulkFavoriteScreenModel::addFavorite,
|
||||
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import eu.kanade.presentation.util.AssistContentScreen
|
|||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
|
|
@ -137,10 +138,11 @@ data class BrowseSourceScreen(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
when {
|
||||
state.selectionMode -> screenModel.toggleSelectionMode()
|
||||
}
|
||||
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
|
||||
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||
bulkFavoriteScreenModel.backHandler()
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
|
|
@ -152,11 +154,11 @@ data class BrowseSourceScreen(
|
|||
topBar = {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
// KMK -->
|
||||
if (state.selectionMode)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
SelectionToolbar(
|
||||
selectedCount = state.selection.size,
|
||||
onClickClearSelection = screenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = screenModel::addFavorite,
|
||||
selectedCount = bulkFavoriteState.selection.size,
|
||||
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite,
|
||||
)
|
||||
else
|
||||
// KMK <--
|
||||
|
|
@ -172,7 +174,7 @@ data class BrowseSourceScreen(
|
|||
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
|
||||
onSearch = screenModel::search,
|
||||
// KMK -->
|
||||
toggleBulkSelectionMode = screenModel::toggleSelectionMode
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
|
|
@ -269,18 +271,18 @@ data class BrowseSourceScreen(
|
|||
onLocalSourceHelpClick = onHelpClick,
|
||||
onMangaClick = {
|
||||
// KMK -->
|
||||
if (state.selectionMode)
|
||||
screenModel.toggleSelection(it)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
bulkFavoriteScreenModel.toggleSelection(it)
|
||||
else
|
||||
// KMK <--
|
||||
navigator.push(MangaScreen(it.id, true, smartSearchConfig))
|
||||
},
|
||||
onMangaLongClick = { manga ->
|
||||
// KMK -->
|
||||
if (state.selectionMode) {
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
} else {
|
||||
// KMK <--
|
||||
else
|
||||
// KMK <--
|
||||
scope.launchIO {
|
||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
|
|
@ -294,11 +296,10 @@ data class BrowseSourceScreen(
|
|||
else -> screenModel.addFavorite(manga)
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// KMK -->
|
||||
selection = state.selection,
|
||||
selection = bulkFavoriteState.selection,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
@ -385,42 +386,47 @@ data class BrowseSourceScreen(
|
|||
screenModel.deleteSearch(dialog.idToDelete)
|
||||
},
|
||||
)
|
||||
// KMK -->
|
||||
is BrowseSourceScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
else -> {}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||
when (val dialog = bulkFavoriteState.dialog) {
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
screenModel.setMangaCategories(dialog.mangas, include, exclude)
|
||||
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourceScreenModel.Dialog.AllowDuplicate -> {
|
||||
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||
AllowDuplicateDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onAllowAllDuplicate = {
|
||||
screenModel.addFavoriteDuplicate()
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||
},
|
||||
onSkipAllDuplicate = {
|
||||
screenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||
},
|
||||
onOpenManga = {
|
||||
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
||||
},
|
||||
onAllowDuplicate = {
|
||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||
},
|
||||
onSkipDuplicate = {
|
||||
screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||
screenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||
},
|
||||
duplicatedName = dialog.duplicatedManga.second.title,
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
else -> {}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
queryEvent.receiveAsFlow()
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
|
|
@ -36,8 +33,6 @@ import exh.metadata.metadata.RaisedSearchMetadata
|
|||
import exh.source.getMainSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
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.Flow
|
||||
|
|
@ -339,7 +334,7 @@ open class BrowseSourceScreenModel(
|
|||
false -> Instant.now().toEpochMilli()
|
||||
},
|
||||
)
|
||||
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
|
||||
|
||||
if (!new.favorite) {
|
||||
new = new.removeCovers(coverCache)
|
||||
} else {
|
||||
|
|
@ -452,170 +447,6 @@ open class BrowseSourceScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
sealed interface Dialog {
|
||||
data object Filter : Dialog
|
||||
data class RemoveManga(val manga: Manga) : Dialog
|
||||
|
|
@ -630,13 +461,6 @@ open class BrowseSourceScreenModel(
|
|||
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog
|
||||
data class CreateSavedSearch(val currentSavedSearches: ImmutableList<String>) : Dialog
|
||||
// SY <--
|
||||
// KMK -->
|
||||
data class ChangeMangasCategory(
|
||||
val mangas: List<Manga>,
|
||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
) : Dialog
|
||||
data class AllowDuplicate(val duplicatedManga: Pair<Int, Manga>) : Dialog
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -649,10 +473,6 @@ open class BrowseSourceScreenModel(
|
|||
val savedSearches: ImmutableList<EXHSavedSearch> = persistentListOf(),
|
||||
val filterable: Boolean = true,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
val selection: PersistentList<Manga> = persistentListOf(),
|
||||
val selectionMode: Boolean = false,
|
||||
// KMK <--
|
||||
) {
|
||||
val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
|
|
@ -12,12 +13,15 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.browse.SourceFeedScreen
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
|
||||
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
|
||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.manga.AllowDuplicateDialog
|
||||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
|
|
@ -25,6 +29,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
|||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.md.follows.MangaDexFollowsScreen
|
||||
import exh.util.nullIfBlank
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||
import tachiyomi.domain.source.model.SavedSearch
|
||||
|
|
@ -37,7 +42,12 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
|
|||
val state by screenModel.state.collectAsState()
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
|
||||
// KMK -->
|
||||
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
// KMK <--
|
||||
|
||||
|
|
@ -53,8 +63,8 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
|
|||
onClickDelete = screenModel::openDeleteFeed,
|
||||
onClickManga = {
|
||||
// KMK -->
|
||||
if (state.selectionMode)
|
||||
screenModel.toggleSelection(it)
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
bulkFavoriteScreenModel.toggleSelection(it)
|
||||
else
|
||||
// KMK <--
|
||||
onMangaClick(navigator, it)
|
||||
|
|
@ -66,15 +76,25 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
|
|||
// KMK -->
|
||||
sourceId = screenModel.source.id,
|
||||
onLongClickManga = { manga ->
|
||||
if (state.selectionMode) {
|
||||
if (!bulkFavoriteState.selectionMode)
|
||||
scope.launchIO {
|
||||
val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga))
|
||||
duplicateManga != null -> bulkFavoriteScreenModel.setDialog(
|
||||
BulkFavoriteScreenModel.Dialog.AddDuplicateManga(
|
||||
manga,
|
||||
duplicateManga,
|
||||
),
|
||||
)
|
||||
else -> bulkFavoriteScreenModel.addFavorite(manga)
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
else
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
} else {
|
||||
screenModel.toggleSelection(manga)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
},
|
||||
screenModel = screenModel,
|
||||
screenState = state,
|
||||
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
|
|
@ -99,40 +119,6 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
|
|||
},
|
||||
)
|
||||
}
|
||||
// KMK -->
|
||||
is SourceFeedScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
screenModel.setMangaCategories(dialog.mangas, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is SourceFeedScreenModel.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,
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
SourceFeedScreenModel.Dialog.Filter -> {
|
||||
SourceFilterDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
|
@ -199,8 +185,79 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
|
|||
null -> Unit
|
||||
}
|
||||
|
||||
BackHandler(state.searchQuery != null) {
|
||||
screenModel.search(null)
|
||||
// KMK -->
|
||||
val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||
when (val dialog = bulkFavoriteState.dialog) {
|
||||
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.RemoveManga -> {
|
||||
RemoveMangaDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = {
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
mangaToRemove = dialog.manga,
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, _ ->
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||
AllowDuplicateDialog(
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onAllowAllDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||
},
|
||||
onSkipAllDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true)
|
||||
},
|
||||
onOpenManga = {
|
||||
navigator.push(MangaScreen(dialog.duplicatedManga.second.id))
|
||||
},
|
||||
onAllowDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1)
|
||||
},
|
||||
onSkipDuplicate = {
|
||||
bulkFavoriteScreenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first)
|
||||
bulkFavoriteScreenModel.addFavorite(startIdx = dialog.duplicatedManga.first)
|
||||
},
|
||||
duplicatedName = dialog.duplicatedManga.second.title,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
BackHandler(state.searchQuery != null/* KMK --> */ || bulkFavoriteState.selectionMode /* KMK <-- */) {
|
||||
// KMK -->
|
||||
if(bulkFavoriteState.selectionMode)
|
||||
bulkFavoriteScreenModel.backHandler()
|
||||
else
|
||||
// KMK <--
|
||||
screenModel.search(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
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 dev.icerock.moko.resources.StringResource
|
||||
|
|
@ -25,15 +22,12 @@ import exh.source.getMainSource
|
|||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.nullIfBlank
|
||||
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.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -41,16 +35,10 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.common.preference.CheckboxState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
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.source.interactor.CountFeedSavedSearchBySourceId
|
||||
|
|
@ -82,12 +70,6 @@ open class SourceFeedScreenModel(
|
|||
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
|
||||
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
|
||||
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
||||
// KMK -->
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<SourceFeedState>(SourceFeedState()) {
|
||||
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
|
|
@ -326,188 +308,10 @@ open class SourceFeedScreenModel(
|
|||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
fun clearSelection() {
|
||||
mutableState.update { it.copy(selection = persistentListOf()) }
|
||||
}
|
||||
|
||||
fun toggleSelection(manga: DomainManga) {
|
||||
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<DomainManga> {
|
||||
return state.value.selection.filterNot { manga ->
|
||||
getDuplicateLibraryManga.await(manga).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, DomainManga>? {
|
||||
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<DomainManga>, 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)
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
private suspend fun getCommonCategories(mangas: List<DomainManga>): 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<DomainManga>): 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)
|
||||
}
|
||||
|
||||
private fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
sealed class Dialog {
|
||||
data object Filter : Dialog()
|
||||
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
||||
data class AddFeed(val feedId: Long, val name: String) : Dialog()
|
||||
// KMK -->
|
||||
data class ChangeMangasCategory(
|
||||
val mangas: List<DomainManga>,
|
||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
) : Dialog()
|
||||
data class AllowDuplicate(val duplicatedManga: Pair<Int, DomainManga>) : Dialog()
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
|
|
@ -523,14 +327,7 @@ data class SourceFeedState(
|
|||
val filters: FilterList = FilterList(),
|
||||
val savedSearches: ImmutableList<EXHSavedSearch> = persistentListOf(),
|
||||
val dialog: SourceFeedScreenModel.Dialog? = null,
|
||||
// KMK -->
|
||||
val selection: PersistentList<DomainManga> = persistentListOf(),
|
||||
// KMK <--
|
||||
) {
|
||||
val isLoading
|
||||
get() = items.isEmpty()
|
||||
|
||||
// KMK -->
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
// KMK <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ class GlobalSearchScreen(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
BackHandler(enabled = bulkFavoriteState.selectionMode) {
|
||||
bulkFavoriteScreenModel.backHandler()
|
||||
}
|
||||
|
|
@ -99,11 +99,7 @@ class GlobalSearchScreen(
|
|||
},
|
||||
onLongClickItem = { manga ->
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode)
|
||||
// KMK <--
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
// KMK -->
|
||||
else
|
||||
if (!bulkFavoriteState.selectionMode)
|
||||
scope.launchIO {
|
||||
val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
|
|
@ -118,29 +114,29 @@ class GlobalSearchScreen(
|
|||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
else
|
||||
// KMK <--
|
||||
navigator.push(MangaScreen(manga.id, true))
|
||||
},
|
||||
// KMK -->
|
||||
bulkFavoriteState = bulkFavoriteState,
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
addFavorite = bulkFavoriteScreenModel::addFavorite,
|
||||
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||
val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) }
|
||||
when (val dialog = bulkFavoriteState.dialog) {
|
||||
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) },
|
||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||
)
|
||||
}
|
||||
is BulkFavoriteScreenModel.Dialog.RemoveManga -> {
|
||||
RemoveMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onConfirm = {
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
|
|
@ -150,7 +146,7 @@ class GlobalSearchScreen(
|
|||
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, _ ->
|
||||
bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga)
|
||||
|
|
@ -161,7 +157,7 @@ class GlobalSearchScreen(
|
|||
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||
onConfirm = { include, exclude ->
|
||||
bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude)
|
||||
|
|
@ -170,7 +166,7 @@ class GlobalSearchScreen(
|
|||
}
|
||||
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> {
|
||||
AllowDuplicateDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onBulkDismissRequest,
|
||||
onAllowAllDuplicate = {
|
||||
bulkFavoriteScreenModel.addFavoriteDuplicate()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -976,8 +976,8 @@ class MangaScreenModel(
|
|||
downloadManager.getQueuedDownloadOrNull(chapter.id)
|
||||
}
|
||||
// SY -->
|
||||
val mangaMerged = mergedData?.manga?.get(chapter.mangaId) ?: manga
|
||||
val source = mergedData?.sources?.find { mangaMerged.source == it.id }?.takeIf { mergedData.sources.size > 2 }
|
||||
val manga = mergedData?.manga?.get(chapter.mangaId) ?: manga
|
||||
val source = mergedData?.sources?.find { manga.source == it.id }?.takeIf { mergedData.sources.size > 2 }
|
||||
// SY <--
|
||||
val downloaded = if (isLocal) {
|
||||
true
|
||||
|
|
@ -986,8 +986,8 @@ class MangaScreenModel(
|
|||
// SY -->
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
mangaMerged.ogTitle,
|
||||
mangaMerged.source,
|
||||
manga.ogTitle,
|
||||
manga.source,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue