diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig index 83ffed1..f36d2d1 100644 --- a/iosApp/Configuration/Config.xcconfig +++ b/iosApp/Configuration/Config.xcconfig @@ -1,4 +1,4 @@ -TEAM_ID= +TEAM_ID=S99A5B637C PRODUCT_NAME=SatsPrice PRODUCT_BUNDLE_IDENTIFIER=xyz.tyiu.SatsPrice diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 43169bd..f869f92 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -15,6 +15,15 @@ struct ContentView: View { } } .navigationTitle("SatsPrice") + #if os(iOS) + // macOS Lists support drag-to-reorder directly; iOS only shows reorder handles + // once edit mode is active, which this toggles. + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } + } + #endif } } @@ -102,7 +111,7 @@ struct ContentView: View { Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) { Button( - state.selectedCurrencyCodes.isEmpty + state.selectedCurrencyCodes.count <= 1 ? IosLocalizationKt.localizedString(resource: MR.strings.shared.add_currency) : IosLocalizationKt.localizedFormattedString( resource: MR.strings.shared.currencies_selected_count, @@ -112,14 +121,30 @@ struct ContentView: View { showCurrencyPicker = true } - ForEach(state.fiatRows, id: \.code) { row in + ForEach(Array(state.fiatRows.enumerated()), id: \.element.code) { index, row in amountRow( label: row.code, value: row.amount, keyboardType: .decimalPad, sanitize: sanitizeDecimalInput, - onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) } + onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) }, + onMoveUp: index > 0 ? { + var codes = state.fiatRows.map(\.code) + codes.move(fromOffsets: [index], toOffset: index - 1) + viewModel.onFiatCurrenciesReordered(codes) + } : nil, + onMoveDown: index < state.fiatRows.count - 1 ? { + var codes = state.fiatRows.map(\.code) + codes.move(fromOffsets: [index], toOffset: index + 2) + viewModel.onFiatCurrenciesReordered(codes) + } : nil ) + .deleteDisabled(row.code == state.defaultCurrencyCode) + } + .onMove { indices, newOffset in + var codes = state.fiatRows.map(\.code) + codes.move(fromOffsets: indices, toOffset: newOffset) + viewModel.onFiatCurrenciesReordered(codes) } #if os(iOS) .onDelete { indexSet in @@ -135,8 +160,9 @@ struct ContentView: View { #endif .sheet(isPresented: $showCurrencyPicker) { CurrencyPickerSheet( - currencies: state.availableCurrencies, - selectedCodes: state.selectedCurrencyCodes, + currentCurrency: state.currentCurrency, + selectedOtherCurrencies: state.selectedOtherCurrencies, + unselectedCurrencies: state.unselectedCurrencies, localeCurrencyCode: state.localeCurrencyCode, onToggle: { viewModel.onFiatCurrencyToggled($0) } ) @@ -149,9 +175,31 @@ struct ContentView: View { value: String, keyboardType: NumericFieldKeyboard, sanitize: @escaping (String) -> String, - onChange: @escaping (String) -> Void + onChange: @escaping (String) -> Void, + onMoveUp: (() -> Void)? = nil, + onMoveDown: (() -> Void)? = nil ) -> some View { HStack { + #if os(macOS) + // macOS's Form isn't List-backed, so .onMove's drag-to-reorder has no effect here; + // these buttons are macOS's equivalent of iOS's Edit-mode drag handles. + if onMoveUp != nil || onMoveDown != nil { + VStack(spacing: 2) { + Button(action: { onMoveUp?() }) { + Image(systemName: "chevron.up") + .frame(width: 16, height: 10) + } + .disabled(onMoveUp == nil) + Button(action: { onMoveDown?() }) { + Image(systemName: "chevron.down") + .frame(width: 16, height: 10) + } + .disabled(onMoveDown == nil) + } + .font(.system(size: 10)) + .buttonStyle(.borderless) + } + #endif Text(label) Spacer() NumericField( diff --git a/iosApp/iosApp/ConverterViewModel.swift b/iosApp/iosApp/ConverterViewModel.swift index 24c40a2..5ccb7f9 100644 --- a/iosApp/iosApp/ConverterViewModel.swift +++ b/iosApp/iosApp/ConverterViewModel.swift @@ -35,6 +35,10 @@ final class ConverterViewModel: ObservableObject { bridge.onFiatCurrencyToggled(code: code) } + func onFiatCurrenciesReordered(_ newOrder: [String]) { + bridge.onFiatCurrenciesReordered(newOrder: newOrder) + } + func onSourceSelected(_ name: String) { bridge.onSourceSelected(name: name) } diff --git a/iosApp/iosApp/CurrencyPickerSheet.swift b/iosApp/iosApp/CurrencyPickerSheet.swift index dac2ff9..3f65493 100644 --- a/iosApp/iosApp/CurrencyPickerSheet.swift +++ b/iosApp/iosApp/CurrencyPickerSheet.swift @@ -2,8 +2,9 @@ import Shared import SwiftUI struct CurrencyPickerSheet: View { - let currencies: [CurrencyInfo] - let selectedCodes: [String] + let currentCurrency: CurrencyInfo + let selectedOtherCurrencies: [CurrencyInfo] + let unselectedCurrencies: [CurrencyInfo] let localeCurrencyCode: String? let onToggle: (String) -> Void @@ -11,20 +12,24 @@ struct CurrencyPickerSheet: View { var body: some View { NavigationStack { - List(currencies, id: \.code) { info in - Button { - onToggle(info.code) - } label: { - HStack { - Text(currencyLabel(for: info)) - .foregroundColor(.primary) - Spacer() - if selectedCodes.contains(info.code) { - Image(systemName: "checkmark") - .foregroundColor(.accentColor) + List { + Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.current_currency_section_title)) { + currencyRow(for: currentCurrency, isSelected: true, onTap: nil) + } + + if !selectedOtherCurrencies.isEmpty { + Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.selected_currencies_section_title)) { + ForEach(selectedOtherCurrencies, 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) }) + } + } } .navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) #if os(iOS) @@ -44,6 +49,24 @@ struct CurrencyPickerSheet: View { #endif } + @ViewBuilder + private func currencyRow(for info: CurrencyInfo, isSelected: Bool, onTap: (() -> Void)?) -> some View { + let content = HStack { + Text(currencyLabel(for: info)) + .foregroundColor(.primary) + Spacer() + if isSelected { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + if let onTap { + Button(action: onTap) { content } + } else { + content + } + } + private func currencyLabel(for info: CurrencyInfo) -> String { if info.code == localeCurrencyCode { return IosLocalizationKt.localizedFormattedString( diff --git a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt index 82d08c7..d94c589 100644 --- a/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt +++ b/shared/src/appleMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt @@ -6,9 +6,12 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import xyz.tyiu.satsprice.ui.ConverterUiState import xyz.tyiu.satsprice.ui.PriceViewModel +import xyz.tyiu.satsprice.ui.currentCurrency import xyz.tyiu.satsprice.ui.defaultCurrencyRate import xyz.tyiu.satsprice.ui.exceedsMaxSupply +import xyz.tyiu.satsprice.ui.selectedOtherCurrencies import xyz.tyiu.satsprice.ui.statusLine +import xyz.tyiu.satsprice.ui.unselectedCurrencies data class FiatRow(val code: String, val amount: String, val rateDisplay: String) @@ -17,7 +20,9 @@ data class IosConverterState( val satsAmount: String, val exceedsMaxSupply: Boolean, val fiatRows: List, - val availableCurrencies: List, + val currentCurrency: CurrencyInfo, + val selectedOtherCurrencies: List, + val unselectedCurrencies: List, val selectedCurrencyCodes: List, val localeCurrencyCode: String?, val defaultCurrencyCode: String, @@ -53,6 +58,7 @@ class IosPriceViewModel { fun onSatsAmountChanged(value: String) = viewModel.onSatsAmountChanged(value) fun onFiatAmountChanged(code: String, value: String) = viewModel.onFiatAmountChanged(code, value) fun onFiatCurrencyToggled(code: String) = viewModel.onFiatCurrencyToggled(code) + fun onFiatCurrenciesReordered(newOrder: List) = viewModel.onFiatCurrenciesReordered(newOrder) fun onSourceSelected(name: String) = viewModel.onSourceSelected(name) fun onManualRateChanged(value: String) = viewModel.onManualRateChanged(value) } @@ -64,7 +70,9 @@ private fun ConverterUiState.toIosState(): IosConverterState = IosConverterState fiatRows = selectedFiatCurrencies.map { code -> FiatRow(code, fiatAmounts[code].orEmpty(), rateDisplays[code].orEmpty()) }, - availableCurrencies = availableFiatCurrencies, + currentCurrency = currentCurrency(), + selectedOtherCurrencies = selectedOtherCurrencies(), + unselectedCurrencies = unselectedCurrencies(), selectedCurrencyCodes = selectedFiatCurrencies, localeCurrencyCode = localeCurrencyCode, defaultCurrencyCode = defaultCurrencyCode, diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt index 63fb9bc..ca488e2 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt @@ -2,6 +2,7 @@ package xyz.tyiu.satsprice.ui import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import xyz.tyiu.satsprice.CurrencyInfo import xyz.tyiu.satsprice.domain.CurrencyConverter import xyz.tyiu.satsprice.domain.toBigDecimalOrNull import kotlin.time.Instant @@ -23,6 +24,19 @@ fun ConverterUiState.exceedsMaxSupply(): Boolean { fun ConverterUiState.defaultCurrencyRate(): String = rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: "" +/** The pinned, non-removable "Current Currency" shown in the currency picker's own section. */ +fun ConverterUiState.currentCurrency(): CurrencyInfo = + availableFiatCurrencies.find { it.code == defaultCurrencyCode } ?: CurrencyInfo(defaultCurrencyCode, defaultCurrencyCode) + +/** Selected currencies other than [ConverterUiState.defaultCurrencyCode], in the user's chosen order. */ +fun ConverterUiState.selectedOtherCurrencies(): List = + selectedFiatCurrencies.filter { it != defaultCurrencyCode } + .mapNotNull { code -> availableFiatCurrencies.find { it.code == code } } + +/** Currencies not yet added, offered in the currency picker's "Currencies" section. */ +fun ConverterUiState.unselectedCurrencies(): List = + availableFiatCurrencies.filterNot { it.code in selectedFiatCurrencies } + private fun Instant.toDateTimeString(): String { val local = toLocalDateTime(TimeZone.currentSystemDefault()) return local.toString().substringBefore('.').replace('T', ' ') 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 dc90c7b..4a0cb55 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt @@ -1,6 +1,7 @@ package xyz.tyiu.satsprice.ui import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,11 +19,14 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -58,6 +62,16 @@ fun PriceScreen( viewModel: PriceViewModel = viewModel { PriceViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + var showCurrencyPicker by remember { mutableStateOf(false) } + + if (showCurrencyPicker) { + CurrencyPickerScreen( + state = state, + onToggle = viewModel::onFiatCurrencyToggled, + onDone = { showCurrencyPicker = false }, + ) + return + } Surface( modifier = Modifier.fillMaxSize(), @@ -207,21 +221,41 @@ fun PriceScreen( verticalAlignment = Alignment.CenterVertically, ) { Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.titleMedium) - MultiCurrencySelector( - selectedCodes = state.selectedFiatCurrencies, - options = state.availableFiatCurrencies, - localeCurrencyCode = state.localeCurrencyCode, - onToggle = viewModel::onFiatCurrencyToggled, - ) + OutlinedButton(onClick = { showCurrencyPicker = true }) { + Text( + if (state.selectedFiatCurrencies.size <= 1) { + stringResource(MR.strings.add_currency) + } else { + stringResource(MR.strings.currencies_selected_count, state.selectedFiatCurrencies.size) + }, + ) + } } - state.selectedFiatCurrencies.forEach { code -> + state.selectedFiatCurrencies.forEachIndexed { index, code -> key(code) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { + CurrencyRowMenu( + code = code, + canMoveUp = index > 0, + canMoveDown = index < state.selectedFiatCurrencies.lastIndex, + canRemove = code != state.defaultCurrencyCode, + onMoveUp = { + viewModel.onFiatCurrenciesReordered( + state.selectedFiatCurrencies.moved(index, index - 1), + ) + }, + onMoveDown = { + viewModel.onFiatCurrenciesReordered( + state.selectedFiatCurrencies.moved(index, index + 1), + ) + }, + onRemove = { viewModel.onFiatCurrencyToggled(code) }, + ) OutlinedTextField( value = state.fiatAmounts[code].orEmpty(), onValueChange = { viewModel.onFiatAmountChanged(code, it) }, @@ -230,15 +264,6 @@ fun PriceScreen( singleLine = true, modifier = Modifier.weight(1f), ) - IconButton(onClick = { viewModel.onFiatCurrencyToggled(code) }) { - Icon( - Icons.Default.Close, - contentDescription = stringResource( - MR.strings.remove_currency_content_description, - code, - ), - ) - } } } } @@ -248,42 +273,176 @@ fun PriceScreen( } } +private fun List.moved(fromIndex: Int, toIndex: Int): List = + toMutableList().apply { add(toIndex, removeAt(fromIndex)) } + @Composable -private fun MultiCurrencySelector( - selectedCodes: List, - options: List, - localeCurrencyCode: String?, - onToggle: (String) -> Unit, +private fun CurrencyRowMenu( + code: String, + canMoveUp: Boolean, + canMoveDown: Boolean, + canRemove: Boolean, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, + onRemove: () -> Unit, ) { var expanded by remember { mutableStateOf(false) } Box { - OutlinedButton(onClick = { expanded = true }) { - Text( - if (selectedCodes.isEmpty()) { - stringResource(MR.strings.add_currency) - } else { - stringResource(MR.strings.currencies_selected_count, selectedCodes.size) - }, + IconButton(onClick = { expanded = true }) { + Icon( + Icons.Default.MoreVert, + contentDescription = stringResource(MR.strings.currency_options_content_description, code), ) } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - options.forEach { info -> - val checked = info.code in selectedCodes - val label = if (info.code == localeCurrencyCode) { - stringResource(MR.strings.currency_option_label_local, info.code, info.displayName) - } else { - stringResource(MR.strings.currency_option_label, info.code, info.displayName) - } + DropdownMenuItem( + text = { Text(stringResource(MR.strings.move_currency_up_content_description, code)) }, + leadingIcon = { Icon(Icons.Default.KeyboardArrowUp, contentDescription = null) }, + enabled = canMoveUp, + onClick = { + expanded = false + onMoveUp() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(MR.strings.move_currency_down_content_description, code)) }, + leadingIcon = { Icon(Icons.Default.KeyboardArrowDown, contentDescription = null) }, + enabled = canMoveDown, + onClick = { + expanded = false + onMoveDown() + }, + ) + if (canRemove) { DropdownMenuItem( - text = { Text(label) }, - leadingIcon = { Checkbox(checked = checked, onCheckedChange = { onToggle(info.code) }) }, - onClick = { onToggle(info.code) }, + text = { Text(stringResource(MR.strings.remove_currency_content_description, code)) }, + leadingIcon = { Icon(Icons.Default.Close, contentDescription = null) }, + onClick = { + expanded = false + onRemove() + }, ) } } } } +/** + * A full-screen takeover (rather than a dropdown) matching the previous Skip-based app's + * dedicated currency selection screen: a pinned, non-removable "Current Currency", the other + * currencies the user added (removable), and the remaining ones available to add. + */ +@Composable +private fun CurrencyPickerScreen( + state: ConverterUiState, + onToggle: (String) -> Unit, + onDone: () -> Unit, +) { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)), + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = screenHorizontalPadding, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.headlineSmall) + TextButton(onClick = onDone) { Text(stringResource(MR.strings.done)) } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = screenHorizontalPadding, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + val selectedOthers = state.selectedOtherCurrencies() + + Column { + Text( + stringResource(MR.strings.current_currency_section_title), + style = MaterialTheme.typography.titleMedium, + ) + CurrencyRow( + info = state.currentCurrency(), + isSelected = true, + localeCurrencyCode = state.localeCurrencyCode, + onClick = null, + ) + } + + if (selectedOthers.isNotEmpty()) { + Column { + Text( + stringResource(MR.strings.selected_currencies_section_title), + style = MaterialTheme.typography.titleMedium, + ) + selectedOthers.forEach { info -> + key(info.code) { + CurrencyRow( + info = info, + isSelected = true, + localeCurrencyCode = state.localeCurrencyCode, + onClick = { onToggle(info.code) }, + ) + } + } + } + } + + Column { + Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.titleMedium) + state.unselectedCurrencies().forEach { info -> + key(info.code) { + CurrencyRow( + info = info, + isSelected = false, + localeCurrencyCode = state.localeCurrencyCode, + onClick = { onToggle(info.code) }, + ) + } + } + } + } + } + } +} + +@Composable +private fun CurrencyRow( + info: CurrencyInfo, + isSelected: Boolean, + localeCurrencyCode: String?, + onClick: (() -> Unit)?, +) { + val label = if (info.code == localeCurrencyCode) { + stringResource(MR.strings.currency_option_label_local, info.code, info.displayName) + } else { + stringResource(MR.strings.currency_option_label, info.code, info.displayName) + } + Row( + modifier = Modifier + .fillMaxWidth() + .let { if (onClick != null) it.clickable(onClick = onClick) else it } + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(label) + if (isSelected) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } +} + @Composable private fun DropdownSelector( selectedLabel: String, diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt index 95284fd..ac77d48 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt @@ -113,8 +113,12 @@ class PriceViewModel( matching + rest } val availableCodes = available.map { it.code }.toSet() - val selection = state.selectedFiatCurrencies.filter { it in availableCodes } - .ifEmpty { listOfNotNull(available.firstOrNull()?.code) } + // defaultCurrencyCode is the pinned, non-removable "Current Currency" and must + // always be present, even if it briefly lacks a rate; everything else is + // dropped once its rate disappears. Filtering (rather than re-deriving) keeps + // whatever order the user picked via onFiatCurrenciesReordered. + val filtered = state.selectedFiatCurrencies.filter { it == defaultCurrencyCode || it in availableCodes } + val selection = if (defaultCurrencyCode in filtered) filtered else listOf(defaultCurrencyCode) + filtered recomputeFromKnownField( state.copy( isLoading = false, @@ -141,7 +145,9 @@ class PriceViewModel( fun onFiatAmountChanged(code: String, value: String) = updateAmount(EditedField.Fiat(code), sanitizeDecimalInput(value)) + /** [defaultCurrencyCode] is the pinned "Current Currency" and can't be removed. */ fun onFiatCurrencyToggled(code: String) { + if (code == defaultCurrencyCode) return _uiState.update { state -> val newSelection = if (code in state.selectedFiatCurrencies) { state.selectedFiatCurrencies - code @@ -153,6 +159,23 @@ class PriceViewModel( } } + /** + * Reorders the selected currencies to [newOrder]. Any code in [newOrder] that isn't + * currently selected is ignored, and any currently-selected code missing from [newOrder] + * keeps its relative position at the end — callers only need to describe the reordering of + * codes they know about (e.g. a Swift `List.onMove`'s resulting order). + */ + fun onFiatCurrenciesReordered(newOrder: List) { + _uiState.update { state -> + val known = newOrder.filter { it in state.selectedFiatCurrencies } + val missing = state.selectedFiatCurrencies.filterNot { it in known } + val newSelection = known + missing + if (newSelection == state.selectedFiatCurrencies) return@update state + val newState = state.copy(selectedFiatCurrencies = newSelection) + rates?.let { recomputeFromKnownField(newState, it) } ?: newState + } + } + fun onSourceSelected(displayName: String) { val selected = sources.firstOrNull { it.displayName == displayName } ?: return currentSource = selected diff --git a/shared/src/commonMain/moko-resources/base/strings.xml b/shared/src/commonMain/moko-resources/base/strings.xml index 01772eb..be5c017 100644 --- a/shared/src/commonMain/moko-resources/base/strings.xml +++ b/shared/src/commonMain/moko-resources/base/strings.xml @@ -9,10 +9,15 @@ BTC Exceeds the 21,000,000 BTC maximum supply Currencies + Current Currency + Selected currencies Add currency %1$d selected Remove %1$s + Move %1$s up + Move %1$s down + Options for %1$s %1$s - %2$s - %1$s - %2$s (Local) + %1$s - %2$s Done