- 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
282 lines
9.8 KiB
Kotlin
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))
|
|
}
|
|
},
|
|
)
|
|
}
|