diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index f869f92..136e248 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -1,3 +1,4 @@ +import Foundation import Shared import SwiftUI @@ -103,7 +104,7 @@ struct ContentView: View { onChange: { viewModel.onBtcAmountChanged($0) } ) if state.exceedsMaxSupply { - Text(IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply)) + exceedsMaxSupplyText .font(.caption) .foregroundColor(.red) } @@ -167,6 +168,31 @@ struct ContentView: View { onToggle: { viewModel.onFiatCurrencyToggled($0) } ) } + .environment(\.openURL, OpenURLAction { url in + guard url == Self.maxSupplyLinkURL else { return .systemAction } + viewModel.onBtcAmountChanged(Self.maxSupplyBtcAmount) + return .handled + }) + } + + // Matches the literal text of MR.strings.exceeds_max_supply so it can be turned into a link; + // falls back to plain text if that ever drifts apart. `maxSupplyBtcAmount` is the sanitized + // digit-only form CurrencyConverter.MAX_BTC_SUPPLY formats to, mirroring the shared Compose UI. + private static let maxSupplyLinkText = "21,000,000 BTC" + private static let maxSupplyBtcAmount = "21000000" + private static let maxSupplyLinkURL = URL(string: "satsprice://set-max-supply")! + + private var exceedsMaxSupplyText: Text { + let warning = IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply) + var attributed = AttributedString(warning) + if let range = attributed.range(of: Self.maxSupplyLinkText) { + attributed[range].link = Self.maxSupplyLinkURL + attributed[range].underlineStyle = .single + // SwiftUI renders `.link` runs in the accent color regardless of the Text's own + // .foregroundColor modifier, so it has to be set directly on the link's range. + attributed[range].foregroundColor = .red + } + return Text(attributed) } @ViewBuilder 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 9aefb5f..01d0214 100644 --- a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt @@ -46,13 +46,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import dev.icerock.moko.resources.compose.stringResource import xyz.tyiu.satsprice.CurrencyInfo +import xyz.tyiu.satsprice.domain.CurrencyConverter +import xyz.tyiu.satsprice.domain.formatAmount import xyz.tyiu.satsprice.shared.MR private val SectionColors @@ -207,8 +216,43 @@ fun PriceScreen( ) if (exceedsMaxSupply) { + val warning = stringResource(MR.strings.exceeds_max_supply) + // Matches the literal text of MR.strings.exceeds_max_supply so it can be + // turned into a link; falls back to plain text if that ever drifts apart. + val maxSupplyText = "21,000,000 BTC" + val linkStart = warning.indexOf(maxSupplyText) + val annotatedWarning = if (linkStart < 0) { + AnnotatedString(warning) + } else { + // Set explicitly (and identically) for every interaction state, since + // LinkAnnotation otherwise renders in the theme's link/accent color + // rather than inheriting the surrounding warning text's color. + val linkStyle = SpanStyle( + color = MaterialTheme.colorScheme.error, + textDecoration = TextDecoration.Underline, + ) + buildAnnotatedString { + append(warning.substring(0, linkStart)) + withLink( + LinkAnnotation.Clickable( + tag = "max_supply", + styles = TextLinkStyles( + style = linkStyle, + focusedStyle = linkStyle, + hoveredStyle = linkStyle, + pressedStyle = linkStyle, + ), + ) { + viewModel.onBtcAmountChanged(formatAmount(CurrencyConverter.MAX_BTC_SUPPLY, 0)) + }, + ) { + append(maxSupplyText) + } + append(warning.substring(linkStart + maxSupplyText.length)) + } + } Text( - text = stringResource(MR.strings.exceeds_max_supply), + text = annotatedWarning, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(horizontal = 16.dp),