Add search to the currency picker
Filters by code or display name, case-insensitively, across all three sections (current, selected, and remaining currencies) — a section disappears entirely once nothing in it matches. The matching logic lives in shared code (matchesCurrencySearch) so Compose and SwiftUI stay in sync; SwiftUI wires it into the native .searchable() field, Compose gets a plain search text field with a clear button. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
@@ -2,6 +2,16 @@ package xyz.tyiu.satsprice
|
||||
|
||||
data class CurrencyInfo(val code: String, val displayName: String)
|
||||
|
||||
/**
|
||||
* Whether [info]'s code or display name contains [query], case-insensitively. A plain function
|
||||
* (rather than a `CurrencyInfo` extension) so Kotlin/Native exports a predictable, positionally
|
||||
* clear Swift signature, matching [currencyFlagEmoji]'s style.
|
||||
*/
|
||||
fun matchesCurrencySearch(info: CurrencyInfo, query: String): Boolean =
|
||||
query.isBlank() ||
|
||||
info.code.contains(query, ignoreCase = true) ||
|
||||
info.displayName.contains(query, ignoreCase = true)
|
||||
|
||||
/**
|
||||
* ISO 4217 codes for the precious metals actively traded today. These aren't tied to any
|
||||
* country, so a currently-used-currency filter derived from country/locale data (as the
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -70,6 +71,7 @@ import xyz.tyiu.satsprice.domain.CurrencyConverter
|
||||
import xyz.tyiu.satsprice.domain.formatAmount
|
||||
import xyz.tyiu.satsprice.domain.groupDigits
|
||||
import xyz.tyiu.satsprice.domain.localizedDecimalSeparator
|
||||
import xyz.tyiu.satsprice.matchesCurrencySearch
|
||||
import xyz.tyiu.satsprice.shared.MR
|
||||
|
||||
private val SectionColors
|
||||
@@ -497,6 +499,28 @@ private fun CurrencyPickerScreen(
|
||||
TextButton(onClick = onDone) { Text(stringResource(MR.strings.done)) }
|
||||
}
|
||||
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text(stringResource(MR.strings.search_currencies_placeholder)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = if (searchQuery.isNotEmpty()) {
|
||||
{
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(MR.strings.clear_search_content_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = screenHorizontalPadding),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -504,22 +528,26 @@ private fun CurrencyPickerScreen(
|
||||
.padding(horizontal = screenHorizontalPadding, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val selectedOthers = state.selectedOtherCurrencies()
|
||||
val currentCurrency = state.currentCurrency()
|
||||
val selectedOthers = state.selectedOtherCurrencies().filter { matchesCurrencySearch(it, searchQuery) }
|
||||
val unselected = state.unselectedCurrencies().filter { matchesCurrencySearch(it, searchQuery) }
|
||||
|
||||
Column {
|
||||
Text(
|
||||
stringResource(MR.strings.current_currency_section_title).uppercase(),
|
||||
style = SectionHeaderStyle,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
CurrencyRow(
|
||||
info = state.currentCurrency(),
|
||||
isSelected = true,
|
||||
isPriced = state.isPriced(state.currentCurrency().code),
|
||||
sourceName = state.sourceName,
|
||||
localeCurrencyCode = state.localeCurrencyCode,
|
||||
onClick = null,
|
||||
)
|
||||
if (matchesCurrencySearch(currentCurrency, searchQuery)) {
|
||||
Column {
|
||||
Text(
|
||||
stringResource(MR.strings.current_currency_section_title).uppercase(),
|
||||
style = SectionHeaderStyle,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
CurrencyRow(
|
||||
info = currentCurrency,
|
||||
isSelected = true,
|
||||
isPriced = state.isPriced(currentCurrency.code),
|
||||
sourceName = state.sourceName,
|
||||
localeCurrencyCode = state.localeCurrencyCode,
|
||||
onClick = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedOthers.isNotEmpty()) {
|
||||
@@ -544,22 +572,24 @@ private fun CurrencyPickerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
stringResource(MR.strings.currencies_section_title).uppercase(),
|
||||
style = SectionHeaderStyle,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
state.unselectedCurrencies().forEach { info ->
|
||||
key(info.code) {
|
||||
CurrencyRow(
|
||||
info = info,
|
||||
isSelected = false,
|
||||
isPriced = state.isPriced(info.code),
|
||||
sourceName = state.sourceName,
|
||||
localeCurrencyCode = state.localeCurrencyCode,
|
||||
onClick = { onToggle(info.code) },
|
||||
)
|
||||
if (unselected.isNotEmpty()) {
|
||||
Column {
|
||||
Text(
|
||||
stringResource(MR.strings.currencies_section_title).uppercase(),
|
||||
style = SectionHeaderStyle,
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
unselected.forEach { info ->
|
||||
key(info.code) {
|
||||
CurrencyRow(
|
||||
info = info,
|
||||
isSelected = false,
|
||||
isPriced = state.isPriced(info.code),
|
||||
sourceName = state.sourceName,
|
||||
localeCurrencyCode = state.localeCurrencyCode,
|
||||
onClick = { onToggle(info.code) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user