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) }