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 d5f891db2..5494801d1 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, + // 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/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index 9d77b94e0..af15cc21a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -69,6 +69,10 @@ fun FeedScreen( onClickSource: (CatalogueSource) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + selection: List, + // KMK <-- onRefresh: () -> Unit, getMangaState: @Composable (Manga, CatalogueSource?) -> State, ) { @@ -121,6 +125,10 @@ fun FeedScreen( item = item, getMangaState = { getMangaState(it, item.source) }, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + selection = selection, + // KMK <-- ) } } @@ -135,6 +143,10 @@ fun FeedItem( item: FeedItemUI, getMangaState: @Composable ((Manga) -> State), onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + selection: List, + // KMK <-- ) { when { item.results == null -> { @@ -148,7 +160,10 @@ fun FeedItem( titles = item.results, getManga = getMangaState, onClick = onClickManga, - onLongClick = onClickManga, + // KMK --> + onLongClick = onLongClickManga, + selection = selection, + // KMK <-- ) } } 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 80e13f03c..69303afd7 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -4,13 +4,17 @@ 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 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.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 @@ -32,22 +36,42 @@ fun GlobalSearchScreen( onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, + // KMK --> + bulkFavoriteScreenModel: BulkFavoriteScreenModel, + // KMK <-- ) { + // KMK --> + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + // KMK <-- + 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 (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::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 --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, ) { paddingValues -> GlobalSearchContent( @@ -57,6 +81,9 @@ fun GlobalSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } } @@ -70,6 +97,9 @@ internal fun GlobalSearchContent( onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, fromSourceId: Long? = null, + // KMK --> + selection: List, + // KMK <-- ) { LazyColumn( contentPadding = contentPadding, @@ -107,6 +137,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 31abb596c..781718972 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -2,8 +2,12 @@ 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 +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 @@ -22,22 +26,42 @@ fun MigrateSearchScreen( onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, + // KMK --> + bulkFavoriteScreenModel: BulkFavoriteScreenModel, + // KMK <-- ) { + // KMK --> + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + // KMK <-- + 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 (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::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 --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, ) { paddingValues -> GlobalSearchContent( @@ -48,6 +72,9 @@ fun MigrateSearchScreen( onClickSource = onClickSource, onClickItem = onClickItem, onLongClickItem = onLongClickItem, + // KMK --> + selection = bulkFavoriteState.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 a09fe1d71..40cf05828 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -7,6 +7,8 @@ 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,8 +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.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 @@ -99,21 +105,38 @@ fun SourceFeedScreen( onSearchQueryChange: (String?) -> Unit, getMangaState: @Composable (Manga) -> State, // KMK --> - id: Long, + sourceId: Long, + onLongClickManga: (Manga) -> Unit, + bulkFavoriteScreenModel: BulkFavoriteScreenModel, // KMK <-- ) { + // KMK --> + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + // KMK <-- + Scaffold( topBar = { scrollBehavior -> - SourceFeedToolbar( - title = name, - searchQuery = searchQuery, - onSearchQueryChange = onSearchQueryChange, - scrollBehavior = scrollBehavior, - onClickSearch = onClickSearch, - // KMK --> - id = id, + // KMK --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } else { // KMK <-- - ) + SourceFeedToolbar( + title = name, + searchQuery = searchQuery, + onSearchQueryChange = onSearchQueryChange, + scrollBehavior = scrollBehavior, + onClickSearch = onClickSearch, + // KMK --> + sourceId = sourceId, + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } }, floatingActionButton = { BrowseSourceFloatingActionButton( @@ -135,6 +158,10 @@ fun SourceFeedScreen( onClickSavedSearch = onClickSavedSearch, onClickDelete = onClickDelete, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + selection = bulkFavoriteState.selection, + // KMK <-- ) } } @@ -152,6 +179,10 @@ fun SourceFeedList( onClickSavedSearch: (SavedSearch) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, + // KMK --> + onLongClickManga: (Manga) -> Unit, + selection: List, + // KMK <-- ) { ScrollbarLazyColumn( contentPadding = paddingValues + topSmallPaddingValues, @@ -183,6 +214,10 @@ fun SourceFeedList( item = item, getMangaState = { getMangaState(it) }, onClickManga = onClickManga, + // KMK --> + onLongClickManga = onLongClickManga, + selection = selection, + // KMK <-- ) } } @@ -194,6 +229,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 +247,10 @@ fun SourceFeedItem( titles = item.results.orEmpty(), getManga = getMangaState, onClick = onClickManga, - onLongClick = onClickManga, + // KMK --> + onLongClick = onLongClickManga, + selection = selection, + // KMK <-- ) } } @@ -222,7 +264,8 @@ fun SourceFeedToolbar( scrollBehavior: TopAppBarScrollBehavior, onClickSearch: (String) -> Unit, // KMK --> - id: Long + sourceId: Long, + toggleSelectionMode: () -> Unit, // KMK <-- ) { SearchToolbar( @@ -235,10 +278,15 @@ fun SourceFeedToolbar( placeholderText = stringResource(MR.strings.action_search_hint), // KMK --> actions = { - persistentListOf( - SourceSettingsButton(id) + AppBarActions( + actions = persistentListOf( + bulkSelectionButton(toggleSelectionMode), + ) ) - } + persistentListOf( + SourceSettingsButton(sourceId), + ) + }, // 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 3ff54e9a0..eaa031966 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, + // 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 8d6a07ab0..1e18ec55b 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, + // 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 8d301047a..9eb3a7d88 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, + // 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/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt index 341587fc5..c4b33e53a 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 @@ -15,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 @@ -27,6 +28,9 @@ fun BrowseSourceSimpleToolbar( displayMode: LibraryDisplayMode?, onDisplayModeChange: (LibraryDisplayMode) -> Unit, scrollBehavior: TopAppBarScrollBehavior, + // KMK --> + toggleSelectionMode: () -> Unit, + // KMK <-- ) { AppBar( navigateUp = navigateUp, @@ -41,6 +45,9 @@ fun BrowseSourceSimpleToolbar( icon = Icons.Outlined.ViewModule, onClick = { selectingDisplayMode = true }, ), + // KMK --> + bulkSelectionButton(toggleSelectionMode), + // KMK <-- ), ) DropdownMenu( 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 d152534da..f873ffdfd 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.Help 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 @@ -41,6 +41,9 @@ fun BrowseSourceToolbar( onSettingsClick: () -> Unit, onSearch: (String) -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null, + // KMK --> + toggleSelectionMode: () -> Unit, + // KMK <-- ) { // Avoid capturing unstable source in actions lambda val title = source?.name @@ -73,6 +76,9 @@ fun BrowseSourceToolbar( ), ) } + // KMK --> + add(bulkSelectionButton(toggleSelectionMode)) + // KMK <-- if (isLocalSource) { if (isConfigurableSource && displayMode != null) { add( 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 456269b7c..e23f50604 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, + // 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 }, + // 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/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index fa61696ae..abd223e68 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 @@ -26,8 +26,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.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 import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -45,6 +48,9 @@ fun GlobalSearchToolbar( onlyShowHasResults: Boolean, onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, + // KMK --> + toggleSelectionMode: () -> Unit, + // KMK <-- ) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Box { @@ -55,6 +61,15 @@ fun GlobalSearchToolbar( onClickCloseSearch = navigateUp, navigateUp = navigateUp, scrollBehavior = scrollBehavior, + // KMK --> + actions = { + AppBarActions( + actions = persistentListOf( + bulkSelectionButton(toggleSelectionMode), + ) + ) + }, + // KMK <-- ) if (progress in 1.. 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 efb1e2e04..4ed9a519d 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab 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,6 +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.BulkFavoriteScreenModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @@ -36,11 +39,18 @@ fun TabbedScreen( startIndex: Int? = null, searchQuery: String? = null, onChangeSearchQuery: (String?) -> Unit = {}, + // KMK --> + bulkFavoriteScreenModel: BulkFavoriteScreenModel, + // KMK <-- ) { val scope = rememberCoroutineScope() val state = rememberPagerState { tabs.size } val snackbarHostState = remember { SnackbarHostState() } + // KMK --> + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + // KMK <-- + LaunchedEffect(startIndex) { if (startIndex != null) { state.scrollToPage(startIndex) @@ -51,14 +61,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 --> + if (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::addFavorite, + ) + } 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 -> 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 92957384a..46c950635 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,10 @@ object CommonMangaItemDefaults { val GridVerticalSpacer = 4.dp const val BrowseFavoriteCoverAlpha = 0.34f + + // KMK --> + const val BrowseSelectedCoverAlpha = 0.17f + // KMK <-- } private val ContinueReadingButtonSize = 28.dp 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 e3ff20ad2..d2b70f091 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -1,14 +1,18 @@ 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 androidx.compose.ui.tooling.preview.Preview import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -57,3 +61,111 @@ fun DuplicateMangaDialog( }, ) } + +// KMK --> +@Composable +fun DuplicateMangasDialog( + onDismissRequest: () -> Unit, + onAllowAllDuplicate: () -> Unit, + onSkipAllDuplicate: () -> Unit, + onOpenManga: () -> Unit, + onAllowDuplicate: () -> Unit, + onSkipDuplicate: () -> Unit, + duplicatedName: String, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = duplicatedName) + }, + text = { + Text(text = stringResource(MR.strings.confirm_add_duplicate_manga)) + }, + confirmButton = { + FlowRow( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + ) { + 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)) + } + } + + 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)) + } + } + + 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)) + } + } + } + }, + ) +} + +@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/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 20c9e79d6..e256e116f 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 @@ -65,6 +65,10 @@ data class BrowseTab( val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() } val extensionsState by extensionsScreenModel.state.collectAsState() + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + // KMK <-- + TabbedScreen( titleRes = MR.strings.browse, // SY --> @@ -76,7 +80,7 @@ data class BrowseTab( ) } else if (feedTabInFront) { persistentListOf( - feedTab(), + feedTab(bulkFavoriteScreenModel), sourcesTab(), extensionsTab(extensionsScreenModel), migrateSourceTab(), @@ -84,7 +88,7 @@ data class BrowseTab( } else { persistentListOf( sourcesTab(), - feedTab(), + feedTab(bulkFavoriteScreenModel), extensionsTab(extensionsScreenModel), migrateSourceTab(), ) @@ -93,6 +97,9 @@ data class BrowseTab( // SY <-- searchQuery = extensionsState.searchQuery, onChangeSearchQuery = extensionsScreenModel::search, + // KMK --> + bulkFavoriteScreenModel = bulkFavoriteScreenModel, + // KMK <-- ) LaunchedEffect(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 new file mode 100644 index 000000000..28cf67d2c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -0,0 +1,484 @@ +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.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 +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.components.AppBar +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 +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 tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +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 + if (mangaList.isEmpty()) { + toggleSelectionMode() + return@launch + } + 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 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) + } + } + + 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 + 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, + ) +} + +@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, + ) +} + +@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 ba9300c86..77deb6445 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.outlined.Add import androidx.compose.runtime.Composable @@ -7,6 +8,7 @@ import androidx.compose.runtime.DisposableEffect 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 @@ -18,7 +20,15 @@ import eu.kanade.presentation.browse.FeedDeleteConfirmDialog import eu.kanade.presentation.browse.FeedScreen import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent +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.bulkSelectionButton import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen +import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.collectLatest @@ -29,11 +39,29 @@ import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.i18n.stringResource @Composable -fun Screen.feedTab(): TabContent { +fun Screen.feedTab( + // KMK --> + bulkFavoriteScreenModel: BulkFavoriteScreenModel, + // KMK <-- +): TabContent { val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { FeedScreenModel() } val state by screenModel.state.collectAsState() + // KMK --> + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + val haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.backHandler() + } + + LaunchedEffect(bulkFavoriteState.selectionMode) { + HomeScreen.showBottomNav(!bulkFavoriteState.selectionMode) + } + // KMK <-- + DisposableEffect(navigator.lastEvent) { if (navigator.lastEvent == StackEvent.Push) { screenModel.pushed = true @@ -58,6 +86,9 @@ fun Screen.feedTab(): TabContent { screenModel.openAddDialog() }, ), + // KMK --> + bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode), + // KMK <-- ), content = { contentPadding, snackbarHostState -> FeedScreen( @@ -89,8 +120,24 @@ fun Screen.feedTab(): TabContent { }, onClickDelete = screenModel::openDeleteDialog, onClickManga = { manga -> - navigator.push(MangaScreen(manga.id, true)) + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(manga) + } else { + // KMK <-- + navigator.push(MangaScreen(manga.id, true)) + } }, + // KMK --> + onLongClickManga = { manga -> + if (!bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) + } else { + navigator.push(MangaScreen(manga.id, true)) + } + }, + selection = bulkFavoriteState.selection, + // KMK <-- onRefresh = screenModel::init, getMangaState = { manga, source -> screenModel.getManga(initialManga = manga, source = source) }, ) @@ -133,6 +180,22 @@ fun Screen.feedTab(): TabContent { } } + // KMK --> + 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 <-- + 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 67c8f05b8..042e30ecc 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 @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.search +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -8,6 +9,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.MigrateSearchScreen 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.manga.MangaScreen @@ -23,6 +27,15 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) } val dialogState by dialogScreenModel.state.collectAsState() + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + MigrateSearchScreen( state = state, fromSourceId = state.fromSourceId, @@ -38,15 +51,35 @@ 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 (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.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 --> + bulkFavoriteScreenModel = bulkFavoriteScreenModel, + // KMK <-- ) + + // KMK --> + 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 1537b2dab..1b7083e9e 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,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.search +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -14,9 +15,15 @@ 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.AppBarActions import eu.kanade.presentation.components.SearchToolbar +import eu.kanade.presentation.components.SelectionToolbar 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.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 @@ -44,15 +51,43 @@ 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( + bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode), + ) + ) + }, + // KMK <-- + ) + } }, floatingActionButton = { // SY --> @@ -96,8 +131,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 +169,15 @@ data class SourceSearchScreen( } else -> {} } + + // KMK --> + 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 543a4720e..0ed54e43a 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 @@ -43,11 +44,15 @@ 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.category.components.ChangeCategoryDialog +import eu.kanade.presentation.components.SelectionToolbar 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 @@ -133,6 +138,15 @@ data class BrowseSourceScreen( ) } + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.backHandler() + } + // KMK <-- + LaunchedEffect(screenModel.source) { assistUrl = (screenModel.source as? HttpSource)?.baseUrl } @@ -140,18 +154,31 @@ 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 (bulkFavoriteState.selectionMode) { + SelectionToolbar( + selectedCount = bulkFavoriteState.selection.size, + onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode, + onChangeCategoryClicked = bulkFavoriteScreenModel::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 --> + toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode, + // KMK <-- + ) + } Row( modifier = Modifier @@ -244,23 +271,42 @@ data class BrowseSourceScreen( onWebViewClick = onWebViewClick, onHelpClick = { uriHandler.openUri(Constants.URL_HELP) }, onLocalSourceHelpClick = onHelpClick, - onMangaClick = { 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) - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onMangaClick = { + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(it) + } else { + // KMK <-- + navigator.push(MangaScreen(it.id, true, smartSearchConfig)) } }, + onMangaLongClick = { 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) + } + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } @@ -349,6 +395,16 @@ data class BrowseSourceScreen( else -> {} } + // KMK --> + when (bulkFavoriteState.dialog) { + is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> + ChangeMangasCategoryDialog(bulkFavoriteScreenModel) + is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> + AllowDuplicateDialog(bulkFavoriteScreenModel) + else -> {} + } + // KMK <-- + LaunchedEffect(Unit) { queryEvent.receiveAsFlow() .collectLatest { 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 5f69323f6..dfedfc57a 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 @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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 @@ -14,6 +15,12 @@ import eu.kanade.presentation.browse.components.SourceFeedAddDialog import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog 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.manga.MangaScreen @@ -33,6 +40,13 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + val haptic = LocalHapticFeedback.current + // KMK <-- + SourceFeedScreen( name = screenModel.source.name, isLoading = state.isLoading, @@ -43,13 +57,29 @@ 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 (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.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 --> - id = screenModel.source.id, + sourceId = screenModel.source.id, + onLongClickManga = { manga -> + if (!bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) + } else { + navigator.push(MangaScreen(manga.id, true)) + } + }, + bulkFavoriteScreenModel = bulkFavoriteScreenModel, // KMK <-- ) @@ -140,8 +170,30 @@ class SourceFeedScreen(val sourceId: Long) : Screen() { null -> Unit } - BackHandler(state.searchQuery != null) { - screenModel.search(null) + // KMK --> + 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 <-- + + BackHandler(state.searchQuery != null || bulkFavoriteState.selectionMode) { + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.backHandler() + } 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 a09595c4d..a641c59dd 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 @@ -7,11 +8,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +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.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.manga.MangaScreen import tachiyomi.presentation.core.screens.LoadingScreen @@ -36,6 +44,17 @@ class GlobalSearchScreen( mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1) } + // KMK --> + val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } + val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState() + + val haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.backHandler() + } + // KMK <-- + if (showSingleLoadingScreen) { LoadingScreen() @@ -66,9 +85,44 @@ 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 (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(it) + } else { + // KMK <-- + navigator.push(MangaScreen(it.id, true)) + } + }, + onLongClickItem = { manga -> + // KMK --> + if (!bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) + } else { + // KMK <-- + navigator.push(MangaScreen(manga.id, true)) + } + }, + // KMK --> + bulkFavoriteScreenModel = bulkFavoriteScreenModel, + // KMK <-- ) } + + // KMK --> + 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 6fbf6c376..ed30d3919 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,12 @@ 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.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 @@ -38,17 +43,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 +96,42 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { onWebViewClick = null, onHelpClick = null, onLocalSourceHelpClick = null, - onMangaClick = { 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) - } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onMangaClick = { + // KMK --> + if (bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelection(it) + } else { + // KMK <-- + navigator.push(MangaScreen(it.id, true)) } }, + onMangaLongClick = { 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) + } + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } @@ -123,5 +169,15 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { } else -> {} } + + // KMK --> + 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 62394333b..772d5c0b5 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -1,5 +1,6 @@ package exh.md.similar +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -7,13 +8,21 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.components.SelectionToolbar 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.manga.MangaScreen import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.sy.SYMR @@ -28,6 +37,17 @@ 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 haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + val onMangaClick: (Manga) -> Unit = { navigator.push(MangaScreen(it.id, true)) } @@ -36,13 +56,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 +94,44 @@ 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) { + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) + } else { + // KMK <-- + onMangaClick(manga) + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } + + // KMK --> + 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 39cec8033..c1d385aac 100644 --- a/app/src/main/java/exh/recs/RecommendsScreen.kt +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -1,5 +1,6 @@ package exh.recs +import androidx.activity.compose.BackHandler import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -7,6 +8,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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,7 +16,14 @@ 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.components.SelectionToolbar 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 tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @@ -27,6 +36,17 @@ 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 haptic = LocalHapticFeedback.current + + BackHandler(enabled = bulkFavoriteState.selectionMode) { + bulkFavoriteScreenModel.toggleSelectionMode() + } + // KMK <-- + val onMangaClick: (Manga) -> Unit = { manga -> openSmartSearch(navigator, manga.ogTitle) } @@ -35,13 +55,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 +93,45 @@ 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) { + bulkFavoriteScreenModel.addRemoveManga(manga, haptic) + } else { + // KMK <-- + onMangaClick(manga) + } + }, + // KMK --> + selection = bulkFavoriteState.selection, + // KMK <-- ) } + + // KMK --> + 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 <-- } private fun openSmartSearch(navigator: Navigator, title: String) { diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 62f68a272..af56253ca 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 @@ -677,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