komikku/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt
Cuong-Tran fa7b18590e
Allow migrate mangas when using Bulk-favorite (#824)
* Combine ChangeMangaCategoryDialog

* Allow migrating a single manga when using Bulk-favorite

* Allow migrating mangas one by one when using Bulk-favorite

* set visibility

* Fix duplicate toggleSelection after migration

* After migration, continue adding favorites if there are more to bulk-favorite or push new Manga screen if it's a single manga being added by long-click

* fix usage of ChangeMangaCategory

* improve toggleSelection code

* refactoring ChangeMangasCategory

* switch LocalNavigator.currentOrThrow to current

* split composable to `presentation`

* hoisting the state to a common parent composable

* Fix migration dialog dismissed when clicking on Show entry

* Don't push new Manga screen after migration of duplicated entries everywhere
2025-04-09 11:42:43 +07:00

361 lines
13 KiB
Kotlin

package eu.kanade.presentation.browse
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
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.bulkSelectionButton
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.BulkSelectionToolbar
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
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.kmk.KMR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.plus
sealed class SourceFeedUI {
abstract val id: Long
abstract val title: Any
abstract val results: List<Manga>?
abstract fun withResults(results: List<Manga>?): SourceFeedUI
data class Latest(override val results: List<Manga>?) : SourceFeedUI() {
override val id: Long = -1
override val title: StringResource
get() = MR.strings.latest
override fun withResults(results: List<Manga>?): SourceFeedUI {
return copy(results = results)
}
}
data class Browse(override val results: List<Manga>?) : SourceFeedUI() {
override val id: Long = -2
override val title: StringResource
get() = MR.strings.browse
override fun withResults(results: List<Manga>?): SourceFeedUI {
return copy(results = results)
}
}
data class SourceSavedSearch(
val feed: FeedSavedSearch,
val savedSearch: SavedSearch,
override val results: List<Manga>?,
) : SourceFeedUI() {
override val id: Long
get() = feed.id
override val title: String
get() = savedSearch.name
override fun withResults(results: List<Manga>?): SourceFeedUI {
return copy(results = results)
}
}
}
@Composable
fun SourceFeedScreen(
name: String,
isLoading: Boolean,
items: ImmutableList<SourceFeedUI>,
hasFilters: Boolean,
onFabClick: () -> Unit,
onClickBrowse: () -> Unit,
onClickLatest: () -> Unit,
onClickSavedSearch: (SavedSearch) -> Unit,
// KMK -->
// onClickDelete: (FeedSavedSearch) -> Unit,
onLongClickFeed: (SourceFeedUI.SourceSavedSearch) -> Unit,
// KMK <--
onClickManga: (Manga) -> Unit,
onClickSearch: (String) -> Unit,
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
getMangaState: @Composable (Manga) -> State<Manga>,
// KMK -->
navigateUp: () -> Unit,
onWebViewClick: (() -> Unit)?,
onSourceSettingClick: (() -> Unit?)?,
onSortFeedClick: (() -> Unit)?,
onLongClickManga: (Manga) -> Unit,
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
// KMK <--
) {
// KMK -->
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
// KMK <--
Scaffold(
topBar = { scrollBehavior ->
// KMK -->
if (bulkFavoriteState.selectionMode) {
BulkSelectionToolbar(
selectedCount = bulkFavoriteState.selection.size,
isRunning = bulkFavoriteState.isRunning,
onClickClearSelection = bulkFavoriteScreenModel::toggleSelectionMode,
onChangeCategoryClick = bulkFavoriteScreenModel::addFavorite,
onSelectAll = {
items.forEach {
it.results?.forEach { manga ->
bulkFavoriteScreenModel.select(manga)
}
}
},
onReverseSelection = {
bulkFavoriteScreenModel.reverseSelection(
items.mapNotNull { it.results }
.flatten(),
)
},
)
} else {
// KMK <--
SourceFeedToolbar(
title = name,
searchQuery = searchQuery,
onSearchQueryChange = onSearchQueryChange,
scrollBehavior = scrollBehavior,
onClickSearch = onClickSearch,
// KMK -->
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onSourceSettingClick = onSourceSettingClick,
onSortFeedClick = onSortFeedClick,
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
isRunning = bulkFavoriteState.isRunning,
// KMK <--
)
}
},
floatingActionButton = {
BrowseSourceFloatingActionButton(
isVisible = hasFilters,
onFabClick = onFabClick,
)
},
) { paddingValues ->
Crossfade(targetState = isLoading, label = "source_feed") { state ->
when (state) {
true -> LoadingScreen()
false -> {
SourceFeedList(
items = items,
paddingValues = paddingValues,
getMangaState = getMangaState,
onClickBrowse = onClickBrowse,
onClickLatest = onClickLatest,
onClickSavedSearch = onClickSavedSearch,
// KMK -->
// onClickDelete = onClickDelete,
onLongClickFeed = onLongClickFeed,
// KMK <--
onClickManga = onClickManga,
// KMK -->
onLongClickManga = onLongClickManga,
selection = bulkFavoriteState.selection,
// KMK <--
)
}
}
}
}
}
@Composable
fun SourceFeedList(
items: ImmutableList<SourceFeedUI>,
paddingValues: PaddingValues,
getMangaState: @Composable ((Manga) -> State<Manga>),
onClickBrowse: () -> Unit,
onClickLatest: () -> Unit,
onClickSavedSearch: (SavedSearch) -> Unit,
// KMK -->
// onClickDelete: (FeedSavedSearch) -> Unit,
onLongClickFeed: (SourceFeedUI.SourceSavedSearch) -> Unit,
// KMK <--
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
selection: List<Manga>,
// KMK <--
) {
ScrollbarLazyColumn(
contentPadding = paddingValues + topSmallPaddingValues,
) {
// KMK -->
items(
items,
key = { "source-feed-${it.id}" },
) { item ->
// KMK <--
GlobalSearchResultItem(
modifier = Modifier.animateItem(),
title =
// KMK -->
if (item !is SourceFeedUI.SourceSavedSearch) {
stringResource(item.title as StringResource)
} else {
// KMK <--
item.title
},
subtitle = null,
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {
{
// KMK -->
onLongClickFeed(item)
// KMK <--
}
} else {
null
},
onClick = when (item) {
is SourceFeedUI.Browse -> onClickBrowse
is SourceFeedUI.Latest -> onClickLatest
is SourceFeedUI.SourceSavedSearch -> {
{ onClickSavedSearch(item.savedSearch) }
}
},
) {
SourceFeedItem(
item = item,
getMangaState = { getMangaState(it) },
onClickManga = onClickManga,
// KMK -->
onLongClickManga = onLongClickManga,
selection = selection,
// KMK <--
)
}
}
}
}
@Composable
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 {
results == null -> {
GlobalSearchLoadingResultItem()
}
results.isEmpty() -> {
GlobalSearchErrorResultItem(message = stringResource(MR.strings.no_results_found))
}
else -> {
GlobalSearchCardRow(
titles = item.results.orEmpty(),
getManga = getMangaState,
onClick = onClickManga,
// KMK -->
onLongClick = onLongClickManga,
selection = selection,
// KMK <--
)
}
}
}
@Composable
fun SourceFeedToolbar(
title: String,
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
onClickSearch: (String) -> Unit,
// KMK -->
navigateUp: () -> Unit,
onWebViewClick: (() -> Unit)?,
onSourceSettingClick: (() -> Unit?)?,
onSortFeedClick: (() -> Unit)?,
toggleSelectionMode: () -> Unit,
isRunning: Boolean,
// KMK <--
) {
SearchToolbar(
titleContent = { AppBarTitle(title) },
searchQuery = searchQuery,
onChangeSearchQuery = onSearchQueryChange,
onSearch = onClickSearch,
// KMK -->
navigateUp = navigateUp,
onClickCloseSearch = navigateUp,
// KMK <--
scrollBehavior = scrollBehavior,
placeholderText = stringResource(MR.strings.action_search_hint),
// KMK -->
actions = {
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
add(bulkSelectionButton(isRunning, toggleSelectionMode))
onWebViewClick?.let {
add(
AppBar.Action(
title = stringResource(MR.strings.action_web_view),
onClick = { onWebViewClick() },
icon = Icons.Outlined.Public,
),
)
}
onSortFeedClick?.let {
add(
AppBar.OverflowAction(
title = stringResource(KMR.strings.action_sort_feed),
onClick = { onSortFeedClick() },
),
)
}
onSourceSettingClick?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.label_settings),
onClick = { onSourceSettingClick() },
),
)
}
}
.build(),
)
},
// KMK <--
)
}