migrate the rest to BulkSelectionScreenModel

This commit is contained in:
Cuong M. Tran 2024-03-09 17:40:42 +07:00 committed by Cuong Tran
parent cc5b6a1d5f
commit df229af0eb
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
17 changed files with 352 additions and 817 deletions

View file

@ -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 ->

View file

@ -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 <--
)
},

View file

@ -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 <--
)
},

View file

@ -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),
)

View file

@ -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 <--

View file

@ -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,
),
)
}

View file

@ -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 <--

View file

@ -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 <--
)

View file

@ -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

View file

@ -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) {

View file

@ -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 <--
)

View file

@ -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()

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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 <--
}

View file

@ -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()
},

View file

@ -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 <--
)
}