From 583b9bd14ecb1215f823967be0c608e09e597f22 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Wed, 9 Sep 2026 09:03:18 +0300 Subject: [PATCH] Hide refresh for Manual, seed its rate from another source Refreshing has no meaning for a manually-typed rate, so the button (and its loading spinner) is now hidden while Manual is selected. Switching to Manual for the first time now seeds its rate field from whichever source was active before, falling back to any other source's last cached rate for the default currency. When neither is available, the rate and amount fields are left empty instead of showing stale values. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy --- iosApp/iosApp/ContentView.swift | 18 ++++++------ .../xyz/tyiu/satsprice/ui/PriceScreen.kt | 18 ++++++------ .../xyz/tyiu/satsprice/ui/PriceViewModel.kt | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 8c44246..8edc792 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -65,15 +65,17 @@ struct ContentView: View { } else { Spacer() } - if state.isLoading { - ProgressView() - } else { - Button { - viewModel.refresh() - } label: { - Image(systemName: "arrow.clockwise") + if !state.isManualSource { + if state.isLoading { + ProgressView() + } else { + Button { + viewModel.refresh() + } label: { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.borderless) } - .buttonStyle(.borderless) } } } 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 d25a985..0432e46 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt @@ -195,14 +195,16 @@ fun PriceScreen( Spacer(modifier = Modifier.weight(1f)) } } - if (state.isLoading) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - } else { - IconButton(onClick = { viewModel.refresh() }) { - Icon( - Icons.Default.Refresh, - contentDescription = stringResource(MR.strings.refresh_content_description), - ) + if (!state.isManualSource) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } else { + IconButton(onClick = { viewModel.refresh() }) { + Icon( + Icons.Default.Refresh, + contentDescription = stringResource(MR.strings.refresh_content_description), + ) + } } } } 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 78cab3c..a40c00b 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt @@ -207,6 +207,7 @@ class PriceViewModel( fun onSourceSelected(displayName: String) { val selected = sources.firstOrNull { it.displayName == displayName } ?: return + val previousRates = rates currentSource = selected rates = null _uiState.update { @@ -216,6 +217,27 @@ class PriceViewModel( rateDisplays = emptyMap(), ) } + + if (selected === manualSource && manualSource.rate == null) { + // Switching to Manual for the first time: seed it with whatever rate is already + // known for the default currency — the source just switched away from, or else + // whatever other source was last fetched — rather than starting blank. + viewModelScope.launch { + selectedSourceStore.saveSelectedSourceId(selected.id) + val seedRate = previousRates?.rates?.get(defaultCurrencyCode) ?: fallbackManualRate() + if (seedRate != null) { + manualSource.rate = seedRate + _uiState.update { + it.copy(manualRateInput = formatAmount(seedRate, decimalDigitsFor(defaultCurrencyCode))) + } + } else { + _uiState.update { it.copy(pricedCurrencyCodes = emptySet()) } + } + refresh() + } + return + } + viewModelScope.launch { selectedSourceStore.saveSelectedSourceId(selected.id) seedFromCache(selected) @@ -223,6 +245,12 @@ class PriceViewModel( } } + /** The default currency's last known rate from any non-Manual source's cache, if any. */ + private suspend fun fallbackManualRate(): BigDecimal? = + sources.filterNot { it === manualSource }.firstNotNullOfOrNull { source -> + exchangeRateStore.loadLastKnownRates(source.id)?.rates?.get(defaultCurrencyCode) + } + fun onManualRateChanged(value: String) { val sanitized = sanitizeDecimalInput(value) _uiState.update { it.copy(manualRateInput = sanitized) }