Add support for macOS and backport to older OS versions

This commit is contained in:
2026-09-03 20:06:59 +03:00
parent abf7cf2e83
commit deef01a58a
13 changed files with 123 additions and 39 deletions
@@ -1,78 +0,0 @@
package xyz.tyiu.satsprice
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.defaultCurrencyRate
import xyz.tyiu.satsprice.ui.exceedsMaxSupply
import xyz.tyiu.satsprice.ui.statusLine
data class FiatRow(val code: String, val amount: String, val rateDisplay: String)
data class IosConverterState(
val btcAmount: String,
val satsAmount: String,
val exceedsMaxSupply: Boolean,
val fiatRows: List<FiatRow>,
val availableCurrencies: List<CurrencyInfo>,
val selectedCurrencyCodes: List<String>,
val localeCurrencyCode: String?,
val defaultCurrencyCode: String,
val sourceName: String,
val isManualSource: Boolean,
val manualRateInput: String,
val isLoading: Boolean,
val errorMessage: String?,
val statusLine: String,
val defaultCurrencyRate: String,
)
/**
* Swift-facing bridge over [PriceViewModel]. Exposes a flattened, Map-free state shape
* (Kotlin/Native's ObjC/Swift export handles `List<DataClass>` far better than `Map<K, V>`)
* and observes via a plain callback rather than raw `Flow`, so no extra Swift interop
* tooling (e.g. SKIE) is required.
*/
class IosPriceViewModel {
private val viewModel = PriceViewModel()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
val sourceNames: List<String> get() = viewModel.availableSources
fun observeState(onChange: (IosConverterState) -> Unit) {
scope.launch {
viewModel.uiState.collect { state -> onChange(state.toIosState()) }
}
}
fun refresh() = viewModel.refresh()
fun onBtcAmountChanged(value: String) = viewModel.onBtcAmountChanged(value)
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 onSourceSelected(name: String) = viewModel.onSourceSelected(name)
fun onManualRateChanged(value: String) = viewModel.onManualRateChanged(value)
}
private fun ConverterUiState.toIosState(): IosConverterState = IosConverterState(
btcAmount = btcAmount,
satsAmount = satsAmount,
exceedsMaxSupply = exceedsMaxSupply(),
fiatRows = selectedFiatCurrencies.map { code ->
FiatRow(code, fiatAmounts[code].orEmpty(), rateDisplays[code].orEmpty())
},
availableCurrencies = availableFiatCurrencies,
selectedCurrencyCodes = selectedFiatCurrencies,
localeCurrencyCode = localeCurrencyCode,
defaultCurrencyCode = defaultCurrencyCode,
sourceName = sourceName,
isManualSource = isManualSource,
manualRateInput = manualRateInput,
isLoading = isLoading,
errorMessage = errorMessage,
statusLine = statusLine(),
defaultCurrencyRate = defaultCurrencyRate(),
)
@@ -1,20 +0,0 @@
package xyz.tyiu.satsprice
import platform.Foundation.*
actual fun systemCurrencies(): List<CurrencyInfo> {
@Suppress("UNCHECKED_CAST")
val codes = NSLocale.Companion.ISOCurrencyCodes as List<String>
return codes
.map { code -> CurrencyInfo(code, NSLocale.currentLocale.localizedStringForCurrencyCode(code) ?: code) }
.sortedBy { it.code }
}
actual fun localeCurrencyCode(): String? = NSLocale.currentLocale.currencyCode
actual fun currencyDecimalDigits(code: String): Int {
val formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterCurrencyStyle
formatter.currencyCode = code
return formatter.maximumFractionDigits.toInt()
}
@@ -1,8 +0,0 @@
package xyz.tyiu.satsprice.ui
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
// PriceScreen.kt is unused on iOS (native SwiftUI is used instead), but this module still
// needs an actual to satisfy the expect declaration for this target.
actual val screenHorizontalPadding: Dp = 16.dp
@@ -1,11 +0,0 @@
package xyz.tyiu.satsprice.ui
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.ResourceFormatted
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.desc.desc
fun localizedString(resource: StringResource): String = resource.desc().localized()
fun localizedFormattedString(resource: StringResource, args: List<Any>): String =
StringDesc.ResourceFormatted(resource, args).localized()