Bulk-selection favorite

- Bulk selection to add to library & change categories of multiple entries all at once, everywhere.
- It can detect duplication being added and give option to allow/skip one by one or allow/skip all duplication.
- Also allow long-click to add/remove single entry to/from library, everywhere.
- It supports default category.

commit 7bca814a8d8481af148fbbe72a6e43623c701356
Merge: 3ee4f35aa 67534b391
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 14:26:29 2024 +0700

    Merge branch 'feature/bulk-favorite' into develop

commit 67534b391ffad5a6586fb1c18394b25afc3b432d
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 14:24:00 2024 +0700

    reuse method to add/remove single manga

commit f24b528c45b49286430286da6fc93a05c2917aa4
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 13:58:14 2024 +0700

    reuse bulkSelectionButton as AppBar.Action

commit d026241d9b742f6acf64d0090a9e6e7be53923c6
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 13:26:46 2024 +0700

    reuse Dialog composer

commit 7eb23481926ec981bece2e2e9ced87c795ef4eb8
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 07:20:58 2024 +0700

    fix detekt

commit 9643b9d834257836300b69bcb95cd587a38e34e0
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sun Mar 10 00:07:18 2024 +0700

    fix: won’t show dialog if skip all then selection list is empty

commit 7faafe9fb440175dcac065bc2963ba3637d10d38
Author: Cuong Tran <cuongtran.tm@gmail.com>
Date:   Sat Mar 9 23:58:46 2024 +0700

    no need using builder to create AppBarAction

commit 5564b366eb879407510a733128c26a73ad8d6f2e
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Sat Mar 9 18:26:58 2024 +0700

    add BulkSelection to the rest of Migrate, MangaDex…

commit 804bbb3943463298ae0b6eec33c9d3457b2891c9
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Sat Mar 9 17:40:42 2024 +0700

    migrate the rest to BulkSelectionScreenModel

commit 8e82fc1e87e1e246b7b676ba0fe7ef0c5bf1d016
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Sat Mar 9 11:00:40 2024 +0700

    separate BulkSelection into standalone BulkFavoriteScreenModel

commit 94a127b94d67841339ebdc674e398144e515cd14
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 13:18:35 2024 +0700

    improve: toggle selection mode when last item is unselected

commit ba0009409637f872f37ac407041f937c3afceda8
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 13:00:31 2024 +0700

    renovate code

commit 2d92ff9181297cfe795149968fdcd7e898852792
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 12:25:27 2024 +0700

    Feat: allow long click to favorite on GlobalSearch

commit a24bb06a2a66dcd473c50728b04dc4d6118d0085
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 11:45:48 2024 +0700

    Bulk favorite for GlobalSearchScreen & MigrateSearchScreen

commit 1d844ac74e356a43211c752442cb4279aea30b6f
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 11:29:58 2024 +0700

    renovate: move SelectionToolbar to its own component

commit 62cb90d74c2bde7f7e824de277a3bf6e0256cadc
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Mar 7 09:26:40 2024 +0700

    Fix: Won’t try call favorite if none selected

commit d2ef151c95a4432eddd3848b8e2e102dfd683681
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 18:27:54 2024 +0700

    Allow/Skip one by one

commit 5e138f701ed66b6c18ae1c7a9d58dae482c211e0
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 15:42:21 2024 +0700

    Allow skip duplicates

commit 9ac7a0e393d5aa78d77afde307263a85c8fa051c
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 14:24:52 2024 +0700

    Bulk selection mode for BrowseSourceScreen

commit 9749adca106c3fb911d1b6808f000f9e16008585
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 14:06:12 2024 +0700

    rename

commit 4b5e5105976ca3e488979b93043530a678a36e9c
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 12:34:01 2024 +0700

    Bulk selection for SourceFeedScreen

commit 7775c65af0fd957d020acbccd2bb42073ebcba11
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Wed Mar 6 12:21:55 2024 +0700

    Use topBar action to add favorite instead of bottomBar

    some minor name changes

commit 66417b7adffede1c92e1dbd75b2eac0cc8b9a13c
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Tue Mar 5 16:32:21 2024 +0700

    Allow bulk-favorite default category

