From a4604809bc6ae48491e30051e9600aef8790e756 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 29 Feb 2024 18:36:57 +0700 Subject: [PATCH 01/36] selection list & toggleSelection/clearSelection --- .../kanade/presentation/browse/FeedScreen.kt | 15 +++++++++++- .../ui/browse/feed/FeedScreenModel.kt | 24 +++++++++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 11 +++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 9d77b94e01..8c978e71ba 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -26,7 +26,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem @@ -69,6 +71,9 @@ fun FeedScreen( onClickSource: (CatalogueSource) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + // KMK <-- onRefresh: () -> Unit, getMangaState: @Composable (Manga, CatalogueSource?) -> State, ) { @@ -121,6 +126,9 @@ fun FeedScreen( item = item, getMangaState = { getMangaState(it, item.source) }, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + // KMK <-- ) } } @@ -135,6 +143,9 @@ fun FeedItem( item: FeedItemUI, getMangaState: @Composable ((Manga) -> State), onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + // KMK <-- ) { when { item.results == null -> { @@ -148,7 +159,9 @@ fun FeedItem( titles = item.results, getManga = getMangaState, onClick = onClickManga, - onLongClick = onClickManga, + // KMK --> + onLongClick = onLongClickManga, + // KMK <-- ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 8197def219..7e86fc8cc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -14,6 +14,8 @@ 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 @@ -305,6 +307,25 @@ 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) + } + } + // KMK <-- + sealed class Dialog { data class AddFeed(val options: ImmutableList) : Dialog() data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList) : Dialog() @@ -320,6 +341,9 @@ open class FeedScreenModel( data class FeedScreenState( val dialog: FeedScreenModel.Dialog? = null, val items: List? = null, + // KMK --> + val selection: PersistentList = persistentListOf(), + // KMK <-- ) { val isLoading get() = items == null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index ba9300c864..3649cacab7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -7,6 +7,8 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +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 @@ -33,6 +35,9 @@ fun Screen.feedTab(): TabContent { val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { FeedScreenModel() } val state by screenModel.state.collectAsState() + // KMK --> + val haptic = LocalHapticFeedback.current + // KMK <-- DisposableEffect(navigator.lastEvent) { if (navigator.lastEvent == StackEvent.Push) { @@ -91,6 +96,12 @@ fun Screen.feedTab(): TabContent { onClickManga = { manga -> navigator.push(MangaScreen(manga.id, true)) }, + // KMK --> + onLongClickManga = { + screenModel.toggleSelection(it) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + // KMK <-- onRefresh = screenModel::init, getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) }, ) From 5c5a0f5fcfe636337d8969e6bde333451c3acf17 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Fri, 1 Mar 2024 14:33:32 +0700 Subject: [PATCH 02/36] Toggle and toolbar count --- .../kanade/presentation/browse/FeedScreen.kt | 6 +- .../presentation/components/TabbedScreen.kt | 63 ++++++++++++++++--- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 12 +++- .../tachiyomi/ui/browse/feed/FeedTab.kt | 61 +++++++++++++----- 4 files changed, 115 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 8c978e71ba..47275d45d3 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -26,9 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem @@ -159,7 +157,9 @@ fun FeedItem( titles = item.results, getManga = getMangaState, onClick = onClickManga, - // KMK --> + /* KMK --> + onLongClick = onClickManga, + */ onLongClick = onLongClickManga, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index efb1e2e049..0bd32c9243 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -2,19 +2,25 @@ package eu.kanade.presentation.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Bookmark import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -22,9 +28,11 @@ 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch +import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.i18n.stringResource @@ -36,6 +44,9 @@ fun TabbedScreen( startIndex: Int? = null, searchQuery: String? = null, onChangeSearchQuery: (String?) -> Unit = {}, + // KMK --> + feedScreenModel: FeedScreenModel, + // KMK <-- ) { val scope = rememberCoroutineScope() val state = rememberPagerState { tabs.size } @@ -51,14 +62,23 @@ fun TabbedScreen( topBar = { val tab = tabs[state.currentPage] val searchEnabled = tab.searchEnabled - - SearchToolbar( - titleContent = { AppBarTitle(stringResource(titleRes)) }, - searchEnabled = searchEnabled, - searchQuery = if (searchEnabled) searchQuery else null, - onChangeSearchQuery = onChangeSearchQuery, - actions = { AppBarActions(tab.actions) }, - ) + // KMK --> + val feedScreenState by feedScreenModel.state.collectAsState() + if (feedScreenState.selection.isNotEmpty()) + FeedSelectionToolbar( + selectedCount = feedScreenState.selection.size, + onClickClearSelection = { feedScreenModel.clearSelection() }, + actions = { AppBarActions(tab.actions) }, + ) + else + // KMK <-- + SearchToolbar( + titleContent = { AppBarTitle(stringResource(titleRes)) }, + searchEnabled = searchEnabled, + searchQuery = if (searchEnabled) searchQuery else null, + onChangeSearchQuery = onChangeSearchQuery, + actions = { AppBarActions(tab.actions) }, + ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> @@ -104,3 +124,30 @@ data class TabContent( val actions: ImmutableList = persistentListOf(), val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit, ) + +// KMK --> +@Composable +private fun FeedSelectionToolbar( + selectedCount: Int, + onClickClearSelection: () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, +) { + AppBar( + titleContent = { Text(text = "$selectedCount") }, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bookmark), + icon = Icons.Outlined.Bookmark, + // TODO: method to add bookmark goes here + onClick = { }, + ), + ), + ) + }, + isActionMode = true, + onCancelActionMode = onClickClearSelection, + ) +} +// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 20c9e79d6a..1a7644fb25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -21,6 +21,7 @@ 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 @@ -65,6 +66,10 @@ data class BrowseTab( val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() } val extensionsState by extensionsScreenModel.state.collectAsState() + // KMK --> + val feedScreenModel = rememberScreenModel { FeedScreenModel() } + // KMK <-- + TabbedScreen( titleRes = MR.strings.browse, // SY --> @@ -76,7 +81,7 @@ data class BrowseTab( ) } else if (feedTabInFront) { persistentListOf( - feedTab(), + feedTab(/* KMK --> */feedScreenModel/* KMK <-- */), sourcesTab(), extensionsTab(extensionsScreenModel), migrateSourceTab(), @@ -84,7 +89,7 @@ data class BrowseTab( } else { persistentListOf( sourcesTab(), - feedTab(), + feedTab(/* KMK --> */feedScreenModel/* KMK <-- */), extensionsTab(extensionsScreenModel), migrateSourceTab(), ) @@ -93,6 +98,9 @@ data class BrowseTab( // SY <-- searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, + // KMK --> + feedScreenModel = feedScreenModel + // KMK <-- ) LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 3649cacab7..b4488631d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.ui.browse.feed 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.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -9,7 +11,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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 @@ -31,9 +32,15 @@ import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.i18n.stringResource @Composable -fun Screen.feedTab(): TabContent { +fun Screen.feedTab( + // KMK --> + screenModel: FeedScreenModel + // KMK <-- +): TabContent { val navigator = LocalNavigator.currentOrThrow + /* KMK --> val screenModel = rememberScreenModel { FeedScreenModel() } + KMK <-- */ val state by screenModel.state.collectAsState() // KMK --> val haptic = LocalHapticFeedback.current @@ -55,15 +62,32 @@ fun Screen.feedTab(): TabContent { return TabContent( titleRes = SYMR.strings.feed, - actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_add), - icon = Icons.Outlined.Add, - onClick = { - screenModel.openAddDialog() - }, + actions = + // KMK --> + if (state.selection.isNotEmpty()) + 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() + }, + ), ), - ), content = { contentPadding, snackbarHostState -> FeedScreen( state = state, @@ -94,12 +118,21 @@ fun Screen.feedTab(): TabContent { }, onClickDelete = screenModel::openDeleteDialog, onClickManga = { manga -> - navigator.push(MangaScreen(manga.id, true)) + // KMK --> + if (state.selection.isNotEmpty()) + screenModel.toggleSelection(manga) + else + // KMK <-- + navigator.push(MangaScreen(manga.id, true)) }, // KMK --> - onLongClickManga = { - screenModel.toggleSelection(it) - haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClickManga = { manga -> + if (state.selection.isNotEmpty()) { + navigator.push(MangaScreen(manga.id, true)) + } else { + screenModel.toggleSelection(manga) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } }, // KMK <-- onRefresh = screenModel::init, From 459bbcc9bdb75b6bd1ee71d9d7dd29b1d79c023a Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Fri, 1 Mar 2024 16:09:00 +0700 Subject: [PATCH 03/36] shade for selected items --- .../kanade/presentation/browse/FeedScreen.kt | 3 +++ .../browse/components/GlobalSearchCardRow.kt | 18 +++++++++++++++++- .../presentation/components/TabbedScreen.kt | 4 ++-- .../library/components/CommonMangaItem.kt | 3 +++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 47275d45d3..913559408d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -126,6 +126,7 @@ fun FeedScreen( onClickManga = onClickManga, // KMK --> onLongClickManga = onLongClickManga, + selection = state.selection, // KMK <-- ) } @@ -143,6 +144,7 @@ fun FeedItem( onClickManga: (Manga) -> Unit, // KMK --> onLongClickManga: (Manga) -> Unit, + selection: List, // KMK <-- ) { when { @@ -161,6 +163,7 @@ fun FeedItem( onLongClick = onClickManga, */ onLongClick = onLongClickManga, + selection = selection, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt index 456269b7c6..2f4240ecb7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAny import eu.kanade.presentation.library.components.CommonMangaItemDefaults import eu.kanade.presentation.library.components.MangaComfortableGridItem import tachiyomi.domain.manga.model.Manga @@ -29,6 +30,9 @@ fun GlobalSearchCardRow( getManga: @Composable (Manga) -> State, onClick: (Manga) -> Unit, onLongClick: (Manga) -> Unit, + // KMK --> + selection: List? = null, + // KMK <-- ) { if (titles.isEmpty()) { EmptyResultItem() @@ -47,6 +51,9 @@ fun GlobalSearchCardRow( isFavorite = title.favorite, onClick = { onClick(title) }, onLongClick = { onLongClick(title) }, + // KMK --> + isSelected = selection?.fastAny { selected -> selected.id == title.id } ?: false, + // KMK <-- ) } } @@ -59,6 +66,9 @@ private fun MangaItem( isFavorite: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, + // KMK --> + isSelected: Boolean = false, + // KMK <-- ) { Box(modifier = Modifier.width(96.dp)) { MangaComfortableGridItem( @@ -68,7 +78,13 @@ private fun MangaItem( coverBadgeStart = { InLibraryBadge(enabled = isFavorite) }, - coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, + coverAlpha = when { + // KMK --> + isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha + // KMK <-- + isFavorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha + else -> 1f + }, onClick = onClick, onLongClick = onLongClick, ) diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 0bd32c9243..db742cfd0c 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Bookmark +import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.SnackbarHost @@ -139,7 +139,7 @@ private fun FeedSelectionToolbar( persistentListOf( AppBar.Action( title = stringResource(MR.strings.action_bookmark), - icon = Icons.Outlined.Bookmark, + icon = Icons.Outlined.BookmarkAdd, // TODO: method to add bookmark goes here onClick = { }, ), diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt index 92957384a8..07d01e0d04 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt @@ -48,6 +48,9 @@ object CommonMangaItemDefaults { val GridVerticalSpacer = 4.dp const val BrowseFavoriteCoverAlpha = 0.34f + // KMK --> + const val BrowseSelectedCoverAlpha = 0.17f + // KMK <-- } private val ContinueReadingButtonSize = 28.dp From 468518bdc3e9cacfcc1c8f4ff321bf2484347dde Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Fri, 1 Mar 2024 16:40:32 +0700 Subject: [PATCH 04/36] handle back button --- .../java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index b4488631d3..466ba10439 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -1,5 +1,6 @@ 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 @@ -60,6 +61,14 @@ fun Screen.feedTab( } } + // KMK --> + BackHandler(enabled = state.selection.isNotEmpty()) { + when { + state.selection.isNotEmpty() -> screenModel.clearSelection() + } + } + // KMK <-- + return TabContent( titleRes = SYMR.strings.feed, actions = From c7ed0a4324e5bd999c97717963d04314451575ec Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Mon, 4 Mar 2024 18:30:44 +0700 Subject: [PATCH 05/36] Set Favorite & Libraries OK now --- .../category/components/CategoryDialogs.kt | 5 +- .../presentation/components/TabbedScreen.kt | 28 +++++- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 5 +- .../ui/browse/feed/FeedScreenModel.kt | 98 +++++++++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 32 +++++- .../tachiyomi/ui/manga/MangaScreenModel.kt | 8 +- 6 files changed, 163 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt index f343f9034c..fd79aa9914 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -254,8 +254,11 @@ fun ChangeCategoryDialog( onDismissRequest: () -> Unit, onEditCategories: () -> Unit, onConfirm: (List, List) -> Unit, + // KMK --> + setFavorite: Boolean = false, + // KMK <-- ) { - if (initialSelection.isEmpty()) { + if (initialSelection.isEmpty()/* KMK --> */ && !setFavorite/* KMK <-- */) { AlertDialog( onDismissRequest = onDismissRequest, confirmButton = { diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index db742cfd0c..3968367d99 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -25,10 +25,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.zIndex import dev.icerock.moko.resources.StringResource +import eu.kanade.presentation.manga.components.LibraryBottomActionMenu import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel +import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @@ -46,12 +49,19 @@ fun TabbedScreen( onChangeSearchQuery: (String?) -> Unit = {}, // KMK --> feedScreenModel: FeedScreenModel, + libraryScreenModel: LibraryScreenModel, // KMK <-- ) { + val context = LocalContext.current val scope = rememberCoroutineScope() val state = rememberPagerState { tabs.size } val snackbarHostState = remember { SnackbarHostState() } + // KMK --> + val feedScreenState by feedScreenModel.state.collectAsState() + val libraryScreenState by libraryScreenModel.state.collectAsState() + // KMK <-- + LaunchedEffect(startIndex) { if (startIndex != null) { state.scrollToPage(startIndex) @@ -63,11 +73,10 @@ fun TabbedScreen( val tab = tabs[state.currentPage] val searchEnabled = tab.searchEnabled // KMK --> - val feedScreenState by feedScreenModel.state.collectAsState() if (feedScreenState.selection.isNotEmpty()) FeedSelectionToolbar( selectedCount = feedScreenState.selection.size, - onClickClearSelection = { feedScreenModel.clearSelection() }, + onClickClearSelection = feedScreenModel::clearSelection, actions = { AppBarActions(tab.actions) }, ) else @@ -80,6 +89,21 @@ fun TabbedScreen( actions = { AppBarActions(tab.actions) }, ) }, + // KMK --> + bottomBar = { + LibraryBottomActionMenu( + visible = feedScreenState.selectionMode, + onChangeCategoryClicked = { feedScreenModel.openChangeCategoryDialog(libraryScreenModel) }, + onMarkAsReadClicked = { libraryScreenModel.markReadSelection(true) }, + onMarkAsUnreadClicked = { libraryScreenModel.markReadSelection(false) }, + onDownloadClicked = libraryScreenModel::runDownloadActionSelection, + onDeleteClicked = libraryScreenModel::openDeleteMangaDialog, + onClickCleanTitles = null, + onClickMigrate = null, + onClickAddToMangaDex = null, + ) + }, + // KMK <-- snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> Column( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 1a7644fb25..19234ae10a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -26,6 +26,7 @@ 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 import eu.kanade.tachiyomi.ui.browse.source.sourcesTab +import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import eu.kanade.tachiyomi.ui.main.MainActivity import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR @@ -68,6 +69,7 @@ data class BrowseTab( // KMK --> val feedScreenModel = rememberScreenModel { FeedScreenModel() } + val libraryScreenModel = rememberScreenModel { LibraryScreenModel() } // KMK <-- TabbedScreen( @@ -99,7 +101,8 @@ data class BrowseTab( searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, // KMK --> - feedScreenModel = feedScreenModel + feedScreenModel = feedScreenModel, + libraryScreenModel = libraryScreenModel, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 7e86fc8cc3..62342839c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -12,6 +12,7 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.browse.FeedItemUI import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -32,11 +33,16 @@ 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.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga +import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal import tachiyomi.domain.source.interactor.DeleteFeedSavedSearchById import tachiyomi.domain.source.interactor.GetFeedSavedSearchGlobal @@ -67,6 +73,10 @@ 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(), + // KMK <-- ) : StateScreenModel(FeedScreenState()) { private val _events = Channel(Int.MAX_VALUE) @@ -324,12 +334,96 @@ open class FeedScreenModel( state.copy(selection = newSelection) } } + + fun openChangeCategoryDialog(libraryScreenModel: LibraryScreenModel) { + screenModelScope.launchIO { + // Create a copy of selected manga + val mangaList = state.value.selection + + // Hide the default category because it has a different behavior than the ones from db. + // SY --> + val categories = libraryScreenModel.state.value.categories.filter { it.id != 0L } + // SY <-- + + // 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() + mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } + } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } + } + + /** + * Returns the mix (non-common) categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getMixCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) + } + + fun closeDialog() { + mutableState.update { it.copy(dialog = null) } + } + + /** + * 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 setMangaFavoriteCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.forEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + setMangaCategories.await(manga.id, categoryIds) + if (!manga.favorite) + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + } // KMK <-- sealed class Dialog { data class AddFeed(val options: ImmutableList) : Dialog() data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList) : Dialog() data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() + // KMK --> + data class ChangeCategory( + val manga: List, + val initialSelection: ImmutableList>, + ) : Dialog() + // KMK <-- } sealed class Event { @@ -353,6 +447,10 @@ data class FeedScreenState( val isLoadingItems get() = items?.fastAny { it.results == null } != false + + // KMK --> + val selectionMode = selection.isNotEmpty() + // KMK <-- } const val MAX_FEED_ITEMS = 20 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 466ba10439..b4d9166082 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -20,9 +20,11 @@ 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.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen +import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.collectLatest @@ -62,9 +64,9 @@ fun Screen.feedTab( } // KMK --> - BackHandler(enabled = state.selection.isNotEmpty()) { + BackHandler(enabled = state.selectionMode) { when { - state.selection.isNotEmpty() -> screenModel.clearSelection() + state.selectionMode -> screenModel.clearSelection() } } // KMK <-- @@ -73,7 +75,7 @@ fun Screen.feedTab( titleRes = SYMR.strings.feed, actions = // KMK --> - if (state.selection.isNotEmpty()) + if (state.selectionMode) persistentListOf( AppBar.Action( title = stringResource(MR.strings.action_select_all), @@ -128,7 +130,7 @@ fun Screen.feedTab( onClickDelete = screenModel::openDeleteDialog, onClickManga = { manga -> // KMK --> - if (state.selection.isNotEmpty()) + if (state.selectionMode) screenModel.toggleSelection(manga) else // KMK <-- @@ -136,7 +138,7 @@ fun Screen.feedTab( }, // KMK --> onLongClickManga = { manga -> - if (state.selection.isNotEmpty()) { + if (state.selectionMode) { navigator.push(MangaScreen(manga.id, true)) } else { screenModel.toggleSelection(manga) @@ -148,6 +150,9 @@ fun Screen.feedTab( getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) }, ) + // KMK --> + val onDismissRequest = screenModel::closeDialog + // KMK <-- state.dialog?.let { dialog -> when (dialog) { is FeedScreenModel.Dialog.AddFeed -> { @@ -183,6 +188,23 @@ fun Screen.feedTab( }, ) } + // KMK --> + is FeedScreenModel.Dialog.ChangeCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { + screenModel.clearSelection() + navigator.push(CategoryScreen()) + }, + onConfirm = { include, exclude -> + screenModel.clearSelection() + screenModel.setMangaFavoriteCategories(dialog.manga, include, exclude) + }, + setFavorite = true + ) + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 33895fb5aa..c6bbb429ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -976,8 +976,8 @@ class MangaScreenModel( downloadManager.getQueuedDownloadOrNull(chapter.id) } // SY --> - val manga = mergedData?.manga?.get(chapter.mangaId) ?: manga - val source = mergedData?.sources?.find { manga.source == it.id }?.takeIf { mergedData.sources.size > 2 } + val mangaMerged = mergedData?.manga?.get(chapter.mangaId) ?: manga + val source = mergedData?.sources?.find { mangaMerged.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, - manga.ogTitle, - manga.source, + mangaMerged.ogTitle, + mangaMerged.source, // SY <-- ) } From 34f35e24618f056054bba623797572d0d91bf3ec Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 5 Mar 2024 15:25:34 +0700 Subject: [PATCH 06/36] Allow add to favorite even when no category available --- .../category/components/CategoryDialogs.kt | 5 +- .../presentation/components/TabbedScreen.kt | 3 +- .../ui/browse/feed/FeedScreenModel.kt | 124 +++++++++++------- .../tachiyomi/ui/browse/feed/FeedTab.kt | 3 +- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt index fd79aa9914..f343f9034c 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -254,11 +254,8 @@ fun ChangeCategoryDialog( onDismissRequest: () -> Unit, onEditCategories: () -> Unit, onConfirm: (List, List) -> Unit, - // KMK --> - setFavorite: Boolean = false, - // KMK <-- ) { - if (initialSelection.isEmpty()/* KMK --> */ && !setFavorite/* KMK <-- */) { + if (initialSelection.isEmpty()) { AlertDialog( onDismissRequest = onDismissRequest, confirmButton = { diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 3968367d99..2de4e6c576 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -59,7 +59,6 @@ fun TabbedScreen( // KMK --> val feedScreenState by feedScreenModel.state.collectAsState() - val libraryScreenState by libraryScreenModel.state.collectAsState() // KMK <-- LaunchedEffect(startIndex) { @@ -93,7 +92,7 @@ fun TabbedScreen( bottomBar = { LibraryBottomActionMenu( visible = feedScreenState.selectionMode, - onChangeCategoryClicked = { feedScreenModel.openChangeCategoryDialog(libraryScreenModel) }, + onChangeCategoryClicked = { feedScreenModel.addFavorite() }, onMarkAsReadClicked = { libraryScreenModel.markReadSelection(true) }, onMarkAsUnreadClicked = { libraryScreenModel.markReadSelection(false) }, onDownloadClicked = libraryScreenModel::runDownloadActionSelection, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 62342839c9..8f86e74026 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -4,6 +4,7 @@ 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 cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.manga.interactor.UpdateManga @@ -12,7 +13,6 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.browse.FeedItemUI import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -26,6 +26,7 @@ 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 @@ -42,7 +43,6 @@ import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga -import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal import tachiyomi.domain.source.interactor.DeleteFeedSavedSearchById import tachiyomi.domain.source.interactor.GetFeedSavedSearchGlobal @@ -335,39 +335,90 @@ open class FeedScreenModel( } } - fun openChangeCategoryDialog(libraryScreenModel: LibraryScreenModel) { + fun addFavorite() { screenModelScope.launchIO { - // Create a copy of selected manga val mangaList = state.value.selection + val categories = getCategories() - // Hide the default category because it has a different behavior than the ones from db. - // SY --> - val categories = libraryScreenModel.state.value.categories.filter { it.id != 0L } - // SY <-- - - // 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) - } + when { + 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() + mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } } - .toImmutableList() - mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } + } + } + } + + /** + * Bulk update categories of manga using old and new common categories. + * + * @param mangaList the list of manga to move. + * @param addCategories the categories to add for all mangas. + * @param removeCategories the categories to remove in all mangas. + */ + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.fastForEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) + } + } + } + + private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) } } + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + suspend fun getCategories(): List { + 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): Collection { + private suspend fun getCommonCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() return mangas .map { getCategories.await(it.id).toSet() } @@ -379,7 +430,7 @@ open class FeedScreenModel( * * @param mangas the list of manga. */ - private suspend fun getMixCategories(mangas: List): Collection { + private suspend fun getMixCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } @@ -389,29 +440,6 @@ open class FeedScreenModel( fun closeDialog() { mutableState.update { it.copy(dialog = null) } } - - /** - * 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 setMangaFavoriteCategories(mangaList: List, addCategories: List, removeCategories: List) { - screenModelScope.launchNonCancellable { - mangaList.forEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() - - setMangaCategories.await(manga.id, categoryIds) - if (!manga.favorite) - updateManga.awaitUpdateFavorite(manga.id, true) - } - } - } // KMK <-- sealed class Dialog { @@ -420,7 +448,7 @@ open class FeedScreenModel( data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() // KMK --> data class ChangeCategory( - val manga: List, + val mangas: List, val initialSelection: ImmutableList>, ) : Dialog() // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index b4d9166082..1901e799c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -199,9 +199,8 @@ fun Screen.feedTab( }, onConfirm = { include, exclude -> screenModel.clearSelection() - screenModel.setMangaFavoriteCategories(dialog.manga, include, exclude) + screenModel.setMangaCategories(dialog.mangas, include, exclude) }, - setFavorite = true ) } // KMK <-- From e3e2141282bfd6d095031bd19b921b0f983ac714 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 5 Mar 2024 15:35:58 +0700 Subject: [PATCH 07/36] Selection: * Clear selection after set Favorite * Keep selection when editing categories --- .../eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt | 1 + .../main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt | 6 +----- .../main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 8f86e74026..d819715a45 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -384,6 +384,7 @@ open class FeedScreenModel( moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) } } + clearSelection() } private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 1901e799c8..a538082550 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -193,12 +193,8 @@ fun Screen.feedTab( ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, - onEditCategories = { - screenModel.clearSelection() - navigator.push(CategoryScreen()) - }, + onEditCategories = { navigator.push(CategoryScreen()) }, onConfirm = { include, exclude -> - screenModel.clearSelection() screenModel.setMangaCategories(dialog.mangas, include, exclude) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 279304c8dc..56e4040f13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -281,7 +281,6 @@ object LibraryTab : Tab { initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, onEditCategories = { - screenModel.clearSelection() navigator.push(CategoryScreen()) }, onConfirm = { include, exclude -> From 466f2b0c776cf930d8cb65221045c79308ceb4c3 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 5 Mar 2024 15:40:02 +0700 Subject: [PATCH 08/36] Hide BottomNav when in selection mode --- .../main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index a538082550..65ee84560a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -25,6 +25,7 @@ import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen +import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.collectLatest @@ -69,6 +70,10 @@ fun Screen.feedTab( state.selectionMode -> screenModel.clearSelection() } } + + LaunchedEffect(state.selectionMode, state.dialog) { + HomeScreen.showBottomNav(!state.selectionMode) + } // KMK <-- return TabContent( From 8ed81b91baa653737bba095948b9cb1d9966c784 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 5 Mar 2024 16:32:21 +0700 Subject: [PATCH 09/36] Allow bulk-favoite default category --- .../ui/browse/feed/FeedScreenModel.kt | 19 +++++++++++++++---- .../tachiyomi/ui/browse/feed/FeedTab.kt | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index d819715a45..bcd0879d0d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -41,6 +41,7 @@ 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.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.source.interactor.CountFeedSavedSearchGlobal @@ -76,6 +77,7 @@ open class FeedScreenModel( // KMK --> private val getCategories: GetCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), // KMK <-- ) : StateScreenModel(FeedScreenState()) { @@ -339,12 +341,21 @@ open class FeedScreenModel( screenModelScope.launchIO { val mangaList = state.value.selection val categories = getCategories() + val defaultCategoryId = libraryPreferences.defaultCategory().get() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } when { - categories.isEmpty() -> { + // 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) @@ -359,7 +370,7 @@ open class FeedScreenModel( } } .toImmutableList() - mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } + setDialog(Dialog.ChangeCategory(mangaList, preselected)) } } } @@ -438,8 +449,8 @@ open class FeedScreenModel( return mangaCategories.flatten().distinct().subtract(common) } - fun closeDialog() { - mutableState.update { it.copy(dialog = null) } + fun setDialog(dialog: Dialog?) { + mutableState.update { it.copy(dialog = dialog) } } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 65ee84560a..10c1df4b29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -156,7 +156,7 @@ fun Screen.feedTab( ) // KMK --> - val onDismissRequest = screenModel::closeDialog + val onDismissRequest = screenModel::dismissDialog // KMK <-- state.dialog?.let { dialog -> when (dialog) { From 46ad83a79b5afc9554d02816fd08734946b16a45 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 12:21:55 +0700 Subject: [PATCH 10/36] Use topBar action to add favorite instead of bottomBar some minor name changes --- .../kanade/presentation/browse/FeedScreen.kt | 4 +- .../presentation/browse/SourceFeedScreen.kt | 10 ++--- .../presentation/components/TabbedScreen.kt | 42 +++++-------------- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 5 +-- .../ui/browse/feed/FeedScreenModel.kt | 2 +- .../ui/browse/source/feed/SourceFeedScreen.kt | 2 +- 6 files changed, 19 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 913559408d..0abad3e113 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -159,9 +159,7 @@ fun FeedItem( titles = item.results, getManga = getMangaState, onClick = onClickManga, - /* KMK --> - onLongClick = onClickManga, - */ + // KMK --> onLongClick = onLongClickManga, selection = selection, // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index a09fe1d716..f0f11746d4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -99,7 +99,7 @@ fun SourceFeedScreen( onSearchQueryChange: (String?) -> Unit, getMangaState: @Composable (Manga) -> State, // KMK --> - id: Long, + sourceId: Long, // KMK <-- ) { Scaffold( @@ -111,7 +111,7 @@ fun SourceFeedScreen( scrollBehavior = scrollBehavior, onClickSearch = onClickSearch, // KMK --> - id = id, + sourceId = sourceId, // KMK <-- ) }, @@ -222,7 +222,7 @@ fun SourceFeedToolbar( scrollBehavior: TopAppBarScrollBehavior, onClickSearch: (String) -> Unit, // KMK --> - id: Long + sourceId: Long, // KMK <-- ) { SearchToolbar( @@ -236,9 +236,9 @@ fun SourceFeedToolbar( // KMK --> actions = { persistentListOf( - SourceSettingsButton(id) + SourceSettingsButton(sourceId), ) - } + }, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 2de4e6c576..9405fc6470 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -2,7 +2,6 @@ package eu.kanade.presentation.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize @@ -25,13 +24,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.zIndex import dev.icerock.moko.resources.StringResource -import eu.kanade.presentation.manga.components.LibraryBottomActionMenu import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel -import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @@ -48,17 +44,15 @@ fun TabbedScreen( searchQuery: String? = null, onChangeSearchQuery: (String?) -> Unit = {}, // KMK --> - feedScreenModel: FeedScreenModel, - libraryScreenModel: LibraryScreenModel, + screenModel: FeedScreenModel, // KMK <-- ) { - val context = LocalContext.current val scope = rememberCoroutineScope() val state = rememberPagerState { tabs.size } val snackbarHostState = remember { SnackbarHostState() } // KMK --> - val feedScreenState by feedScreenModel.state.collectAsState() + val screenState by screenModel.state.collectAsState() // KMK <-- LaunchedEffect(startIndex) { @@ -72,11 +66,11 @@ fun TabbedScreen( val tab = tabs[state.currentPage] val searchEnabled = tab.searchEnabled // KMK --> - if (feedScreenState.selection.isNotEmpty()) - FeedSelectionToolbar( - selectedCount = feedScreenState.selection.size, - onClickClearSelection = feedScreenModel::clearSelection, - actions = { AppBarActions(tab.actions) }, + if (screenState.selectionMode) + SelectionToolbar( + selectedCount = screenState.selection.size, + onClickClearSelection = screenModel::clearSelection, + onChangeCategoryClicked = screenModel::addFavorite, ) else // KMK <-- @@ -88,21 +82,6 @@ fun TabbedScreen( actions = { AppBarActions(tab.actions) }, ) }, - // KMK --> - bottomBar = { - LibraryBottomActionMenu( - visible = feedScreenState.selectionMode, - onChangeCategoryClicked = { feedScreenModel.addFavorite() }, - onMarkAsReadClicked = { libraryScreenModel.markReadSelection(true) }, - onMarkAsUnreadClicked = { libraryScreenModel.markReadSelection(false) }, - onDownloadClicked = libraryScreenModel::runDownloadActionSelection, - onDeleteClicked = libraryScreenModel::openDeleteMangaDialog, - onClickCleanTitles = null, - onClickMigrate = null, - onClickAddToMangaDex = null, - ) - }, - // KMK <-- snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> Column( @@ -150,10 +129,10 @@ data class TabContent( // KMK --> @Composable -private fun FeedSelectionToolbar( +private fun SelectionToolbar( selectedCount: Int, onClickClearSelection: () -> Unit = {}, - actions: @Composable RowScope.() -> Unit = {}, + onChangeCategoryClicked: () -> Unit = {}, ) { AppBar( titleContent = { Text(text = "$selectedCount") }, @@ -163,8 +142,7 @@ private fun FeedSelectionToolbar( AppBar.Action( title = stringResource(MR.strings.action_bookmark), icon = Icons.Outlined.BookmarkAdd, - // TODO: method to add bookmark goes here - onClick = { }, + onClick = onChangeCategoryClicked, ), ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 19234ae10a..0ee52b0c28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -26,7 +26,6 @@ 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 import eu.kanade.tachiyomi.ui.browse.source.sourcesTab -import eu.kanade.tachiyomi.ui.library.LibraryScreenModel import eu.kanade.tachiyomi.ui.main.MainActivity import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR @@ -69,7 +68,6 @@ data class BrowseTab( // KMK --> val feedScreenModel = rememberScreenModel { FeedScreenModel() } - val libraryScreenModel = rememberScreenModel { LibraryScreenModel() } // KMK <-- TabbedScreen( @@ -101,8 +99,7 @@ data class BrowseTab( searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, // KMK --> - feedScreenModel = feedScreenModel, - libraryScreenModel = libraryScreenModel, + screenModel = feedScreenModel, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index bcd0879d0d..0905a36810 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -449,7 +449,7 @@ open class FeedScreenModel( return mangaCategories.flatten().distinct().subtract(common) } - fun setDialog(dialog: Dialog?) { + private fun setDialog(dialog: Dialog?) { mutableState.update { it.copy(dialog = dialog) } } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 5f69323f6b..fc1c7e1974 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -49,7 +49,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { onSearchQueryChange = screenModel::search, getMangaState = { screenModel.getManga(initialManga = it) }, // KMK --> - id = screenModel.source.id, + sourceId = screenModel.source.id, // KMK <-- ) From 0da6b78d7457070d9e32cd56a13c138ea26a12bb Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 12:34:01 +0700 Subject: [PATCH 11/36] Bulk selection for SourceFeedScreen --- .../presentation/browse/SourceFeedScreen.kt | 86 +++++++-- .../ui/browse/source/feed/SourceFeedScreen.kt | 38 +++- .../source/feed/SourceFeedScreenModel.kt | 163 ++++++++++++++++++ 3 files changed, 275 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index f0f11746d4..cbf6f64719 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -3,6 +3,9 @@ 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.BookmarkAdd +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -14,8 +17,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.tachiyomi.ui.browse.source.feed.SourceFeedScreenModel +import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.manga.model.Manga @@ -100,20 +107,32 @@ fun SourceFeedScreen( getMangaState: @Composable (Manga) -> State, // KMK --> sourceId: Long, + onLongClickManga: (Manga) -> Unit, + screenModel: SourceFeedScreenModel, + screenState: SourceFeedState, // KMK <-- ) { Scaffold( topBar = { scrollBehavior -> - SourceFeedToolbar( - title = name, - searchQuery = searchQuery, - onSearchQueryChange = onSearchQueryChange, - scrollBehavior = scrollBehavior, - onClickSearch = onClickSearch, - // KMK --> - sourceId = sourceId, - // KMK <-- - ) + // KMK --> + if (screenState.selectionMode) + SelectionToolbar( + selectedCount = screenState.selection.size, + onClickClearSelection = screenModel::clearSelection, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + SourceFeedToolbar( + title = name, + searchQuery = searchQuery, + onSearchQueryChange = onSearchQueryChange, + scrollBehavior = scrollBehavior, + onClickSearch = onClickSearch, + // KMK --> + sourceId = sourceId, + // KMK <-- + ) }, floatingActionButton = { BrowseSourceFloatingActionButton( @@ -135,6 +154,10 @@ fun SourceFeedScreen( onClickSavedSearch = onClickSavedSearch, onClickDelete = onClickDelete, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + screenState = screenState, + // KMK <-- ) } } @@ -152,6 +175,10 @@ fun SourceFeedList( onClickSavedSearch: (SavedSearch) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + screenState: SourceFeedState, + // KMK <-- ) { ScrollbarLazyColumn( contentPadding = paddingValues + topSmallPaddingValues, @@ -183,6 +210,10 @@ fun SourceFeedList( item = item, getMangaState = { getMangaState(it) }, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + selection = screenState.selection, + // KMK <-- ) } } @@ -194,6 +225,10 @@ fun SourceFeedItem( item: SourceFeedUI, getMangaState: @Composable ((Manga) -> State), onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + selection: List, + // KMK <-- ) { val results = item.results when { @@ -208,7 +243,10 @@ fun SourceFeedItem( titles = item.results.orEmpty(), getManga = getMangaState, onClick = onClickManga, - onLongClick = onClickManga, + // KMK --> + onLongClick = onLongClickManga, + selection = selection, + // KMK <-- ) } } @@ -242,3 +280,29 @@ fun SourceFeedToolbar( // KMK <-- ) } + +// KMK --> +@Composable +private fun SelectionToolbar( + selectedCount: Int, + onClickClearSelection: () -> Unit = {}, + onChangeCategoryClicked: () -> Unit = {}, +) { + AppBar( + titleContent = { Text(text = "$selectedCount") }, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bookmark), + icon = Icons.Outlined.BookmarkAdd, + onClick = onChangeCategoryClicked, + ), + ), + ) + }, + isActionMode = true, + onCancelActionMode = onClickClearSelection, + ) +} +// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index fc1c7e1974..5e487bc356 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -4,7 +4,9 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -12,10 +14,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.SourceFeedScreen 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.util.Screen import eu.kanade.tachiyomi.source.Source 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 import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.system.toast import exh.md.follows.MangaDexFollowsScreen @@ -32,6 +36,9 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current + // KMK --> + val haptic = LocalHapticFeedback.current + // KMK <-- SourceFeedScreen( name = screenModel.source.name, @@ -43,13 +50,30 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { onClickLatest = { onLatestClick(navigator, screenModel.source) }, onClickSavedSearch = { onSavedSearchClick(navigator, screenModel.source, it) }, onClickDelete = screenModel::openDeleteFeed, - onClickManga = { onMangaClick(navigator, it) }, + onClickManga = { + // KMK --> + if (state.selectionMode) + screenModel.toggleSelection(it) + else + // KMK <-- + onMangaClick(navigator, it) + }, onClickSearch = { onSearchClick(navigator, screenModel.source, it) }, searchQuery = state.searchQuery, onSearchQueryChange = screenModel::search, getMangaState = { screenModel.getManga(initialManga = it) }, // KMK --> sourceId = screenModel.source.id, + onLongClickManga = { manga -> + if (state.selectionMode) { + navigator.push(MangaScreen(manga.id, true)) + } else { + screenModel.toggleSelection(manga) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + }, + screenModel = screenModel, + screenState = state, // KMK <-- ) @@ -74,6 +98,18 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { }, ) } + // KMK --> + is SourceFeedScreenModel.Dialog.ChangeCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + // KMK <-- SourceFeedScreenModel.Dialog.Filter -> { SourceFilterDialog( onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 32eeded6dd..c07bf1e2ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -5,6 +5,8 @@ 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 cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource @@ -22,12 +24,15 @@ 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 @@ -35,10 +40,15 @@ 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.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.source.interactor.CountFeedSavedSearchBySourceId @@ -70,6 +80,11 @@ 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(), + // KMK <-- ) : StateScreenModel(SourceFeedState()) { val source = sourceManager.getOrStub(sourceId) @@ -308,10 +323,151 @@ 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() { + screenModelScope.launchIO { + val mangaList = 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.ChangeCategory(mangaList, preselected)) + } + } + } + } + + /** + * Bulk update categories of manga using old and new common categories. + * + * @param mangaList the list of manga to move. + * @param addCategories the categories to add for all mangas. + * @param removeCategories the categories to remove in all mangas. + */ + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.fastForEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) + } + } + clearSelection() + } + + private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + suspend fun getCategories(): List { + 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): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } + } + + /** + * Returns the mix (non-common) categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getMixCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) + } + + 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 ChangeCategory( + val mangas: List, + val initialSelection: ImmutableList>, + ) : Dialog() + // KMK <-- } override fun onDispose() { @@ -327,7 +483,14 @@ data class SourceFeedState( val filters: FilterList = FilterList(), val savedSearches: ImmutableList = persistentListOf(), val dialog: SourceFeedScreenModel.Dialog? = null, + // KMK --> + val selection: PersistentList = persistentListOf(), + // KMK <-- ) { val isLoading get() = items.isEmpty() + + // KMK --> + val selectionMode = selection.isNotEmpty() + // KMK <-- } From 93fb56b7715073f828a2849613bd263e95d694f0 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 14:06:12 +0700 Subject: [PATCH 12/36] rename --- .../eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt | 6 +++--- .../main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt | 2 +- .../tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt | 2 +- .../ui/browse/source/feed/SourceFeedScreenModel.kt | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 0905a36810..31cbc83caa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -338,7 +338,7 @@ open class FeedScreenModel( } fun addFavorite() { - screenModelScope.launchIO { + screenModelScope.launch { val mangaList = state.value.selection val categories = getCategories() val defaultCategoryId = libraryPreferences.defaultCategory().get() @@ -370,7 +370,7 @@ open class FeedScreenModel( } } .toImmutableList() - setDialog(Dialog.ChangeCategory(mangaList, preselected)) + setDialog(Dialog.ChangeMangasCategory(mangaList, preselected)) } } } @@ -459,7 +459,7 @@ open class FeedScreenModel( data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList) : Dialog() data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() // KMK --> - data class ChangeCategory( + data class ChangeMangasCategory( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 10c1df4b29..bd99eb49aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -194,7 +194,7 @@ fun Screen.feedTab( ) } // KMK --> - is FeedScreenModel.Dialog.ChangeCategory -> { + is FeedScreenModel.Dialog.ChangeMangasCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 5e487bc356..328f2677bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -99,7 +99,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { ) } // KMK --> - is SourceFeedScreenModel.Dialog.ChangeCategory -> { + is SourceFeedScreenModel.Dialog.ChangeMangasCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index c07bf1e2ea..96fd1db3c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -342,7 +342,7 @@ open class SourceFeedScreenModel( } fun addFavorite() { - screenModelScope.launchIO { + screenModelScope.launch { val mangaList = state.value.selection val categories = getCategories() val defaultCategoryId = libraryPreferences.defaultCategory().get() @@ -374,7 +374,7 @@ open class SourceFeedScreenModel( } } .toImmutableList() - setDialog(Dialog.ChangeCategory(mangaList, preselected)) + setDialog(Dialog.ChangeMangasCategory(mangaList, preselected)) } } } @@ -463,7 +463,7 @@ open class SourceFeedScreenModel( data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() data class AddFeed(val feedId: Long, val name: String) : Dialog() // KMK --> - data class ChangeCategory( + data class ChangeMangasCategory( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog() From b64c7f30c03c40e35791404233c9d7079466c6c5 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 14:24:52 +0700 Subject: [PATCH 13/36] Bulk selection mode for BrowseSourceScreen --- .../presentation/browse/BrowseSourceScreen.kt | 12 ++ .../components/BrowseSourceComfortableGrid.kt | 18 ++- .../components/BrowseSourceCompactGrid.kt | 18 ++- .../browse/components/BrowseSourceList.kt | 18 ++- .../browse/components/BrowseSourceToolbar.kt | 44 +++++- .../source/browse/BrowseSourceScreen.kt | 100 +++++++++---- .../source/browse/BrowseSourceScreenModel.kt | 139 ++++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 1 + 8 files changed, 321 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index d5f891db2f..a626dd5c39 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -58,6 +58,9 @@ fun BrowseSourceContent( // SY <-- onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, + // KMK --> + selection: List? = null, + // KMK <-- ) { val context = LocalContext.current @@ -155,6 +158,9 @@ fun BrowseSourceContent( contentPadding = contentPadding, onMangaClick = onMangaClick, onMangaLongClick = onMangaLongClick, + // KMK --> + selection = selection, + // KMK <-- ) } LibraryDisplayMode.List -> { @@ -163,6 +169,9 @@ fun BrowseSourceContent( contentPadding = contentPadding, onMangaClick = onMangaClick, onMangaLongClick = onMangaLongClick, + // KMK --> + selection = selection, + // KMK <-- ) } LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { @@ -172,6 +181,9 @@ fun BrowseSourceContent( contentPadding = contentPadding, onMangaClick = onMangaClick, onMangaLongClick = onMangaLongClick, + // KMK --> + selection = selection, + // KMK <-- ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt index 3ff54e9a08..e44734c598 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAny import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import eu.kanade.presentation.library.components.CommonMangaItemDefaults @@ -33,6 +34,9 @@ fun BrowseSourceComfortableGrid( contentPadding: PaddingValues, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, + // KMK --> + selection: List? = null, + // KMK <-- ) { LazyVerticalGrid( columns = columns, @@ -60,6 +64,9 @@ fun BrowseSourceComfortableGrid( // SY <-- onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, + // KMK --> + isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + // KMK <-- ) } @@ -79,6 +86,9 @@ private fun BrowseSourceComfortableGridItem( // SY <-- onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, + // KMK --> + isSelected: Boolean = false, + // KMK <-- ) { MangaComfortableGridItem( title = manga.title, @@ -89,7 +99,13 @@ private fun BrowseSourceComfortableGridItem( ogUrl = manga.thumbnailUrl, lastModified = manga.coverLastModified, ), - coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, + coverAlpha = when { + // KMK --> + isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha + // KMK <-- + manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha + else -> 1f + }, coverBadgeStart = { InLibraryBadge(enabled = manga.favorite) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt index 8d6a07ab09..e164625c94 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAny import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import eu.kanade.presentation.library.components.CommonMangaItemDefaults @@ -33,6 +34,9 @@ fun BrowseSourceCompactGrid( contentPadding: PaddingValues, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, + // KMK --> + selection: List? = null, + // KMK <-- ) { LazyVerticalGrid( columns = columns, @@ -60,6 +64,9 @@ fun BrowseSourceCompactGrid( // SY <-- onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, + // KMK --> + isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + // KMK <-- ) } @@ -79,6 +86,9 @@ private fun BrowseSourceCompactGridItem( // SY <-- onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, + // KMK --> + isSelected: Boolean = false, + // KMK <-- ) { MangaCompactGridItem( title = manga.title, @@ -89,7 +99,13 @@ private fun BrowseSourceCompactGridItem( ogUrl = manga.thumbnailUrl, lastModified = manga.coverLastModified, ), - coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, + coverAlpha = when { + // KMK --> + isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha + // KMK <-- + manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha + else -> 1f + }, coverBadgeStart = { InLibraryBadge(enabled = manga.favorite) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt index 8d301047a4..385adcbb02 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAny import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import eu.kanade.presentation.library.components.CommonMangaItemDefaults @@ -29,6 +30,9 @@ fun BrowseSourceList( contentPadding: PaddingValues, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, + // KMK --> + selection: List? = null, + // KMK <-- ) { LazyColumn( contentPadding = contentPadding + PaddingValues(vertical = 8.dp), @@ -53,6 +57,9 @@ fun BrowseSourceList( // SY <-- onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, + // KMK --> + isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + // KMK <-- ) } @@ -72,6 +79,9 @@ private fun BrowseSourceListItem( // SY <-- onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, + // KMK --> + isSelected: Boolean = false, + // KMK <-- ) { MangaListItem( title = manga.title, @@ -82,7 +92,13 @@ private fun BrowseSourceListItem( ogUrl = manga.thumbnailUrl, lastModified = manga.coverLastModified, ), - coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, + coverAlpha = when { + // KMK --> + isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha + // KMK <-- + manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha + else -> 1f + }, badge = { InLibraryBadge(enabled = manga.favorite) // SY --> diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index d152534da7..2558b24439 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -4,7 +4,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.automirrored.outlined.Help import androidx.compose.material.icons.filled.ViewModule -import androidx.compose.material.icons.outlined.Help +import androidx.compose.material.icons.outlined.BookmarkAdd +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior @@ -41,6 +42,9 @@ fun BrowseSourceToolbar( onSettingsClick: () -> Unit, onSearch: (String) -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null, + // KMK --> + toggleBulkSelectionMode: () -> Unit, + // KMK <-- ) { // Avoid capturing unstable source in actions lambda val title = source?.name @@ -73,6 +77,15 @@ fun BrowseSourceToolbar( ), ) } + // KMK --> + add( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleBulkSelectionMode, + ), + ) + // KMK <-- if (isLocalSource) { if (isConfigurableSource && displayMode != null) { add( @@ -165,3 +178,32 @@ fun BrowseSourceToolbar( scrollBehavior = scrollBehavior, ) } + +// KMK --> +@Composable +fun SelectionToolbar( + selectedCount: Int, + onClickClearSelection: () -> Unit = {}, + onChangeCategoryClicked: () -> Unit = {}, +) { + AppBar( + titleContent = { Text(text = "$selectedCount") }, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bookmark), + icon = Icons.Outlined.BookmarkAdd, + onClick = { +// if (selectedCount > 0) + onChangeCategoryClicked() + }, + ), + ), + ) + }, + isActionMode = true, + onCancelActionMode = onClickClearSelection, + ) +} +// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 543a4720e8..76f5dbcfd6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -42,6 +43,7 @@ import eu.kanade.presentation.browse.components.BrowseSourceToolbar import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.browse.components.SavedSearchCreateDialog import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog +import eu.kanade.presentation.browse.components.SelectionToolbar import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.AssistContentScreen @@ -133,6 +135,14 @@ data class BrowseSourceScreen( ) } + // KMK --> + BackHandler(enabled = state.selectionMode) { + when { + state.selectionMode -> screenModel.toggleSelectionMode() + } + } + // KMK <-- + LaunchedEffect(screenModel.source) { assistUrl = (screenModel.source as? HttpSource)?.baseUrl } @@ -140,18 +150,30 @@ data class BrowseSourceScreen( Scaffold( topBar = { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { - BrowseSourceToolbar( - searchQuery = state.toolbarQuery, - onSearchQueryChange = screenModel::setToolbarQuery, - source = screenModel.source, - displayMode = screenModel.displayMode, - onDisplayModeChange = { screenModel.displayMode = it }, - navigateUp = navigateUp, - onWebViewClick = onWebViewClick, - onHelpClick = onHelpClick, - onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) }, - onSearch = screenModel::search, - ) + // KMK --> + if (state.selectionMode) + SelectionToolbar( + selectedCount = state.selection.size, + onClickClearSelection = screenModel::toggleSelectionMode, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + BrowseSourceToolbar( + searchQuery = state.toolbarQuery, + onSearchQueryChange = screenModel::setToolbarQuery, + source = screenModel.source, + displayMode = screenModel.displayMode, + onDisplayModeChange = { screenModel.displayMode = it }, + navigateUp = navigateUp, + onWebViewClick = onWebViewClick, + onHelpClick = onHelpClick, + onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) }, + onSearch = screenModel::search, + // KMK --> + toggleBulkSelectionMode = screenModel::toggleSelectionMode + // KMK <-- + ) Row( modifier = Modifier @@ -244,23 +266,39 @@ data class BrowseSourceScreen( onWebViewClick = onWebViewClick, onHelpClick = { uriHandler.openUri(Constants.URL_HELP) }, onLocalSourceHelpClick = onHelpClick, - onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) }, + onMangaClick = { + // KMK --> + if (state.selectionMode) + screenModel.toggleSelection(it) + else + // KMK <-- + navigator.push(MangaScreen(it.id, true, smartSearchConfig)) + }, onMangaLongClick = { manga -> - scope.launchIO { - val duplicateManga = screenModel.getDuplicateLibraryManga(manga) - when { - manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) - duplicateManga != null -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), - ) - else -> screenModel.addFavorite(manga) + // KMK --> + if (state.selectionMode) { + navigator.push(MangaScreen(manga.id, true)) + } else { + // KMK <-- + scope.launchIO { + val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + when { + manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) + duplicateManga != null -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.AddDuplicateManga( + manga, + duplicateManga, + ), + ) + else -> screenModel.addFavorite(manga) + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } }, + // KMK --> + selection = state.selection, + // KMK <-- ) } @@ -346,6 +384,18 @@ data class BrowseSourceScreen( screenModel.deleteSearch(dialog.idToDelete) }, ) + // KMK --> + is BrowseSourceScreenModel.Dialog.ChangeMangasCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + // KMK <-- else -> {} } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 5d42384e00..59a7f33c28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -6,6 +6,8 @@ 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.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn @@ -33,6 +35,8 @@ 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 @@ -447,6 +451,131 @@ 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) + } + } + + fun addFavorite() { + screenModelScope.launch { + val mangaList = 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)) + } + } + } + } + + /** + * Bulk update categories of manga using old and new common categories. + * + * @param mangaList the list of manga to move. + * @param addCategories the categories to add for all mangas. + * @param removeCategories the categories to remove in all mangas. + */ + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.fastForEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) + } + } + toggleSelectionMode() + } + + private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } + } + + /** + * Returns the mix (non-common) categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getMixCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) + } + // KMK <-- + sealed interface Dialog { data object Filter : Dialog data class RemoveManga(val manga: Manga) : Dialog @@ -461,6 +590,12 @@ open class BrowseSourceScreenModel( data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog data class CreateSavedSearch(val currentSavedSearches: ImmutableList) : Dialog // SY <-- + // KMK --> + data class ChangeMangasCategory( + val mangas: List, + val initialSelection: ImmutableList>, + ) : Dialog + // KMK <-- } @Immutable @@ -473,6 +608,10 @@ open class BrowseSourceScreenModel( val savedSearches: ImmutableList = persistentListOf(), val filterable: Boolean = true, // SY <-- + // KMK --> + val selection: PersistentList = persistentListOf(), + val selectionMode: Boolean = false, + // KMK <-- ) { val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty() } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 62f68a272e..78ef237d92 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -76,6 +76,7 @@ Toggle NSFW only Select all Select inverse + Bulk selection mode Mark as read Mark as unread Mark previous as read From 83910501b79b25fa8e6b3ab7589c9468ed2f0d15 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 15:42:21 +0700 Subject: [PATCH 14/36] Allow skip duplicates --- .../manga/DuplicateMangaDialog.kt | 57 +++++++++++++++++++ .../source/browse/BrowseSourceScreen.kt | 12 ++++ .../source/browse/BrowseSourceScreenModel.kt | 24 +++++++- .../commonMain/resources/MR/base/strings.xml | 2 + 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index e3ff20ad26..859bb5820e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -57,3 +57,60 @@ fun DuplicateMangaDialog( }, ) } + +// KMK --> +@Composable +fun AllowDuplicateDialog( + onDismissRequest: () -> Unit, + onAllowDuplicate: () -> Unit, + onSkipDuplicate: () -> Unit, + onOpenManga: () -> Unit = { }, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + title = { + Text(text = stringResource(MR.strings.are_you_sure)) + }, + text = { + Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) + }, + confirmButton = { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + ) { + TextButton( + onClick = { + onDismissRequest() + onOpenManga() + }, + ) { + Text(text = stringResource(MR.strings.action_show_manga)) + } + + TextButton( + onClick = { + onDismissRequest() + onSkipDuplicate() + }, + ) { + Text(text = stringResource(MR.strings.action_skip_duplicate_manga)) + } + + TextButton( + onClick = { + onDismissRequest() + onAllowDuplicate() + }, + ) { + Text(text = stringResource(MR.strings.action_allow_duplicate_manga)) + } + } + }, + ) +} +// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 76f5dbcfd6..45bd990130 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -45,6 +45,7 @@ import eu.kanade.presentation.browse.components.SavedSearchCreateDialog import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog import eu.kanade.presentation.browse.components.SelectionToolbar import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen @@ -395,6 +396,17 @@ data class BrowseSourceScreen( }, ) } + is BrowseSourceScreenModel.Dialog.AllowDuplicate -> { + AllowDuplicateDialog( + onDismissRequest = onDismissRequest, + onAllowDuplicate = { + screenModel.addFavoriteDuplicate() + }, + onSkipDuplicate = { + screenModel.addFavoriteDuplicate(skipDuplicate = true) + }, + ) + } // KMK <-- else -> {} } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 59a7f33c28..b19ffbde40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -477,7 +477,16 @@ open class BrowseSourceScreenModel( fun addFavorite() { screenModelScope.launch { - val mangaList = state.value.selection + if (hasDuplicateLibraryMangas(state.value.selection)) + setDialog(Dialog.AllowDuplicate) + else + addFavoriteDuplicate() + } + } + + fun addFavoriteDuplicate(skipDuplicate: Boolean = false) { + screenModelScope.launch { + val mangaList = if (skipDuplicate) getNotDuplicateLibraryMangas() else state.value.selection val categories = getCategories() val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } @@ -574,6 +583,18 @@ open class BrowseSourceScreenModel( val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } return mangaCategories.flatten().distinct().subtract(common) } + + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun hasDuplicateLibraryMangas(mangas: List): Boolean { + return mangas.fastAny { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } // KMK <-- sealed interface Dialog { @@ -595,6 +616,7 @@ open class BrowseSourceScreenModel( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog + data object AllowDuplicate : Dialog // KMK <-- } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 78ef237d92..3d3cc78664 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -689,6 +689,8 @@ Source not installed: %1$s Add to library? No description + Allow duplicate + Skip duplicate Chapter %1$s From a617528fdad1665b36e4118e8f24a0268c5599dd Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Wed, 6 Mar 2024 18:27:54 +0700 Subject: [PATCH 15/36] Allow/Skip one by one --- .../manga/DuplicateMangaDialog.kt | 103 ++++++++++++------ .../ui/browse/feed/FeedScreenModel.kt | 44 +++++++- .../tachiyomi/ui/browse/feed/FeedTab.kt | 23 ++++ .../source/browse/BrowseSourceScreen.kt | 15 ++- .../source/browse/BrowseSourceScreenModel.kt | 52 ++++++--- .../ui/browse/source/feed/SourceFeedScreen.kt | 23 ++++ .../source/feed/SourceFeedScreenModel.kt | 44 +++++++- .../commonMain/resources/MR/base/strings.xml | 6 +- 8 files changed, 253 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 859bb5820e..e571c5bb99 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -1,13 +1,16 @@ package eu.kanade.presentation.manga import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowColumn import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding @@ -62,53 +65,91 @@ fun DuplicateMangaDialog( @Composable fun AllowDuplicateDialog( onDismissRequest: () -> Unit, - onAllowDuplicate: () -> Unit, - onSkipDuplicate: () -> Unit, - onOpenManga: () -> Unit = { }, + onAllowAllDuplicate: () -> Unit, + onSkipAllDuplicate: () -> Unit, + onOpenManga: () -> Unit = {}, + onAllowDuplicate: () -> Unit = {}, + onSkipDuplicate: () -> Unit = {}, + duplicatedName: String = "", ) { AlertDialog( onDismissRequest = onDismissRequest, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, title = { - Text(text = stringResource(MR.strings.are_you_sure)) + Text(text = duplicatedName) }, text = { Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) }, confirmButton = { FlowRow( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, ) { - TextButton( - onClick = { - onDismissRequest() - onOpenManga() - }, - ) { - Text(text = stringResource(MR.strings.action_show_manga)) + FlowColumn { + TextButton( + onClick = { + onDismissRequest() + onAllowDuplicate() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_allow_duplicate_manga)) + } + + TextButton( + onClick = { + onDismissRequest() + onAllowAllDuplicate() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_allow_all_duplicate_manga)) + } } - TextButton( - onClick = { - onDismissRequest() - onSkipDuplicate() - }, - ) { - Text(text = stringResource(MR.strings.action_skip_duplicate_manga)) + FlowColumn { + TextButton( + onClick = { + onDismissRequest() + onSkipDuplicate() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_skip_duplicate_manga)) + } + + TextButton( + onClick = { + onDismissRequest() + onSkipAllDuplicate() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_skip_all_duplicate_manga)) + } } - TextButton( - onClick = { - onDismissRequest() - onAllowDuplicate() - }, - ) { - Text(text = stringResource(MR.strings.action_allow_duplicate_manga)) + FlowColumn { + TextButton( + onClick = { + onDismissRequest() + onOpenManga() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_show_manga)) + } + + TextButton( + onClick = { + onDismissRequest() + }, + Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = stringResource(MR.strings.action_cancel)) + } } + } }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 31cbc83caa..9bd8dfc4fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -5,6 +5,7 @@ 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 @@ -42,6 +43,7 @@ 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 @@ -78,6 +80,7 @@ open class FeedScreenModel( 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()) { @@ -337,9 +340,19 @@ open class FeedScreenModel( } } - fun addFavorite() { + fun addFavorite(startIdx: Int = 0) { screenModelScope.launch { - val mangaList = state.value.selection + 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() } @@ -376,6 +389,32 @@ open class FeedScreenModel( } } + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + val mangas = state.value.selection + mangas.fastForEachIndexed { index, manga -> + if (index < startIdx) return@fastForEachIndexed + val dup = getDuplicateLibraryManga.await(manga) + if (dup.isEmpty()) return@fastForEachIndexed + return Pair(index, dup.first()) + } + return null + } + + fun removeDuplicateSelectedManga(index: Int) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + list.removeAt(index) + } + state.copy(selection = newSelection) + } + } + /** * Bulk update categories of manga using old and new common categories. * @@ -463,6 +502,7 @@ open class FeedScreenModel( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog() + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog() // KMK <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index bd99eb49aa..71617924e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -23,6 +23,7 @@ import eu.kanade.presentation.browse.FeedScreen 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.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.home.HomeScreen @@ -204,6 +205,28 @@ fun Screen.feedTab( }, ) } + 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 <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 45bd990130..468d8de31f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -399,12 +399,23 @@ data class BrowseSourceScreen( is BrowseSourceScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onDismissRequest, - onAllowDuplicate = { + 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.addFavoriteDuplicate(skipDuplicate = true) + screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first) + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first) }, + duplicatedName = dialog.duplicatedManga.second.title, ) } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index b19ffbde40..ad0e0c99e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -8,6 +8,7 @@ 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 @@ -475,18 +476,19 @@ open class BrowseSourceScreenModel( } } - fun addFavorite() { + fun addFavorite(startIdx: Int = 0) { screenModelScope.launch { - if (hasDuplicateLibraryMangas(state.value.selection)) - setDialog(Dialog.AllowDuplicate) + val mangaWithDup = getDuplicateLibraryManga(startIdx) + if (mangaWithDup != null) + setDialog(Dialog.AllowDuplicate(mangaWithDup)) else addFavoriteDuplicate() } } - fun addFavoriteDuplicate(skipDuplicate: Boolean = false) { + fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) { screenModelScope.launch { - val mangaList = if (skipDuplicate) getNotDuplicateLibraryMangas() else state.value.selection + 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() } @@ -523,6 +525,32 @@ open class BrowseSourceScreenModel( } } + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + val mangas = state.value.selection + mangas.fastForEachIndexed { index, manga -> + if (index < startIdx) return@fastForEachIndexed + val dup = getDuplicateLibraryManga.await(manga) + if (dup.isEmpty()) return@fastForEachIndexed + return Pair(index, dup.first()) + } + return null + } + + fun removeDuplicateSelectedManga(index: Int) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + list.removeAt(index) + } + state.copy(selection = newSelection) + } + } + /** * Bulk update categories of manga using old and new common categories. * @@ -583,18 +611,6 @@ open class BrowseSourceScreenModel( val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } return mangaCategories.flatten().distinct().subtract(common) } - - private suspend fun getNotDuplicateLibraryMangas(): List { - return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } - - private suspend fun hasDuplicateLibraryMangas(mangas: List): Boolean { - return mangas.fastAny { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } // KMK <-- sealed interface Dialog { @@ -616,7 +632,7 @@ open class BrowseSourceScreenModel( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog - data object AllowDuplicate : Dialog + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog // KMK <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 328f2677bd..33fa5c3eec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.SourceFeedScreen 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.util.Screen import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen @@ -109,6 +110,28 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { }, ) } + 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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 96fd1db3c5..6f7e8b7011 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -7,6 +7,7 @@ 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 @@ -49,6 +50,7 @@ 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 @@ -84,6 +86,7 @@ open class SourceFeedScreenModel( 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()) { @@ -341,9 +344,19 @@ open class SourceFeedScreenModel( } } - fun addFavorite() { + fun addFavorite(startIdx: Int = 0) { screenModelScope.launch { - val mangaList = state.value.selection + 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() } @@ -380,6 +393,32 @@ open class SourceFeedScreenModel( } } + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + val mangas = state.value.selection + mangas.fastForEachIndexed { index, manga -> + if (index < startIdx) return@fastForEachIndexed + val dup = getDuplicateLibraryManga.await(manga) + if (dup.isEmpty()) return@fastForEachIndexed + return Pair(index, dup.first()) + } + return null + } + + fun removeDuplicateSelectedManga(index: Int) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + list.removeAt(index) + } + state.copy(selection = newSelection) + } + } + /** * Bulk update categories of manga using old and new common categories. * @@ -467,6 +506,7 @@ open class SourceFeedScreenModel( val mangas: List, val initialSelection: ImmutableList>, ) : Dialog() + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog() // KMK <-- } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 3d3cc78664..af56253cac 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -678,6 +678,10 @@ Remove from library Unknown title You have an entry in your library with the same name.\n\nDo you still wish to continue? + Allow it + Skip it + Allow all + Skip all Added to library Removed from library More @@ -689,8 +693,6 @@ Source not installed: %1$s Add to library? No description - Allow duplicate - Skip duplicate Chapter %1$s From 2df7f13e4e2134c2f77b8c114031b7bd5da66cb6 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 09:26:40 +0700 Subject: [PATCH 16/36] =?UTF-8?q?Fix:=20Won=E2=80=99t=20try=20call=20favor?= =?UTF-8?q?ite=20if=20none=20selected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/browse/components/BrowseSourceToolbar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index 2558b24439..e643ee9b20 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -195,7 +195,7 @@ fun SelectionToolbar( title = stringResource(MR.strings.action_bookmark), icon = Icons.Outlined.BookmarkAdd, onClick = { -// if (selectedCount > 0) + if (selectedCount > 0) onChangeCategoryClicked() }, ), From 8d46ab8fc233a29e2e5800ad269ff9005740c3f2 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 11:29:58 +0700 Subject: [PATCH 17/36] renovate: move SelectionToolbar to its own component --- .../browse/components/BrowseSourceToolbar.kt | 30 ------------ .../browse/components/GlobalSearchCardRow.kt | 4 +- .../components/SelectionToolbar.kt | 47 +++++++++++++++++++ .../presentation/components/TabbedScreen.kt | 30 ------------ .../manga/DuplicateMangaDialog.kt | 8 ++-- .../source/browse/BrowseSourceScreen.kt | 2 +- 6 files changed, 54 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index e643ee9b20..5981ae015a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -4,7 +4,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.automirrored.outlined.Help import androidx.compose.material.icons.filled.ViewModule -import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.Text @@ -178,32 +177,3 @@ fun BrowseSourceToolbar( scrollBehavior = scrollBehavior, ) } - -// KMK --> -@Composable -fun SelectionToolbar( - selectedCount: Int, - onClickClearSelection: () -> Unit = {}, - onChangeCategoryClicked: () -> Unit = {}, -) { - AppBar( - titleContent = { Text(text = "$selectedCount") }, - actions = { - AppBarActions( - persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bookmark), - icon = Icons.Outlined.BookmarkAdd, - onClick = { - if (selectedCount > 0) - onChangeCategoryClicked() - }, - ), - ), - ) - }, - isActionMode = true, - onCancelActionMode = onClickClearSelection, - ) -} -// KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt index 2f4240ecb7..e23f506047 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -31,7 +31,7 @@ fun GlobalSearchCardRow( onClick: (Manga) -> Unit, onLongClick: (Manga) -> Unit, // KMK --> - selection: List? = null, + selection: List, // KMK <-- ) { if (titles.isEmpty()) { @@ -52,7 +52,7 @@ fun GlobalSearchCardRow( onClick = { onClick(title) }, onLongClick = { onLongClick(title) }, // KMK --> - isSelected = selection?.fastAny { selected -> selected.id == title.id } ?: false, + isSelected = selection.fastAny { selected -> selected.id == title.id }, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt new file mode 100644 index 0000000000..271c5aea75 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt @@ -0,0 +1,47 @@ +package eu.kanade.presentation.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun SelectionToolbar( + selectedCount: Int, + onClickClearSelection: () -> Unit, + onChangeCategoryClicked: () -> Unit, +) { + AppBar( + titleContent = { Text(text = "$selectedCount") }, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bookmark), + icon = Icons.Filled.BookmarkAdd, + onClick = { + if (selectedCount > 0) + onChangeCategoryClicked() + }, + ), + ), + ) + }, + isActionMode = true, + onCancelActionMode = onClickClearSelection, + ) +} + +@Preview +@Composable +fun SelectionToolbarPreview() { + SelectionToolbar( + selectedCount = 9, + {}, + {}, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 9405fc6470..bda9e86c22 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -8,14 +8,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -31,7 +28,6 @@ import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch -import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.i18n.stringResource @@ -126,29 +122,3 @@ data class TabContent( val actions: ImmutableList = persistentListOf(), val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit, ) - -// KMK --> -@Composable -private fun SelectionToolbar( - selectedCount: Int, - onClickClearSelection: () -> Unit = {}, - onChangeCategoryClicked: () -> Unit = {}, -) { - AppBar( - titleContent = { Text(text = "$selectedCount") }, - actions = { - AppBarActions( - persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bookmark), - icon = Icons.Outlined.BookmarkAdd, - onClick = onChangeCategoryClicked, - ), - ), - ) - }, - isActionMode = true, - onCancelActionMode = onClickClearSelection, - ) -} -// KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index e571c5bb99..7da55ef69d 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -67,10 +67,10 @@ fun AllowDuplicateDialog( onDismissRequest: () -> Unit, onAllowAllDuplicate: () -> Unit, onSkipAllDuplicate: () -> Unit, - onOpenManga: () -> Unit = {}, - onAllowDuplicate: () -> Unit = {}, - onSkipDuplicate: () -> Unit = {}, - duplicatedName: String = "", + onOpenManga: () -> Unit, + onAllowDuplicate: () -> Unit, + onSkipDuplicate: () -> Unit, + duplicatedName: String, ) { AlertDialog( onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 468d8de31f..99e86b2109 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -43,8 +43,8 @@ import eu.kanade.presentation.browse.components.BrowseSourceToolbar import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.browse.components.SavedSearchCreateDialog import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog -import eu.kanade.presentation.browse.components.SelectionToolbar import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.AssistContentScreen From c30a8873a2921820e8f37b17bd1a0e22edb91992 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 11:45:48 +0700 Subject: [PATCH 18/36] Bulk favorite for GlobalSearchScreen & MigrateSearchScreen --- .../presentation/browse/GlobalSearchScreen.kt | 52 +++-- .../browse/MigrateSearchScreen.kt | 45 ++-- .../browse/components/GlobalSearchToolbar.kt | 24 ++ .../migration/search/MigrateSearchScreen.kt | 77 ++++++- .../source/globalsearch/GlobalSearchScreen.kt | 67 +++++- .../source/globalsearch/SearchScreenModel.kt | 215 ++++++++++++++++++ 6 files changed, 445 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 80e13f03c2..a7d390f341 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -10,7 +10,9 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchToolbar +import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter @@ -22,6 +24,9 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun GlobalSearchScreen( + // KMK --> + screenModel: GlobalSearchScreenModel, + // KMK <-- state: SearchScreenModel.State, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, @@ -35,19 +40,31 @@ fun GlobalSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - sourceFilter = state.sourceFilter, - onChangeSearchFilter = onChangeSearchFilter, - onlyShowHasResults = state.onlyShowHasResults, - onToggleResults = onToggleResults, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (state.selectionMode) + SelectionToolbar( + selectedCount = state.selection.size, + onClickClearSelection = screenModel::toggleSelectionMode, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, + scrollBehavior = scrollBehavior, + // KMK --> + toggleBulkSelectionMode = screenModel::toggleSelectionMode + // KMK <-- + ) }, ) { paddingValues -> GlobalSearchContent( @@ -57,6 +74,9 @@ fun GlobalSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = state.selection, + // KMK <-- ) } } @@ -70,6 +90,9 @@ internal fun GlobalSearchContent( onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, + // KMK --> + selection: List, + // KMK <-- ) { LazyColumn( contentPadding = contentPadding, @@ -107,6 +130,9 @@ internal fun GlobalSearchContent( getManga = getManga, onClick = onClickItem, onLongClick = onLongClickItem, + // KMK --> + selection = selection, + // KMK <-- ) } is SearchItemResult.Error -> { diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 31abb596c7..d32f3ffdda 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -11,6 +12,9 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun MigrateSearchScreen( + // KMK --> + screenModel: MigrateSearchScreenModel, + // KMK <-- state: SearchScreenModel.State, fromSourceId: Long?, navigateUp: () -> Unit, @@ -25,19 +29,31 @@ fun MigrateSearchScreen( ) { Scaffold( topBar = { scrollBehavior -> - GlobalSearchToolbar( - searchQuery = state.searchQuery, - progress = state.progress, - total = state.total, - navigateUp = navigateUp, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - sourceFilter = state.sourceFilter, - onChangeSearchFilter = onChangeSearchFilter, - onlyShowHasResults = state.onlyShowHasResults, - onToggleResults = onToggleResults, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (state.selectionMode) + eu.kanade.presentation.components.SelectionToolbar( + selectedCount = state.selection.size, + onClickClearSelection = screenModel::toggleSelectionMode, + onChangeCategoryClicked = screenModel::addFavorite, + ) + else + // KMK <-- + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, + scrollBehavior = scrollBehavior, + // KMK --> + toggleBulkSelectionMode = screenModel::toggleSelectionMode + // KMK <-- + ) }, ) { paddingValues -> GlobalSearchContent( @@ -48,6 +64,9 @@ fun MigrateSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = state.selection, + // KMK <-- ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index fa61696ae7..3dbc87fce8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.PushPin @@ -26,8 +27,11 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter +import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -45,6 +49,9 @@ fun GlobalSearchToolbar( onlyShowHasResults: Boolean, onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, + // KMK --> + toggleBulkSelectionMode: () -> Unit, + // KMK <-- ) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Box { @@ -55,6 +62,23 @@ fun GlobalSearchToolbar( onClickCloseSearch = navigateUp, navigateUp = navigateUp, scrollBehavior = scrollBehavior, + // KMK --> + actions = { + AppBarActions( + actions = persistentListOf().builder() + .apply { + add( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleBulkSelectionMode, + ), + ) + } + .build(), + ) + }, + // KMK <-- ) if (progress in 1..) : Screen() { @@ -23,7 +28,18 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) } val dialogState by dialogScreenModel.state.collectAsState() + // KMK --> + BackHandler(enabled = state.selectionMode) { + when { + state.selectionMode -> screenModel.toggleSelectionMode() + } + } + // KMK <-- + MigrateSearchScreen( + // KMK --> + screenModel = screenModel, + // KMK <-- state = state, fromSourceId = state.fromSourceId, navigateUp = navigator::pop, @@ -38,15 +54,62 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L // SY <-- }, onClickItem = { - // SY --> - navigator.items - .filterIsInstance() - .last() - .newSelectedItem = mangaId to it.id - navigator.popUntil { it is MigrationListScreen } - // SY <-- + // KMK --> + if (state.selectionMode) { + screenModel.toggleSelection(it) + } + else + // KMK <-- + { + // SY --> + navigator.items + .filterIsInstance() + .last() + .newSelectedItem = mangaId to it.id + navigator.popUntil { it is MigrationListScreen } + // SY <-- + } }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, ) + + // KMK --> + val onDismissRequest = { screenModel.setDialog(null) } + when (val dialog = state.dialog) { + is SearchScreenModel.Dialog.ChangeMangasCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + is SearchScreenModel.Dialog.AllowDuplicate -> { + AllowDuplicateDialog( + onDismissRequest = onDismissRequest, + onAllowAllDuplicate = { + screenModel.addFavoriteDuplicate() + }, + onSkipAllDuplicate = { + screenModel.addFavoriteDuplicate(skipAllDuplicates = true) + }, + onOpenManga = { + navigator.push(MangaScreen(dialog.duplicatedManga.second.id)) + }, + onAllowDuplicate = { + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1) + }, + onSkipDuplicate = { + screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first) + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first) + }, + duplicatedName = dialog.duplicatedManga.second.title, + ) + } + else -> {} + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index a09595c4d4..da382a5742 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -11,8 +12,11 @@ import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.GlobalSearchScreen +import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen +import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import tachiyomi.presentation.core.screens.LoadingScreen @@ -36,6 +40,14 @@ class GlobalSearchScreen( mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1) } + // KMK --> + BackHandler(enabled = state.selectionMode) { + when { + state.selectionMode -> screenModel.toggleSelectionMode() + } + } + // KMK <-- + if (showSingleLoadingScreen) { LoadingScreen() @@ -56,6 +68,9 @@ class GlobalSearchScreen( } } else { GlobalSearchScreen( + // KMK --> + screenModel = screenModel, + // KMK <-- state = state, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, @@ -66,9 +81,57 @@ class GlobalSearchScreen( onClickSource = { navigator.push(BrowseSourceScreen(it.id, state.searchQuery)) }, - onClickItem = { navigator.push(MangaScreen(it.id, true)) }, - onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, + onClickItem = { + // KMK --> + if (state.selectionMode) + screenModel.toggleSelection(it) + else + // KMK <-- + navigator.push(MangaScreen(it.id, true)) + }, + onLongClickItem = { + navigator.push(MangaScreen(it.id, true)) + }, ) } + + // KMK --> + val onDismissRequest = { screenModel.setDialog(null) } + when (val dialog = state.dialog) { + is SearchScreenModel.Dialog.ChangeMangasCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + screenModel.setMangaCategories(dialog.mangas, include, exclude) + }, + ) + } + is SearchScreenModel.Dialog.AllowDuplicate -> { + AllowDuplicateDialog( + onDismissRequest = onDismissRequest, + onAllowAllDuplicate = { + screenModel.addFavoriteDuplicate() + }, + onSkipAllDuplicate = { + screenModel.addFavoriteDuplicate(skipAllDuplicates = true) + }, + onOpenManga = { + navigator.push(MangaScreen(dialog.duplicatedManga.second.id)) + }, + onAllowDuplicate = { + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first + 1) + }, + onSkipDuplicate = { + screenModel.removeDuplicateSelectedManga(index = dialog.duplicatedManga.first) + screenModel.addFavorite(startIdx = dialog.duplicatedManga.first) + }, + duplicatedName = dialog.duplicatedManga.second.title, + ) + } + else -> {} + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index fe1fe79d16..8fbede5544 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -3,15 +3,24 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.produceState +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.CatalogueSource +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.Job @@ -20,10 +29,19 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import tachiyomi.core.common.preference.CheckboxState +import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.interactor.SetMangaCategories +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -39,6 +57,13 @@ abstract class SearchScreenModel( private val extensionManager: ExtensionManager = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getManga: GetManga = Injekt.get(), + // KMK --> + private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + // KMK <-- ) : StateScreenModel(initialState) { private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() @@ -193,6 +218,191 @@ abstract class SearchScreenModel( updateItems(newItems) } + // KMK --> + fun toggleSelectionMode() { + if (state.value.selectionMode) + clearSelection() + mutableState.update { it.copy(selectionMode = !it.selectionMode) } + } + + private fun clearSelection() { + mutableState.update { it.copy(selection = persistentListOf()) } + } + + fun toggleSelection(manga: Manga) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + if (list.fastAny { it.id == manga.id }) { + list.removeAll { it.id == manga.id } + } else { + list.add(manga) + } + } + state.copy(selection = newSelection) + } + } + + fun addFavorite(startIdx: Int = 0) { + screenModelScope.launch { + val mangaWithDup = getDuplicateLibraryManga(startIdx) + if (mangaWithDup != null) + setDialog(Dialog.AllowDuplicate(mangaWithDup)) + else + addFavoriteDuplicate() + } + } + + fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) { + screenModelScope.launch { + val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection + val categories = getCategories() + val defaultCategoryId = libraryPreferences.defaultCategory().get() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } + + when { + // Default category set + defaultCategory != null -> { + setMangaCategories(mangaList, listOf(defaultCategory.id), emptyList()) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + // Automatic 'Default' or no categories + setMangaCategories(mangaList, emptyList(), emptyList()) + } + + else -> { + // Get indexes of the common categories to preselect. + val common = getCommonCategories(mangaList) + // Get indexes of the mix categories to preselect. + val mix = getMixCategories(mangaList) + val preselected = categories + .map { + when (it) { + in common -> CheckboxState.State.Checked(it) + in mix -> CheckboxState.TriState.Exclude(it) + else -> CheckboxState.State.None(it) + } + } + .toImmutableList() + setDialog(Dialog.ChangeMangasCategory(mangaList, preselected)) + } + } + } + } + + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + val mangas = state.value.selection + mangas.fastForEachIndexed { index, manga -> + if (index < startIdx) return@fastForEachIndexed + val dup = getDuplicateLibraryManga.await(manga) + if (dup.isEmpty()) return@fastForEachIndexed + return Pair(index, dup.first()) + } + return null + } + + fun removeDuplicateSelectedManga(index: Int) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + list.removeAt(index) + } + state.copy(selection = newSelection) + } + } + + /** + * Bulk update categories of manga using old and new common categories. + * + * @param mangaList the list of manga to move. + * @param addCategories the categories to add for all mangas. + * @param removeCategories the categories to remove in all mangas. + */ + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.fastForEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) + } + } + toggleSelectionMode() + } + + private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } + } + + /** + * Returns the mix (non-common) categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getMixCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + suspend fun getCategories(): List { + return getCategories.subscribe() + .firstOrNull() + ?.filterNot { it.isSystemCategory } + .orEmpty() + } + + fun setDialog(dialog: Dialog?) { + mutableState.update { it.copy(dialog = dialog) } + } + + sealed interface Dialog { + data class ChangeMangasCategory( + val mangas: List, + val initialSelection: ImmutableList>, + ) : Dialog + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog + } + // KMK <-- + @Immutable data class State( val fromSourceId: Long? = null, @@ -200,6 +410,11 @@ abstract class SearchScreenModel( val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, val items: PersistentMap = persistentMapOf(), + // KMK --> + val dialog: Dialog? = null, + val selection: PersistentList = persistentListOf(), + val selectionMode: Boolean = false, + // KMK <-- ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size From 3bc673ae176d833486084e20a6856b06229d3846 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 12:25:27 +0700 Subject: [PATCH 19/36] Feat: allow long click to favorite on GlobalSearch --- .../presentation/browse/GlobalSearchScreen.kt | 2 +- .../source/browse/BrowseSourceScreenModel.kt | 2 +- .../source/globalsearch/GlobalSearchScreen.kt | 60 +++++++++++- .../source/globalsearch/SearchScreenModel.kt | 96 +++++++++++++++++++ 4 files changed, 156 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index a7d390f341..da244db462 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -62,7 +62,7 @@ fun GlobalSearchScreen( onToggleResults = onToggleResults, scrollBehavior = scrollBehavior, // KMK --> - toggleBulkSelectionMode = screenModel::toggleSelectionMode + toggleBulkSelectionMode = screenModel::toggleSelectionMode, // KMK <-- ) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index ad0e0c99e6..ae159e8eed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -339,7 +339,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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index da382a5742..a2cf8381b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -7,17 +7,23 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.GlobalSearchScreen +import eu.kanade.presentation.browse.components.RemoveMangaDialog 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.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.presentation.core.screens.LoadingScreen class GlobalSearchScreen( @@ -41,6 +47,9 @@ class GlobalSearchScreen( } // KMK --> + val scope = rememberCoroutineScope() + val haptic = LocalHapticFeedback.current + BackHandler(enabled = state.selectionMode) { when { state.selectionMode -> screenModel.toggleSelectionMode() @@ -89,8 +98,28 @@ class GlobalSearchScreen( // KMK <-- navigator.push(MangaScreen(it.id, true)) }, - onLongClickItem = { - navigator.push(MangaScreen(it.id, true)) + onLongClickItem = { manga -> + // KMK --> + if (state.selectionMode) + // KMK <-- + navigator.push(MangaScreen(manga.id, true)) + // KMK --> + else + scope.launchIO { + val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + when { + manga.favorite -> screenModel.setDialog(SearchScreenModel.Dialog.RemoveManga(manga)) + duplicateManga != null -> screenModel.setDialog( + SearchScreenModel.Dialog.AddDuplicateManga( + manga, + duplicateManga, + ), + ) + else -> screenModel.addFavorite(manga) + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + // KMK <-- }, ) } @@ -98,6 +127,33 @@ class GlobalSearchScreen( // KMK --> val onDismissRequest = { screenModel.setDialog(null) } when (val dialog = state.dialog) { + is SearchScreenModel.Dialog.AddDuplicateManga -> { + DuplicateMangaDialog( + onDismissRequest = onDismissRequest, + onConfirm = { screenModel.addFavorite(dialog.manga) }, + onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + ) + } + is SearchScreenModel.Dialog.RemoveManga -> { + RemoveMangaDialog( + onDismissRequest = onDismissRequest, + onConfirm = { + screenModel.changeMangaFavorite(dialog.manga) + }, + mangaToRemove = dialog.manga, + ) + } + is SearchScreenModel.Dialog.ChangeMangaCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, _ -> + screenModel.changeMangaFavorite(dialog.manga) + screenModel.moveMangaToCategories(dialog.manga, include) + }, + ) + } is SearchScreenModel.Dialog.ChangeMangasCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 8fbede5544..9016236624 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -11,9 +11,12 @@ import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.util.ioCoroutineScope +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.util.removeCovers import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap @@ -35,19 +38,23 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import tachiyomi.core.common.preference.CheckboxState +import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category +import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.time.Instant import java.util.concurrent.Executors abstract class SearchScreenModel( @@ -63,6 +70,9 @@ abstract class SearchScreenModel( private val getCategories: GetCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), + private val addTracks: AddTracks = Injekt.get(), // KMK <-- ) : StateScreenModel(initialState) { @@ -390,11 +400,97 @@ abstract class SearchScreenModel( .orEmpty() } + suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { + return getDuplicateLibraryManga.await(manga).getOrNull(0) + } + + private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { + moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) + } + + fun moveMangaToCategories(manga: Manga, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await( + mangaId = manga.id, + categoryIds = categoryIds.toList(), + ) + } + } + + /** + * Adds or removes a manga from the library. + * + * @param manga the manga to update. + */ + fun changeMangaFavorite(manga: Manga) { + val source = sourceManager.getOrStub(manga.source) + + screenModelScope.launch { + var new = manga.copy( + favorite = !manga.favorite, + dateAdded = when (manga.favorite) { + true -> 0 + false -> Instant.now().toEpochMilli() + }, + ) + // TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel]) + if (!new.favorite) { + new = new.removeCovers(coverCache) + } else { + setMangaDefaultChapterFlags.await(manga) + addTracks.bindEnhancedTrackers(manga, source) + } + + updateManga.await(new.toMangaUpdate()) + } + } + + fun addFavorite(manga: Manga) { + screenModelScope.launch { + val categories = getCategories() + val defaultCategoryId = libraryPreferences.defaultCategory().get() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } + + when { + // Default category set + defaultCategory != null -> { + moveMangaToCategories(manga, defaultCategory) + + changeMangaFavorite(manga) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + moveMangaToCategories(manga) + + changeMangaFavorite(manga) + } + + // Choose a category + else -> { + val preselectedIds = getCategories.await(manga.id).map { it.id } + setDialog( + Dialog.ChangeMangaCategory( + manga, + categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(), + ), + ) + } + } + } + } + fun setDialog(dialog: Dialog?) { mutableState.update { it.copy(dialog = dialog) } } sealed interface Dialog { + data class RemoveManga(val manga: Manga) : Dialog + data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class ChangeMangaCategory( + val manga: Manga, + val initialSelection: ImmutableList>, + ) : Dialog data class ChangeMangasCategory( val mangas: List, val initialSelection: ImmutableList>, From 0ad2680b45e027592ee381f77bfac4f4bb655b5b Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 13:00:31 +0700 Subject: [PATCH 20/36] renovate code --- .../browse/MigrateSearchScreen.kt | 3 +- .../presentation/browse/SourceFeedScreen.kt | 32 +------------------ 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index d32f3ffdda..c3a1e2c211 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.browse import androidx.compose.runtime.Composable import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar +import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel @@ -31,7 +32,7 @@ fun MigrateSearchScreen( topBar = { scrollBehavior -> // KMK --> if (state.selectionMode) - eu.kanade.presentation.components.SelectionToolbar( + SelectionToolbar( selectedCount = state.selection.size, onClickClearSelection = screenModel::toggleSelectionMode, onChangeCategoryClicked = screenModel::addFavorite, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index cbf6f64719..3ed1449ce4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -3,9 +3,6 @@ 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.BookmarkAdd -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -17,10 +14,9 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.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 kotlinx.collections.immutable.ImmutableList @@ -280,29 +276,3 @@ fun SourceFeedToolbar( // KMK <-- ) } - -// KMK --> -@Composable -private fun SelectionToolbar( - selectedCount: Int, - onClickClearSelection: () -> Unit = {}, - onChangeCategoryClicked: () -> Unit = {}, -) { - AppBar( - titleContent = { Text(text = "$selectedCount") }, - actions = { - AppBarActions( - persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bookmark), - icon = Icons.Outlined.BookmarkAdd, - onClick = onChangeCategoryClicked, - ), - ), - ) - }, - isActionMode = true, - onCancelActionMode = onClickClearSelection, - ) -} -// KMK <-- From 42022bf661a4b04dbae8014749907e04d8ec2268 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Thu, 7 Mar 2024 13:18:35 +0700 Subject: [PATCH 21/36] improve: toggle selection mode when last item is unselected --- .../ui/browse/source/browse/BrowseSourceScreenModel.kt | 3 +++ .../ui/browse/source/globalsearch/SearchScreenModel.kt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index ae159e8eed..b97f445de2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -473,6 +473,9 @@ open class BrowseSourceScreenModel( } } state.copy(selection = newSelection) + }.also { + if (state.value.selection.isEmpty()) + toggleSelectionMode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 9016236624..6eeae8bd7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -249,6 +249,9 @@ abstract class SearchScreenModel( } } state.copy(selection = newSelection) + }.also { + if (state.value.selection.isEmpty()) + toggleSelectionMode() } } From cc5b6a1d5f24f0f70936785abe97543b55b6cba9 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Sat, 9 Mar 2024 11:00:40 +0700 Subject: [PATCH 22/36] separate BulkSelection into standalone BulkFavoriteScreenModel --- .../presentation/browse/GlobalSearchScreen.kt | 22 +- .../browse/MigrateSearchScreen.kt | 22 +- .../ui/browse/BulkFavoriteScreenModel.kt | 336 ++++++++++++++++++ .../migration/search/MigrateSearchScreen.kt | 43 +-- .../source/globalsearch/GlobalSearchScreen.kt | 68 ++-- .../source/globalsearch/SearchScreenModel.kt | 314 ---------------- 6 files changed, 419 insertions(+), 386 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index da244db462..2faa80563c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreenModel +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter @@ -24,9 +24,6 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun GlobalSearchScreen( - // KMK --> - screenModel: GlobalSearchScreenModel, - // KMK <-- state: SearchScreenModel.State, navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, @@ -37,15 +34,20 @@ fun GlobalSearchScreen( onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, + // KMK --> + bulkFavoriteState: BulkFavoriteScreenModel.State, + toggleSelectionMode: () -> Unit, + addFavorite: () -> Unit, + // KMK <-- ) { Scaffold( topBar = { scrollBehavior -> // KMK --> - if (state.selectionMode) + if (bulkFavoriteState.selectionMode) SelectionToolbar( - selectedCount = state.selection.size, - onClickClearSelection = screenModel::toggleSelectionMode, - onChangeCategoryClicked = screenModel::addFavorite, + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = toggleSelectionMode, + onChangeCategoryClicked = addFavorite, ) else // KMK <-- @@ -62,7 +64,7 @@ fun GlobalSearchScreen( onToggleResults = onToggleResults, scrollBehavior = scrollBehavior, // KMK --> - toggleBulkSelectionMode = screenModel::toggleSelectionMode, + toggleBulkSelectionMode = toggleSelectionMode, // KMK <-- ) }, @@ -75,7 +77,7 @@ fun GlobalSearchScreen( onClickItem = onClickItem, onLongClickItem = onLongClickItem, // KMK --> - selection = state.selection, + selection = bulkFavoriteState.selection, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index c3a1e2c211..2823b5c9cd 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -13,9 +13,6 @@ import tachiyomi.presentation.core.components.material.Scaffold @Composable fun MigrateSearchScreen( - // KMK --> - screenModel: MigrateSearchScreenModel, - // KMK <-- state: SearchScreenModel.State, fromSourceId: Long?, navigateUp: () -> Unit, @@ -27,15 +24,20 @@ fun MigrateSearchScreen( onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, + // KMK --> + bulkFavoriteState: BulkFavoriteScreenModel.State, + toggleSelectionMode: () -> Unit, + addFavorite: () -> Unit, + // KMK <-- ) { Scaffold( topBar = { scrollBehavior -> // KMK --> - if (state.selectionMode) + if (bulkFavoriteState.selectionMode) SelectionToolbar( - selectedCount = state.selection.size, - onClickClearSelection = screenModel::toggleSelectionMode, - onChangeCategoryClicked = screenModel::addFavorite, + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = toggleSelectionMode, + onChangeCategoryClicked = addFavorite, ) else // KMK <-- @@ -52,7 +54,7 @@ fun MigrateSearchScreen( onToggleResults = onToggleResults, scrollBehavior = scrollBehavior, // KMK --> - toggleBulkSelectionMode = screenModel::toggleSelectionMode + toggleBulkSelectionMode = toggleSelectionMode // KMK <-- ) }, @@ -66,7 +68,7 @@ fun MigrateSearchScreen( onClickItem = onClickItem, onLongClickItem = onLongClickItem, // KMK --> - selection = state.selection, + selection = bulkFavoriteState.selection, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt new file mode 100644 index 0000000000..c9f195d133 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -0,0 +1,336 @@ +package eu.kanade.tachiyomi.ui.browse + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.track.interactor.AddTracks +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.util.removeCovers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import tachiyomi.core.common.preference.CheckboxState +import tachiyomi.core.common.preference.mapAsCheckboxState +import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.interactor.SetMangaCategories +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags +import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.manga.model.toMangaUpdate +import tachiyomi.domain.source.service.SourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.time.Instant + + +class BulkFavoriteScreenModel( + initialState: State = State(), + private val sourceManager: SourceManager = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), + private val addTracks: AddTracks = Injekt.get(), +) : StateScreenModel(initialState) { + + fun backHandler() { + toggleSelectionMode() + } + + fun toggleSelectionMode() { + if (state.value.selectionMode) + clearSelection() + mutableState.update { it.copy(selectionMode = !it.selectionMode) } + } + + private fun clearSelection() { + mutableState.update { it.copy(selection = persistentListOf()) } + } + + fun toggleSelection(manga: Manga) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + if (list.fastAny { it.id == manga.id }) { + list.removeAll { it.id == manga.id } + } else { + list.add(manga) + } + } + state.copy(selection = newSelection) + }.also { + if (state.value.selection.isEmpty()) + toggleSelectionMode() + } + } + + fun addFavorite(startIdx: Int = 0) { + screenModelScope.launch { + val mangaWithDup = getDuplicateLibraryManga(startIdx) + if (mangaWithDup != null) + setDialog(Dialog.AllowDuplicate(mangaWithDup)) + else + addFavoriteDuplicate() + } + } + + fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) { + screenModelScope.launch { + val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection + val categories = getCategories() + val defaultCategoryId = libraryPreferences.defaultCategory().get() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } + + when { + // Default category set + defaultCategory != null -> { + setMangasCategories(mangaList, listOf(defaultCategory.id), emptyList()) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + // Automatic 'Default' or no categories + setMangasCategories(mangaList, emptyList(), emptyList()) + } + + else -> { + // Get indexes of the common categories to preselect. + val common = getCommonCategories(mangaList) + // Get indexes of the mix categories to preselect. + val mix = getMixCategories(mangaList) + val preselected = categories + .map { + when (it) { + in common -> CheckboxState.State.Checked(it) + in mix -> CheckboxState.TriState.Exclude(it) + else -> CheckboxState.State.None(it) + } + } + .toImmutableList() + setDialog(Dialog.ChangeMangasCategory(mangaList, preselected)) + } + } + } + } + + private suspend fun getNotDuplicateLibraryMangas(): List { + return state.value.selection.filterNot { manga -> + getDuplicateLibraryManga.await(manga).isNotEmpty() + } + } + + private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { + val mangas = state.value.selection + mangas.fastForEachIndexed { index, manga -> + if (index < startIdx) return@fastForEachIndexed + val dup = getDuplicateLibraryManga.await(manga) + if (dup.isEmpty()) return@fastForEachIndexed + return Pair(index, dup.first()) + } + return null + } + + fun removeDuplicateSelectedManga(index: Int) { + mutableState.update { state -> + val newSelection = state.selection.mutate { list -> + list.removeAt(index) + } + state.copy(selection = newSelection) + } + } + + /** + * Bulk update categories of manga using old and new common categories. + * + * @param mangaList the list of manga to move. + * @param addCategories the categories to add for all mangas. + * @param removeCategories the categories to remove in all mangas. + */ + fun setMangasCategories(mangaList: List, addCategories: List, removeCategories: List) { + screenModelScope.launchNonCancellable { + mangaList.fastForEach { manga -> + val categoryIds = getCategories.await(manga.id) + .map { it.id } + .subtract(removeCategories.toSet()) + .plus(addCategories) + .toList() + + moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) + } + } + toggleSelectionMode() + } + + private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } + } + + /** + * Returns the mix (non-common) categories for the given list of manga. + * + * @param mangas the list of manga. + */ + private suspend fun getMixCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + suspend fun getCategories(): List { + return getCategories.subscribe() + .firstOrNull() + ?.filterNot { it.isSystemCategory } + .orEmpty() + } + + suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { + return getDuplicateLibraryManga.await(manga).getOrNull(0) + } + + private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { + moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) + } + + fun moveMangaToCategories(manga: Manga, categoryIds: List) { + screenModelScope.launchIO { + setMangaCategories.await( + mangaId = manga.id, + categoryIds = categoryIds.toList(), + ) + } + } + + /** + * Adds or removes a manga from the library. + * + * @param manga the manga to update. + */ + fun changeMangaFavorite(manga: Manga) { + val source = sourceManager.getOrStub(manga.source) + + screenModelScope.launch { + var new = manga.copy( + favorite = !manga.favorite, + dateAdded = when (manga.favorite) { + true -> 0 + false -> Instant.now().toEpochMilli() + }, + ) + // TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel]) + if (!new.favorite) { + new = new.removeCovers(coverCache) + } else { + setMangaDefaultChapterFlags.await(manga) + addTracks.bindEnhancedTrackers(manga, source) + } + + updateManga.await(new.toMangaUpdate()) + } + } + + fun addFavorite(manga: Manga) { + screenModelScope.launch { + val categories = getCategories() + val defaultCategoryId = libraryPreferences.defaultCategory().get() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } + + when { + // Default category set + defaultCategory != null -> { + moveMangaToCategories(manga, defaultCategory) + + changeMangaFavorite(manga) + } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + moveMangaToCategories(manga) + + changeMangaFavorite(manga) + } + + // Choose a category + else -> { + val preselectedIds = getCategories.await(manga.id).map { it.id } + setDialog( + Dialog.ChangeMangaCategory( + manga, + categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(), + ), + ) + } + } + } + } + + fun setDialog(dialog: Dialog?) { + mutableState.update { + it.copy(dialog = dialog) + } + } + + interface Dialog { + data class RemoveManga(val manga: Manga) : Dialog + data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog + data class ChangeMangaCategory( + val manga: Manga, + val initialSelection: ImmutableList>, + ) : Dialog + data class ChangeMangasCategory( + val mangas: List, + val initialSelection: ImmutableList>, + ) : Dialog + data class AllowDuplicate(val duplicatedManga: Pair) : Dialog + } + + @Immutable + data class State ( + val dialog: Dialog? = null, + val selection: PersistentList = persistentListOf(), + val selectionMode: Boolean = false, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index d2eb329248..2096ad42e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -11,8 +11,8 @@ import eu.kanade.presentation.browse.MigrateSearchScreen import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen @@ -29,17 +29,15 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L val dialogState by dialogScreenModel.state.collectAsState() // 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.toggleSelectionMode() } // KMK <-- MigrateSearchScreen( - // KMK --> - screenModel = screenModel, - // KMK <-- state = state, fromSourceId = state.fromSourceId, navigateUp = navigator::pop, @@ -55,8 +53,8 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L }, onClickItem = { // KMK --> - if (state.selectionMode) { - screenModel.toggleSelection(it) + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(it) } else // KMK <-- @@ -71,39 +69,44 @@ 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, + // KMK <-- ) // KMK --> - val onDismissRequest = { screenModel.setDialog(null) } - when (val dialog = state.dialog) { - is SearchScreenModel.Dialog.ChangeMangasCategory -> { + val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } + when (val dialog = bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, onEditCategories = { navigator.push(CategoryScreen()) }, onConfirm = { include, exclude -> - screenModel.setMangaCategories(dialog.mangas, include, exclude) + bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude) }, ) } - is SearchScreenModel.Dialog.AllowDuplicate -> { + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onDismissRequest, 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, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index a2cf8381b7..96bf82ba69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -20,6 +20,7 @@ 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.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen @@ -50,10 +51,11 @@ class GlobalSearchScreen( val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current - 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 <-- @@ -77,9 +79,6 @@ class GlobalSearchScreen( } } else { GlobalSearchScreen( - // KMK --> - screenModel = screenModel, - // KMK <-- state = state, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, @@ -92,96 +91,101 @@ class GlobalSearchScreen( }, onClickItem = { // KMK --> - if (state.selectionMode) - screenModel.toggleSelection(it) + if (bulkFavoriteState.selectionMode) + bulkFavoriteScreenModel.toggleSelection(it) else // KMK <-- navigator.push(MangaScreen(it.id, true)) }, onLongClickItem = { manga -> // KMK --> - if (state.selectionMode) + if (bulkFavoriteState.selectionMode) // KMK <-- navigator.push(MangaScreen(manga.id, true)) // KMK --> else scope.launchIO { - val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> screenModel.setDialog(SearchScreenModel.Dialog.RemoveManga(manga)) - duplicateManga != null -> screenModel.setDialog( - SearchScreenModel.Dialog.AddDuplicateManga( + manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga)) + duplicateManga != null -> bulkFavoriteScreenModel.setDialog( + BulkFavoriteScreenModel.Dialog.AddDuplicateManga( manga, duplicateManga, ), ) - else -> screenModel.addFavorite(manga) + else -> bulkFavoriteScreenModel.addFavorite(manga) } haptic.performHapticFeedback(HapticFeedbackType.LongPress) } // KMK <-- }, + // KMK --> + bulkFavoriteState = bulkFavoriteState, + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + addFavorite = bulkFavoriteScreenModel::addFavorite, + // KMK <-- ) } // KMK --> - val onDismissRequest = { screenModel.setDialog(null) } - when (val dialog = state.dialog) { - is SearchScreenModel.Dialog.AddDuplicateManga -> { + val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } + when (val dialog = bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> { DuplicateMangaDialog( onDismissRequest = onDismissRequest, - onConfirm = { screenModel.addFavorite(dialog.manga) }, + onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) }, onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, ) } - is SearchScreenModel.Dialog.RemoveManga -> { + is BulkFavoriteScreenModel.Dialog.RemoveManga -> { RemoveMangaDialog( onDismissRequest = onDismissRequest, onConfirm = { - screenModel.changeMangaFavorite(dialog.manga) + bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga) }, mangaToRemove = dialog.manga, ) } - is SearchScreenModel.Dialog.ChangeMangaCategory -> { + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, onEditCategories = { navigator.push(CategoryScreen()) }, onConfirm = { include, _ -> - screenModel.changeMangaFavorite(dialog.manga) - screenModel.moveMangaToCategories(dialog.manga, include) + bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga) + bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include) }, ) } - is SearchScreenModel.Dialog.ChangeMangasCategory -> { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, onEditCategories = { navigator.push(CategoryScreen()) }, onConfirm = { include, exclude -> - screenModel.setMangaCategories(dialog.mangas, include, exclude) + bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude) }, ) } - is SearchScreenModel.Dialog.AllowDuplicate -> { + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onDismissRequest, 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, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 6eeae8bd7f..fe1fe79d16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -3,27 +3,15 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.produceState -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastForEachIndexed import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.util.ioCoroutineScope -import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.util.removeCovers -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.Job @@ -32,29 +20,16 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import tachiyomi.core.common.preference.CheckboxState -import tachiyomi.core.common.preference.mapAsCheckboxState -import tachiyomi.core.common.util.lang.launchIO -import tachiyomi.core.common.util.lang.launchNonCancellable -import tachiyomi.domain.category.interactor.GetCategories -import tachiyomi.domain.category.interactor.SetMangaCategories -import tachiyomi.domain.category.model.Category -import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags -import tachiyomi.domain.library.service.LibraryPreferences -import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.time.Instant import java.util.concurrent.Executors abstract class SearchScreenModel( @@ -64,16 +39,6 @@ abstract class SearchScreenModel( private val extensionManager: ExtensionManager = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val getManga: GetManga = Injekt.get(), - // KMK --> - private val libraryPreferences: LibraryPreferences = Injekt.get(), - private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), - private val getCategories: GetCategories = Injekt.get(), - private val setMangaCategories: SetMangaCategories = Injekt.get(), - private val updateManga: UpdateManga = Injekt.get(), - private val coverCache: CoverCache = Injekt.get(), - private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), - private val addTracks: AddTracks = Injekt.get(), - // KMK <-- ) : StateScreenModel(initialState) { private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() @@ -228,280 +193,6 @@ abstract class SearchScreenModel( updateItems(newItems) } - // KMK --> - fun toggleSelectionMode() { - if (state.value.selectionMode) - clearSelection() - mutableState.update { it.copy(selectionMode = !it.selectionMode) } - } - - private fun clearSelection() { - mutableState.update { it.copy(selection = persistentListOf()) } - } - - fun toggleSelection(manga: Manga) { - mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - if (list.fastAny { it.id == manga.id }) { - list.removeAll { it.id == manga.id } - } else { - list.add(manga) - } - } - state.copy(selection = newSelection) - }.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 { - return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } - - private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { - val mangas = state.value.selection - mangas.fastForEachIndexed { index, manga -> - if (index < startIdx) return@fastForEachIndexed - val dup = getDuplicateLibraryManga.await(manga) - if (dup.isEmpty()) return@fastForEachIndexed - return Pair(index, dup.first()) - } - return null - } - - fun removeDuplicateSelectedManga(index: Int) { - mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - list.removeAt(index) - } - state.copy(selection = newSelection) - } - } - - /** - * Bulk update categories of manga using old and new common categories. - * - * @param mangaList the list of manga to move. - * @param addCategories the categories to add for all mangas. - * @param removeCategories the categories to remove in all mangas. - */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { - screenModelScope.launchNonCancellable { - mangaList.fastForEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() - - moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) - } - } - toggleSelectionMode() - } - - private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { - moveMangaToCategory(manga.id, categories) - if (manga.favorite) return - - screenModelScope.launchIO { - updateManga.awaitUpdateFavorite(manga.id, true) - } - } - - private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { - screenModelScope.launchIO { - setMangaCategories.await(mangaId, categoryIds) - } - } - - /** - * Returns the common categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getCommonCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas - .map { getCategories.await(it.id).toSet() } - .reduce { set1, set2 -> set1.intersect(set2) } - } - - /** - * Returns the mix (non-common) categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getMixCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } - val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } - return mangaCategories.flatten().distinct().subtract(common) - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - suspend fun getCategories(): List { - return getCategories.subscribe() - .firstOrNull() - ?.filterNot { it.isSystemCategory } - .orEmpty() - } - - suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { - return getDuplicateLibraryManga.await(manga).getOrNull(0) - } - - private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { - moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) - } - - fun moveMangaToCategories(manga: Manga, categoryIds: List) { - screenModelScope.launchIO { - setMangaCategories.await( - mangaId = manga.id, - categoryIds = categoryIds.toList(), - ) - } - } - - /** - * Adds or removes a manga from the library. - * - * @param manga the manga to update. - */ - fun changeMangaFavorite(manga: Manga) { - val source = sourceManager.getOrStub(manga.source) - - screenModelScope.launch { - var new = manga.copy( - favorite = !manga.favorite, - dateAdded = when (manga.favorite) { - true -> 0 - false -> Instant.now().toEpochMilli() - }, - ) - // TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel]) - if (!new.favorite) { - new = new.removeCovers(coverCache) - } else { - setMangaDefaultChapterFlags.await(manga) - addTracks.bindEnhancedTrackers(manga, source) - } - - updateManga.await(new.toMangaUpdate()) - } - } - - fun addFavorite(manga: Manga) { - screenModelScope.launch { - val categories = getCategories() - val defaultCategoryId = libraryPreferences.defaultCategory().get() - val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } - - when { - // Default category set - defaultCategory != null -> { - moveMangaToCategories(manga, defaultCategory) - - changeMangaFavorite(manga) - } - - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - moveMangaToCategories(manga) - - changeMangaFavorite(manga) - } - - // Choose a category - else -> { - val preselectedIds = getCategories.await(manga.id).map { it.id } - setDialog( - Dialog.ChangeMangaCategory( - manga, - categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(), - ), - ) - } - } - } - } - - fun setDialog(dialog: Dialog?) { - mutableState.update { it.copy(dialog = dialog) } - } - - sealed interface Dialog { - data class RemoveManga(val manga: Manga) : Dialog - data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog - data class ChangeMangaCategory( - val manga: Manga, - val initialSelection: ImmutableList>, - ) : Dialog - data class ChangeMangasCategory( - val mangas: List, - val initialSelection: ImmutableList>, - ) : Dialog - data class AllowDuplicate(val duplicatedManga: Pair) : Dialog - } - // KMK <-- - @Immutable data class State( val fromSourceId: Long? = null, @@ -509,11 +200,6 @@ abstract class SearchScreenModel( val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, val items: PersistentMap = persistentMapOf(), - // KMK --> - val dialog: Dialog? = null, - val selection: PersistentList = persistentListOf(), - val selectionMode: Boolean = false, - // KMK <-- ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size From df229af0ebdde41e71c8213c40b08d5c52084efc Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Sat, 9 Mar 2024 17:40:42 +0700 Subject: [PATCH 23/36] migrate the rest to BulkSelectionScreenModel --- .../kanade/presentation/browse/FeedScreen.kt | 5 +- .../presentation/browse/GlobalSearchScreen.kt | 16 +- .../browse/MigrateSearchScreen.kt | 16 +- .../presentation/browse/SourceFeedScreen.kt | 45 +++- .../browse/components/BrowseSourceToolbar.kt | 4 +- .../browse/components/GlobalSearchToolbar.kt | 4 +- .../presentation/components/TabbedScreen.kt | 14 +- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 9 +- .../ui/browse/feed/FeedScreenModel.kt | 202 ----------------- .../tachiyomi/ui/browse/feed/FeedTab.kt | 208 ++++++++++-------- .../migration/search/MigrateSearchScreen.kt | 12 +- .../source/browse/BrowseSourceScreen.kt | 64 +++--- .../source/browse/BrowseSourceScreenModel.kt | 182 +-------------- .../ui/browse/source/feed/SourceFeedScreen.kt | 147 +++++++++---- .../source/feed/SourceFeedScreenModel.kt | 203 ----------------- .../source/globalsearch/GlobalSearchScreen.kt | 30 ++- .../tachiyomi/ui/manga/MangaScreenModel.kt | 8 +- 17 files changed, 352 insertions(+), 817 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 0abad3e113..54e1977b16 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -71,6 +71,7 @@ fun FeedScreen( onClickManga: (Manga) -> Unit, // KMK --> onLongClickManga: (Manga) -> Unit, + selection: List, // KMK <-- onRefresh: () -> Unit, getMangaState: @Composable (Manga, CatalogueSource?) -> State, @@ -126,7 +127,7 @@ fun FeedScreen( onClickManga = onClickManga, // KMK --> onLongClickManga = onLongClickManga, - selection = state.selection, + selection = selection, // KMK <-- ) } @@ -239,7 +240,7 @@ fun RadioSelector( options: ImmutableList, selected: Int?, optionStrings: ImmutableList = remember { options.map { it.toString() }.toImmutableList() }, - onSelectOption: (Int) -> Unit /* KMK --> */ = {} /* KMK <-- */, + onSelectOption: (Int) -> Unit/* KMK --> */ = {},/* KMK <-- */ ) { Column(Modifier.verticalScroll(rememberScrollState())) { optionStrings.forEachIndexed { index, option -> diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 2faa80563c..bee20e7d29 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -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 <-- ) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 2823b5c9cd..4714b86b6a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -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 <-- ) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 3ed1449ce4..caf5c15972 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -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, // 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().builder() + .apply { + add( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + ), + ) + } + .build(), + ) persistentListOf( SourceSettingsButton(sourceId), ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index 5981ae015a..b158f18477 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -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 <-- diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index 3dbc87fce8..4b8980f0a4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -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, ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index bda9e86c22..7e44c2d352 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 0ee52b0c28..0302a2d4db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -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 <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index 9bd8dfc4fc..8197def219 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -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()) { private val _events = Channel(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 { - return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } - - private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { - val mangas = state.value.selection - mangas.fastForEachIndexed { index, manga -> - if (index < startIdx) return@fastForEachIndexed - val dup = getDuplicateLibraryManga.await(manga) - if (dup.isEmpty()) return@fastForEachIndexed - return Pair(index, dup.first()) - } - return null - } - - fun removeDuplicateSelectedManga(index: Int) { - mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - list.removeAt(index) - } - state.copy(selection = newSelection) - } - } - - /** - * Bulk update categories of manga using old and new common categories. - * - * @param mangaList the list of manga to move. - * @param addCategories the categories to add for all mangas. - * @param removeCategories the categories to remove in all mangas. - */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { - screenModelScope.launchNonCancellable { - mangaList.fastForEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() - - moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) - } - } - clearSelection() - } - - private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { - moveMangaToCategory(manga.id, categories) - if (manga.favorite) return - - screenModelScope.launchIO { - updateManga.awaitUpdateFavorite(manga.id, true) - } - } - - private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { - screenModelScope.launchIO { - setMangaCategories.await(mangaId, categoryIds) - } - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - suspend fun getCategories(): List { - 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): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas - .map { getCategories.await(it.id).toSet() } - .reduce { set1, set2 -> set1.intersect(set2) } - } - - /** - * Returns the mix (non-common) categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getMixCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } - val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } - return mangaCategories.flatten().distinct().subtract(common) - } - - private fun setDialog(dialog: Dialog?) { - mutableState.update { it.copy(dialog = dialog) } - } - // KMK <-- - sealed class Dialog { data class AddFeed(val options: ImmutableList) : Dialog() data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList) : Dialog() data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() - // KMK --> - data class ChangeMangasCategory( - val mangas: List, - val initialSelection: ImmutableList>, - ) : Dialog() - data class AllowDuplicate(val duplicatedManga: Pair) : Dialog() - // KMK <-- } sealed class Event { @@ -515,9 +320,6 @@ open class FeedScreenModel( data class FeedScreenState( val dialog: FeedScreenModel.Dialog? = null, val items: List? = null, - // KMK --> - val selection: PersistentList = 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 71617924e5..4e0ef47789 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 2096ad42e5..d9a540ab34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -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() @@ -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 <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 99e86b2109..327db09f54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index b97f445de2..5d42384e00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -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 { - return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } - - private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { - val mangas = state.value.selection - mangas.fastForEachIndexed { index, manga -> - if (index < startIdx) return@fastForEachIndexed - val dup = getDuplicateLibraryManga.await(manga) - if (dup.isEmpty()) return@fastForEachIndexed - return Pair(index, dup.first()) - } - return null - } - - fun removeDuplicateSelectedManga(index: Int) { - mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - list.removeAt(index) - } - state.copy(selection = newSelection) - } - } - - /** - * Bulk update categories of manga using old and new common categories. - * - * @param mangaList the list of manga to move. - * @param addCategories the categories to add for all mangas. - * @param removeCategories the categories to remove in all mangas. - */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { - screenModelScope.launchNonCancellable { - mangaList.fastForEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() - - moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) - } - } - toggleSelectionMode() - } - - private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { - moveMangaToCategory(manga.id, categories) - if (manga.favorite) return - - screenModelScope.launchIO { - updateManga.awaitUpdateFavorite(manga.id, true) - } - } - - private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { - screenModelScope.launchIO { - setMangaCategories.await(mangaId, categoryIds) - } - } - - /** - * Returns the common categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getCommonCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas - .map { getCategories.await(it.id).toSet() } - .reduce { set1, set2 -> set1.intersect(set2) } - } - - /** - * Returns the mix (non-common) categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getMixCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } - val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } - return mangaCategories.flatten().distinct().subtract(common) - } - // 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) : Dialog // SY <-- - // KMK --> - data class ChangeMangasCategory( - val mangas: List, - val initialSelection: ImmutableList>, - ) : Dialog - data class AllowDuplicate(val duplicatedManga: Pair) : Dialog - // KMK <-- } @Immutable @@ -649,10 +473,6 @@ open class BrowseSourceScreenModel( val savedSearches: ImmutableList = persistentListOf(), val filterable: Boolean = true, // SY <-- - // KMK --> - val selection: PersistentList = persistentListOf(), - val selectionMode: Boolean = false, - // KMK <-- ) { val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 33fa5c3eec..f71714ec44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 6f7e8b7011..32eeded6dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -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()) { 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 { - return state.value.selection.filterNot { manga -> - getDuplicateLibraryManga.await(manga).isNotEmpty() - } - } - - private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair? { - val mangas = state.value.selection - mangas.fastForEachIndexed { index, manga -> - if (index < startIdx) return@fastForEachIndexed - val dup = getDuplicateLibraryManga.await(manga) - if (dup.isEmpty()) return@fastForEachIndexed - return Pair(index, dup.first()) - } - return null - } - - fun removeDuplicateSelectedManga(index: Int) { - mutableState.update { state -> - val newSelection = state.selection.mutate { list -> - list.removeAt(index) - } - state.copy(selection = newSelection) - } - } - - /** - * Bulk update categories of manga using old and new common categories. - * - * @param mangaList the list of manga to move. - * @param addCategories the categories to add for all mangas. - * @param removeCategories the categories to remove in all mangas. - */ - fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { - screenModelScope.launchNonCancellable { - mangaList.fastForEach { manga -> - val categoryIds = getCategories.await(manga.id) - .map { it.id } - .subtract(removeCategories.toSet()) - .plus(addCategories) - .toList() - - moveMangaToCategoriesAndAddToLibrary(manga, categoryIds) - } - } - clearSelection() - } - - private fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List) { - moveMangaToCategory(manga.id, categories) - if (manga.favorite) return - - screenModelScope.launchIO { - updateManga.awaitUpdateFavorite(manga.id, true) - } - } - - private fun moveMangaToCategory(mangaId: Long, categoryIds: List) { - screenModelScope.launchIO { - setMangaCategories.await(mangaId, categoryIds) - } - } - - /** - * Get user categories. - * - * @return List of categories, not including the default category - */ - suspend fun getCategories(): List { - 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): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas - .map { getCategories.await(it.id).toSet() } - .reduce { set1, set2 -> set1.intersect(set2) } - } - - /** - * Returns the mix (non-common) categories for the given list of manga. - * - * @param mangas the list of manga. - */ - private suspend fun getMixCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } - val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } - return mangaCategories.flatten().distinct().subtract(common) - } - - 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, - val initialSelection: ImmutableList>, - ) : Dialog() - data class AllowDuplicate(val duplicatedManga: Pair) : Dialog() - // KMK <-- } override fun onDispose() { @@ -523,14 +327,7 @@ data class SourceFeedState( val filters: FilterList = FilterList(), val savedSearches: ImmutableList = persistentListOf(), val dialog: SourceFeedScreenModel.Dialog? = null, - // KMK --> - val selection: PersistentList = persistentListOf(), - // KMK <-- ) { val isLoading get() = items.isEmpty() - - // KMK --> - val selectionMode = selection.isNotEmpty() - // KMK <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index 96bf82ba69..a59b66ec46 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -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() }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index c6bbb429ed..33895fb5aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -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 <-- ) } From a7c6232a502d145bd830bc5c49901e2c66e4f632 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Sat, 9 Mar 2024 18:26:58 +0700 Subject: [PATCH 24/36] =?UTF-8?q?add=20BulkSelection=20to=20the=20rest=20o?= =?UTF-8?q?f=20Migrate,=20MangaDex=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/browse/BrowseSourceScreen.kt | 2 +- .../components/BrowseSourceSimpleToolbar.kt | 11 ++ .../migration/search/MigrateSearchScreen.kt | 27 +-- .../migration/search/SourceSearchScreen.kt | 114 ++++++++++++- .../exh/md/follows/MangaDexFollowsScreen.kt | 124 +++++++++++--- .../exh/md/similar/MangaDexSimilarScreen.kt | 155 ++++++++++++++++- .../main/java/exh/recs/RecommendsScreen.kt | 156 +++++++++++++++++- 7 files changed, 529 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index a626dd5c39..5494801d18 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -59,7 +59,7 @@ fun BrowseSourceContent( onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, // KMK --> - selection: List? = null, + selection: List, // KMK <-- ) { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt index 341587fc5b..17d59bbc87 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt @@ -2,6 +2,7 @@ package eu.kanade.presentation.browse.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.ViewModule import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -27,6 +28,9 @@ fun BrowseSourceSimpleToolbar( displayMode: LibraryDisplayMode?, onDisplayModeChange: (LibraryDisplayMode) -> Unit, scrollBehavior: TopAppBarScrollBehavior, + // KMK --> + toggleSelectionMode: () -> Unit, + // KMK <-- ) { AppBar( navigateUp = navigateUp, @@ -41,6 +45,13 @@ fun BrowseSourceSimpleToolbar( icon = Icons.Outlined.ViewModule, onClick = { selectingDisplayMode = true }, ), + // KMK --> + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + ), + // KMK <-- ), ) DropdownMenu( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index d9a540ab34..1a47d1f713 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -53,18 +53,19 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L }, onClickItem = { // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.toggleSelection(it) - else { + } else // KMK <-- - // SY --> - navigator.items - .filterIsInstance() - .last() - .newSelectedItem = mangaId to it.id - navigator.popUntil { it is MigrationListScreen } - // SY <-- - } + { + // SY --> + navigator.items + .filterIsInstance() + .last() + .newSelectedItem = mangaId to it.id + navigator.popUntil { it is MigrationListScreen } + // SY <-- + } }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, // KMK --> @@ -73,12 +74,12 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L ) // KMK --> - val onDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } + 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 -> bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude) @@ -87,7 +88,7 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L } is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( - onDismissRequest = onDismissRequest, + onDismissRequest = onBulkDismissRequest, onAllowAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate() }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 1537b2daba..3572e295ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -1,5 +1,8 @@ package eu.kanade.tachiyomi.ui.browse.migration.search +import androidx.activity.compose.BackHandler +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -14,18 +17,27 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton +import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar +import eu.kanade.presentation.components.SelectionToolbar +import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog +import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen import kotlinx.collections.immutable.persistentListOf import tachiyomi.core.common.Constants import tachiyomi.domain.manga.model.Manga +import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.source.local.LocalSource data class SourceSearchScreen( @@ -44,15 +56,51 @@ data class SourceSearchScreen( val snackbarHostState = remember { SnackbarHostState() } + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + Scaffold( topBar = { scrollBehavior -> - SearchToolbar( - searchQuery = state.toolbarQuery ?: "", - onChangeSearchQuery = screenModel::setToolbarQuery, - onClickCloseSearch = navigator::pop, - onSearch = screenModel::search, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } else { + // KMK <-- + SearchToolbar( + searchQuery = state.toolbarQuery ?: "", + onChangeSearchQuery = screenModel::setToolbarQuery, + onClickCloseSearch = navigator::pop, + onSearch = screenModel::search, + scrollBehavior = scrollBehavior, + // KMK --> + actions = { + AppBarActions( + actions = persistentListOf().builder() + .apply { + add( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = bulkFavoriteScreenModel::toggleSelectionMode, + ), + ) + } + .build(), + ) + }, + // KMK <-- + ) + } }, floatingActionButton = { // SY --> @@ -96,8 +144,19 @@ data class SourceSearchScreen( }, onHelpClick = { uriHandler.openUri(Constants.URL_HELP) }, onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }, - onMangaClick = openMigrateDialog, + onMangaClick = { manga -> + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(manga) + } else { + // KMK <-- + openMigrateDialog(manga) + } + }, onMangaLongClick = { navigator.push(MangaScreen(it.id, true)) }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } @@ -123,5 +182,44 @@ data class SourceSearchScreen( } else -> {} } + + // KMK --> + val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } + when (val dialog = bulkFavoriteState.dialog) { + 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 <-- } } diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 6fbf6c376d..a643f63660 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -1,5 +1,6 @@ package exh.md.follows +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -18,8 +19,11 @@ import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.SelectionToolbar +import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen @@ -38,17 +42,39 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { val screenModel = rememberScreenModel { MangaDexFollowsScreenModel(sourceId) } val state by screenModel.state.collectAsState() + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { scrollBehavior -> - BrowseSourceSimpleToolbar( - title = stringResource(SYMR.strings.mangadex_follows), - displayMode = screenModel.displayMode, - onDisplayModeChange = { screenModel.displayMode = it }, - navigateUp = navigator::pop, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } else { + // KMK <-- + BrowseSourceSimpleToolbar( + title = stringResource(SYMR.strings.mangadex_follows), + displayMode = screenModel.displayMode, + onDisplayModeChange = { screenModel.displayMode = it }, + navigateUp = navigator::pop, + scrollBehavior = scrollBehavior, + // KMK --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) @@ -69,23 +95,42 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { onWebViewClick = null, onHelpClick = null, onLocalSourceHelpClick = null, - onMangaClick = { navigator.push(MangaScreen(it.id, true)) }, + onMangaClick = { + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(it) + } else { + // KMK <-- + navigator.push(MangaScreen(it.id, true)) + } + }, onMangaLongClick = { manga -> - scope.launchIO { - val duplicateManga = screenModel.getDuplicateLibraryManga(manga) - when { - manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) - duplicateManga != null -> screenModel.setDialog( - BrowseSourceScreenModel.Dialog.AddDuplicateManga( - manga, - duplicateManga, - ), - ) - else -> screenModel.addFavorite(manga) + // KMK --> + if (bulkFavoriteState.selectionMode) { + navigator.push(MangaScreen(manga.id, true)) + } else { + // KMK <-- + scope.launchIO { + val duplicateManga = screenModel.getDuplicateLibraryManga(manga) + when { + manga.favorite -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.RemoveManga(manga) + ) + duplicateManga != null -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.AddDuplicateManga( + manga, + duplicateManga, + ), + ) + else -> screenModel.addFavorite(manga) + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } @@ -123,5 +168,44 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { } else -> {} } + + // KMK --> + val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } + when (val dialog = bulkFavoriteState.dialog) { + 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 <-- } } diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 62394333b5..0980f52577 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -1,20 +1,32 @@ package exh.md.similar +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar +import eu.kanade.presentation.browse.components.RemoveMangaDialog +import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.SelectionToolbar +import eu.kanade.presentation.manga.AllowDuplicateDialog +import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold @@ -28,6 +40,18 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + val scope = rememberCoroutineScope() + val haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + val onMangaClick: (Manga) -> Unit = { navigator.push(MangaScreen(it.id, true)) } @@ -36,13 +60,26 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { Scaffold( topBar = { scrollBehavior -> - BrowseSourceSimpleToolbar( - navigateUp = navigator::pop, - title = stringResource(SYMR.strings.similar, screenModel.manga.title), - displayMode = screenModel.displayMode, - onDisplayModeChange = { screenModel.displayMode = it }, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } else { + // KMK <-- + BrowseSourceSimpleToolbar( + navigateUp = navigator::pop, + title = stringResource(SYMR.strings.similar, screenModel.manga.title), + displayMode = screenModel.displayMode, + onDisplayModeChange = { screenModel.displayMode = it }, + scrollBehavior = scrollBehavior, + // KMK --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { paddingValues -> @@ -61,9 +98,109 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { onWebViewClick = null, onHelpClick = null, onLocalSourceHelpClick = null, - onMangaClick = onMangaClick, - onMangaLongClick = onMangaClick, + onMangaClick = { manga -> + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(manga) + } else { + // KMK <-- + onMangaClick(manga) + } + }, + onMangaLongClick = { manga -> + // KMK --> + 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 { + // KMK <-- + onMangaClick(manga) + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // 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 <-- } } diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index 39cec8033b..bfd1b7aad2 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -1,12 +1,16 @@ package exh.recs +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -14,8 +18,17 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar +import eu.kanade.presentation.browse.components.RemoveMangaDialog +import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.SelectionToolbar +import eu.kanade.presentation.manga.AllowDuplicateDialog +import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen +import eu.kanade.tachiyomi.ui.category.CategoryScreen +import eu.kanade.tachiyomi.ui.manga.MangaScreen +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @@ -27,6 +40,18 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + val scope = rememberCoroutineScope() + val haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + val onMangaClick: (Manga) -> Unit = { manga -> openSmartSearch(navigator, manga.ogTitle) } @@ -35,13 +60,26 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { Scaffold( topBar = { scrollBehavior -> - BrowseSourceSimpleToolbar( - navigateUp = navigator::pop, - title = screenModel.manga.title, - displayMode = screenModel.displayMode, - onDisplayModeChange = { screenModel.displayMode = it }, - scrollBehavior = scrollBehavior, - ) + // KMK --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } else { + // KMK <-- + BrowseSourceSimpleToolbar( + navigateUp = navigator::pop, + title = screenModel.manga.title, + displayMode = screenModel.displayMode, + onDisplayModeChange = { screenModel.displayMode = it }, + scrollBehavior = scrollBehavior, + // KMK --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { paddingValues -> @@ -60,10 +98,110 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { onWebViewClick = null, onHelpClick = null, onLocalSourceHelpClick = null, - onMangaClick = onMangaClick, - onMangaLongClick = onMangaClick, + onMangaClick = { manga -> + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(manga) + } else { + // KMK <-- + onMangaClick(manga) + } + }, + onMangaLongClick = { manga -> + // KMK --> + 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 { + // KMK <-- + onMangaClick(manga) + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // 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 <-- } private fun openSmartSearch(navigator: Navigator, title: String) { From 89a5f2e702d4aa52b87b20cdef28ea6d41fa90f1 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sat, 9 Mar 2024 23:58:46 +0700 Subject: [PATCH 25/36] no need using builder to create AppBarAction --- .../presentation/browse/SourceFeedScreen.kt | 18 +++++++----------- .../browse/components/GlobalSearchToolbar.kt | 18 +++++++----------- .../browse/components/SourceSettingsButton.kt | 7 +++---- .../migration/search/SourceSearchScreen.kt | 18 +++++++----------- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index caf5c15972..b7493ee8f2 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -280,17 +280,13 @@ fun SourceFeedToolbar( // KMK --> actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), - ) - } - .build(), + actions = persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + ), + ) ) persistentListOf( SourceSettingsButton(sourceId), diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index 4b8980f0a4..295ee04ff0 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -65,17 +65,13 @@ fun GlobalSearchToolbar( // KMK --> actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), - ) - } - .build(), + actions = persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + ), + ) ) }, // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/SourceSettingsButton.kt b/app/src/main/java/eu/kanade/presentation/browse/components/SourceSettingsButton.kt index bed5bddac9..79f0482629 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/SourceSettingsButton.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/SourceSettingsButton.kt @@ -17,14 +17,13 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.source.local.LocalSource -// KMK --> @Composable fun SourceSettingsButton( id: Long, @Suppress("UNUSED_PARAMETER") modifier: Modifier = Modifier ) { // Create a fake source - val source = Source(id,"", "", supportsLatest = false, isStub = false) + val source = Source(id, "", "", supportsLatest = false, isStub = false) SourceSettingsButton(source = source) } @@ -38,8 +37,9 @@ fun SourceSettingsButton( val navigator = LocalNavigator.currentOrThrow IconButton(onClick = { - if (source.installedExtension !== null) + if (source.installedExtension !== null) { navigator.push(ExtensionDetailsScreen(source.installedExtension!!.pkgName)) + } }) { Icon( imageVector = Icons.Outlined.Settings, @@ -47,4 +47,3 @@ fun SourceSettingsButton( ) } } -// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 3572e295ac..58b17ce7ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -85,17 +85,13 @@ data class SourceSearchScreen( // KMK --> actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = bulkFavoriteScreenModel::toggleSelectionMode, - ), - ) - } - .build(), + actions = persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = bulkFavoriteScreenModel::toggleSelectionMode, + ), + ) ) }, // KMK <-- From cd016ab29e9e140beaf388068461771e7377058a Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 00:07:18 +0700 Subject: [PATCH 26/36] =?UTF-8?q?fix:=20won=E2=80=99t=20show=20dialog=20if?= =?UTF-8?q?=20skip=20all=20then=20selection=20list=20is=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index c9f195d133..d896849d17 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -92,6 +92,10 @@ class BulkFavoriteScreenModel( fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) { screenModelScope.launch { val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection + if (mangaList.isEmpty()) { + toggleSelectionMode() + return@launch + } val categories = getCategories() val defaultCategoryId = libraryPreferences.defaultCategory().get() val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } From 55226e3016430c4010797ae6c40fec9415e9cc20 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 07:20:58 +0700 Subject: [PATCH 27/36] fix detekt --- .../presentation/browse/GlobalSearchScreen.kt | 7 ++-- .../browse/MigrateSearchScreen.kt | 9 ++--- .../presentation/browse/SourceFeedScreen.kt | 7 ++-- .../components/SelectionToolbar.kt | 3 +- .../presentation/components/TabbedScreen.kt | 5 +-- .../library/components/CommonMangaItem.kt | 1 + .../manga/DuplicateMangaDialog.kt | 1 - .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 4 +-- .../ui/browse/BulkFavoriteScreenModel.kt | 16 ++++----- .../tachiyomi/ui/browse/feed/FeedTab.kt | 20 ++++++----- .../migration/search/MigrateSearchScreen.kt | 4 +-- .../migration/search/SourceSearchScreen.kt | 4 +-- .../source/browse/BrowseSourceScreen.kt | 33 ++++++++++--------- .../ui/browse/source/feed/SourceFeedScreen.kt | 27 ++++++++------- .../source/globalsearch/GlobalSearchScreen.kt | 22 +++++++------ .../exh/md/follows/MangaDexFollowsScreen.kt | 4 +-- .../exh/md/similar/MangaDexSimilarScreen.kt | 4 +-- .../main/java/exh/recs/RecommendsScreen.kt | 4 +-- 18 files changed, 90 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index bee20e7d29..69303afd70 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -47,14 +47,14 @@ fun GlobalSearchScreen( Scaffold( topBar = { scrollBehavior -> // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { SelectionToolbar( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, ) - else - // KMK <-- + } else { + // KMK <-- GlobalSearchToolbar( searchQuery = state.searchQuery, progress = state.progress, @@ -71,6 +71,7 @@ fun GlobalSearchScreen( toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, // KMK <-- ) + } }, ) { paddingValues -> GlobalSearchContent( diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 4714b86b6a..7817189723 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -37,14 +37,14 @@ fun MigrateSearchScreen( Scaffold( topBar = { scrollBehavior -> // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { SelectionToolbar( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, ) - else - // KMK <-- + } else { + // KMK <-- GlobalSearchToolbar( searchQuery = state.searchQuery, progress = state.progress, @@ -58,9 +58,10 @@ fun MigrateSearchScreen( onToggleResults = onToggleResults, scrollBehavior = scrollBehavior, // KMK --> - toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, // KMK <-- ) + } }, ) { paddingValues -> GlobalSearchContent( diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index b7493ee8f2..8fc3dbed0e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -119,14 +119,14 @@ fun SourceFeedScreen( Scaffold( topBar = { scrollBehavior -> // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { SelectionToolbar( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, ) - else - // KMK <-- + } else { + // KMK <-- SourceFeedToolbar( title = name, searchQuery = searchQuery, @@ -138,6 +138,7 @@ fun SourceFeedScreen( toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, // KMK <-- ) + } }, floatingActionButton = { BrowseSourceFloatingActionButton( diff --git a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt index 271c5aea75..ae25a406ad 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt @@ -24,8 +24,9 @@ fun SelectionToolbar( title = stringResource(MR.strings.action_bookmark), icon = Icons.Filled.BookmarkAdd, onClick = { - if (selectedCount > 0) + if (selectedCount > 0) { onChangeCategoryClicked() + } }, ), ), diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 7e44c2d352..4ed9a519da 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -62,13 +62,13 @@ fun TabbedScreen( val tab = tabs[state.currentPage] val searchEnabled = tab.searchEnabled // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { SelectionToolbar( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, ) - else + } else { // KMK <-- SearchToolbar( titleContent = { AppBarTitle(stringResource(titleRes)) }, @@ -77,6 +77,7 @@ fun TabbedScreen( onChangeSearchQuery = onChangeSearchQuery, actions = { AppBarActions(tab.actions) }, ) + } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt index 07d01e0d04..46c9506355 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt @@ -48,6 +48,7 @@ object CommonMangaItemDefaults { val GridVerticalSpacer = 4.dp const val BrowseFavoriteCoverAlpha = 0.34f + // KMK --> const val BrowseSelectedCoverAlpha = 0.17f // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 7da55ef69d..23d877c01b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -149,7 +149,6 @@ fun AllowDuplicateDialog( Text(text = stringResource(MR.strings.action_cancel)) } } - } }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 0302a2d4db..e256e116f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -80,7 +80,7 @@ data class BrowseTab( ) } else if (feedTabInFront) { persistentListOf( - feedTab(/* KMK --> */bulkFavoriteScreenModel/* KMK <-- */), + feedTab(bulkFavoriteScreenModel), sourcesTab(), extensionsTab(extensionsScreenModel), migrateSourceTab(), @@ -88,7 +88,7 @@ data class BrowseTab( } else { persistentListOf( sourcesTab(), - feedTab(/* KMK --> */bulkFavoriteScreenModel/* KMK <-- */), + feedTab(bulkFavoriteScreenModel), extensionsTab(extensionsScreenModel), migrateSourceTab(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index d896849d17..913e35d7e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -35,7 +35,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.Instant - class BulkFavoriteScreenModel( initialState: State = State(), private val sourceManager: SourceManager = Injekt.get(), @@ -54,8 +53,9 @@ class BulkFavoriteScreenModel( } fun toggleSelectionMode() { - if (state.value.selectionMode) + if (state.value.selectionMode) { clearSelection() + } mutableState.update { it.copy(selectionMode = !it.selectionMode) } } @@ -74,18 +74,20 @@ class BulkFavoriteScreenModel( } state.copy(selection = newSelection) }.also { - if (state.value.selection.isEmpty()) + if (state.value.selection.isEmpty()) { toggleSelectionMode() + } } } fun addFavorite(startIdx: Int = 0) { screenModelScope.launch { val mangaWithDup = getDuplicateLibraryManga(startIdx) - if (mangaWithDup != null) + if (mangaWithDup != null) { setDialog(Dialog.AllowDuplicate(mangaWithDup)) - else + } else { addFavoriteDuplicate() + } } } @@ -286,14 +288,12 @@ class BulkFavoriteScreenModel( // Default category set defaultCategory != null -> { moveMangaToCategories(manga, defaultCategory) - changeMangaFavorite(manga) } // Automatic 'Default' or no categories defaultCategoryId == 0 || categories.isEmpty() -> { moveMangaToCategories(manga) - changeMangaFavorite(manga) } @@ -332,7 +332,7 @@ class BulkFavoriteScreenModel( } @Immutable - data class State ( + data class State( val dialog: Dialog? = null, val selection: PersistentList = persistentListOf(), val selectionMode: Boolean = false, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 4e0ef47789..2351940748 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -129,19 +129,22 @@ fun Screen.feedTab( onClickDelete = screenModel::openDeleteDialog, onClickManga = { manga -> // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.toggleSelection(manga) - else - // KMK <-- + } else { + // KMK <-- navigator.push(MangaScreen(manga.id, true)) + } }, // KMK --> onLongClickManga = { manga -> - if (!bulkFavoriteState.selectionMode) + if (!bulkFavoriteState.selectionMode) { scope.launchIO { val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga)) + manga.favorite -> bulkFavoriteScreenModel.setDialog( + BulkFavoriteScreenModel.Dialog.RemoveManga(manga) + ) duplicateManga != null -> bulkFavoriteScreenModel.setDialog( BulkFavoriteScreenModel.Dialog.AddDuplicateManga( manga, @@ -152,8 +155,9 @@ fun Screen.feedTab( } haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - else + } else { navigator.push(MangaScreen(manga.id, true)) + } }, selection = bulkFavoriteState.selection, // KMK <-- @@ -242,9 +246,7 @@ fun Screen.feedTab( is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 1a47d1f713..75067d6bad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -89,9 +89,7 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 58b17ce7ab..7768413432 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -195,9 +195,7 @@ data class SourceSearchScreen( is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 327db09f54..5b39a96434 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -154,14 +154,14 @@ data class BrowseSourceScreen( topBar = { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { SelectionToolbar( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, ) - else - // KMK <-- + } else { + // KMK <-- BrowseSourceToolbar( searchQuery = state.toolbarQuery, onSearchQueryChange = screenModel::setToolbarQuery, @@ -174,9 +174,10 @@ data class BrowseSourceScreen( onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) }, onSearch = screenModel::search, // KMK --> - toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, // KMK <-- ) + } Row( modifier = Modifier @@ -271,22 +272,25 @@ data class BrowseSourceScreen( onLocalSourceHelpClick = onHelpClick, onMangaClick = { // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.toggleSelection(it) - else - // KMK <-- + } else { + // KMK <-- navigator.push(MangaScreen(it.id, true, smartSearchConfig)) + } }, onMangaLongClick = { manga -> // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { navigator.push(MangaScreen(manga.id, true)) - else - // KMK <-- + } else { + // KMK <-- scope.launchIO { val duplicateManga = screenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) + manga.favorite -> screenModel.setDialog( + BrowseSourceScreenModel.Dialog.RemoveManga(manga) + ) duplicateManga != null -> screenModel.setDialog( BrowseSourceScreenModel.Dialog.AddDuplicateManga( manga, @@ -296,7 +300,8 @@ data class BrowseSourceScreen( else -> screenModel.addFavorite(manga) } haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } + } + } }, // KMK --> selection = bulkFavoriteState.selection, @@ -405,9 +410,7 @@ data class BrowseSourceScreen( is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index f71714ec44..232597f910 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -63,11 +63,12 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { onClickDelete = screenModel::openDeleteFeed, onClickManga = { // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.toggleSelection(it) - else - // KMK <-- + } else { + // KMK <-- onMangaClick(navigator, it) + } }, onClickSearch = { onSearchClick(navigator, screenModel.source, it) }, searchQuery = state.searchQuery, @@ -76,11 +77,13 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { // KMK --> sourceId = screenModel.source.id, onLongClickManga = { manga -> - if (!bulkFavoriteState.selectionMode) + if (!bulkFavoriteState.selectionMode) { scope.launchIO { val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga)) + manga.favorite -> bulkFavoriteScreenModel.setDialog( + BulkFavoriteScreenModel.Dialog.RemoveManga(manga) + ) duplicateManga != null -> bulkFavoriteScreenModel.setDialog( BulkFavoriteScreenModel.Dialog.AddDuplicateManga( manga, @@ -91,8 +94,9 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { } haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - else + } else { navigator.push(MangaScreen(manga.id, true)) + } }, bulkFavoriteScreenModel = bulkFavoriteScreenModel, // KMK <-- @@ -228,9 +232,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, @@ -251,13 +253,14 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { } // KMK <-- - BackHandler(state.searchQuery != null/* KMK --> */ || bulkFavoriteState.selectionMode /* KMK <-- */) { + BackHandler(state.searchQuery != null || bulkFavoriteState.selectionMode) { // KMK --> - if(bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.backHandler() - else + } else { // KMK <-- screenModel.search(null) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index a59b66ec46..d1f51a4a42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -91,19 +91,22 @@ class GlobalSearchScreen( }, onClickItem = { // KMK --> - if (bulkFavoriteState.selectionMode) + if (bulkFavoriteState.selectionMode) { bulkFavoriteScreenModel.toggleSelection(it) - else - // KMK <-- + } else { + // KMK <-- navigator.push(MangaScreen(it.id, true)) + } }, onLongClickItem = { manga -> // KMK --> - if (!bulkFavoriteState.selectionMode) + if (!bulkFavoriteState.selectionMode) { scope.launchIO { val duplicateManga = bulkFavoriteScreenModel.getDuplicateLibraryManga(manga) when { - manga.favorite -> bulkFavoriteScreenModel.setDialog(BulkFavoriteScreenModel.Dialog.RemoveManga(manga)) + manga.favorite -> bulkFavoriteScreenModel.setDialog( + BulkFavoriteScreenModel.Dialog.RemoveManga(manga) + ) duplicateManga != null -> bulkFavoriteScreenModel.setDialog( BulkFavoriteScreenModel.Dialog.AddDuplicateManga( manga, @@ -114,9 +117,10 @@ class GlobalSearchScreen( } haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - else - // KMK <-- + } else { + // KMK <-- navigator.push(MangaScreen(manga.id, true)) + } }, // KMK --> bulkFavoriteScreenModel = bulkFavoriteScreenModel, @@ -167,9 +171,7 @@ class GlobalSearchScreen( is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index a643f63660..397fb2aff7 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -185,9 +185,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 0980f52577..8f998bfee1 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -180,9 +180,7 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index bfd1b7aad2..55e40be9af 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -180,9 +180,7 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> { AllowDuplicateDialog( onDismissRequest = onBulkDismissRequest, - onAllowAllDuplicate = { - bulkFavoriteScreenModel.addFavoriteDuplicate() - }, + onAllowAllDuplicate = bulkFavoriteScreenModel::addFavoriteDuplicate, onSkipAllDuplicate = { bulkFavoriteScreenModel.addFavoriteDuplicate(skipAllDuplicates = true) }, From 722ebe757e866b88d51447a4834b614e07a5df0e Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 13:26:46 +0700 Subject: [PATCH 28/36] reuse Dialog composer --- .../manga/DuplicateMangaDialog.kt | 2 +- .../ui/browse/BulkFavoriteScreenModel.kt | 103 ++++++++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 80 +++----------- .../migration/search/MigrateSearchScreen.kt | 42 ++----- .../migration/search/SourceSearchScreen.kt | 42 ++----- .../source/browse/BrowseSourceScreen.kt | 40 ++----- .../ui/browse/source/feed/SourceFeedScreen.kt | 80 +++----------- .../source/globalsearch/GlobalSearchScreen.kt | 80 +++----------- .../exh/md/follows/MangaDexFollowsScreen.kt | 40 ++----- .../exh/md/similar/MangaDexSimilarScreen.kt | 80 +++----------- .../main/java/exh/recs/RecommendsScreen.kt | 81 +++----------- 11 files changed, 212 insertions(+), 458 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 23d877c01b..1a9e67fb26 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -63,7 +63,7 @@ fun DuplicateMangaDialog( // KMK --> @Composable -fun AllowDuplicateDialog( +fun DuplicateMangasDialog( onDismissRequest: () -> Unit, onAllowAllDuplicate: () -> Unit, onSkipAllDuplicate: () -> Unit, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 913e35d7e3..87e5039226 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -1,14 +1,25 @@ package eu.kanade.tachiyomi.ui.browse +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue 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 cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.track.interactor.AddTracks +import eu.kanade.presentation.browse.components.RemoveMangaDialog +import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.manga.DuplicateMangaDialog +import eu.kanade.presentation.manga.DuplicateMangasDialog import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.ui.category.CategoryScreen +import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.removeCovers import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -317,6 +328,12 @@ class BulkFavoriteScreenModel( } } + fun dismissDialog() { + mutableState.update { + it.copy(dialog = null) + } + } + interface Dialog { data class RemoveManga(val manga: Manga) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog @@ -338,3 +355,89 @@ class BulkFavoriteScreenModel( val selectionMode: Boolean = false, ) } + +@Composable +fun AddDuplicateMangaDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { + val navigator = LocalNavigator.currentOrThrow + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.AddDuplicateManga + + DuplicateMangaDialog( + onDismissRequest = bulkFavoriteScreenModel::dismissDialog, + onConfirm = { bulkFavoriteScreenModel.addFavorite(dialog.manga) }, + onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, + ) +} + +@Composable +fun RemoveMangaDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.RemoveManga + + RemoveMangaDialog( + onDismissRequest = bulkFavoriteScreenModel::dismissDialog, + onConfirm = { + bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga) + }, + mangaToRemove = dialog.manga, + ) +} + +@Composable +fun ChangeMangaCategoryDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { + val navigator = LocalNavigator.currentOrThrow + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.ChangeMangaCategory + + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = bulkFavoriteScreenModel::dismissDialog, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, _ -> + bulkFavoriteScreenModel.changeMangaFavorite(dialog.manga) + bulkFavoriteScreenModel.moveMangaToCategories(dialog.manga, include) + }, + ) +} + +@Composable +fun ChangeMangasCategoryDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { + val navigator = LocalNavigator.currentOrThrow + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.ChangeMangasCategory + + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = bulkFavoriteScreenModel::dismissDialog, + onEditCategories = { navigator.push(CategoryScreen()) }, + onConfirm = { include, exclude -> + bulkFavoriteScreenModel.setMangasCategories(dialog.mangas, include, exclude) + }, + ) +} + +@Composable +fun AllowDuplicateDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { + val navigator = LocalNavigator.currentOrThrow + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + val dialog = bulkFavoriteState.dialog as BulkFavoriteScreenModel.Dialog.AllowDuplicate + + DuplicateMangasDialog( + onDismissRequest = bulkFavoriteScreenModel::dismissDialog, + 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, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 2351940748..b34a990556 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -21,15 +21,15 @@ 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.AddDuplicateMangaDialog +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen -import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import kotlinx.collections.immutable.persistentListOf @@ -204,65 +204,17 @@ fun Screen.feedTab( } // 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> + AddDuplicateMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.RemoveManga -> + RemoveMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> + ChangeMangaCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 75067d6bad..042e30ecc0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -8,12 +8,11 @@ import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.MigrateSearchScreen -import eu.kanade.presentation.category.components.ChangeCategoryDialog -import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen -import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen class MigrateSearchScreen(private val mangaId: Long, private val validSources: List) : Screen() { @@ -74,38 +73,11 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L ) // KMK --> - val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } - when (val dialog = bulkFavoriteState.dialog) { - 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 7768413432..edbbf14a9e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -17,19 +17,18 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton -import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SelectionToolbar -import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog -import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen import kotlinx.collections.immutable.persistentListOf @@ -180,38 +179,11 @@ data class SourceSearchScreen( } // KMK --> - val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } - when (val dialog = bulkFavoriteState.dialog) { - 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 5b39a96434..0ed54e43a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -45,13 +45,14 @@ import eu.kanade.presentation.browse.components.SavedSearchCreateDialog import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.SelectionToolbar -import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.DuplicateMangaDialog 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.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog 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 @@ -395,38 +396,11 @@ data class BrowseSourceScreen( } // KMK --> - val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } - when (val dialog = bulkFavoriteState.dialog) { - 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 232597f910..5e82e3b085 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -13,18 +13,18 @@ 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.AddDuplicateMangaDialog +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog 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 import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.system.toast import exh.md.follows.MangaDexFollowsScreen @@ -190,65 +190,17 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { } // 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> + AddDuplicateMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.RemoveManga -> + RemoveMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> + ChangeMangaCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index d1f51a4a42..0dae993bca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -15,14 +15,14 @@ import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.GlobalSearchScreen -import eu.kanade.presentation.browse.components.RemoveMangaDialog -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.ui.browse.AddDuplicateMangaDialog +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen -import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import tachiyomi.core.common.util.lang.launchIO import tachiyomi.presentation.core.screens.LoadingScreen @@ -129,65 +129,17 @@ class GlobalSearchScreen( } // 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> + AddDuplicateMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.RemoveManga -> + RemoveMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> + ChangeMangaCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 397fb2aff7..ed30d39194 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -20,10 +20,11 @@ import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.SelectionToolbar -import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen @@ -170,38 +171,11 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { } // KMK --> - val onBulkDismissRequest = { bulkFavoriteScreenModel.setDialog(null) } - when (val dialog = bulkFavoriteState.dialog) { - 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 8f998bfee1..267147806c 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -17,14 +17,14 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar -import eu.kanade.presentation.browse.components.RemoveMangaDialog -import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.SelectionToolbar -import eu.kanade.presentation.manga.AllowDuplicateDialog -import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.AddDuplicateMangaDialog +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel -import eu.kanade.tachiyomi.ui.category.CategoryScreen +import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.manga.MangaScreen import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga @@ -138,65 +138,17 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { } // 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> + AddDuplicateMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.RemoveManga -> + RemoveMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> + ChangeMangaCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index 55e40be9af..2a44db298e 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -18,16 +18,15 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar -import eu.kanade.presentation.browse.components.RemoveMangaDialog -import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.components.SelectionToolbar -import eu.kanade.presentation.manga.AllowDuplicateDialog -import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.ui.browse.AddDuplicateMangaDialog +import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog +import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen -import eu.kanade.tachiyomi.ui.category.CategoryScreen -import eu.kanade.tachiyomi.ui.manga.MangaScreen import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @@ -138,65 +137,17 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { } // 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, - ) - } + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> + AddDuplicateMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.RemoveManga -> + RemoveMangaDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> + ChangeMangaCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) else -> {} } // KMK <-- From 56cabe3c22713b8a83af1b27c99b4d62311fd359 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 13:58:14 +0700 Subject: [PATCH 29/36] reuse bulkSelectionButton as AppBar.Action --- .../presentation/browse/SourceFeedScreen.kt | 10 ++-------- .../components/BrowseSourceSimpleToolbar.kt | 8 ++------ .../browse/components/BrowseSourceToolbar.kt | 10 ++-------- .../browse/components/GlobalSearchToolbar.kt | 9 ++------- .../manga/DuplicateMangaDialog.kt | 15 ++++++++++++++ .../ui/browse/BulkFavoriteScreenModel.kt | 20 +++++++++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 8 ++------ .../migration/search/SourceSearchScreen.kt | 12 ++--------- 8 files changed, 47 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 8fc3dbed0e..40cf05828c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -3,8 +3,6 @@ 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 @@ -18,12 +16,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.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.manga.model.Manga @@ -282,11 +280,7 @@ fun SourceFeedToolbar( actions = { AppBarActions( actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), + bulkSelectionButton(toggleSelectionMode), ) ) persistentListOf( diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt index 17d59bbc87..c4b33e53aa 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt @@ -2,7 +2,6 @@ package eu.kanade.presentation.browse.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check -import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.ViewModule import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -16,6 +15,7 @@ import androidx.compose.runtime.setValue import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.i18n.MR @@ -46,11 +46,7 @@ fun BrowseSourceSimpleToolbar( onClick = { selectingDisplayMode = true }, ), // KMK --> - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), + bulkSelectionButton(toggleSelectionMode), // KMK <-- ), ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index b158f18477..f873ffdfde 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -4,7 +4,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.automirrored.outlined.Help import androidx.compose.material.icons.filled.ViewModule -import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior @@ -21,6 +20,7 @@ import eu.kanade.presentation.components.RadioMenuItem import eu.kanade.presentation.components.SearchToolbar import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import exh.source.anyIs import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.library.model.LibraryDisplayMode @@ -77,13 +77,7 @@ fun BrowseSourceToolbar( ) } // KMK --> - add( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), - ) + add(bulkSelectionButton(toggleSelectionMode)) // KMK <-- if (isLocalSource) { if (isConfigurableSource && displayMode != null) { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index 295ee04ff0..abd223e682 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.PushPin @@ -27,9 +26,9 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR @@ -66,11 +65,7 @@ fun GlobalSearchToolbar( actions = { AppBarActions( actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = toggleSelectionMode, - ), + bulkSelectionButton(toggleSelectionMode), ) ) }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 1a9e67fb26..d2b70f091c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -153,4 +154,18 @@ fun DuplicateMangasDialog( }, ) } + +@Preview +@Composable +fun DuplicateMangasDialogPreview() { + DuplicateMangasDialog( + onDismissRequest = { }, + onAllowAllDuplicate = { }, + onSkipAllDuplicate = { }, + onOpenManga = { }, + onAllowDuplicate = { }, + onSkipDuplicate = { }, + duplicatedName = "Berserk", + ) +} // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 87e5039226..6e746bc792 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -1,9 +1,12 @@ package eu.kanade.tachiyomi.ui.browse +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed @@ -15,6 +18,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.browse.components.RemoveMangaDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.manga.DuplicateMangasDialog import eu.kanade.tachiyomi.data.cache.CoverCache @@ -42,6 +46,8 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.service.SourceManager +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.Instant @@ -441,3 +447,17 @@ fun AllowDuplicateDialog(bulkFavoriteScreenModel: BulkFavoriteScreenModel) { duplicatedName = dialog.duplicatedManga.second.title, ) } + +@Composable +fun bulkSelectionButton(toggleSelectionMode: () -> Unit) = + AppBar.Action( + title = stringResource(MR.strings.action_bulk_select), + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + ) + +@Preview +@Composable +fun BulkSelectionButtonPreview() { + bulkSelectionButton { } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index b34a990556..bfd99199bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.feed import androidx.activity.compose.BackHandler import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Checklist import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -29,6 +28,7 @@ import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen @@ -91,11 +91,7 @@ fun Screen.feedTab( }, ), // KMK --> - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = bulkFavoriteScreenModel::toggleSelectionMode, - ), + bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode), // KMK <-- ), content = { contentPadding, snackbarHostState -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index edbbf14a9e..1b7083e9e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import androidx.activity.compose.BackHandler -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -17,7 +15,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton -import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SelectionToolbar @@ -26,6 +23,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog +import eu.kanade.tachiyomi.ui.browse.bulkSelectionButton import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog @@ -34,9 +32,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen import kotlinx.collections.immutable.persistentListOf import tachiyomi.core.common.Constants import tachiyomi.domain.manga.model.Manga -import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold -import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.source.local.LocalSource data class SourceSearchScreen( @@ -85,11 +81,7 @@ data class SourceSearchScreen( actions = { AppBarActions( actions = persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bulk_select), - icon = Icons.Outlined.Checklist, - onClick = bulkFavoriteScreenModel::toggleSelectionMode, - ), + bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode), ) ) }, From 3ea17388380262887ae864dff31321a1976688df Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 14:24:00 +0700 Subject: [PATCH 30/36] reuse method to add/remove single manga --- .../ui/browse/BulkFavoriteScreenModel.kt | 21 +++++++++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 21 +------------------ .../ui/browse/source/feed/SourceFeedScreen.kt | 21 +------------------ .../source/globalsearch/GlobalSearchScreen.kt | 21 +------------------ .../exh/md/similar/MangaDexSimilarScreen.kt | 21 +------------------ .../main/java/exh/recs/RecommendsScreen.kt | 21 +------------------ 6 files changed, 26 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 6e746bc792..28cf67d2c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach @@ -328,6 +330,25 @@ class BulkFavoriteScreenModel( } } + fun addRemoveManga(manga: Manga, haptic: HapticFeedback? = null) { + screenModelScope.launchIO { + val duplicateManga = getDuplicateLibraryManga(manga) + when { + manga.favorite -> setDialog( + Dialog.RemoveManga(manga), + ) + duplicateManga != null -> setDialog( + Dialog.AddDuplicateManga( + manga, + duplicateManga, + ), + ) + else -> addFavorite(manga) + } + haptic?.performHapticFeedback(HapticFeedbackType.LongPress) + } + } + fun setDialog(dialog: Dialog?) { mutableState.update { it.copy(dialog = dialog) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index bfd99199bf..77deb6445b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -8,8 +8,6 @@ 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 @@ -35,7 +33,6 @@ 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 @@ -54,7 +51,6 @@ fun Screen.feedTab( // KMK --> val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current BackHandler(enabled = bulkFavoriteState.selectionMode) { @@ -135,22 +131,7 @@ fun Screen.feedTab( // KMK --> onLongClickManga = { manga -> 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) - } + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) } else { navigator.push(MangaScreen(manga.id, true)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 5e82e3b085..dfedfc57a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -4,8 +4,6 @@ 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 import cafe.adriel.voyager.core.model.rememberScreenModel @@ -29,7 +27,6 @@ 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 @@ -47,7 +44,6 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current // KMK <-- @@ -78,22 +74,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { sourceId = screenModel.source.id, onLongClickManga = { manga -> 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) - } + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) } else { navigator.push(MangaScreen(manga.id, true)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index 0dae993bca..a641c59ddc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -7,9 +7,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -24,7 +22,6 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen -import tachiyomi.core.common.util.lang.launchIO import tachiyomi.presentation.core.screens.LoadingScreen class GlobalSearchScreen( @@ -51,7 +48,6 @@ class GlobalSearchScreen( val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current BackHandler(enabled = bulkFavoriteState.selectionMode) { @@ -101,22 +97,7 @@ class GlobalSearchScreen( onLongClickItem = { manga -> // KMK --> 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) - } + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) } else { // KMK <-- navigator.push(MangaScreen(manga.id, true)) diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 267147806c..772d5c0b53 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -7,8 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalHapticFeedback import androidx.paging.compose.collectAsLazyPagingItems @@ -26,7 +24,6 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.manga.MangaScreen -import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold @@ -44,7 +41,6 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current BackHandler(enabled = bulkFavoriteState.selectionMode) { @@ -110,22 +106,7 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { onMangaLongClick = { manga -> // KMK --> 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) - } + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) } else { // KMK <-- onMangaClick(manga) diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index 2a44db298e..c1d385aac8 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -7,8 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalHapticFeedback import androidx.paging.compose.collectAsLazyPagingItems @@ -27,7 +25,6 @@ import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen -import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @@ -43,7 +40,6 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() - val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current BackHandler(enabled = bulkFavoriteState.selectionMode) { @@ -109,22 +105,7 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { onMangaLongClick = { manga -> // KMK --> 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) - } + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) } else { // KMK <-- onMangaClick(manga) From 8888bfec9d1ade58d31c596de30838a314940e5a Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Sun, 10 Mar 2024 16:41:29 +0700 Subject: [PATCH 31/36] detekt: cleanup code # Conflicts: # app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt --- .../kanade/presentation/browse/FeedScreen.kt | 14 +++++++++---- .../presentation/browse/GlobalSearchScreen.kt | 21 ++++++++++++------- .../tachiyomi/ui/browse/feed/FeedTab.kt | 7 ++++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 54e1977b16..b374b3aaa7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -211,10 +211,11 @@ fun FeedAddSearchDialog( val savedSearchStrings = remember { savedSearches.map { // KMK --> - it?.name ?: if (source.supportsLatest) + it?.name ?: if (source.supportsLatest) { context.stringResource(MR.strings.latest) - else + } else { context.stringResource(MR.strings.popular) + } // KMK <-- }.toImmutableList() } @@ -228,7 +229,12 @@ fun FeedAddSearchDialog( }, onDismissRequest = onDismiss, confirmButton = { - TextButton(onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }, /* KMK --> */ enabled = selected != null /* KMK <-- */) { + TextButton( + onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }, + // KMK --> + enabled = selected != null, + // KMK <-- + ) { Text(text = stringResource(MR.strings.action_ok)) } }, @@ -240,7 +246,7 @@ fun RadioSelector( options: ImmutableList, selected: Int?, optionStrings: ImmutableList = remember { options.map { it.toString() }.toImmutableList() }, - onSelectOption: (Int) -> Unit/* KMK --> */ = {},/* KMK <-- */ + onSelectOption: (Int) -> Unit /* KMK --> */ = {} /* KMK <-- */, ) { Column(Modifier.verticalScroll(rememberScrollState())) { optionStrings.forEachIndexed { index, option -> diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 69303afd70..953defc695 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -21,8 +21,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.util.system.LocaleHelper import kotlinx.collections.immutable.ImmutableMap import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.source.model.Source as DomainSource import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.domain.source.model.Source as DomainSource @Composable fun GlobalSearchScreen( @@ -109,20 +109,25 @@ internal fun GlobalSearchContent( // KMK --> val domainSource = DomainSource( source.id, - "", "", + "", + "", supportsLatest = false, isStub = false ) // KMK <-- GlobalSearchResultItem( - title = (fromSourceId?.let { - "▶ ${source.name}".takeIf { source.id == fromSourceId } - } ?: source.name) + + title = ( + fromSourceId?.let { + "▶ ${source.name}".takeIf { source.id == fromSourceId } + } ?: source.name + ) + // KMK --> - (domainSource.installedExtension?.let { extension -> - " (${extension.name})".takeIf { extension.name != source.name } - } ?: ""), + ( + domainSource.installedExtension?.let { extension -> + " (${extension.name})".takeIf { extension.name != source.name } + } ?: "" + ), // KMK <-- subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), onClick = { onClickSource(source) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 77deb6445b..e74005b6d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -110,10 +110,11 @@ fun Screen.feedTab( BrowseSourceScreen( source.id, // KMK --> - listingQuery = if (!source.supportsLatest) + listingQuery = if (!source.supportsLatest) { GetRemoteManga.QUERY_POPULAR - else - GetRemoteManga.QUERY_LATEST, + } else { + GetRemoteManga.QUERY_LATEST + }, // KMK <-- ), ) From 7e4fba9bfe274908c055c7179afa32667d44a3df Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Mon, 18 Mar 2024 14:08:56 +0700 Subject: [PATCH 32/36] Bulk-selection: Fix warning in BrowseSource --- .../browse/components/BrowseSourceComfortableGrid.kt | 4 ++-- .../presentation/browse/components/BrowseSourceCompactGrid.kt | 4 ++-- .../kanade/presentation/browse/components/BrowseSourceList.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt index e44734c598..27c7d81b2c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -35,7 +35,7 @@ fun BrowseSourceComfortableGrid( onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, // KMK --> - selection: List? = null, + selection: List, // KMK <-- ) { LazyVerticalGrid( @@ -65,7 +65,7 @@ fun BrowseSourceComfortableGrid( onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, // KMK --> - isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + isSelected = selection.fastAny { selected -> selected.id == manga.id }, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt index e164625c94..8998dbd6bb 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -35,7 +35,7 @@ fun BrowseSourceCompactGrid( onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, // KMK --> - selection: List? = null, + selection: List, // KMK <-- ) { LazyVerticalGrid( @@ -65,7 +65,7 @@ fun BrowseSourceCompactGrid( onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, // KMK --> - isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + isSelected = selection.fastAny { selected -> selected.id == manga.id }, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt index 385adcbb02..012c39e630 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -31,7 +31,7 @@ fun BrowseSourceList( onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, // KMK --> - selection: List? = null, + selection: List, // KMK <-- ) { LazyColumn( @@ -58,7 +58,7 @@ fun BrowseSourceList( onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, // KMK --> - isSelected = selection?.fastAny { selected -> selected.id == manga.id } ?: false, + isSelected = selection.fastAny { selected -> selected.id == manga.id }, // KMK <-- ) } From fddfe6280a10d007770dee7f1e6821944bf62328 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Tue, 12 Mar 2024 15:03:52 +0700 Subject: [PATCH 33/36] Select All for GlobalSearch (cherry picked from commit a28233735cac5bce5a630de84555b501ffcccc8d) --- .../presentation/browse/GlobalSearchScreen.kt | 14 ++++++++++++++ .../presentation/components/SelectionToolbar.kt | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 953defc695..0c1fdb8027 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -52,6 +52,20 @@ fun GlobalSearchScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.filteredItems.forEach { (_, result) -> + when (result) { + is SearchItemResult.Success -> { + result.result.map{manga -> + + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.toggleSelection(manga) + } + } + else -> {} + } + } + }, ) } else { // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt index ae25a406ad..76a585291b 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt @@ -2,6 +2,7 @@ package eu.kanade.presentation.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview @@ -14,6 +15,7 @@ fun SelectionToolbar( selectedCount: Int, onClickClearSelection: () -> Unit, onChangeCategoryClicked: () -> Unit, + onSelectAll: () -> Unit = {}, ) { AppBar( titleContent = { Text(text = "$selectedCount") }, @@ -29,6 +31,13 @@ fun SelectionToolbar( } }, ), + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Filled.SelectAll, + onClick = { + onSelectAll.invoke() + }, + ), ), ) }, From 097ce0e2c33131f62468867ff6531ae7ac0f05b3 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Thu, 25 Apr 2024 22:02:55 +0700 Subject: [PATCH 34/36] =?UTF-8?q?Hide=20bulkSelection=E2=80=99s=20?= =?UTF-8?q?=E2=80=9CSelect=20all=E2=80=9D=20button=20when=20not=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 8f8214f65467cde97161a5fd9edab5b1543b333e) --- .../components/SelectionToolbar.kt | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt index 76a585291b..432ee1079e 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt @@ -15,30 +15,38 @@ fun SelectionToolbar( selectedCount: Int, onClickClearSelection: () -> Unit, onChangeCategoryClicked: () -> Unit, - onSelectAll: () -> Unit = {}, + onSelectAll: (() -> Unit)? = null, ) { AppBar( titleContent = { Text(text = "$selectedCount") }, actions = { AppBarActions( - persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_bookmark), - icon = Icons.Filled.BookmarkAdd, - onClick = { - if (selectedCount > 0) { - onChangeCategoryClicked() - } - }, - ), - AppBar.Action( - title = stringResource(MR.strings.action_select_all), - icon = Icons.Filled.SelectAll, - onClick = { - onSelectAll.invoke() - }, - ), - ), + actions = persistentListOf().builder() + .apply { + if (onSelectAll != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Filled.SelectAll, + onClick = { + onSelectAll.invoke() + }, + ), + ) + } + add( + AppBar.Action( + title = stringResource(MR.strings.action_bookmark), + icon = Icons.Filled.BookmarkAdd, + onClick = { + if (selectedCount > 0) { + onChangeCategoryClicked() + } + }, + ), + ) + } + .build(), ) }, isActionMode = true, From 45553eb096b4329fbda617481ddca484ca37b03b Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Thu, 25 Apr 2024 22:03:45 +0700 Subject: [PATCH 35/36] BulkSelection: avoid using toggle when Select all (cherry picked from commit 695f872b381feca1051f0f5133a0288d252691e5) --- .../presentation/browse/GlobalSearchScreen.kt | 5 ++--- .../ui/browse/BulkFavoriteScreenModel.kt | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 0c1fdb8027..0547a6bbc8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -56,10 +56,9 @@ fun GlobalSearchScreen( state.filteredItems.forEach { (_, result) -> when (result) { is SearchItemResult.Success -> { - result.result.map{manga -> - + result.result.forEach { manga -> if (!bulkFavoriteState.selection.contains(manga)) - bulkFavoriteScreenModel.toggleSelection(manga) + bulkFavoriteScreenModel.select(manga) } } else -> {} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 491d057cbd..a043b936da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -84,12 +84,23 @@ class BulkFavoriteScreenModel( mutableState.update { it.copy(selection = persistentListOf()) } } - fun toggleSelection(manga: Manga) { + fun select(manga: Manga) { + toggleSelection(manga, toSelectedState = true) + } + + fun unselect(manga: Manga) { + toggleSelection(manga, toSelectedState = false) + } + + /** + * @param toSelectedState set to true to only Select, set to false to only Unselect + */ + fun toggleSelection(manga: Manga, toSelectedState: Boolean? = null) { mutableState.update { state -> val newSelection = state.selection.mutate { list -> - if (list.fastAny { it.id == manga.id }) { + if (toSelectedState != true && list.fastAny { it.id == manga.id }) { list.removeAll { it.id == manga.id } - } else { + } else if (toSelectedState != false) { list.add(manga) } } From f6be3a494ceafd1e3492a1f617330f2cea4d8535 Mon Sep 17 00:00:00 2001 From: Cuong Tran Date: Fri, 26 Apr 2024 01:05:28 +0700 Subject: [PATCH 36/36] =?UTF-8?q?BulkSelection:=20=E2=80=9CSelect=20all?= =?UTF-8?q?=E2=80=9D=20for=20all=20screens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit bddcc089df738e23331b351cc20c8425cb316c74) --- .../presentation/browse/BrowseSourceScreen.kt | 16 ++++++++++++++++ .../presentation/browse/MigrateSearchScreen.kt | 14 ++++++++++++++ .../presentation/browse/SourceFeedScreen.kt | 8 ++++++++ .../presentation/components/SelectionToolbar.kt | 2 +- .../presentation/components/TabbedScreen.kt | 11 +++++++++++ .../eu/kanade/tachiyomi/ui/browse/BrowseTab.kt | 7 +++++-- .../kanade/tachiyomi/ui/browse/feed/FeedTab.kt | 6 ++---- .../migration/search/SourceSearchScreen.kt | 7 +++++++ .../browse/source/browse/BrowseSourceScreen.kt | 7 +++++++ .../source/browse/BrowseSourceScreenModel.kt | 3 +++ .../java/exh/md/follows/MangaDexFollowsScreen.kt | 7 +++++++ .../java/exh/md/similar/MangaDexSimilarScreen.kt | 8 ++++++++ app/src/main/java/exh/recs/RecommendsScreen.kt | 8 ++++++++ 13 files changed, 97 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index 5494801d18..a6eda94c34 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.paging.LoadState @@ -23,6 +24,7 @@ import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.formattedMessage import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import exh.metadata.metadata.RaisedSearchMetadata import exh.source.isEhBasedSource import kotlinx.collections.immutable.persistentListOf @@ -60,6 +62,7 @@ fun BrowseSourceContent( onMangaLongClick: (Manga) -> Unit, // KMK --> selection: List, + browseSourceState: BrowseSourceScreenModel.State, // KMK <-- ) { val context = LocalContext.current @@ -135,9 +138,22 @@ fun BrowseSourceContent( LoadingScreen( modifier = Modifier.padding(contentPadding), ) + // KMK --> + browseSourceState.mangaDisplayingList.clear() + // KMK <-- return } + // KMK --> + for (idx in browseSourceState.mangaDisplayingList.size.. + if (!browseSourceState.mangaDisplayingList.map { it.id }.contains(manga.id)) { + browseSourceState.mangaDisplayingList.add(manga) + } + } + } + // KMK <-- + // SY --> if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) { BrowseSourceEHentaiList( diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 7817189723..440075d992 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -8,6 +8,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.components.SelectionToolbar import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga @@ -42,6 +43,19 @@ fun MigrateSearchScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.filteredItems.forEach { (_, result) -> + when (result) { + is SearchItemResult.Success -> { + result.result.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + } + else -> {} + } + } + }, ) } else { // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 40cf05828c..66cee34911 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -122,6 +122,14 @@ fun SourceFeedScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + items.forEach { + it.results?.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + } + }, ) } else { // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt index 432ee1079e..258c5aa711 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SelectionToolbar.kt @@ -29,7 +29,7 @@ fun SelectionToolbar( title = stringResource(MR.strings.action_select_all), icon = Icons.Filled.SelectAll, onClick = { - onSelectAll.invoke() + onSelectAll() }, ), ) diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 4ed9a519da..adddec7b72 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.zIndex import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel +import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @@ -40,6 +41,7 @@ fun TabbedScreen( searchQuery: String? = null, onChangeSearchQuery: (String?) -> Unit = {}, // KMK --> + feedScreenModel: FeedScreenModel, bulkFavoriteScreenModel: BulkFavoriteScreenModel, // KMK <-- ) { @@ -48,6 +50,7 @@ fun TabbedScreen( val snackbarHostState = remember { SnackbarHostState() } // KMK --> + val feedState by feedScreenModel.state.collectAsState() val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() // KMK <-- @@ -67,6 +70,14 @@ fun TabbedScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + feedState.items?.forEach { + it.results?.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + } + }, ) } else { // KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index e256e116f2..96397144c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -21,6 +21,7 @@ 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 @@ -66,6 +67,7 @@ data class BrowseTab( val extensionsState by extensionsScreenModel.state.collectAsState() // KMK --> + val feedScreenModel = rememberScreenModel { FeedScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } // KMK <-- @@ -80,7 +82,7 @@ data class BrowseTab( ) } else if (feedTabInFront) { persistentListOf( - feedTab(bulkFavoriteScreenModel), + feedTab(/* KMK --> */feedScreenModel, bulkFavoriteScreenModel/* KMK <-- */), sourcesTab(), extensionsTab(extensionsScreenModel), migrateSourceTab(), @@ -88,7 +90,7 @@ data class BrowseTab( } else { persistentListOf( sourcesTab(), - feedTab(bulkFavoriteScreenModel), + feedTab(/* KMK --> */feedScreenModel, bulkFavoriteScreenModel/* KMK <-- */), extensionsTab(extensionsScreenModel), migrateSourceTab(), ) @@ -98,6 +100,7 @@ data class BrowseTab( searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, // KMK --> + feedScreenModel = feedScreenModel, bulkFavoriteScreenModel = bulkFavoriteScreenModel, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 242fba8748..b959e9b5a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -9,8 +9,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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 import cafe.adriel.voyager.navigator.currentOrThrow @@ -39,13 +37,13 @@ import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.i18n.stringResource @Composable -fun Screen.feedTab( +fun feedTab( // KMK --> + screenModel: FeedScreenModel, bulkFavoriteScreenModel: BulkFavoriteScreenModel, // KMK <-- ): TabContent { val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { FeedScreenModel() } val state by screenModel.state.collectAsState() // KMK --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 0236869208..61ddfdf3d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -75,6 +75,12 @@ data class SourceSearchScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.mangaDisplayingList.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + }, ) } else { // KMK <-- @@ -150,6 +156,7 @@ data class SourceSearchScreen( onMangaLongClick = { navigator.push(MangaScreen(it.id, true)) }, // KMK --> selection = bulkFavoriteState.selection, + browseSourceState = state, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 4ef59ac815..ef30073639 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -171,6 +171,12 @@ data class BrowseSourceScreen( selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.mangaDisplayingList.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + }, ) } else { // KMK <-- @@ -317,6 +323,7 @@ data class BrowseSourceScreen( }, // KMK --> selection = bulkFavoriteState.selection, + browseSourceState = state, // KMK <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 9a0a85c04d..94820b7274 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -473,6 +473,9 @@ open class BrowseSourceScreenModel( val savedSearches: ImmutableList = persistentListOf(), val filterable: Boolean = true, // SY <-- + // KMK --> + val mangaDisplayingList: MutableSet = emptySet().toMutableSet() + // KMK <-- ) { val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty() } diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 5371417cf3..63b8643184 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -73,6 +73,12 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.mangaDisplayingList.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + }, ) } else { // KMK <-- @@ -142,6 +148,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { }, // KMK --> selection = bulkFavoriteState.selection, + browseSourceState = state, // KMK <-- ) } diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt index 33cd209c50..9a03d6a0d5 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -44,6 +44,7 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { val navigator = LocalNavigator.currentOrThrow // KMK --> + val state by screenModel.state.collectAsState() val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() @@ -68,6 +69,12 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.mangaDisplayingList.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + }, ) } else { // KMK <-- @@ -120,6 +127,7 @@ class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen() { }, // KMK --> selection = bulkFavoriteState.selection, + browseSourceState = state, // KMK <-- ) } diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt index 0aa37cb333..11144cf62c 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -43,6 +43,7 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { val navigator = LocalNavigator.currentOrThrow // KMK --> + val state by screenModel.state.collectAsState() val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() @@ -67,6 +68,12 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { selectedCount = bulkFavoriteState.selection.size, onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + onSelectAll = { + state.mangaDisplayingList.forEach { manga -> + if (!bulkFavoriteState.selection.contains(manga)) + bulkFavoriteScreenModel.select(manga) + } + }, ) } else { // KMK <-- @@ -119,6 +126,7 @@ class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen() { }, // KMK --> selection = bulkFavoriteState.selection, + browseSourceState = state, // KMK <-- ) }