feat(ui): Add sources search to Migrate Source Screen (#232)

This commit is contained in:
Tran M. Cuong 2024-08-06 03:38:09 +07:00 committed by Cuong-Tran
parent c0c395061b
commit 8da0a145bc
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
4 changed files with 148 additions and 62 deletions

View file

@ -1,5 +1,6 @@
package eu.kanade.presentation.browse
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -7,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ArrowUpward
@ -26,14 +28,16 @@ import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
@ -54,11 +58,17 @@ fun MigrateSourceScreen(
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
// KMK -->
onChangeSearchQuery: (String?) -> Unit,
// KMK <--
) {
val context = LocalContext.current
when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen(
// KMK -->
state.searchQuery.isNullOrBlank() &&
// KMK <--
state.isEmpty -> EmptyScreen(
stringRes = MR.strings.information_empty_library,
modifier = Modifier.padding(contentPadding),
)
@ -78,6 +88,10 @@ fun MigrateSourceScreen(
// SY -->
onClickAll = onClickAll,
// SY <--
// KMK -->
state = state,
onChangeSearchQuery = onChangeSearchQuery,
// KMK <--
)
}
}
@ -95,64 +109,94 @@ private fun MigrateSourceList(
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
// KMK -->
state: MigrateSourceScreenModel.State,
onChangeSearchQuery: (String?) -> Unit,
// KMK <--
) {
ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues,
) {
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
Row(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(start = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.migration_selection_prompt),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.header,
)
// KMK -->
val lazyListState = rememberLazyListState()
IconButton(onClick = onToggleSortingMode) {
when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
Icons.Outlined.SortByAlpha,
contentDescription = stringResource(MR.strings.action_sort_alpha),
)
SetMigrateSorting.Mode.TOTAL -> Icon(
Icons.Outlined.Numbers,
contentDescription = stringResource(MR.strings.action_sort_count),
)
BackHandler(enabled = !state.searchQuery.isNullOrBlank()) {
onChangeSearchQuery(null)
}
Column(
// Wrap around so we can use stickyHeader
modifier = Modifier.padding(contentPadding + topSmallPaddingValues),
) {
AnimatedFloatingSearchBox(
listState = lazyListState,
searchQuery = state.searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
placeholderText = stringResource(KMR.strings.action_search_for_source),
)
FastScrollLazyColumn(
state = lazyListState,
// contentPadding = contentPadding + topSmallPaddingValues,
// KMK <--
) {
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
Row(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(start = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.migration_selection_prompt),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.header,
)
IconButton(onClick = onToggleSortingMode) {
when (sortingMode) {
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
Icons.Outlined.SortByAlpha,
contentDescription = stringResource(MR.strings.action_sort_alpha),
)
SetMigrateSorting.Mode.TOTAL -> Icon(
Icons.Outlined.Numbers,
contentDescription = stringResource(MR.strings.action_sort_count),
)
}
}
}
IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(
Icons.Outlined.ArrowUpward,
contentDescription = stringResource(MR.strings.action_asc),
)
SetMigrateSorting.Direction.DESCENDING -> Icon(
Icons.Outlined.ArrowDownward,
contentDescription = stringResource(MR.strings.action_desc),
)
IconButton(onClick = onToggleSortingDirection) {
when (sortingDirection) {
SetMigrateSorting.Direction.ASCENDING -> Icon(
Icons.Outlined.ArrowUpward,
contentDescription = stringResource(MR.strings.action_asc),
)
SetMigrateSorting.Direction.DESCENDING -> Icon(
Icons.Outlined.ArrowDownward,
contentDescription = stringResource(MR.strings.action_desc),
)
}
}
}
}
}
items(
items = list,
key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) ->
MigrateSourceItem(
modifier = Modifier.animateItem(),
source = source,
count = count,
onClickItem = { onClickItem(source) },
onLongClickItem = { onLongClickItem(source) },
// SY -->
onClickAll = { onClickAll(source) },
// SY <--
)
items(
items = list,
key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) ->
MigrateSourceItem(
// KMK -->
// modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
// KMK <--
source = source,
count = count,
onClickItem = { onClickItem(source) },
onLongClickItem = { onLongClickItem(source) },
// SY -->
onClickAll = { onClickAll(source) },
// SY <--
)
}
}
}
}

View file

@ -5,14 +5,20 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.installedExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
@ -34,7 +40,29 @@ class MigrateSourceScreenModel(
init {
screenModelScope.launchIO {
getSourcesWithFavoriteCount.subscribe()
// KMK -->
combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
// KMK <--
getSourcesWithFavoriteCount.subscribe(),
// KMK -->
) { searchQuery, sourceCounts ->
val queryFilter: (String?) -> ((Pair<Source, Long>) -> Boolean) = { query ->
filter@{ pair ->
val source = pair.first
if (query.isNullOrBlank()) return@filter true
query.split(",").any {
val input = it.trim()
if (input.isEmpty()) return@any false
source.installedExtension?.name?.contains(input, ignoreCase = true) == true ||
source.name.contains(input, ignoreCase = true) ||
source.id == input.toLongOrNull()
}
}
}
sourceCounts.filter(queryFilter(searchQuery))
}
// KMK <--
.catch {
logcat(LogPriority.ERROR, it)
_channel.send(Event.FailedFetchingSourcesWithCount)
@ -80,12 +108,23 @@ class MigrateSourceScreenModel(
}
}
// KMK -->
fun search(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
}
}
// KMK <--
@Immutable
data class State(
val isLoading: Boolean = true,
val items: ImmutableList<Pair<Source, Long>> = persistentListOf(),
val sortingMode: SetMigrateSorting.Mode = SetMigrateSorting.Mode.ALPHABETICAL,
val sortingDirection: SetMigrateSorting.Direction = SetMigrateSorting.Direction.ASCENDING,
// KMK -->
val searchQuery: String? = null,
// KMK <--
) {
val isEmpty = items.isEmpty()
}

View file

@ -71,6 +71,9 @@ fun Screen.migrateSourceTab(): TabContent {
}
},
// SY <--
// KMK -->
onChangeSearchQuery = screenModel::search,
// KMK <--
)
},
)

View file

@ -196,6 +196,14 @@ class SourcesScreenModel(
}
// SY <--
fun showSourceDialog(source: Source) {
mutableState.update { it.copy(dialog = Dialog.SourceLongClick(source)) }
}
fun closeDialog() {
mutableState.update { it.copy(dialog = null) }
}
// KMK -->
fun search(query: String?) {
mutableState.update {
@ -210,14 +218,6 @@ class SourcesScreenModel(
}
// KMK <--
fun showSourceDialog(source: Source) {
mutableState.update { it.copy(dialog = Dialog.SourceLongClick(source)) }
}
fun closeDialog() {
mutableState.update { it.copy(dialog = null) }
}
sealed interface Event {
data object FailedFetchingSources : Event
}