commit 0870a9db3c1986c5ccab641bc77b8a0a9cf47017
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Tue Mar 5 15:40:02 2024 +0700

    Hide BottomNav when in selection mode

commit 3865c6d467ca0c63be5b387fcac9e9be5a096125
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Tue Mar 5 15:35:58 2024 +0700

    Selection:

    * Clear selection after set Favorite
    * Keep selection when editing categories

commit e01a5c5d35f226133d15b7ed800688bf442885bc
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Tue Mar 5 15:25:34 2024 +0700

    Allow add to favorite even when no category available

commit a4556058c86801eaabf7bbfae58cdc40af95f467
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Mon Mar 4 18:30:44 2024 +0700

    Set Favorite & Libraries OK now

commit 7e045cb23b2d6da58f0f71f9edcf0c2a2fc60038
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Fri Mar 1 16:40:32 2024 +0700

    handle back button

commit 8e3915431e0d461d45c34fa0ca9188bbdc074203
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Fri Mar 1 16:09:00 2024 +0700

    shade for selected items

commit d89b7bffb5161498bd9e7ebfd26aa3731e808361
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Fri Mar 1 14:33:32 2024 +0700

    Toggle and toolbar count

commit 1ea3a4c5496ca0d002fdbafde2ce913fd28c1987
Author: Cuong M. Tran <cuongtran.tm@gmail.com>
Date:   Thu Feb 29 18:36:57 2024 +0700

    selection list & toggleSelection/clearSelection
This commit is contained in:
Cuong Tran 2024-03-10 16:34:20 +07:00
parent 56673f25b1
commit f22fea2702
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
28 changed files with 1559 additions and 147 deletions

View file

@ -58,6 +58,9 @@ fun BrowseSourceContent(
// SY <--
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
// KMK -->
selection: List<Manga>,
// 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 <--
)
}
}

View file

@ -69,6 +69,10 @@ fun FeedScreen(
onClickSource: (CatalogueSource) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
selection: List<Manga>,
// KMK <--
onRefresh: () -> Unit,
getMangaState: @Composable (Manga, CatalogueSource?) -> State<Manga>,
) {
@ -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<Manga>),
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
selection: List<Manga>,
// 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 <--
)
}
}

View file

@ -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<Manga>,
// KMK <--
) {
LazyColumn(
contentPadding = contentPadding,
@ -107,6 +137,9 @@ internal fun GlobalSearchContent(
getManga = getManga,
onClick = onClickItem,
onLongClick = onLongClickItem,
// KMK -->
selection = selection,
// KMK <--
)
}
is SearchItemResult.Error -> {

View file

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

View file

@ -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<Manga>,
// 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<Manga>,
// 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<Manga>),
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
selection: List<Manga>,
// 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 <--
)
}

View file

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

View file

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

View file

@ -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<Manga>,
// 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 -->

View file

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

View file

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

View file

@ -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<Manga>,
onClick: (Manga) -> Unit,
onLongClick: (Manga) -> Unit,
// KMK -->
selection: List<Manga>,
// 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,
)

View file

