feat(ui): Add sources search to Migrate Source Screen (#232)
This commit is contained in:
parent
c0c395061b
commit
8da0a145bc
4 changed files with 148 additions and 62 deletions
|
|
@ -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 <--
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ fun Screen.migrateSourceTab(): TabContent {
|
|||
}
|
||||
},
|
||||
// SY <--
|
||||
// KMK -->
|
||||
onChangeSearchQuery = screenModel::search,
|
||||
// KMK <--
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue