diff --git a/iosApp/iosApp/CurrencyPickerSheet.swift b/iosApp/iosApp/CurrencyPickerSheet.swift
index ddc4d2e..bfc8780 100644
--- a/iosApp/iosApp/CurrencyPickerSheet.swift
+++ b/iosApp/iosApp/CurrencyPickerSheet.swift
@@ -11,28 +11,43 @@ struct CurrencyPickerSheet: View {
let onToggle: (String) -> Void
@Environment(\.dismiss) private var dismiss
+ @State private var searchQuery = ""
+
+ private func matches(_ info: CurrencyInfo) -> Bool {
+ SystemCurrenciesKt.matchesCurrencySearch(info: info, query: searchQuery)
+ }
var body: some View {
NavigationStack {
List {
- Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.current_currency_section_title)) {
- currencyRow(for: currentCurrency, isSelected: true, onTap: nil)
+ if matches(currentCurrency) {
+ Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.current_currency_section_title)) {
+ currencyRow(for: currentCurrency, isSelected: true, onTap: nil)
+ }
}
- if !selectedOtherCurrencies.isEmpty {
+ let matchingSelectedOthers = selectedOtherCurrencies.filter(matches)
+ if !matchingSelectedOthers.isEmpty {
Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.selected_currencies_section_title)) {
- ForEach(selectedOtherCurrencies, id: \.code) { info in
+ ForEach(matchingSelectedOthers, id: \.code) { info in
currencyRow(for: info, isSelected: true, onTap: { onToggle(info.code) })
}
}
}
- Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) {
- ForEach(unselectedCurrencies, id: \.code) { info in
- currencyRow(for: info, isSelected: false, onTap: { onToggle(info.code) })
+ let matchingUnselected = unselectedCurrencies.filter(matches)
+ if !matchingUnselected.isEmpty {
+ Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) {
+ ForEach(matchingUnselected, id: \.code) { info in
+ currencyRow(for: info, isSelected: false, onTap: { onToggle(info.code) })
+ }
}
}
}
+ .searchable(
+ text: $searchQuery,
+ prompt: IosLocalizationKt.localizedString(resource: MR.strings.shared.search_currencies_placeholder)
+ )
.navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt
index 9fe195a..95c3459 100644
--- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt
+++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt
@@ -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
diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt
index bec13c8..dc411e1 100644
--- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt
+++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt
@@ -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) },
+ )
+ }
}
}
}
diff --git a/shared/src/commonMain/moko-resources/base/strings.xml b/shared/src/commonMain/moko-resources/base/strings.xml
index 85d6ea3..85998ae 100644
--- a/shared/src/commonMain/moko-resources/base/strings.xml
+++ b/shared/src/commonMain/moko-resources/base/strings.xml
@@ -3,6 +3,7 @@
Bitcoin
BTC
1 BTC to %1$s
+ Clear search
Currencies
%1$d selected
Current Currency
@@ -22,5 +23,6 @@
Remove
Retry
Sats
+ Search currencies
Selected Currencies
diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt
index de273fe..ecacf10 100644
--- a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt
+++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt
@@ -2,6 +2,7 @@ package xyz.tyiu.satsprice
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SystemCurrenciesTest {
@@ -22,4 +23,17 @@ class SystemCurrenciesTest {
assertEquals(0, currencyDecimalDigits("JPY"))
assertEquals(3, currencyDecimalDigits("BHD"))
}
+
+ @Test
+ fun matchesCurrencySearch_matchesByCodeOrDisplayNameCaseInsensitively() {
+ val usd = CurrencyInfo("USD", "US Dollar")
+
+ assertTrue(matchesCurrencySearch(usd, ""))
+ assertTrue(matchesCurrencySearch(usd, "usd"))
+ assertTrue(matchesCurrencySearch(usd, "USD"))
+ assertTrue(matchesCurrencySearch(usd, "dollar"))
+ assertTrue(matchesCurrencySearch(usd, "US Doll"))
+ assertFalse(matchesCurrencySearch(usd, "EUR"))
+ assertFalse(matchesCurrencySearch(usd, "Euro"))
+ }
}