@ -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..<total) {
LinearProgressIndicator(

View file

@ -0,0 +1,48 @@
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,
{},
{},
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<BulkFavoriteScreenModel.State>(initialState) {
fun backHandler() {
toggleSelectionMode()
}
fun toggleSelectionMode() {
if (state.value.selectionMode) {
clearSelection()
}
mutableState.update { it.copy(selectionMode = !it.selectionMode) }
}
private fun clearSelection() {
mutableState.update { it.copy(selection = persistentListOf()) }
}
fun toggleSelection(manga: Manga) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
if (list.fastAny { it.id == manga.id }) {
list.removeAll { it.id == manga.id }
} else {
list.add(manga)
}
}
state.copy(selection = newSelection)
}.also {
if (state.value.selection.isEmpty()) {
toggleSelectionMode()
}
}
}
fun addFavorite(startIdx: Int = 0) {
screenModelScope.launch {
val mangaWithDup = getDuplicateLibraryManga(startIdx)
if (mangaWithDup != null) {
setDialog(Dialog.AllowDuplicate(mangaWithDup))
} else {
addFavoriteDuplicate()
}
}
}
fun addFavoriteDuplicate(skipAllDuplicates: Boolean = false) {
screenModelScope.launch {
val mangaList = if (skipAllDuplicates) getNotDuplicateLibraryMangas() else state.value.selection
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<Manga> {
return state.value.selection.filterNot { manga ->
getDuplicateLibraryManga.await(manga).isNotEmpty()
}
}
private suspend fun getDuplicateLibraryManga(startIdx: Int = 0): Pair<Int, Manga>? {
val mangas = state.value.selection
mangas.fastForEachIndexed { index, manga ->
if (index < startIdx) return@fastForEachIndexed
val dup = getDuplicateLibraryManga.await(manga)
if (dup.isEmpty()) return@fastForEachIndexed
return Pair(index, dup.first())
}
return null
}
fun removeDuplicateSelectedManga(index: Int) {
mutableState.update { state ->
val newSelection = state.selection.mutate { list ->
list.removeAt(index)
}
state.copy(selection = newSelection)
}
}
/**
* Bulk update categories of manga using old and new common categories.
*
* @param mangaList the list of manga to move.
* @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas.
*/
fun setMangasCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
screenModelScope.launchNonCancellable {
mangaList.fastForEach { manga ->
val categoryIds = getCategories.await(manga.id)
.map { it.id }
.subtract(removeCategories.toSet())
.plus(addCategories)
.toList()
moveMangaToCategoriesAndAddToLibrary(manga, categoryIds)
}
}
toggleSelectionMode()
}
private fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List<Long>) {
moveMangaToCategory(manga.id, categories)
if (manga.favorite) return
screenModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true)
}
}
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds)
}
}
/**
* Returns the common categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas
.map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2) }
}
/**
* Returns the mix (non-common) categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common)
}
/**
* Get user categories.
*
* @return List of categories, not including the default category
*/
suspend fun getCategories(): List<Category> {
return getCategories.subscribe()
.firstOrNull()
?.filterNot { it.isSystemCategory }
.orEmpty()
}
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga).getOrNull(0)
}
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
)
}
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
val source = sourceManager.getOrStub(manga.source)
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
},
)
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
if (!new.favorite) {
new = new.removeCovers(coverCache)
} else {
setMangaDefaultChapterFlags.await(manga)
addTracks.bindEnhancedTrackers(manga, source)
}
updateManga.await(new.toMangaUpdate())
}
}
fun addFavorite(manga: Manga) {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
moveMangaToCategories(manga, defaultCategory)
changeMangaFavorite(manga)
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
moveMangaToCategories(manga)
changeMangaFavorite(manga)
}
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
setDialog(
Dialog.ChangeMangaCategory(
manga,
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
),
)
}
}
}
}
fun 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<CheckboxState.State<Category>>,
) : Dialog
data class ChangeMangasCategory(
val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class AllowDuplicate(val duplicatedManga: Pair<Int, Manga>) : Dialog
}
@Immutable
data class State(
val dialog: Dialog? = null,
val selection: PersistentList<Manga> = persistentListOf(),
val selectionMode: Boolean = false,
)
}
@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 { }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -76,6 +76,7 @@
<string name="action_toggle_nsfw_only">Toggle NSFW only</string>
<string name="action_select_all">Select all</string>
<string name="action_select_inverse">Select inverse</string>
<string name="action_bulk_select">Bulk selection mode</string>
<string name="action_mark_as_read">Mark as read</string>
<string name="action_mark_as_unread">Mark as unread</string>
<string name="action_mark_previous_as_read">Mark previous as read</string>
@ -677,6 +678,10 @@
<string name="remove_from_library">Remove from library</string>
<string name="unknown_title">Unknown title</string>
<string name="confirm_add_duplicate_manga">You have an entry in your library with the same name.\n\nDo you still wish to continue?</string>
<string name="action_allow_duplicate_manga">Allow it</string>
<string name="action_skip_duplicate_manga">Skip it</string>
<string name="action_allow_all_duplicate_manga">Allow all</string>
<string name="action_skip_all_duplicate_manga">Skip all</string>
<string name="manga_added_library">Added to library</string>
<string name="manga_removed_library">Removed from library</string>
<string name="manga_info_expand">More</string>