komikku/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt
Cuong Tran f22fea2702
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
2024-03-10 16:53:40 +07:00

282 lines
9.8 KiB
Kotlin

package eu.kanade.presentation.browse
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.plus
import kotlin.time.Duration.Companion.seconds
data class FeedItemUI(
val feed: FeedSavedSearch,
val savedSearch: SavedSearch?,
val source: CatalogueSource?,
val title: String,
val subtitle: String,
val results: List<Manga>?,
)
@Composable
fun FeedScreen(
state: FeedScreenState,
contentPadding: PaddingValues,
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
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>,
) {
when {
state.isLoading -> LoadingScreen()
state.isEmpty -> EmptyScreen(
SYMR.strings.feed_tab_empty,
modifier = Modifier.padding(contentPadding),
)
else -> {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(1.seconds)
refreshing = false
}
}
PullRefresh(
refreshing = refreshing && state.isLoadingItems,
onRefresh = {
refreshing = true
onRefresh()
},
enabled = { !state.isLoadingItems },
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
modifier = Modifier.fillMaxSize(),
) {
items(
state.items.orEmpty(),
key = { it.feed.id },
) { item ->
GlobalSearchResultItem(
modifier = Modifier.animateItemPlacement(),
title = item.title,
subtitle = item.subtitle,
onLongClick = {
onClickDelete(item.feed)
},
onClick = {
if (item.savedSearch != null && item.source != null) {
onClickSavedSearch(item.savedSearch, item.source)
} else if (item.source != null) {
onClickSource(item.source)
}
},
) {
FeedItem(
item = item,
getMangaState = { getMangaState(it, item.source) },
onClickManga = onClickManga,
// KMK -->
onLongClickManga = onLongClickManga,
selection = selection,
// KMK <--
)
}
}
}
}
}
}
}
@Composable
fun FeedItem(
item: FeedItemUI,
getMangaState: @Composable ((Manga) -> State<Manga>),
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
selection: List<Manga>,
// KMK <--
) {
when {
item.results == null -> {
GlobalSearchLoadingResultItem()
}
item.results.isEmpty() -> {
GlobalSearchErrorResultItem(message = stringResource(MR.strings.no_results_found))
}
else -> {
GlobalSearchCardRow(
titles = item.results,
getManga = getMangaState,
onClick = onClickManga,
// KMK -->
onLongClick = onLongClickManga,
selection = selection,
// KMK <--
)
}
}
}
@Composable
fun FeedAddDialog(
sources: ImmutableList<CatalogueSource>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = stringResource(SYMR.strings.feed))
},
text = {
RadioSelector(options = sources, selected = selected) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(selected?.let { sources[it] }) }) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
@Composable
fun FeedAddSearchDialog(
source: CatalogueSource,
savedSearches: ImmutableList<SavedSearch?>,
onDismiss: () -> Unit,
onClickAdd: (CatalogueSource, SavedSearch?) -> Unit,
) {
var selected by remember { mutableStateOf<Int?>(null) }
AlertDialog(
title = {
Text(text = source.name)
},
text = {
val context = LocalContext.current
val savedSearchStrings = remember {
savedSearches.map {
// KMK -->
it?.name ?: if (source.supportsLatest)
context.stringResource(MR.strings.latest)
else
context.stringResource(MR.strings.popular)
// KMK <--
}.toImmutableList()
}
RadioSelector(
options = savedSearches,
optionStrings = savedSearchStrings,
selected = selected,
) {
selected = it
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickAdd(source, selected?.let { savedSearches[it] }) }, /* KMK --> */ enabled = selected != null /* KMK <-- */) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
@Composable
fun <T> RadioSelector(
options: ImmutableList<T>,
selected: Int?,
optionStrings: ImmutableList<String> = remember { options.map { it.toString() }.toImmutableList() },
onSelectOption: (Int) -> Unit /* KMK --> */ = {} /* KMK <-- */,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
optionStrings.forEachIndexed { index, option ->
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.clickable { onSelectOption(index) },
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected == index, onClick = null)
Spacer(Modifier.width(4.dp))
Text(option, maxLines = 1)
}
}
}
}
@Composable
fun FeedDeleteConfirmDialog(
feed: FeedSavedSearch,
onDismiss: () -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(SYMR.strings.feed))
},
text = {
Text(text = stringResource(SYMR.strings.feed_delete))
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickDeleteConfirm(feed) }) {
Text(text = stringResource(MR.strings.action_delete))
}
},
)
}